Source code for univention.appcenter.settings

#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention App Center
#  .settings file for Apps
#
# Copyright 2017-2022 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.
#

import os
import os.path

from six import string_types

from univention.appcenter.utils import app_is_running, container_mode, mkdir, _
from univention.appcenter.log import get_base_logger
from univention.appcenter.ucr import ucr_get, ucr_is_true, ucr_run_filter
from univention.appcenter.ini_parser import TypedIniSectionObject, IniSectionBooleanAttribute, IniSectionListAttribute, IniSectionAttribute

settings_logger = get_base_logger().getChild('settings')


[docs]class SettingValueError(Exception): pass
[docs]class Setting(TypedIniSectionObject): '''Based on the .settings file, models additional settings for Apps that can be configured before installation, during run-time, etc.''' type = IniSectionAttribute(default='String', choices=['String', 'Int', 'Bool', 'List', 'Password', 'File', 'PasswordFile', 'Status']) description = IniSectionAttribute(localisable=True, required=True) group = IniSectionAttribute(localisable=True) show = IniSectionListAttribute(default=['Settings'], choices=['Install', 'Upgrade', 'Remove', 'Settings']) show_read_only = IniSectionListAttribute(choices=['Install', 'Upgrade', 'Remove', 'Settings']) initial_value = IniSectionAttribute() required = IniSectionBooleanAttribute() scope = IniSectionListAttribute(choices=['inside', 'outside'])
[docs] @classmethod def get_class(cls, name): if name and not name.endswith('Setting'): name = '%sSetting' % name return super(Setting, cls).get_class(name)
[docs] def is_outside(self, app): # for Non-Docker Apps, Docker Apps when called from inside, Settings specified for 'outside' return not app.docker or container_mode() or 'outside' in self.scope
[docs] def is_inside(self, app): # only for Docker Apps (and called from the Docker Host). And not only 'outside' is specified return app.docker and not container_mode() and ('inside' in self.scope or self.scope == [])
[docs] def get_initial_value(self, app): if self.is_outside(app): value = ucr_get(self.name) if value is not None: return self.sanitize_value(app, value) if isinstance(self.initial_value, string_types): return ucr_run_filter(self.initial_value) return self.initial_value
[docs] def get_value(self, app, phase='Settings'): '''Get the current value for this Setting. Easy implementation''' if self.is_outside(app): value = ucr_get(self.name) else: if app_is_running(app): from univention.appcenter.actions import get_action configure = get_action('configure') ucr = configure._get_app_ucr(app) value = ucr.get(self.name) else: settings_logger.info('Cannot read %s while %s is not running' % (self.name, app)) value = None try: value = self.sanitize_value(app, value) except SettingValueError: settings_logger.info('Cannot use %r for %s' % (value, self.name)) value = None if value is None and phase == 'Install': settings_logger.info('Falling back to initial value for %s' % self.name) value = self.get_initial_value(app) return value
def _log_set_value(self, app, value): if value is None: settings_logger.info('Unsetting %s' % self.name) else: settings_logger.info('Setting %s to %r' % (self.name, value))
[docs] def set_value(self, app, value, together_config_settings, part): together_config_settings[part][self.name] = value
[docs] def set_value_together(self, app, value, together_config_settings): value = self.sanitize_value(app, value) value = self.value_for_setting(app, value) self._log_set_value(app, value) if self.is_outside(app): together_config_settings.setdefault('outside', {}) self.set_value(app, value, together_config_settings, 'outside') if self.is_inside(app): together_config_settings.setdefault('inside', {}) self.set_value(app, value, together_config_settings, 'inside')
[docs] def sanitize_value(self, app, value): if self.required and value in [None, '']: raise SettingValueError('%s is required' % self.name) return value
[docs] def value_for_setting(self, app, value): if value is None: return None value = str(value) if value == '': return None return value
[docs] def should_go_into_image_configuration(self, app): return self.is_inside(app) and ('Install' in self.show or 'Upgrade' in self.show)
[docs]class StringSetting(Setting): pass
[docs]class IntSetting(Setting):
[docs] def sanitize_value(self, app, value): super(IntSetting, self).sanitize_value(app, value) if value is not None: try: return int(value) except (ValueError, TypeError): raise SettingValueError('%s: %r is not a number' % (self.name, value))
[docs]class BoolSetting(Setting):
[docs] def sanitize_value(self, app, value): super(BoolSetting, self).sanitize_value(app, value) if isinstance(value, bool): return value return ucr_is_true(self.name, value=value)
[docs] def value_for_setting(self, app, value): return str(value).lower()
[docs]class ListSetting(Setting): labels = IniSectionListAttribute() values = IniSectionListAttribute()
[docs] def sanitize_value(self, app, value): super(ListSetting, self).sanitize_value(app, value) if value not in self.values: raise SettingValueError('%s: %r is not a valid option' % (self.name, value)) return value
[docs]class UDMListSetting(ListSetting): udm_filter = IniSectionAttribute()
[docs]class FileSetting(Setting): filename = IniSectionAttribute(required=True) def _log_set_value(self, app, value): # do not log complete file content pass def _read_file_content(self, filename): try: with open(filename) as fd: return fd.read() except EnvironmentError: return None def _touch_file(self, filename): if not os.path.exists(filename): mkdir(os.path.dirname(filename)) open(filename, 'wb') def _write_file_content(self, filename, content): try: if content: settings_logger.debug('Writing to %s' % filename) self._touch_file(filename) with open(filename, 'w') as fd: fd.write(content) else: settings_logger.debug('Deleting %s' % filename) if os.path.exists(filename): os.unlink(filename) except EnvironmentError as exc: settings_logger.error('Could not set content: %s' % exc)
[docs] def get_value(self, app, phase='Settings'): if self.is_outside(app): value = self._read_file_content(self.filename) else: if app_is_running(app): from univention.appcenter.docker import Docker docker = Docker(app) value = self._read_file_content(docker.path(self.filename)) else: settings_logger.info('Cannot read %s while %s is not running' % (self.name, app)) value = None if value is None and phase == 'Install': settings_logger.info('Falling back to initial value for %s' % self.name) value = self.get_initial_value(app) return value
[docs] def set_value(self, app, value, together_config_settings, part): if part == 'outside': return self._write_file_content(self.filename, value) else: if not app_is_running(app): settings_logger.error('Cannot write %s while %s is not running' % (self.name, app)) return from univention.appcenter.docker import Docker docker = Docker(app) return self._write_file_content(docker.path(self.filename), value)
[docs] def should_go_into_image_configuration(self, app): return False
[docs]class PasswordSetting(Setting): description = IniSectionAttribute(default=_('Password'), localisable=True) def _log_set_value(self, app, value): # do not log password pass
[docs]class PasswordFileSetting(FileSetting, PasswordSetting): def _touch_file(self, filename): super(PasswordFileSetting, self)._touch_file(filename) os.chmod(filename, 0o600)
[docs]class StatusSetting(Setting):
[docs] def set_value(self, app, value, together_config_settings, part): # do not set value via this function - has to be done directly pass