Source code for univention.appcenter.settings

#!/usr/bin/python3
#
# Univention App Center
#  .settings file for Apps
#
# SPDX-FileCopyrightText: 2017-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
#

import os
import os.path

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


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().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, str): 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().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().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().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 OSError: 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 OSError 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()._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