def _eval_simple_decorated_function(function, with_flavor, single_values=False, progress=False): # name of flavor argument. default: 'flavor' (if given, of course) if with_flavor is True: with_flavor = 'flavor' # argument names of the function, including 'self' arguments, defaults = arginspect(function) # remove self, remove iterator arguments = arguments[2:] # use defaults as dict if defaults: defaults = dict(zip(arguments[-len(defaults):], defaults)) else: defaults = {} @sanitize(DictSanitizer(dict((arg, Sanitizer(required=arg not in defaults and arg != with_flavor, default=defaults.get(arg))) for arg in arguments), _copy_value=False) if not single_values else None) def _response(self, request): # single_values: request.options is, e.g., ["id1", "id2", "id3"], no need for complicated dicts if not single_values: # normalize the whole request.options for element in request.options: # add flavor before default checking if with_flavor: element[with_flavor] = request.flavor or defaults.get(with_flavor) # checked for required arguments, set default... now run! iterator = RequestOptionsIterator(request.options, arguments, single_values) nones = [None] * len(arguments) if progress: number = len(request.options) if progress is True: progress_title = None else: if isinstance(progress, (list, tuple)): progress_title, progress_msg = progress else: progress_title, progress_msg = progress, None if '%d' in progress_title: progress_title = progress_title % number progress_obj = self.new_progress(progress_title, number) def _thread(self, progress_obj, iterator, nones): try: for res in function(self, iterator, *nones): if progress_msg: res_msg = progress_msg % res progress_obj.progress(res, res_msg) except Exception: progress_obj.exception(sys.exc_info()) else: progress_obj.finish() thread = Thread(target=_thread, args=[self, progress_obj, iterator, nones]) thread.start() return progress_obj.initialised() else: return list(function(self, iterator, *nones)) return _response
class Instance(Base, ProgressMixin): PLUGIN_DIR = os.path.dirname(plugins.__file__) def init(self): self.modules = {} self.load() @sanitize(plugin=StringSanitizer(required=True), args=DictSanitizer({})) @simple_response(with_progress=True) def run(self, plugin, args=None): plugin = self.get(plugin) MODULE.process('Running %s' % (plugin, )) for line in plugin.run_descr: MODULE.process(line) args = args or {} return plugin.execute(self, **args) def new_progress(self, *args, **kwargs): progress = super(Instance, self).new_progress(*args, **kwargs) progress.retry_after = 600 return progress @sanitize(pattern=PatternSanitizer(default='.*')) @simple_response def query(self, pattern): return [plugin.dict for plugin in self if plugin.match(pattern)] @property def plugins(self): for plugin in listdir(self.PLUGIN_DIR): if plugin.endswith('.py') and plugin != '__init__.py': yield plugin[:-3] def load(self): for plugin in self.plugins: try: self.modules[plugin] = Plugin(plugin) except ImportError as exc: MODULE.error('Could not load plugin %r: %r' % (plugin, exc)) raise self.modules = OrderedDict( sorted(self.modules.items(), key=lambda t: t[0])) def get(self, plugin): return self.modules[plugin] def __iter__(self): return iter(self.modules.values())
class Instance(Base, ProgressMixin): PLUGIN_DIR = os.path.dirname(plugins.__file__) def init(self): self.modules = {} self.load() @sanitize(plugin=StringSanitizer(required=True), args=DictSanitizer({})) @simple_response def run(self, plugin, args=None): plugin = self.get(plugin) args = args or {} def thread(self, request): return plugin.execute(self, **args) return thread @sanitize(pattern=PatternSanitizer(default='.*')) @simple_response def query(self, pattern): return [plugin.dict for plugin in self if plugin.match(pattern)] @property def plugins(self): for plugin in listdir(self.PLUGIN_DIR): if plugin.endswith('.py') and plugin != '__init__.py': yield plugin[:-3] def load(self): for plugin in self.plugins: try: self.modules[plugin] = Plugin(plugin) except ImportError as exc: MODULE.error('Could not load plugin %r: %r' % (plugin, exc)) raise def get(self, plugin): return self.modules[plugin] def __iter__(self): return iter(self.modules.values())
class Instance(umcm.Base, ProgressMixin): def init(self): os.umask( 0o022 ) # umc umask is too restrictive for app center as it creates a lot of files in docker containers self.ucr = ucr_instance() self.update_applications_done = False install_opener(self.ucr) self._is_working = False try: self.package_manager = PackageManager( info_handler=MODULE.process, step_handler=None, error_handler=MODULE.warn, lock=False, ) except SystemError as exc: MODULE.error(str(exc)) raise umcm.UMC_Error(str(exc), status=500) self.package_manager.set_finished( ) # currently not working. accepting new tasks get_package_manager._package_manager = self.package_manager # build cache _update_modules() get_action('list').get_apps() # not initialize here: error prone due to network errors and also kinda slow self._uu = None self._cm = None # in order to set the correct locale locale.setlocale(locale.LC_ALL, str(self.locale)) try: log_to_logfile() except IOError: pass # connect univention.appcenter.log to the progress-method handler = ProgressInfoHandler(self.package_manager) handler.setLevel(logging.INFO) get_base_logger().addHandler(handler) percentage = ProgressPercentageHandler(self.package_manager) percentage.setLevel(logging.DEBUG) get_base_logger().getChild('actions.install.progress').addHandler( percentage) get_base_logger().getChild('actions.upgrade.progress').addHandler( percentage) get_base_logger().getChild('actions.remove.progress').addHandler( percentage) def get_updater(self): if self._uu is None: self._uu = UniventionUpdater(False) return self._uu def get_component_manager(self): if self._cm is None: self._cm = ComponentManager(self.ucr, self.get_updater()) return self._cm def error_handling(self, etype, exc, etraceback): error_handling(etype, exc, etraceback) return super(Instance, self).error_handling(exc, etype, etraceback) @simple_response def version(self, version=None): info = get_action('info') ret = info.get_compatibility() if not info.is_compatible(version): raise umcm.UMC_Error( 'The App Center version of the requesting host is not compatible with the version of %s (%s)' % (get_local_fqdn(), ret)) return ret @sanitize( version=StringSanitizer(required=True), function=StringSanitizer(required=False), ) @simple_response def version2(self, version, function=None): info = get_action('info') return { 'compatible': info.is_compatible(version, function=function), 'version': info.get_ucs_version() } def _remote_appcenter(self, host, function=None): if host is None: raise ValueError('Cannot connect to None') if not host.endswith('.%s' % self.ucr.get('domainname')): raise ValueError('Only connect to FQDNs within the domain') info = get_action('info') opts = {'version': info.get_ucs_version()} if function is not None: opts['function'] = function try: client = Client(host, self.username, self.password) response = client.umc_command('appcenter/version2', opts) except (HTTPError) as exc: raise umcm.UMC_Error( _('Problems connecting to {0} ({1}). Please update {0}!'). format(host, exc.message)) except (ConnectionError, Exception) as exc: raise umcm.UMC_Error( _('Problems connecting to {} ({}).').format(host, str(exc))) err_msg = _( 'The App Center version of the this host ({}) is not compatible with the version of {} ({})' ).format(opts['version'], host, response.result.get('version')) # i guess this is kind of bad if response.status != 200: raise umcm.UMC_Error(err_msg) # remote says he is not compatible if response.result.get('compatible', True) is False: raise umcm.UMC_Error(err_msg) # i'm not compatible if not info.is_compatible(response.result.get('version')): raise umcm.UMC_Error(err_msg) return client @sanitize(apps=ListSanitizer(AppSanitizer(), required=True), action=ChoicesSanitizer(['install', 'upgrade', 'remove'], required=True)) @simple_response def resolve(self, apps, action): ret = {} ret['apps'] = resolve_dependencies(apps, action) ret['auto_installed'] = [ app.id for app in ret['apps'] if app.id not in [a.id for a in apps] ] apps = ret['apps'] ret['errors'], ret['warnings'] = check(apps, action) domain = get_action('domain') ret['apps'] = domain.to_dict(apps) ret['settings'] = {} self.ucr.load() for app in apps: ret['settings'][app.id] = self._get_config(app, action.title()) return ret @require_apps_update @require_password @sanitize( apps=ListSanitizer(AppSanitizer(), required=True), auto_installed=ListSanitizer(required=True), action=ChoicesSanitizer(['install', 'upgrade', 'remove'], required=True), hosts=DictSanitizer({}, required=True), settings=DictSanitizer({}, required=True), dry_run=BooleanSanitizer(), ) @simple_response(with_progress=True) def run(self, progress, apps, auto_installed, action, hosts, settings, dry_run): localhost = get_local_fqdn() ret = {} if dry_run: for host in hosts: _apps = [ next(app for app in apps if app.id == _app) for _app in hosts[host] ] if host == localhost: ret[host] = self._run_local_dry_run( _apps, action, {}, progress) else: try: ret[host] = self._run_remote_dry_run( host, _apps, action, auto_installed, {}, progress) except umcm.UMC_Error: ret[host] = {'unreachable': [app.id for app in _apps]} else: for app in apps: for host in hosts: if app.id not in hosts[host]: continue host_result = ret.get(host, {}) ret[host] = host_result _settings = {app.id: settings[app.id]} if host == localhost: host_result[app.id] = self._run_local( app, action, _settings, auto_installed, progress) else: host_result[app.id] = self._run_remote( host, app, action, auto_installed, _settings, progress)[app.id] if not host_result[app.id]['success']: break return ret def _run_local_dry_run(self, apps, action, settings, progress): if action == 'upgrade': apps = [Apps().find_candidate(app) or app for app in apps] if len(apps) == 1: progress.title = _('%s: Running tests') % apps[0].name else: progress.title = _('%d Apps: Running tests') % len(apps) ret = {} ret['errors'], ret['warnings'] = check(apps, action) ret['errors'].pop('must_have_no_unmet_dependencies', None) # has to be resolved prior to this call! action = get_action(action)() ret['packages'] = {} for app in apps: args = action._build_namespace( app=[app], dry_run=True, install_master_packages_remotely=False, only_master_packages=False) result = action.dry_run(app, args) if result is not None: ret['packages'][app.id] = result return ret def _run_local(self, app, action, settings, auto_installed, progress): kwargs = { 'noninteractive': True, 'auto_installed': auto_installed, 'skip_checks': [ 'shall_have_enough_ram', 'shall_only_be_installed_in_ad_env_with_password_service', 'must_not_have_concurrent_operation' ], } if settings.get(app.id): kwargs['set_vars'] = settings[app.id] if action == 'install': progress.title = _('Installing %s') % (app.name, ) elif action == 'remove': progress.title = _('Uninstalling %s') % (app.name, ) elif action == 'upgrade': progress.title = _('Upgrading %s') % (app.name, ) action = get_action(action) handler = UMCProgressHandler(progress) handler.setLevel(logging.INFO) action.logger.addHandler(handler) try: package_manager = get_package_manager() with package_manager.no_umc_restart(exclude_apache=True): success = action.call(app=[app], username=self.username, password=self.password, **kwargs) return {'success': success} except AppCenterError as exc: raise umcm.UMC_Error(str(exc), result=dict(display_feedback=True, title='%s %s' % (exc.title, exc.info))) finally: action.logger.removeHandler(handler) def _run_remote_dry_run(self, host, apps, action, auto_installed, settings, progress): return self._run_remote_logic(host, apps, action, auto_installed, settings, progress, dry_run=True) def _run_remote(self, host, app, action, auto_installed, settings, progress): return self._run_remote_logic(host, [app], action, auto_installed, settings, progress, dry_run=False) def _run_remote_logic(self, host, apps, action, auto_installed, settings, progress, dry_run): if len(apps) == 1: progress.title = _('%s: Connecting to %s') % (apps[0].name, host) else: progress.title = _('%d Apps: Connecting to %s') % (len(apps), host) client = self._remote_appcenter(host, function='appcenter/run') opts = { 'apps': [str(app) for app in apps], 'auto_installed': auto_installed, 'action': action, 'hosts': { host: [app.id for app in apps] }, 'settings': settings, 'dry_run': dry_run } progress_id = client.umc_command('appcenter/run', opts).result['id'] while True: result = client.umc_command('appcenter/progress', { 'progress_id': progress_id }).result if result['finished']: return result['result'][host] progress.title = result['title'] progress.intermediate.extend(result['intermediate']) progress.message = result['message'] time.sleep(result['retry_after'] / 1000.0) @simple_response def query(self, quick=False): query_cache_file = '/var/cache/univention-appcenter/umc-query.json' if quick: try: with open(query_cache_file) as fd: return json.load(fd) except (EnvironmentError, ValueError) as exc: MODULE.error('Error returning cached query: %s' % exc) return [] self.update_applications() self.ucr.load() reload_package_manager() list_apps = get_action('list') domain = get_action('domain') apps = list_apps.get_apps() if self.ucr.is_true('appcenter/docker', True): if not self._test_for_docker_service(): raise umcm.UMC_Error( _('The docker service is not running! The App Center will not work properly.' ) + ' ' + _('Make sure docker.io is installed, try starting the service with "service docker start".' )) info = domain.to_dict(apps) with open(query_cache_file, 'w') as fd: json.dump(info, fd) return info def update_applications(self): if self.ucr.is_true('appcenter/umc/update/always', True): update = get_action('update') try: update.call() except NetworkError as err: raise umcm.UMC_Error(str(err)) except Abort: pass self.update_applications_done = True def _test_for_docker_service(self): if docker_bridge_network_conflict(): msg = _( 'A conflict between the system network settings and the docker bridge default network has been detected.' ) + '\n\n' msg += _( 'Please either configure a different network for the docker bridge by setting the UCR variable docker/daemon/default/opts/bip to a different network and restart the system,' ) + ' ' msg += _( 'or disable the docker support in the AppCenter by setting appcenter/docker to false.' ) raise umcm.UMC_Error(msg) if not docker_is_running(): MODULE.warn('Docker is not running! Trying to start it now...') call_process(['invoke-rc.d', 'docker', 'start']) if not docker_is_running(): return False return True @simple_response def suggestions(self, version): try: cache = AppCenterCache.build(server=default_server()) cache_file = cache.get_cache_file('.suggestions.json') with open(cache_file) as fd: json = load(fd) except (EnvironmentError, ValueError): raise umcm.UMC_Error(_('Could not load suggestions.')) else: try: return json[version] except (KeyError, AttributeError): raise umcm.UMC_Error(_('Unexpected suggestions data.')) @simple_response def enable_docker(self): if self._test_for_docker_service(): ucr_save({'appcenter/docker': 'enabled'}) else: raise umcm.UMC_Error( _('Unable to start the docker service!') + ' ' + _('Make sure docker.io is installed, try starting the service with "service docker start".' )) @require_apps_update @require_password @simple_response(with_progress=True) def sync_ldap(self): register = get_action('register') register.call(username=self.username, password=self.password) # used in updater-umc @simple_response def get_by_component_id(self, component_id): domain = get_action('domain') if isinstance(component_id, list): requested_apps = [ Apps().find_by_component_id(cid) for cid in component_id ] return domain.to_dict(requested_apps) else: app = Apps().find_by_component_id(component_id) if app: return domain.to_dict([app])[0] else: raise umcm.UMC_Error( _('Could not find an application for %s') % component_id) # used in updater-umc @simple_response def app_updates(self): upgrade = get_action('upgrade') domain = get_action('domain') return domain.to_dict(list(upgrade.iter_upgradable_apps())) @sanitize(application=StringSanitizer(minimum=1, required=True)) @simple_response def get(self, application): list_apps = get_action('list') domain = get_action('domain') apps = list_apps.get_apps() for app in apps: if app.id == application: break else: app = None if app is None: raise umcm.UMC_Error( _('Could not find an application for %s') % (application, )) return domain.to_dict([app])[0] @sanitize(app=AppSanitizer(required=True)) @simple_response def config(self, app, phase): self.ucr.load() return self._get_config(app, phase) def _get_config(self, app, phase): autostart = self.ucr.get('%s/autostart' % app.id, 'yes') is_running = app_is_running(app) values = {} for setting in app.get_settings(): if phase in setting.show or phase in setting.show_read_only: value = setting.get_value(app, phase) if isinstance(setting, FileSetting) and not isinstance( setting, PasswordFileSetting): if value: value = b64encode( value.encode('utf-8')).decode('ascii') values[setting.name] = value return { 'autostart': autostart, 'is_running': is_running, 'values': values, } @sanitize(app=AppSanitizer(required=True), values=DictSanitizer({})) @simple_response(with_progress=True) def configure(self, progress, app, values, autostart=None): for setting in app.get_settings(): if isinstance(setting, FileSetting) and not isinstance( setting, PasswordFileSetting): if values.get(setting.name): values[setting.name] = b64decode( values[setting.name]).decode('utf-8') configure = get_action('configure') handler = UMCProgressHandler(progress) handler.setLevel(logging.INFO) configure.logger.addHandler(handler) try: return configure.call(app=app, set_vars=values, autostart=autostart) finally: configure.logger.removeHandler(handler) @sanitize(app=AppSanitizer(required=True), mode=ChoicesSanitizer(['start', 'stop'])) @simple_response def app_service(self, app, mode): service = get_action(mode) service.call(app=app) @sanitize(app=AppSanitizer(required=False), action=ChoicesSanitizer(['get', 'buy', 'search', 'vote']), value=StringSanitizer()) @simple_response def track(self, app, action, value): send_information(action, app=app, value=value) @contextmanager def locked(self): try: if self._working(): raise LockError() with package_lock(): yield except LockError: raise umcm.UMC_Error(_('Another package operation is in progress')) def _install_master_packages_on_hosts(self, app, function): if function.startswith('upgrade'): remote_function = 'update-schema' else: remote_function = 'install-schema' master_packages = app.default_packages_master if not master_packages: return hosts = find_hosts_for_master_packages() all_hosts_count = len(hosts) package_manager = get_package_manager() package_manager.set_max_steps( all_hosts_count * 200) # up to 50% if all hosts are installed # maybe we already installed local packages (on master) if self.ucr.get('server/role') == 'domaincontroller_master': # TODO: set_max_steps should reset _start_steps. need function like set_start_steps() package_manager.progress_state._start_steps = all_hosts_count * 100 for host, host_is_master in hosts: package_manager.progress_state.info( _('Installing LDAP packages on %s') % host) try: if not self._install_master_packages_on_host( app, remote_function, host): error_message = 'Unable to install %r on %s. Check /var/log/univention/management-console-module-appcenter.log on the host and this server. All errata updates have been installed on %s?' % ( master_packages, host, host) raise Exception(error_message) except Exception as e: MODULE.error('%s: %s' % (host, e)) if host_is_master: role = 'Primary Directory Node' else: role = 'Backup Directory Node' # ATTENTION: This message is not localised. It is parsed by the frontend to markup this message! If you change this message, be sure to do the same in AppCenterPage.js package_manager.progress_state.error( 'Installing extension of LDAP schema for %s seems to have failed on %s %s' % (app.component_id, role, host)) if host_is_master: raise # only if host_is_master! finally: package_manager.add_hundred_percent() def _install_master_packages_on_host(self, app, function, host): client = Client(host, self.username, self.password) result = client.umc_command( 'appcenter/invoke', { 'function': function, 'application': app.id, 'force': True, 'dont_remote_install': True }).result if result['can_continue']: all_errors = self._query_remote_progress(client) return len(all_errors) == 0 else: MODULE.warn('%r' % result) return False def _install_dry_run_remote(self, app, function, dont_remote_install, force): MODULE.process('Invoke install_dry_run_remote') self.ucr.load() if function.startswith('upgrade'): remote_function = 'update-schema' else: remote_function = 'install-schema' master_packages = app.default_packages_master # connect to Primary/Backup Nodes unreachable = [] hosts_info = {} remote_info = { 'master_unreachable': False, 'problems_with_hosts': False, 'serious_problems_with_hosts': False, } dry_run_threads = [] info = get_action('info') if master_packages and not dont_remote_install: hosts = find_hosts_for_master_packages() # checking remote host is I/O heavy, so use threads # "global" variables: unreachable, hosts_info, remote_info def _check_remote_host(app_id, host, host_is_master, username, password, force, remote_function): MODULE.process('Starting dry_run for %s on %s' % (app_id, host)) MODULE.process('%s: Connecting...' % host) try: client = Client(host, username, password) except (ConnectionError, HTTPError) as exc: MODULE.warn('_check_remote_host: %s: %s' % (host, exc)) unreachable.append(host) if host_is_master: remote_info['master_unreachable'] = True else: MODULE.process('%s: ... done' % host) host_info = {} MODULE.process('%s: Getting version...' % host) try: host_version = client.umc_command( 'appcenter/version', { 'version': info.get_compatibility() }).result except Forbidden: # command is not yet known (older app center) MODULE.process('%s: ... forbidden!' % host) host_version = None except (ConnectionError, HTTPError) as exc: MODULE.warn('%s: Could not get appcenter/version: %s' % (host, exc)) raise except Exception as exc: MODULE.error('%s: Exception: %s' % (host, exc)) raise MODULE.process('%s: ... done' % host) host_info['compatible_version'] = info.is_compatible( host_version) MODULE.process('%s: Invoking %s ...' % (host, remote_function)) try: host_info['result'] = client.umc_command( 'appcenter/invoke_dry_run', { 'function': remote_function, 'application': app_id, 'force': force, 'dont_remote_install': True, }).result except Forbidden: # command is not yet known (older app center) MODULE.process('%s: ... forbidden!' % host) host_info['result'] = { 'can_continue': False, 'serious_problems': False } except (ConnectionError, HTTPError) as exc: MODULE.warn('Could not get appcenter/version: %s' % (exc, )) raise MODULE.process('%s: ... done' % host) if not host_info['compatible_version'] or not host_info[ 'result']['can_continue']: remote_info['problems_with_hosts'] = True if host_info['result'][ 'serious_problems'] or not host_info[ 'compatible_version']: remote_info['serious_problems_with_hosts'] = True hosts_info[host] = host_info MODULE.process('Finished dry_run for %s on %s' % (app_id, host)) for host, host_is_master in hosts: thread = Thread(target=_check_remote_host, args=(app.id, host, host_is_master, self.username, self.password, force, remote_function)) thread.start() dry_run_threads.append(thread) result = {} for thread in dry_run_threads: thread.join() MODULE.process('All %d threads finished' % (len(dry_run_threads))) result['unreachable'] = unreachable result['hosts_info'] = hosts_info result.update(remote_info) return result def _query_remote_progress(self, client): all_errors = set() number_failures = 0 number_failures_max = 20 host = client.hostname while True: try: result = client.umc_command('appcenter/progress').result except (ConnectionError, HTTPError) as exc: MODULE.warn('%s: appcenter/progress returned an error: %s' % (host, exc)) number_failures += 1 if number_failures >= number_failures_max: MODULE.error( '%s: Remote App Center cannot be contacted for more than %d seconds. Maybe just a long Apache Restart? Presume failure! Check logs on remote machine, maybe installation was successful.' % number_failures_max) return False time.sleep(1) continue else: # everything okay. reset "timeout" number_failures = 0 MODULE.info('Result from %s: %r' % (host, result)) info = result['info'] steps = result['steps'] errors = ['%s: %s' % (host, error) for error in result['errors']] if info: self.package_manager.progress_state.info(info) if steps: steps = float( steps ) # bug in package_manager in 3.1-0: int will result in 0 because of division and steps < max_steps self.package_manager.progress_state.percentage(steps) for error in errors: if error not in all_errors: self.package_manager.progress_state.error(error) all_errors.add(error) if result['finished'] is True: break time.sleep(0.1) return all_errors def keep_alive(self, request): ''' Fix for Bug #30611: UMC kills appcenter module if no request is sent for $(ucr get umc/module/timeout). this happens if a user logs out during a very long installation. this function will be run by the frontend to always have one connection open to prevent killing the module. ''' def _thread(): while self._working(): time.sleep(1) def _finished(thread, result): success = not isinstance(result, BaseException) if not success: MODULE.warn('Exception during keep_alive: %s' % result) self.finished(request.id, success) thread = notifier.threads.Simple('keep_alive', notifier.Callback(_thread), _finished) thread.run() @simple_response def ping(self): return True @simple_response def buy(self, application): app = Apps().find(application) if not app or not app.shop_url: return None ret = {} ret['key_id'] = self.ucr.get('license/uuid') ret['ucs_version'] = self.ucr.get('version/version') ret['app_id'] = app.id ret['app_version'] = app.version # ret['locale'] = locale.getlocale()[0] # done by frontend ret['user_count'] = None # FIXME: get users and computers from license ret['computer_count'] = None return ret @simple_response def enable_disable_app(self, application, enable=True): app = Apps().find(application) if not app: return stall = get_action('stall') stall.call(app=app, undo=enable) @simple_response def packages_sections(self): """ fills the 'sections' combobox in the search form """ sections = set() cache = apt.Cache() for package in cache: sections.add(package.section) return sorted(sections) @sanitize(pattern=PatternSanitizer(required=True)) @simple_response def packages_query(self, pattern, section='all', key='package'): """ Query to fill the grid. Structure is fixed here. """ result = [] for package in self.package_manager.packages(reopen=True): if section == 'all' or package.section == section: toshow = False if pattern.pattern == '^.*$': toshow = True elif key == 'package' and pattern.search(package.name): toshow = True elif key == 'description' and package.candidate and pattern.search( package.candidate.raw_description): toshow = True if toshow: result.append(self._package_to_dict(package, full=False)) return result @simple_response def packages_get(self, package): """ retrieves full properties of one package """ package = self.package_manager.get_package(package) if package is not None: return self._package_to_dict(package, full=True) else: # TODO: 404? return {} @sanitize(function=MappingSanitizer( { 'install': 'install', 'upgrade': 'install', 'uninstall': 'remove', }, required=True), packages=ListSanitizer(StringSanitizer(minimum=1), required=True), update=BooleanSanitizer()) @simple_response def packages_invoke_dry_run(self, packages, function, update): if update: self.package_manager.update() packages = self.package_manager.get_packages(packages) kwargs = {'install': [], 'remove': [], 'dry_run': True} if function == 'install': kwargs['install'] = packages else: kwargs['remove'] = packages return dict( zip(['install', 'remove', 'broken'], self.package_manager.mark(**kwargs))) @sanitize(function=MappingSanitizer( { 'install': 'install', 'upgrade': 'install', 'uninstall': 'remove', }, required=True), packages=ListSanitizer(StringSanitizer(minimum=1), required=True)) def packages_invoke(self, request): """ executes an installer action """ packages = request.options.get('packages') function = request.options.get('function') try: if self._working(): # make it multi-tab safe (same session many buttons to be clicked) raise LockError() with self.package_manager.locked(reset_status=True): not_found = [ pkg_name for pkg_name in packages if self.package_manager.get_package(pkg_name) is None ] self.finished(request.id, {'not_found': not_found}) if not not_found: def _thread(package_manager, function, packages): with package_manager.locked(set_finished=True): with package_manager.no_umc_restart( exclude_apache=True): if function == 'install': package_manager.install(*packages) else: package_manager.uninstall(*packages) def _finished(thread, result): if isinstance(result, BaseException): MODULE.warn('Exception during %s %s: %r' % (function, packages, str(result))) thread = notifier.threads.Simple( 'invoke', notifier.Callback(_thread, self.package_manager, function, packages), _finished) thread.run() else: self.package_manager.set_finished( ) # nothing to do, ready to take new commands except LockError: # make it thread safe: another process started a package manager # this module instance already has a running package manager raise umcm.UMC_Error(_('Another package operation is in progress')) @contextmanager def is_working(self): self._is_working = True yield self._is_working = False def _working(self): return self._is_working or os.path.exists( LOCK_FILE) or not self.package_manager.progress_state._finished @simple_response def working(self): # TODO: PackageManager needs is_idle() or something # preferably the package_manager can tell what is currently executed: # package_manager.is_working() => False or _('Installing PKG') return self._working() @simple_response def custom_progress(self): timeout = 5 ret = self.package_manager.poll(timeout) ret['finished'] = not self._working() return ret def _package_to_dict(self, package, full): """ Helper that extracts properties from a 'apt_pkg.Package' object and stores them into a dictionary. Depending on the 'full' switch, stores only limited (for grid display) or full (for detail view) set of properties. """ installed = package.installed # may be None found = True candidate = package.candidate found = candidate is not None if not found: candidate = NoneCandidate() result = { 'package': package.name, 'installed': package.is_installed, 'upgradable': package.is_upgradable and found, 'summary': candidate.summary, } # add (and translate) a combined status field # *** NOTE *** we translate it here: if we would use the Custom Formatter # of the grid then clicking on the sort header would not work. if package.is_installed: if package.is_upgradable: result['status'] = _('upgradable') else: result['status'] = _('installed') else: result['status'] = _('not installed') # additional fields needed for detail view if full: # Some fields differ depending on whether the package is installed or not: if package.is_installed: result['section'] = installed.section result['priority'] = installed.priority or '' result['summary'] = installed.summary # take the current one result['description'] = installed.description result['installed_version'] = installed.version result['size'] = installed.installed_size if package.is_upgradable: result['candidate_version'] = candidate.version else: del result[ 'upgradable'] # not installed: don't show 'upgradable' at all result['section'] = candidate.section result['priority'] = candidate.priority or '' result['description'] = candidate.description result['size'] = candidate.installed_size result['candidate_version'] = candidate.version # format size to handle bytes size = result['size'] byte_mods = ['B', 'kB', 'MB'] for byte_mod in byte_mods: if size < 10000: break size = float(size) / 1000 # MB, not MiB else: size = size * 1000 # once too often if size == int(size): format_string = '%d %s' else: format_string = '%.2f %s' result['size'] = format_string % (size, byte_mod) return result @simple_response def components_query(self): """ Returns components list for the grid in the ComponentsPage. """ # be as current as possible. self.get_updater().ucr_reinit() self.ucr.load() return [ self.get_component_manager().component(comp.name) for comp in self.get_updater().get_components(all=True) ] @sanitize_list(StringSanitizer()) @multi_response(single_values=True) def components_get(self, iterator, component_id): # be as current as possible. self.get_updater().ucr_reinit() self.ucr.load() for component_id in iterator: yield self.get_component_manager().component(component_id) @sanitize_list(DictSanitizer({'object': advanced_components_sanitizer})) @multi_response def components_put(self, iterator, object): """Writes back one or more component definitions. """ # umc.widgets.Form wraps the real data into an array: # # [ # { # 'object' : { ... a dict with the real data .. }, # 'options': None # }, # ... more such entries ... # ] # # Current approach is to return a similarly structured array, # filled with elements, each one corresponding to one array # element of the request: # # [ # { # 'status' : a number where 0 stands for success, anything else # is an error code # 'message' : a result message # 'object' : a dict of field -> error message mapping, allows # the form to show detailed error information # }, # ... more such entries ... # ] with set_save_commit_load(self.ucr) as super_ucr: for object, in iterator: yield self.get_component_manager().put(object, super_ucr) self.package_manager.update() # do the same as components_put (update) # but don't allow adding an already existing entry components_add = sanitize_list( DictSanitizer({'object': add_components_sanitizer}))(components_put) components_add.__name__ = 'components_add' @sanitize_list(StringSanitizer()) @multi_response(single_values=True) def components_del(self, iterator, component_id): for component_id in iterator: yield self.get_component_manager().remove(component_id) self.package_manager.update() @multi_response def settings_get(self, iterator): # *** IMPORTANT *** Our UCR copy must always be current. This is not only # to catch up changes made via other channels (ucr command line etc), # but also to reflect the changes we have made ourselves! self.ucr.load() for _ in iterator: yield { 'unmaintained': self.ucr.is_true('repository/online/unmaintained', False), 'server': self.ucr.get('repository/online/server', ''), 'prefix': self.ucr.get('repository/online/prefix', ''), } @sanitize_list( DictSanitizer({'object': basic_components_sanitizer}), min_elements=1, max_elements=1 # moduleStore with one element... ) @multi_response def settings_put(self, iterator, object): # FIXME: returns values although it should yield (multi_response) changed = False # Set values into our UCR copy. try: with set_save_commit_load(self.ucr) as super_ucr: for object, in iterator: for key, value in object.items(): MODULE.info( " ++ Setting new value for '%s' to '%s'" % (key, value)) super_ucr.set_registry_var( '%s/%s' % (ONLINE_BASE, key), value) changed = super_ucr.changed() except Exception as e: MODULE.warn(" !! Writing UCR failed: %s" % str(e)) return [{'message': str(e), 'status': PUT_WRITE_ERROR}] self.package_manager.update() # Bug #24878: emit a warning if repository is not reachable try: updater = self.get_updater() for line in updater.print_version_repositories().split('\n'): if line.strip(): break else: raise ConfigurationError() except ConfigurationError: msg = _( "There is no repository at this server (or at least none for the current UCS version)" ) MODULE.warn(" !! Updater error: %s" % msg) response = {'message': msg, 'status': PUT_UPDATER_ERROR} # if nothing was committed, we want a different type of error code, # just to appropriately inform the user if changed: response['status'] = PUT_UPDATER_NOREPOS return [response] except BaseException as ex: MODULE.warn(" !! Updater error: %s" % (ex, )) return [{'message': str(ex), 'status': PUT_UPDATER_ERROR}] return [{'status': PUT_SUCCESS}]
class Instance(Base): def init(self): # set the language in order to return the correctly localized labels/descriptions uit.set_language(self.locale.language) def __create_variable_info(self, options): all_info = ConfigRegistryInfo(registered_only=False) info = ConfigRegistryInfo(install_mode=True) info.read_customized() var = Variable() # description for line in options['descriptions']: text = line['text'] if not text: continue if 'lang' in line: var['description[%s]' % line['lang']] = text else: var['description'] = text # categories if options['categories']: var['categories'] = ','.join(options['categories']) # type var['type'] = options['type'] # are there any modifications? old_value = all_info.get_variable(options['key']) if old_value != var: # save info.add_variable(options['key'], var) info.write_customized() def is_readonly(self, key): ucrinfo_system = ConfigRegistryInfo(registered_only=False, load_customized=False) var = ucrinfo_system.get_variable(key) if var: return var.get('readonly') in ('yes', '1', 'true') return False @sanitize( DictSanitizer({ 'object': DictSanitizer({ 'key': UCRKeySanitizer(required=True), 'value': StringSanitizer(default=''), }) })) def add(self, request): # does the same as put ucr.load() already_set = set(ucr.keys()) & set(v['object']['key'] for v in request.options) if already_set: raise UMC_Error( _('The UCR variable %s is already set.') % ('", "'.join(already_set))) self.put(request) @sanitize( DictSanitizer({ 'object': DictSanitizer({ 'key': UCRKeySanitizer(required=True), 'value': StringSanitizer(default=''), }) })) def put(self, request): for _var in request.options: var = _var['object'] value = var['value'] or '' key = var['key'] if self.is_readonly(key): raise UMC_Error( _('The UCR variable %s is read-only and can not be changed!' ) % (key, )) arg = ['%s=%s' % (key.encode(), value.encode())] handler_set(arg) # handle descriptions, type, and categories if 'descriptions' in var or 'type' in var or 'categories' in var: self.__create_variable_info(var) self.finished(request.id, True) def remove(self, request): variables = filter(lambda x: x is not None, map(lambda x: x.get('object'), request.options)) for var in variables: if self.is_readonly(var): raise UMC_Error( _('The UCR variable %s is read-only and can not be removed!' ) % (var, )) handler_unset(variables) self.finished(request.id, True) def get(self, request): ucrReg = ConfigRegistry() ucrReg.load() ucrInfo = ConfigRegistryInfo(registered_only=False) # iterate over all requested variables results = [] for key in request.options: info = ucrInfo.get_variable(str(key)) value = ucrReg.get(str(key)) if not info and (value or '' == value): # only the value available results.append({'key': key, 'value': value}) elif info: # info (categories etc.) available info['value'] = value info['key'] = key results.append(info.normalize()) else: # variable not available, request failed raise UMC_Error( _('The UCR variable %(key)s could not be found') % {'key': key}) self.finished(request.id, results) def categories(self, request): ucrInfo = ConfigRegistryInfo(registered_only=False) categories = [] for id, obj in ucrInfo.categories.iteritems(): name = obj['name'] if ucrInfo.get_variables(id): categories.append({'id': id, 'label': name}) self.finished(request.id, categories) @sanitize(pattern=PatternSanitizer(default='.*'), key=ChoicesSanitizer(['all', 'key', 'value', 'description'], required=True)) @simple_response def query(self, pattern, key, category=None): '''Returns a dictionary of configuration registry variables found by searching for the (wildcard) expression defined by the UMCP request. Additionally a list of configuration registry categories can be defined. The dictionary returned is compatible with the Dojo data store format.''' variables = [] if category == 'all': # load _all_ config registry variables base_info = ConfigRegistryInfo(registered_only=False) else: # load _all registered_ config registry variables base_info = ConfigRegistryInfo() if category in ('all', 'all-registered'): category = None def _match_value(name, var): return var.value and pattern.match(var.value) def _match_key(name, var): return pattern.match(name) def _match_description(name, var): descr = var.get('description') return descr and pattern.match(descr) def _match_all(name, var): return _match_value(name, var) or _match_description( name, var) or _match_key(name, var) func = locals().get('_match_%s' % key) for name, var in base_info.get_variables(category).iteritems(): if func(name, var): variables.append({ 'key': name, 'value': var.value, 'description': var.get('description', None), }) return variables
class Instance(SchoolBaseModule): @sanitize( school=SchoolSanitizer(required=True), pattern=StringSanitizer(default=''), ) @LDAP_Connection() def users(self, request, ldap_user_read=None, ldap_position=None): # parse group parameter group = request.options.get('group') user_type = None if not group or group == 'None': group = None elif group.lower() in ('teacher', 'student'): user_type = group.lower() group = None result = [{ 'id': i.dn, 'label': Display.user(i) } for i in self._users(ldap_user_read, request.options['school'], group=group, user_type=user_type, pattern=request.options['pattern'])] self.finished(request.id, result) @sanitize(pattern=StringSanitizer(default=''), school=SchoolSanitizer(required=True)) @LDAP_Connection() def query(self, request, ldap_user_read=None, ldap_position=None): klasses = [get_group_class(request)] if klasses[0] is Teacher: klasses.append(TeachersAndStaff) groups = [] for klass in klasses: groups.extend( klass.get_all(ldap_user_read, request.options['school'], filter_str=request.options['pattern'], easy_filter=True)) self.finished(request.id, [group.to_dict() for group in groups]) @sanitize(StringSanitizer(required=True)) @LDAP_Connection() def get(self, request, ldap_user_read=None, ldap_position=None): klass = get_group_class(request) for group_dn in request.options: break try: group = klass.from_dn(group_dn, None, ldap_user_read) except udm_exceptions.noObject: raise UMC_Error('unknown object') school = group.school result = group.to_dict() if request.flavor == 'teacher': classes = SchoolClass.get_all(ldap_user_read, school, filter_str=filter_format( 'uniqueMember=%s', (group_dn, ))) result['classes'] = [{ 'id': class_.dn, 'label': class_.get_relative_name() } for class_ in classes] self.finished(request.id, [result]) return result['members'] = self._filter_members(request, group, result.pop('users', []), ldap_user_read) self.finished(request.id, [ result, ]) @staticmethod def _filter_members(request, group, users, ldap_user_read=None): members = [] for member_dn in users: try: user = User.from_dn(member_dn, None, ldap_user_read) except udm_exceptions.noObject: MODULE.process( 'Could not open (foreign) user %r: no permissions/does not exists/not a user' % (member_dn, )) continue if not user.schools or not set(user.schools) & {group.school}: continue if request.flavor == 'class' and not user.is_teacher( ldap_user_read): continue # only display teachers elif request.flavor == 'workgroup' and not user.is_student( ldap_user_read): continue # only display students elif request.flavor == 'workgroup-admin' and not user.is_student( ldap_user_read) and not user.is_administrator( ldap_user_read) and not user.is_staff( ldap_user_read) and not user.is_teacher( ldap_user_read): continue # only display school users members.append({ 'id': user.dn, 'label': Display.user(user.get_udm_object(ldap_user_read)) }) return members @sanitize(DictSanitizer(dict(object=DictSanitizer({}, required=True)))) @LDAP_Connection(USER_READ, MACHINE_WRITE) def put(self, request, ldap_machine_write=None, ldap_user_read=None, ldap_position=None): """Returns the objects for the given IDs requests.options = [ { object : ..., options : ... }, ... ] return: True|<error message> """ if request.flavor == 'teacher': request.options = request.options[0]['object'] return self.add_teacher_to_classes(request) klass = get_group_class(request) for group_from_umc in request.options: group_from_umc = group_from_umc['object'] group_from_umc_dn = group_from_umc['$dn$'] break try: group_from_ldap = klass.from_dn(group_from_umc_dn, None, ldap_machine_write) except udm_exceptions.noObject: raise UMC_Error('unknown group object') old_members = self._filter_members(request, group_from_ldap, group_from_ldap.users, ldap_user_read) removed_members = set(o['id'] for o in old_members) - set( group_from_umc['members']) MODULE.info('Modifying group "%s" with members: %s' % (group_from_ldap.dn, group_from_ldap.users)) MODULE.info('New members: %s' % group_from_umc['members']) MODULE.info('Removed members: %s' % (removed_members, )) if request.flavor == 'workgroup-admin': # do not allow groups to be renamed in order to avoid conflicts with shares # grp.name = '%(school)s-%(name)s' % group group_from_ldap.description = group_from_umc['description'] # Workgroup admin view → update teachers, admins, students, (staff) # Class view → update only the group's teachers (keep all non teachers) # Workgroup teacher view → update only the group's students users = [] # keep specific users from the group for userdn in group_from_ldap.users: try: user = User.from_dn(userdn, None, ldap_machine_write) except udm_exceptions.noObject: # no permissions/is not a user/does not exists → keep the old value users.append(userdn) continue if not user.schools or not set(user.schools) & set( [group_from_ldap.school]): users.append(userdn) continue if (request.flavor == 'class' and not user.is_teacher(ldap_machine_write)) or ( request.flavor == 'workgroup' and not user.is_student(ldap_machine_write) ) or request.flavor == 'workgroup-admin': users.append(userdn) # add only certain users to the group for userdn in group_from_umc['members']: try: user = User.from_dn(userdn, None, ldap_machine_write) except udm_exceptions.noObject as exc: MODULE.error('Not adding not existing user %r to group: %r.' % (userdn, exc)) continue if not user.schools or not set(user.schools) & set( [group_from_ldap.school]): raise UMC_Error( _('User %s does not belong to school %r.') % (Display.user(user.get_udm_object(ldap_machine_write)), group_from_ldap.school)) if request.flavor == 'workgroup-admin' and not user.is_student( ldap_machine_write) and not user.is_administrator( ldap_machine_write) and not user.is_staff( ldap_machine_write) and not user.is_teacher( ldap_machine_write): raise UMC_Error( _('User %s does not belong to school %r.') % (Display.user(user.get_udm_object(ldap_machine_write)), group_from_ldap.school)) if request.flavor == 'class' and not user.is_teacher( ldap_machine_write): raise UMC_Error( _('User %s is not a teacher.') % (Display.user(user.get_udm_object(ldap_machine_write)), )) if request.flavor == 'workgroup' and not user.is_student( ldap_machine_write): raise UMC_Error( _('User %s is not a student.') % (Display.user(user.get_udm_object(ldap_machine_write)), )) users.append(user.dn) group_from_ldap.users = list(set(users) - removed_members) try: success = group_from_ldap.modify(ldap_machine_write) MODULE.info('Modified, group has now members: %s' % (group_from_ldap.users, )) except udm_exceptions.base as exc: MODULE.process('An error occurred while modifying "%s": %s' % (group_from_umc['$dn$'], exc.message)) raise UMC_Error(_('Failed to modify group (%s).') % exc.message) self.finished(request.id, success) @sanitize(DictSanitizer(dict(object=DictSanitizer({}, required=True)))) @only_workgroup_admin @LDAP_Connection(USER_READ, USER_WRITE) def add(self, request, ldap_user_write=None, ldap_user_read=None, ldap_position=None): for group in request.options: group = group['object'] break try: grp = {} grp['school'] = group['school'] grp['name'] = '%(school)s-%(name)s' % group grp['description'] = group['description'] grp['users'] = group['members'] grp = WorkGroup(**grp) success = grp.create(ldap_user_write) if not success and grp.exists(ldap_user_read): raise UMC_Error( _('The workgroup %r already exists!') % grp.name) except udm_exceptions.base as exc: MODULE.process( 'An error occurred while creating the group "%s": %s' % (group['name'], exc.message)) raise UMC_Error(_('Failed to create group (%s).') % exc.message) self.finished(request.id, success) @sanitize(DictSanitizer(dict(object=ListSanitizer(min_elements=1)))) @only_workgroup_admin @LDAP_Connection(USER_READ, USER_WRITE) def remove(self, request, ldap_user_write=None, ldap_user_read=None, ldap_position=None): """Deletes a workgroup""" for group_dn in request.options: group_dn = group_dn['object'][0] break group = WorkGroup.from_dn(group_dn, None, ldap_user_write) if not group.school: raise UMC_Error('Group must within the scope of a school OU: %s' % group_dn) try: success = group.remove(ldap_user_write) except udm_exceptions.base as exc: MODULE.error('Could not remove group "%s": %s' % (group.dn, exc)) self.finished(request.id, [{ 'success': False, 'message': str(exc) }]) return self.finished(request.id, [{'success': success}]) @sanitize( **{ '$dn$': StringSanitizer(required=True), 'classes': ListSanitizer(StringSanitizer(required=True), required=True), 'school': SchoolSanitizer(required=True), }) @LDAP_Connection(USER_READ, MACHINE_WRITE) def add_teacher_to_classes(self, request, ldap_machine_write=None, ldap_user_read=None, ldap_position=None): teacher = request.options['$dn$'] classes = set(request.options['classes']) try: teacher = Teacher.from_dn(teacher, None, ldap_machine_write) if not teacher.is_teacher(ldap_machine_write): raise udm_exceptions.noObject() except udm_exceptions.noObject: raise UMC_Error('The user is not a teacher.') original_classes = set([ x.dn for x in SchoolClass.get_all( ldap_machine_write, request.options['school'], filter_format('uniqueMember=%s', (teacher.dn, ))) ]) classes_to_remove = original_classes - classes classes_to_add = classes - original_classes failed = [] for classdn in (classes_to_add | classes_to_remove): try: class_ = SchoolClass.from_dn(classdn, teacher.school, ldap_machine_write) except udm_exceptions.noObject: failed.append(classdn) continue if classdn in classes_to_add and teacher.dn not in class_.users: class_.users.append(teacher.dn) elif classdn in classes_to_remove and teacher.dn in class_.users: class_.users.remove(teacher.dn) try: if not class_.modify(ldap_machine_write): failed.append(classdn) except udm_exceptions.base as exc: MODULE.error('Could not add teacher %s to class %s: %s' % (teacher.dn, classdn, exc)) failed.append(classdn) self.finished(request.id, not any(failed))
class Instance(Base, ProgressMixin): def __init__(self): Base.__init__(self) self.reports_cfg = None self.modules_with_childs = [] self.__license_checks = set() install_opener(ucr) def init(self): if not self.user_dn: raise UserWithoutDN(self._username) MODULE.info('Initializing module as user %r' % (self.user_dn,)) set_bind_function(self.bind_user_connection) # read user settings and initial UDR self.reports_cfg = udr.Config() self.modules_with_childs = container_modules() def set_locale(self, _locale): super(Instance, self).set_locale(_locale) locale.setlocale(locale.LC_TIME, _locale) def error_handling(self, etype, exc, etraceback): super(Instance, self).error_handling(etype, exc, etraceback) if isinstance(exc, (udm_errors.authFail, INVALID_CREDENTIALS)): MODULE.warn('Authentication failed: %s' % (exc,)) raise LDAP_AuthenticationFailed() if isinstance(exc, (udm_errors.base, LDAPError)): MODULE.error(''.join(traceback.format_exception(etype, exc, etraceback))) def bind_user_connection(self, lo): super(Instance, self).bind_user_connection(lo) self.require_license(lo) def require_license(self, lo): if id(lo) in self.__license_checks: return self.__license_checks.add(id(lo)) try: import univention.admin.license # noqa: F401 except ImportError: return # GPL Version try: check_license(lo, True) except LicenseError: lo.allow_modify = False lo.requireLicense() def _get_module_by_request(self, request, object_type=None): """Tries to determine the UDM module to use. If no specific object type is given the request option 'objectType' is used. In case none if this leads to a valid object type the request flavor is chosen. Failing all this will raise in UMC_OptionMissing exception. On success a UMC_Module object is returned.""" if object_type is None: object_type = request.options.get('objectType') module_name = object_type if not module_name or 'all' == module_name: module_name = request.flavor if not module_name or module_name == 'navigation': raise UMC_OptionMissing(_('No flavor or valid UDM module name specified')) return UDM_Module(module_name) @LDAP_Connection def license(self, request, ldap_connection=None, ldap_position=None): message = None try: check_license(ldap_connection) except LicenseError as exc: message = str(exc) self.finished(request.id, {'message': message}) @LDAP_Connection def license_info(self, request, ldap_connection=None, ldap_position=None): license_data = {} try: import univention.admin.license as udm_license except: license_data['licenseVersion'] = 'gpl' else: license_data['licenseVersion'] = udm_license._license.version if udm_license._license.version == '1': for item in ('licenses', 'real'): license_data[item] = {} for lic_type in ('CLIENT', 'ACCOUNT', 'DESKTOP', 'GROUPWARE'): count = getattr(udm_license._license, item)[udm_license._license.version][getattr(udm_license.License, lic_type)] if isinstance(count, basestring): try: count = int(count) except: count = None license_data[item][lic_type.lower()] = count if 'UGS' in udm_license._license.types: udm_license._license.types = filter(lambda x: x != 'UGS', udm_license._license.types) elif udm_license._license.version == '2': for item in ('licenses', 'real'): license_data[item] = {} for lic_type in ('SERVERS', 'USERS', 'MANAGEDCLIENTS', 'CORPORATECLIENTS'): count = getattr(udm_license._license, item)[udm_license._license.version][getattr(udm_license.License, lic_type)] if isinstance(count, basestring): try: count = int(count) except: count = None license_data[item][lic_type.lower()] = count license_data['keyID'] = udm_license._license.licenseKeyID license_data['support'] = udm_license._license.licenseSupport license_data['premiumSupport'] = udm_license._license.licensePremiumSupport license_data['licenseTypes'] = udm_license._license.types license_data['oemProductTypes'] = udm_license._license.oemProductTypes license_data['endDate'] = udm_license._license.endDate license_data['baseDN'] = udm_license._license.licenseBase free_license = '' if license_data['baseDN'] == 'Free for personal use edition': free_license = 'ffpu' if license_data['baseDN'] == 'UCS Core Edition': free_license = 'core' if free_license: license_data['baseDN'] = ucr.get('ldap/base', '') license_data['freeLicense'] = free_license license_data['sysAccountsFound'] = udm_license._license.sysAccountsFound self.finished(request.id, license_data) @prevent_xsrf_check @LDAP_Connection def license_import(self, request, ldap_connection=None, ldap_position=None): filename = None if isinstance(request.options, (list, tuple)) and request.options: # file upload filename = request.options[0]['tmpfile'] if not os.path.realpath(filename).startswith(TEMPUPLOADDIR): self.finished(request.id, [{'success': False, 'message': 'invalid file path'}]) return else: self.required_options(request, 'license') lic = request.options['license'] # Replace non-breaking space with a normal space # https://forge.univention.org/bugzilla/show_bug.cgi?id=30098 lic = lic.replace(unichr(160), " ") lic_file = tempfile.NamedTemporaryFile(delete=False) lic_file.write(lic) lic_file.close() filename = lic_file.name def _error(msg=None): self.finished(request.id, [{ 'success': False, 'message': msg }]) try: with open(filename, 'rb') as fd: # check license and write it to LDAP importer = LicenseImport(fd) importer.check(ucr.get('ldap/base', '')) importer.write(ldap_connection) except (ValueError, AttributeError, LDAPError) as exc: MODULE.error('License import failed (malformed LDIF): %r' % (exc, )) # AttributeError: missing univentionLicenseBaseDN # ValueError raised by ldif.LDIFParser when e.g. dn is duplicated # LDAPError e.g. LDIF contained non existing attributes if isinstance(exc, LDAPError) and len(exc.args) and isinstance(exc.args[0], dict) and exc.args[0].get('info'): _error(_('LDAP error: %s.') % exc.args[0].get('info')) else: _error() return except LicenseError as exc: MODULE.error('LicenseImport check failed: %r' % (exc, )) _error(str(exc)) return finally: os.unlink(filename) self.finished(request.id, [{'success': True}]) @multi_response(progress=[_('Moving %d object(s)'), _('%($dn$)s moved')]) def move(self, iterator, object, options): for object, options in iterator: if 'container' not in options: yield {'$dn$': object, 'success': False, 'details': _('The destination is missing')} continue module = get_module(None, object) if not module: yield {'$dn$': object, 'success': False, 'details': _('Could not identify the given LDAP object')} elif 'move' not in module.operations: yield {'$dn$': object, 'success': False, 'details': _('This object can not be moved')} else: try: module.move(object, options['container']) yield {'$dn$': object, 'success': True} except UDM_Error as e: yield {'$dn$': object, 'success': False, 'details': str(e)} @sanitize(DictSanitizer(dict( object=DictSanitizer(dict(), required=True), options=DictSanitizer(dict( objectType=StringSanitizer(required=True) ), required=True) ), required=True)) def add(self, request): """Creates LDAP objects. requests.options = [ { 'options' : {}, 'object' : {} }, ... ] return: [ { '$dn$' : <LDAP DN>, 'success' : (True|False), 'details' : <message> }, ... ] """ def _thread(request): result = [] for obj in request.options: options = obj.get('options', {}) properties = obj.get('object', {}) module = self._get_module_by_request(request, object_type=options.get('objectType')) if '$labelObjectType$' in properties: del properties['$labelObjectType$'] try: dn = module.create(properties, container=options.get('container'), superordinate=options.get('superordinate')) result.append({'$dn$': dn, 'success': True}) except UDM_Error as e: result.append({'$dn$': e.dn, 'success': False, 'details': str(e)}) return result thread = notifier.threads.Simple('Get', notifier.Callback(_thread, request), notifier.Callback(self.thread_finished_callback, request)) thread.run() @sanitize(DictSanitizer(dict( object=DictSanitizer({ '$dn$': StringSanitizer(required=True) }, required=True), )), required=True) def put(self, request): """Modifies the given list of LDAP objects. requests.options = [ { 'options' : {}, 'object' : {} }, ... ] return: [ { '$dn$' : <LDAP DN>, 'success' : (True|False), 'details' : <message> }, ... ] """ def _thread(request): result = [] for obj in request.options: properties = obj.get('object') or {} ldap_dn = properties['$dn$'] module = get_module(request.flavor, ldap_dn) if module is None: if len(request.options) == 1: raise ObjectDoesNotExist(ldap_dn) result.append({'$dn$': ldap_dn, 'success': False, 'details': _('LDAP object does not exist.')}) continue MODULE.info('Modifying LDAP object %s' % (ldap_dn,)) if '$labelObjectType$' in properties: del properties['$labelObjectType$'] try: module.modify(properties) result.append({'$dn$': ldap_dn, 'success': True}) except UDM_Error as exc: result.append({'$dn$': ldap_dn, 'success': False, 'details': str(exc)}) return result thread = notifier.threads.Simple('Get', notifier.Callback(_thread, request), notifier.Callback(self.thread_finished_callback, request)) thread.run() def remove(self, request): """Removes the given list of LDAP objects. requests.options = [ { 'object' : <LDAP DN>, 'options' { 'cleanup' : (True|False), 'recursive' : (True|False) } }, ... ] return: [ { '$dn$' : <LDAP DN>, 'success' : (True|False), 'details' : <message> }, ... ] """ def _thread(request): result = [] for item in request.options: ldap_dn = item.get('object') options = item.get('options', {}) module = get_module(request.flavor, ldap_dn) if module is None: result.append({'$dn$': ldap_dn, 'success': False, 'details': _('LDAP object could not be identified')}) continue try: module.remove(ldap_dn, options.get('cleanup', False), options.get('recursive', False)) result.append({'$dn$': ldap_dn, 'success': True}) except UDM_Error as e: result.append({'$dn$': ldap_dn, 'success': False, 'details': str(e)}) return result thread = notifier.threads.Simple('Get', notifier.Callback(_thread, request), notifier.Callback(self.thread_finished_callback, request)) thread.run() @simple_response def meta_info(self, objectType): module = UDM_Module(objectType) if module: return { 'help_link': module.help_link, 'help_text': module.help_text, 'columns': module.columns } def get(self, request): """Retrieves the given list of LDAP objects. Password property will be removed. requests.options = [ <LDAP DN>, ... ] return: [ { '$dn$' : <LDAP DN>, <object properties> }, ... ] """ MODULE.info('Starting thread for udm/get request') thread = notifier.threads.Simple('Get', notifier.Callback(self._get, request), notifier.Callback(self.thread_finished_callback, request)) thread.run() def copy(self, request): thread = notifier.threads.Simple('Copy', notifier.Callback(self._get, request, True), notifier.Callback(self.thread_finished_callback, request)) thread.run() def _get(self, request, copy=False): def _remove_uncopyable_properties(obj): if not copy: return for name, p in obj.descriptions.items(): if not p.copyable: obj.info.pop(name, None) result = [] for ldap_dn in request.options: if request.flavor == 'users/self': ldap_dn = self._user_dn module = get_module(request.flavor, ldap_dn) if module is None: raise ObjectDoesNotExist(ldap_dn) else: obj = module.get(ldap_dn) if obj: _remove_uncopyable_properties(obj) obj.set_defaults = True obj.set_default_values() _remove_uncopyable_properties(obj) props = obj.info empty_props_with_default_set = {} for key in obj.info.keys(): if obj.hasChanged(key): empty_props_with_default_set[key] = { 'default_value': obj.info[key], 'prevent_umc_default_popup': obj.descriptions[key].prevent_umc_default_popup } props['$empty_props_with_default_set$'] = empty_props_with_default_set for passwd in module.password_properties: if passwd in props: del props[passwd] if not copy: props['$dn$'] = obj.dn props['$options$'] = {} for opt in module.get_options(udm_object=obj): props['$options$'][opt['id']] = opt['value'] props['$policies$'] = {} for policy in obj.policies: pol_mod = get_module(None, policy) if pol_mod and pol_mod.name: props['$policies$'].setdefault(pol_mod.name, []).append(policy) props['$labelObjectType$'] = module.title props['$flags$'] = obj.oldattr.get('univentionObjectFlag', []) props['$operations$'] = module.operations props['$references$'] = module.get_references(ldap_dn) result.append(props) else: MODULE.process('The LDAP object for the LDAP DN %s could not be found' % ldap_dn) return result @sanitize( objectPropertyValue=PropertySearchSanitizer( add_asterisks=ADD_ASTERISKS, use_asterisks=USE_ASTERISKS, further_arguments=['objectType', 'objectProperty'], ), objectProperty=ObjectPropertySanitizer(required=True), fields=ListSanitizer(), ) def query(self, request): """Searches for LDAP objects and returns a few properties of the found objects requests.options = {} 'objectType' -- the object type to search for (default: if not given the flavor is used) 'objectProperty' -- the object property that should be scaned 'objectPropertyValue' -- the filter that should be found in the property 'fields' -- the properties which should be returned 'container' -- the base container where the search should be started (default: LDAP base) 'superordinate' -- the superordinate object for the search (default: None) 'scope' -- the search scope (default: sub) return: [ { '$dn$' : <LDAP DN>, 'objectType' : <UDM module name>, 'path' : <location of object> }, ... ] """ def _thread(request): ucr.load() module = self._get_module_by_request(request) superordinate = request.options.get('superordinate') if superordinate == 'None': superordinate = None elif superordinate is not None: MODULE.info('Query defines a superordinate %s' % superordinate) mod = get_module(request.flavor, superordinate) if mod is not None: MODULE.info('Found UDM module %r for superordinate %s' % (mod.name, superordinate)) superordinate = mod.get(superordinate) if not request.options.get('container'): request.options['container'] = superordinate.dn else: raise SuperordinateDoesNotExist(superordinate) container = request.options.get('container') objectProperty = request.options['objectProperty'] objectPropertyValue = request.options['objectPropertyValue'] scope = request.options.get('scope', 'sub') hidden = request.options.get('hidden') fields = (set(request.options.get('fields', []) or []) | set([objectProperty])) - set(['name', 'None']) result = module.search(container, objectProperty, objectPropertyValue, superordinate, scope=scope, hidden=hidden) if result is None: return [] entries = [] object_type = request.options.get('objectType', request.flavor) for obj in result: if obj is None: continue module = get_module(object_type, obj.dn) if module is None: # This happens when concurrent a object is removed between the module.search() and get_module() call MODULE.warn('LDAP object does not exists %s (flavor: %s). The object is ignored.' % (obj.dn, request.flavor)) continue entry = { '$dn$': obj.dn, '$childs$': module.childs, '$flags$': obj.oldattr.get('univentionObjectFlag', []), '$operations$': module.operations, 'objectType': module.name, 'labelObjectType': module.subtitle, 'name': module.obj_description(obj), 'path': ldap_dn2path(obj.dn, include_rdn=False) } if '$value$' in fields: entry['$value$'] = [module.property_description(obj, column['name']) for column in module.columns] for field in fields - set(module.password_properties) - set(entry.keys()): entry[field] = module.property_description(obj, field) entries.append(entry) return entries thread = notifier.threads.Simple('Query', notifier.Callback(_thread, request), notifier.Callback(self.thread_finished_callback, request)) thread.run() def reports_query(self, request): """Returns a list of reports for the given object type""" # i18n: translattion for univention-directory-reports _('PDF Document') self.finished(request.id, [{'id': name, 'label': _(name)} for name in sorted(self.reports_cfg.get_report_names(request.flavor))]) def sanitize_reports_create(self, request): choices = self.reports_cfg.get_report_names(request.flavor) return dict( report=ChoicesSanitizer(choices=choices, required=True), objects=ListSanitizer(DNSanitizer(minimum=1), required=True, min_elements=1) ) @sanitize_func(sanitize_reports_create) def reports_create(self, request): """Creates a report for the given LDAP DNs and returns the URL to access the file""" @LDAP_Connection def _thread(request, ldap_connection=None, ldap_position=None): report = udr.Report(ldap_connection) try: report_file = report.create(request.flavor, request.options['report'], request.options['objects']) except udr.ReportError as exc: raise UMC_Error(str(exc)) path = '/usr/share/univention-management-console-module-udm/' filename = os.path.join(path, os.path.basename(report_file)) shutil.move(report_file, path) os.chmod(filename, 0o600) url = '/univention/command/udm/reports/get?report=%s' % (urllib.quote(os.path.basename(report_file)),) return {'URL': url} thread = notifier.threads.Simple('ReportsCreate', notifier.Callback(_thread, request), notifier.Callback(self.thread_finished_callback, request)) thread.run() @allow_get_request @sanitize(report=StringSanitizer(required=True)) def reports_get(self, request): report = request.options['report'] path = '/usr/share/univention-management-console-module-udm/' filename = os.path.join(path, os.path.basename(report)) try: with open(filename) as fd: self.finished(request.id, fd.read(), mimetype='text/csv' if report.endswith('.csv') else 'application/pdf') except EnvironmentError: raise UMC_Error(_('The report does not exists. Please create a new one.'), status=404) def values(self, request): """Returns the default search pattern/value for the given object property requests.options = {} 'objectProperty' -- the object property that should be scaned return: <value> """ module = self._get_module_by_request(request) property_name = request.options.get('objectProperty') if property_name == 'None': result = None else: result = module.get_default_values(property_name) self.finished(request.id, result) @sanitize( networkDN=StringSanitizer(required=True), increaseCounter=BooleanSanitizer(default=False) ) def network(self, request): """Returns the next IP configuration based on the given network object requests.options = {} 'networkDN' -- the LDAP DN of the network object 'increaseCounter' -- if given and set to True, network object counter for IP addresses is increased return: {} """ module = UDM_Module('networks/network') obj = module.get(request.options['networkDN']) if not obj: raise ObjectDoesNotExist(request.options['networkDN']) try: obj.refreshNextIp() except udm_errors.nextFreeIp: raise NoIpLeft(request.options['networkDN']) result = {'ip': obj['nextIp'], 'dnsEntryZoneForward': obj['dnsEntryZoneForward'], 'dhcpEntryZone': obj['dhcpEntryZone'], 'dnsEntryZoneReverse': obj['dnsEntryZoneReverse']} self.finished(request.id, result) if request.options['increaseCounter']: # increase the next free IP address obj.stepIp() obj.modify() @module_from_request @simple_response() def containers(self, module): """Returns the list of default containers for the given object type. Therefor the python module and the default object in the LDAP directory are searched. requests.options = {} 'objectType' -- The UDM module name return: [ { 'id' : <LDAP DN of container>, 'label' : <name> }, ... ] """ containers = [{'id': x, 'label': ldap_dn2path(x)} for x in module.get_default_containers()] containers.sort(cmp=lambda x, y: cmp(x['label'].lower(), y['label'].lower())) return containers @module_from_request @simple_response def templates(self, module): """Returns the list of template objects for the given object type. requests.options = {} 'objectType' -- The UDM module name return: [ { 'id' : <LDAP DN of container or None>, 'label' : <name> }, ... ] """ result = [] if module.template: template = UDM_Module(module.template) objects = template.search(ucr.get('ldap/base')) for obj in objects: obj.open() result.append({'id': obj.dn, 'label': obj[template.identifies]}) return result @LDAP_Connection def types(self, request, ldap_connection=None, ldap_position=None): """Returns the list of object types matching the given flavor or container. requests.options = {} 'superordinate' -- if available only types for the given superordinate are returned (not for the navigation) 'container' -- if available only types suitable for the given container are returned (only for the navigation) return: [ { 'id' : <LDAP DN of container or None>, 'label' : <name> }, ... ] """ superordinate = request.options.get('superordinate') if request.flavor != 'navigation': module = UDM_Module(request.flavor) if superordinate: module = get_module(request.flavor, superordinate) or module self.finished(request.id, module.child_modules) return container = request.options.get('container') or superordinate if not container: # no container is specified, return all existing object types MODULE.info('no container specified, returning all object types') self.finished(request.id, map(lambda module: {'id': module[0], 'label': getattr(module[1], 'short_description', module[0])}, udm_modules.modules.items())) return if 'None' == container: # if 'None' is given, use the LDAP base container = ucr.get('ldap/base') MODULE.info('no container == \'None\', set LDAP base as container') # create a list of modules that can be created # ... all container types except container/dc allowed_modules = set([m for m in udm_modules.containers if udm_modules.name(m) != 'container/dc']) # the container may be a superordinate or have one as its parent # (or grandparent, ....) superordinate = udm_modules.find_superordinate(container, None, ldap_connection) if superordinate: # there is a superordinate... add its subtypes to the list of allowed modules MODULE.info('container has a superordinate: %s' % superordinate) allowed_modules.update(udm_modules.subordinates(superordinate)) else: # add all types that do not have a superordinate MODULE.info('container has no superordinate') allowed_modules.update(mod for mod in udm_modules.modules.values() if not udm_modules.superordinates(mod)) # make sure that the object type can be created allowed_modules = filter(lambda mod: udm_modules.supports(mod, 'add'), allowed_modules) MODULE.info('all modules that are allowed: %s' % [udm_modules.name(mod) for mod in allowed_modules]) # return the final list of object types self.finished(request.id, map(lambda module: {'id': udm_modules.name(module), 'label': getattr(module, 'short_description', udm_modules.name(module))}, allowed_modules)) @bundled @sanitize(objectType=StringSanitizer()) # objectDN=StringSanitizer(allow_none=True), def layout(self, request): """Returns the layout information for the given object type. requests.options = {} 'objectType' -- The UDM module name. If not available the flavor is used return: <layout data structure (see UDM python modules)> """ module = self._get_module_by_request(request) module.load(force_reload=True) # reload for instant extended attributes if request.flavor == 'users/self': object_dn = None else: object_dn = request.options.get('objectDN') return module.get_layout(object_dn) @bundled @sanitize( objectType=StringSanitizer(), objectDn=StringSanitizer(), searchable=BooleanSanitizer(default=False) ) def properties(self, request): """Returns the properties of the given object type. requests.options = {} 'searchable' -- If given only properties that might be used for search filters are returned return: [ {}, ... ] """ module = self._get_module_by_request(request) module.load(force_reload=True) # reload for instant extended attributes object_dn = request.options.get('objectDN') properties = module.get_properties(object_dn) if request.options.get('searchable', False): properties = filter(lambda prop: prop.get('searchable', False), properties) return properties @module_from_request @simple_response def options(self, module): """Returns the options specified for the given object type requests.options = {} 'objectType' -- The UDM module name. If not available the flavor is used return: [ {}, ... ] """ return module.options @bundled @sanitize( objectType=StringSanitizer() ) def policies(self, request): """Returns a list of policy types that apply to the given object type""" module = self._get_module_by_request(request) return module.policies def validate(self, request): """Validates the correctness of values for properties of the given object type. Therefor the syntax definition of the properties is used. requests.options = {} 'objectType' -- The UDM module name. If not available the flavor is used return: [ { 'property' : <name>, 'valid' : (True|False), 'details' : <message> }, ... ] """ def _thread(request): module = self._get_module_by_request(request) result = [] for property_name, value in request.options.get('properties').items(): # ignore special properties named like $.*$, e.g. $options$ if property_name.startswith('$') and property_name.endswith('$'): continue property_obj = module.get_property(property_name) if property_obj is None: raise UMC_OptionMissing(_('Property %s not found') % property_name) # check each element if 'value' is a list if isinstance(value, (tuple, list)) and property_obj.multivalue: subResults = [] subDetails = [] for ival in value: try: property_obj.syntax.parse(ival) subResults.append(True) subDetails.append('') except (udm_errors.valueInvalidSyntax, udm_errors.valueError, TypeError) as e: subResults.append(False) subDetails.append(str(e)) result.append({'property': property_name, 'valid': subResults, 'details': subDetails}) # otherwise we have a single value else: try: property_obj.syntax.parse(value) result.append({'property': property_name, 'valid': True}) except (udm_errors.valueInvalidSyntax, udm_errors.valueError) as e: result.append({'property': property_name, 'valid': False, 'details': str(e)}) return result thread = notifier.threads.Simple('Validate', notifier.Callback(_thread, request), notifier.Callback(self.thread_finished_callback, request)) thread.run() @sanitize( syntax=StringSanitizer(required=True), key=LDAPSearchSanitizer(use_asterisks=False), ) @simple_response def syntax_choices_key(self, syntax, key): syntax = _get_syntax(syntax) if syntax is None: return return search_syntax_choices_by_key(syntax, key) @sanitize(syntax=StringSanitizer(required=True)) @simple_response def syntax_choices_info(self, syntax): syntax = _get_syntax(syntax) if syntax is None: return return info_syntax_choices(syntax) @sanitize( objectPropertyValue=LDAPSearchSanitizer(), objectProperty=ObjectPropertySanitizer(), syntax=StringSanitizer(required=True) ) def syntax_choices(self, request): """Dynamically determine valid values for a given syntax class requests.options = {} 'syntax' -- The UDM syntax class return: [ { 'id' : <name>, 'label' : <text> }, ... ] """ def _thread(request): syntax = _get_syntax(request.options['syntax']) if syntax is None: return return read_syntax_choices(syntax, request.options) thread = notifier.threads.Simple('SyntaxChoice', notifier.Callback(_thread, request), notifier.Callback(self.thread_finished_callback, request)) thread.run() @sanitize( container=StringSanitizer(default='', allow_none=True) ) def move_container_query(self, request): scope = 'one' modules = self.modules_with_childs container = request.options.get('container') if not container: scope = 'base' thread = notifier.threads.Simple('MoveContainerQuery', notifier.Callback(self._container_query, request, container, modules, scope), notifier.Callback(self.thread_finished_callback, request)) thread.run() @sanitize( container=StringSanitizer(allow_none=True) ) def nav_container_query(self, request): """Returns a list of LDAP containers located under the given LDAP base (option 'container'). If no base container is specified the LDAP base object is returned.""" ldap_base = ucr['ldap/base'] container = request.options.get('container') modules = self.modules_with_childs scope = 'one' if not container: # get the tree root == the ldap base scope = 'base' elif request.flavor != 'navigation' and container and ldap_base.lower() == container.lower(): # this is the tree root of DNS / DHCP, show all zones / services scope = 'sub' modules = [request.flavor] thread = notifier.threads.Simple('NavContainerQuery', notifier.Callback(self._container_query, request, container, modules, scope), notifier.Callback(self.thread_finished_callback, request)) thread.run() def _container_query(self, request, container, modules, scope): """Get a list of containers or child objects of the specified container.""" if not container: container = ucr['ldap/base'] defaults = {} if request.flavor != 'navigation': defaults['$operations$'] = ['search', ], # disallow edit if request.flavor in ('dns/dns', 'dhcp/dhcp'): defaults.update({ 'label': UDM_Module(request.flavor).title, 'icon': 'udm-%s' % (request.flavor.replace('/', '-'),), }) return [dict({ 'id': container, 'label': ldap_dn2path(container), 'icon': 'udm-container-dc', 'path': ldap_dn2path(container), 'objectType': 'container/dc', '$operations$': UDM_Module('container/dc').operations, '$flags$': [], '$childs$': True, '$isSuperordinate$': False, }, **defaults)] result = [] for xmodule in modules: xmodule = UDM_Module(xmodule) superordinate = None if xmodule.superordinate_names: for module_superordinate in xmodule.superordinate_names: try: superordinate = UDM_Module(module_superordinate).get(container) except UDM_Error: # the container is not a direct superordinate # FIXME: get the "real" superordinate; Bug #40885 continue if superordinate is None: continue # superordinate object could not be load -> ignore module try: for item in xmodule.search(container, scope=scope, superordinate=superordinate): module = UDM_Module(item.module) result.append({ 'id': item.dn, 'label': item[module.identifies], 'icon': 'udm-%s' % (module.name.replace('/', '-')), 'path': ldap_dn2path(item.dn), 'objectType': module.name, '$operations$': module.operations, '$flags$': item.oldattr.get('univentionObjectFlag', []), '$childs$': module.childs, '$isSuperordinate$': udm_modules.isSuperordinate(module.module), }) except UDM_Error as exc: raise UMC_Error(str(exc)) return result @sanitize( container=StringSanitizer(required=True) ) @LDAP_Connection def nav_object_query(self, request, ldap_connection=None, ldap_position=None): """Returns a list of objects in a LDAP container (scope: one) requests.options = {} 'container' -- the base container where the search should be started (default: LDAP base) 'objectType' -- the object type that should be displayed (optional) 'objectProperty' -- the object property that should be scaned (optional) 'objectPropertyValue' -- the filter that should b found in the property (optional) return: [ { '$dn$' : <LDAP DN>, 'objectType' : <UDM module name>, 'path' : <location of object> }, ... ] """ object_type = request.options.get('objectType', '') if object_type not in ('None', '$containers$'): # we need to search for a specific objectType, then we should call the standard query # we also need to get the correct superordinate superordinate = udm_objects.get_superordinate(object_type, None, ldap_connection, request.options['container']) if superordinate and superordinate.module == 'settings/cn': # false positive detected superordinate; Bug #32843 superordinate = None if superordinate: superordinate = superordinate.dn request.options['superordinate'] = superordinate request.options['scope'] = 'one' self.query(request) return def _thread(container): entries = [] for module, obj in list_objects(container, object_type=object_type): if obj is None: continue if object_type != '$containers$' and module.childs: continue if object_type == '$containers$' and not module.childs: continue entries.append({ '$dn$': obj.dn, '$childs$': module.childs, 'objectType': module.name, 'labelObjectType': module.subtitle, 'name': udm_objects.description(obj), 'path': ldap_dn2path(obj.dn, include_rdn=False), '$flags$': obj.oldattr.get('univentionObjectFlag', []), '$operations$': module.operations, }) return entries thread = notifier.threads.Simple('NavObjectQuery', notifier.Callback(_thread, request.options['container']), notifier.Callback(self.thread_finished_callback, request)) thread.run() @sanitize(DictSanitizer(dict( objectType=StringSanitizer(required=True), policies=ListSanitizer(), policyType=StringSanitizer(required=True), objectDN=Sanitizer(default=None), container=Sanitizer(default=None) # objectDN=StringSanitizer(default=None, allow_none=True), # container=StringSanitizer(default=None, allow_none=True) ))) def object_policies(self, request): """Returns a virtual policy object containing the values that the given object or container inherits""" def _thread(request): object_dn = None container_dn = None obj = None def _get_object(_dn, _module): '''Get existing UDM object and corresponding module. Verify user input.''' if _module is None or _module.module is None: raise UMC_OptionTypeError('The given object type is not valid') _obj = _module.get(_dn) if _obj is None or (_dn and not _obj.exists()): raise ObjectDoesNotExist(_dn) return _obj def _get_object_parts(_options): '''Get object related information and corresponding UDM object/module. Verify user input.''' _object_type = _options['objectType'] _object_dn = _options['objectDN'] _container_dn = _options['container'] if (object_dn, container_dn) == (_object_dn, _container_dn): # nothing has changed w.r.t. last entry -> return last values return (object_dn, container_dn, obj) _obj = None _module = None if _object_dn: # editing an exiting UDM object -> use the object itself _module = UDM_Module(_object_type) _obj = _get_object(_object_dn, _module) elif _container_dn: # editing a new (i.e. non existing) object -> use the parent container _module = get_module(None, _container_dn) _obj = _get_object(_container_dn, _module) return (_object_dn, _container_dn, _obj) ret = [] for ioptions in request.options: object_dn, container_dn, obj = _get_object_parts(ioptions) policy_dns = ioptions.get('policies', []) policy_module = UDM_Module(ioptions['policyType']) policy_obj = _get_object(policy_dns[0] if policy_dns else None, policy_module) if obj is None: ret.append({}) continue policy_obj.clone(obj) # There are 2x2x2 (=8) cases that may occur (c.f., Bug #31916): # (1) # [edit] editing existing UDM object # -> the existing UDM object itself is loaded # [new] virtually edit non-existing UDM object (when a new object is being created) # -> the parent container UDM object is loaded # (2) # [w/pol] UDM object has assigend policies in LDAP directory # [w/o_pol] UDM object has no policies assigend in LDAP directory # (3) # [inherit] user request to (virtually) change the policy to 'inherited' # [set_pol] user request to (virtually) assign a particular policy faked_policy_reference = None if object_dn and not policy_dns: # case: [edit; w/pol; inherit] # -> current policy is (virtually) overwritten with 'None' faked_policy_reference = [None] elif not object_dn and policy_dns: # cases: # * [new; w/pol; inherit] # * [new; w/pol; set_pol] # -> old + temporary policy are both (virtually) set at the parent container faked_policy_reference = obj.policies + policy_dns else: # cases: # * [new; w/o_pol; inherit] # * [new; w/o_pol; set_pol] # * [edit; w/pol; set_pol] # * [edit; w/o_pol; inherit] # * [edit; w/o_pol; set_pol] faked_policy_reference = policy_dns policy_obj.policy_result(faked_policy_reference) infos = copy.copy(policy_obj.polinfo_more) for key, value in infos.items(): if key in policy_obj.polinfo: if isinstance(infos[key], (tuple, list)): continue infos[key]['value'] = policy_obj.polinfo[key] ret.append(infos) return ret thread = notifier.threads.Simple('ObjectPolicies', notifier.Callback(_thread, request), notifier.Callback(self.thread_finished_callback, request)) thread.run() def object_options(self, request): """Returns the options known by the given objectType. If an LDAP DN is passed the current values for the options of this object are returned, otherwise the default values for the options are returned.""" object_type = request.options.get('objectType') if not object_type: raise UMC_OptionMissing('The object type is missing') object_dn = request.options.get('objectDN') def _thread(object_type, object_dn): module = UDM_Module(object_type) if module.module is None: raise UMC_OptionTypeError('The given object type is not valid') return module.get_option(object_dn) thread = notifier.threads.Simple('ObjectOptions', notifier.Callback(_thread, object_type, object_dn), notifier.Callback(self.thread_finished_callback, request)) thread.run() @sanitize(email=EmailSanitizer(required=True)) @simple_response def request_new_license(self, email): license = dump_license() if license is None: raise UMC_CommandError(_('Cannot parse License from LDAP')) data = {} data['email'] = email data['licence'] = license data = urllib.urlencode(data) url = 'https://license.univention.de/keyid/conversion/submit' request = urllib2.Request(url, data=data, headers={'User-agent': 'UMC/AppCenter'}) self._request_license(request) # creating a new ucr variable to prevent duplicated registration (Bug #35711) handler_set(['ucs/web/license/requested=true']) return True def _request_license(self, request): try: urlopen(request) except (urllib2.HTTPError, urllib2.URLError, IOError) as exc: strerror = '' if hasattr(exc, 'read'): # try to parse an html error body = exc.read() match = re.search('<span id="details">(?P<details>.*?)</span>', body, flags=re.DOTALL) if match: strerror = match.group(1).replace('\n', '') if not strerror: if hasattr(exc, 'getcode') and exc.getcode() >= 400: strerror = _('This seems to be a problem with the license server. Please try again later.') while hasattr(exc, 'reason'): exc = exc.reason if hasattr(exc, 'errno'): version = ucr.get('version/version') errno = exc.errno strerror += getattr(exc, 'strerror', '') or '' if errno == 1: # gaierror(1, something like 'SSL Unknown protocol') link_to_doc = _('http://docs.univention.de/manual-%s.html#ip-config:Web_proxy_for_caching_and_policy_management__virus_scan') % version strerror += '. ' + _('This may be a problem with the proxy of your system. You may find help at %s.') % link_to_doc if errno == -2: # gaierror(-2, 'Name or service not known') link_to_doc = _('http://docs.univention.de/manual-%s.html#networks:dns') % version strerror += '. ' + _('This is probably due to the DNS settings of your server. You may find help at %s.') % link_to_doc if not strerror.strip(): strerror = str(exc) raise UMC_Error(_('An error occurred while contacting the license server: %s') % (strerror,), status=500)
class Instance(SchoolBaseModule): def init(self): super(Instance, self).init() add_module_logger_to_schoollib() @sanitize(school=SchoolSanitizer(required=True), pattern=StringSanitizer(default='')) @LDAP_Connection() def computers(self, request, ldap_user_read=None): pattern = LDAP_Filter.forComputers(request.options.get('pattern', '')) result = [{ 'label': x.name, 'id': x.dn } for x in SchoolComputer.get_all(ldap_user_read, request.options['school'], pattern)] result = sorted(result, cmp=lambda x, y: cmp(x.lower(), y.lower()), key=lambda x: x['label']) # TODO: still necessary? self.finished(request.id, result) @sanitize(school=SchoolSanitizer(required=True), pattern=StringSanitizer(default='')) @LDAP_Connection() def query(self, request, ldap_user_read=None): school = request.options['school'] pattern = LDAP_Filter.forGroups(request.options.get('pattern', ''), school) result = [{ 'name': x.get_relative_name(), 'description': x.description or '', '$dn$': x.dn, } for x in ComputerRoom.get_all(ldap_user_read, school, pattern)] result = sorted(result, cmp=lambda x, y: cmp(x.lower(), y.lower()), key=lambda x: x['name']) # TODO: still necessary? self.finished(request.id, result) @sanitize(DNSanitizer(required=True)) @LDAP_Connection() def get(self, request, ldap_user_read=None): # open the specified room room = ComputerRoom.from_dn(request.options[0], None, ldap_user_read) result = room.to_dict() result['computers'] = result.get('hosts') self.finished(request.id, [result]) @sanitize(DictSanitizer(dict(object=DictSanitizer({}, required=True)))) @LDAP_Connection(USER_READ, USER_WRITE) def add(self, request, ldap_user_write=None, ldap_user_read=None): """Adds a new room""" group_props = request.options[0].get('object', {}) group_props['hosts'] = group_props.get('computers') room = ComputerRoom(**group_props) if room.get_relative_name() == room.name: room.name = '%(school)s-%(name)s' % group_props room.set_dn(room.dn) success = room.create(ldap_user_write) self.finished(request.id, [success]) @sanitize(DictSanitizer(dict(object=DictSanitizer({}, required=True)))) @LDAP_Connection(USER_READ, USER_WRITE) def put(self, request, ldap_user_write=None, ldap_user_read=None): """Modify an existing room""" group_props = request.options[0].get('object', {}) group_props['hosts'] = group_props.get('computers') room = ComputerRoom(**group_props) if room.get_relative_name() == room.name: room.name = '%(school)s-%(name)s' % group_props room.set_dn(group_props['$dn$']) room.modify(ldap_user_write) self.finished(request.id, [True]) @sanitize( DictSanitizer( dict(object=ListSanitizer(DNSanitizer(required=True), min_elements=1)))) @LDAP_Connection(USER_READ, USER_WRITE) def remove(self, request, ldap_user_write=None, ldap_user_read=None): """Deletes a room""" try: room_dn = request.options[0]['object'][0] room = ComputerRoom.from_dn(room_dn, None, ldap_user_write) room.remove(ldap_user_write) except udm_exceptions.base as e: self.finished(request.id, [{'success': False, 'message': str(e)}]) return self.finished(request.id, [{'success': True}])
def _decorator(func): return sanitize(DictSanitizer(dict(object=DictSanitizer(kwargs))))(func)
class Instance(SchoolBaseModule): def __init__(self): SchoolBaseModule.__init__(self) self._tmpDir = None self._progress_state = util.Progress() self._lessons = SchoolLessons() def init(self): SchoolBaseModule.init(self) # initiate paths for data distribution util.distribution.initPaths() def destroy(self): # clean temporary data self._cleanTmpDir() def _cleanTmpDir(self): # copied from distribution module # clean up the temporary upload directory if self._tmpDir: MODULE.info('Clean up temporary directory: %s' % self._tmpDir) shutil.rmtree(self._tmpDir, ignore_errors=True) self._tmpDir = None @file_upload @sanitize( DictSanitizer(dict( filename=StringSanitizer(required=True), tmpfile=StringSanitizer(required=True), ), required=True)) def upload(self, request): # copied from distribution module # create a temporary upload directory, if it does not already exist if not self._tmpDir: self._tmpDir = tempfile.mkdtemp(prefix='ucsschool-exam-upload-') MODULE.info('Created temporary directory: %s' % self._tmpDir) for file in request.options: filename = self.__workaround_filename_bug(file) destPath = os.path.join(self._tmpDir, filename) MODULE.info('Received file %r, saving it to %r' % (file['tmpfile'], destPath)) shutil.move(file['tmpfile'], destPath) self.finished(request.id, None) def __workaround_filename_bug(self, file): # the following code block is a heuristic to support both: fixed and unfixed Bug #37716 filename = file['filename'] try: # The UMC-Webserver decodes filename in latin-1, need to revert filename = filename.encode('ISO8859-1') except UnicodeEncodeError: # we got non-latin characters, Bug #37716 is fixed and string contains e.g. '→' filename = file['filename'].encode('UTF-8') else: # the string contains at least no non-latin1 characters try: # try if the bytes could be UTF-8 # can't fail if Bug #37716 is fixed filename.decode('UTF-8') except UnicodeDecodeError: filename = file['filename'].encode( 'UTF-8') # Bug #37716 was fixed MODULE.info('Detected filename %r as %r' % (file['filename'], filename)) # the code block can be removed and replaced by filename = file['filename'].encode('UTF-8') after Bug #37716 return filename @simple_response def internetrules(self): # copied from computerroom module """Returns a list of available internet rules""" return [x.name for x in internetrules.list()] @simple_response def lesson_end(self): current = self._lessons.current if current is not None: return current.end.strftime('%H:%M') return (datetime.datetime.now() + datetime.timedelta(minutes=45)).strftime('%H:%M') @simple_response def progress(self): return self._progress_state.poll() @sanitize( name=StringSanitizer(required=True), room=StringSanitizer(required=True), school=SchoolSanitizer(required=True), directory=StringSanitizer(required=True), shareMode=StringSanitizer(required=True), internetRule=StringSanitizer(required=True), customRule=StringSanitizer(), examEndTime=StringSanitizer(required=True), recipients=ListSanitizer(StringSanitizer(minimum=1), required=True), files=ListSanitizer(StringSanitizer()), ) @require_password @LDAP_Connection() def start_exam(self, request, ldap_user_read=None, ldap_position=None): # reset the current progress state # steps: # 5 -> for preparing exam room # 25 -> for cloning users # 25 -> for each replicated users + copy of the profile directory # 20 -> distribution of exam files # 10 -> setting room properties progress = self._progress_state progress.reset(85) progress.component(_('Initializing')) # create that holds a reference to project, otherwise _thread() cannot # set the project variable in the scope of start_exam: my = type("", (), dict(project=None))() # create a User object for the teacher # perform this LDAP operation outside the thread, to avoid tracebacks # in case of an LDAP timeout sender = util.distribution.openRecipients(self.user_dn, ldap_user_read) if not sender: raise UMC_Error( _('Could not authenticate user "%s"!') % self.user_dn) def _thread(): # make sure that a project with the same name does not exist directory = request.options['directory'] # get absolute path of project file and test for existance fn_test_project = util.distribution.Project.sanitize_project_filename( directory) if os.path.exists(fn_test_project): raise UMC_Error( _('An exam with the name "%s" already exists. Please choose a different name for the exam.' ) % (directory, )) # validate the project data and save project my.project = util.distribution.Project( dict( name=directory, description=request.options['name'], files=request.options.get('files'), sender=sender, )) my.project.validate() my.project.save() # copy files into project directory if self._tmpDir: for ifile in my.project.files: isrc = os.path.join(self._tmpDir, ifile) itarget = os.path.join(my.project.cachedir, ifile) if os.path.exists(isrc): # copy file to cachedir shutil.move(isrc, itarget) os.chown(itarget, 0, 0) # open a new connection to the master UMC try: master = ucr['ldap/master'] client = Client(master) client.authenticate_with_machine_account() except (ConnectionError, HTTPError) as exc: MODULE.error('Could not connect to UMC on %s: %s' % (master, exc)) raise UMC_Error( _('Could not connect to master server %s.') % ucr.get('ldap/master')) # mark the computer room for exam mode progress.component( _('Preparing the computer room for exam mode...')) client.umc_command( 'schoolexam-master/set-computerroom-exammode', dict( school=request.options['school'], roomdn=request.options['room'], )).result # FIXME: no error handling progress.add_steps(5) # read all recipients and fetch all user objects users = [] for idn in request.options['recipients']: ientry = util.distribution.openRecipients(idn, ldap_user_read) if not ientry: continue # recipients can in theory be users or groups members = [] if isinstance(ientry, util.distribution.User): members = [ientry] elif isinstance(ientry, util.distribution.Group): members = ientry.members for entry in members: # ignore exam users user = User.from_dn(entry.dn, None, ldap_user_read) if not user.is_exam_student(ldap_user_read): users.append(entry) # start to create exam user accounts progress.component(_('Preparing exam accounts')) percentPerUser = 25.0 / (1 + len(users)) examUsers = set() student_dns = set() usersReplicated = set() for iuser in users: progress.info( '%s, %s (%s)' % (iuser.lastname, iuser.firstname, iuser.username)) try: ires = client.umc_command( 'schoolexam-master/create-exam-user', dict( school=request.options['school'], userdn=iuser.dn, )).result examuser_dn = ires.get('examuserdn') examUsers.add(examuser_dn) student_dns.add(iuser.dn) MODULE.info('Exam user has been created: %r' % examuser_dn) except (ConnectionError, HTTPError) as exc: MODULE.warn( 'Could not create exam user account for %r: %s' % (iuser.dn, exc)) # indicate the the user has been processed progress.add_steps(percentPerUser) client.umc_command( 'schoolexam-master/add-exam-users-to-groups', dict( users=list(student_dns), school=request.options['school'], )) progress.add_steps(percentPerUser) # wait for the replication of all users to be finished progress.component(_('Preparing user home directories')) recipients = [] # list of User objects for all exam users openAttempts = 30 * 60 # wait max. 30 minutes for replication while (len(examUsers) > len(usersReplicated)) and (openAttempts > 0): openAttempts -= 1 MODULE.info( 'waiting for replication to be finished, %s user objects missing' % (len(examUsers) - len(usersReplicated))) for idn in examUsers - usersReplicated: try: ldap_user_read.get(idn, required=True) except ldap.NO_SUCH_OBJECT: continue # not replicated yet iuser = util.distribution.openRecipients( idn, ldap_user_read) if not iuser: continue # not a users/user object MODULE.info('user has been replicated: %s' % idn) # call hook scripts if 0 != subprocess.call([ '/bin/run-parts', CREATE_USER_POST_HOOK_DIR, '--arg', iuser.username, '--arg', iuser.dn, '--arg', iuser.homedir ]): raise ValueError( 'failed to run hook scripts for user %r' % (iuser.username)) # store User object in list of final recipients recipients.append(iuser) # mark the user as replicated usersReplicated.add(idn) progress.info( '%s, %s (%s)' % (iuser.lastname, iuser.firstname, iuser.username)) progress.add_steps(percentPerUser) # wait a second time.sleep(1) progress.add_steps(percentPerUser) if openAttempts <= 0: MODULE.error( 'replication timeout - %s user objects missing: %r ' % ((len(examUsers) - len(usersReplicated)), (examUsers - usersReplicated))) raise UMC_Error( _('Replication timeout: could not create all exam users')) # update the final list of recipients my.project.recipients = recipients my.project.save() # update local NSS group cache if ucr.is_true('nss/group/cachefile', True): cmd = ['/usr/lib/univention-pam/ldap-group-to-file.py'] if ucr.is_true('nss/group/cachefile/check_member', False): cmd.append('--check_member') MODULE.info('Updating local nss group cache...') if subprocess.call(cmd): MODULE.error('Updating local nss group cache failed: %s' % ' '.join(cmd)) else: MODULE.info( 'Update of local nss group cache finished successfully.' ) # distribute exam files progress.component(_('Distributing exam files')) progress.info('') my.project.distribute() progress.add_steps(20) # prepare room settings via UMCP... # first step: acquire room # second step: adjust room settings progress.component(_('Prepare room settings')) try: user_client = Client(None, self.username, self.password) except (ConnectionError, HTTPError) as exc: MODULE.warn('Authentication failed: %s' % (exc, )) raise UMC_Error(_('Could not connect to local UMC server.')) room = request.options['room'] MODULE.info('Acquire room: %s' % (room, )) user_client.umc_command('computerroom/room/acquire', dict( room=request.options['room'], )).result progress.add_steps(1) MODULE.info('Adjust room settings:\n%s' % '\n'.join( [' %s=%s' % (k, v) for k, v in request.options.iteritems()])) user_client.umc_command( 'computerroom/exam/start', dict( room=room, examDescription=request.options['name'], exam=directory, examEndTime=request.options.get('examEndTime'), )).result progress.add_steps(4) user_client.umc_command( 'computerroom/settings/set', dict( room=room, internetRule=request.options['internetRule'], customRule=request.options.get('customRule'), shareMode=request.options['shareMode'], printMode='default', )).result progress.add_steps(5) def _finished(thread, result, request): # mark the progress state as finished progress.info(_('finished...')) progress.finish() # finish the request at the end in order to force the module to keep # running until all actions have been completed success = not isinstance(result, BaseException) response = dict(success=success) if success: # remove uploaded files from cache self._cleanTmpDir() else: msg = str(result) if not isinstance(result, UMC_Error): response = result msg = ''.join(traceback.format_exception(*thread.exc_info)) progress.error(msg) # in case a distribution project has already be written to disk, purge it if my.project: my.project.purge() self.thread_finished_callback(thread, response, request) thread = notifier.threads.Simple('start_exam', _thread, notifier.Callback(_finished, request)) thread.run() @sanitize( exam=StringSanitizer(required=True), ) @simple_response def collect_exam(self, exam): project = util.distribution.Project.load(exam) if not project: raise UMC_Error(_('No files have been distributed')) project.collect() return True @sanitize( room=DNSanitizer(required=True), ) @LDAP_Connection() def validate_room(self, request, ldap_user_read=None, ldap_position=None): error = None dn = request.options['room'] room = ComputerRoom.from_dn(dn, None, ldap_user_read) if not room.hosts: # FIXME: raise UMC_Error() error = _( 'Room "%s" does not contain any computers. Empty rooms may not be used to start an exam.' ) % room.get_relative_name() self.finished(request.id, error) @sanitize( room=StringSanitizer(required=True), exam=StringSanitizer(required=True), ) def finish_exam(self, request): # reset the current progress state # steps: # 10 -> collecting exam files # 5 -> for preparing exam room # 25 -> for cloning users progress = self._progress_state progress.reset(40) progress.component(_('Initializing')) # try to open project file project = util.distribution.Project.load(request.options.get('exam')) if not project: # the project file does not exist... ignore problem MODULE.warn( 'The project file for exam %s does not exist. Ignoring and finishing exam mode.' % request.options.get('exam')) def _thread(): # perform all actions inside a thread... # collect files progress.component(_('Collecting exam files...')) if project: project.collect() progress.add_steps(10) # open a new connection to the master UMC master = ucr['ldap/master'] try: client = Client(master) client.authenticate_with_machine_account() except (ConnectionError, HTTPError) as exc: MODULE.error('Could not connect to UMC on %s: %s' % (master, exc)) raise UMC_Error( _('Could not connect to master server %s.') % (master, )) school = SchoolSearchBase.getOU(request.options['room']) # unset exam mode for the given computer room progress.component(_('Configuring the computer room...')) client.umc_command( 'schoolexam-master/unset-computerroom-exammode', dict( roomdn=request.options['room'], school=school, )).result progress.add_steps(5) # delete exam users accounts if project: # get a list of user accounts in parallel exams parallelUsers = dict([ (iuser.username, iproject.description) for iproject in util.distribution.Project.list() if iproject.name != project.name for iuser in iproject.recipients ]) progress.component(_('Removing exam accounts')) percentPerUser = 25.0 / (1 + len(project.recipients)) for iuser in project.recipients: progress.info( '%s, %s (%s)' % (iuser.lastname, iuser.firstname, iuser.username)) try: if iuser.username not in parallelUsers: # remove first the home directory shutil.rmtree(iuser.unixhome, ignore_errors=True) # remove LDAP user entry client.umc_command( 'schoolexam-master/remove-exam-user', dict( userdn=iuser.dn, school=school, )).result MODULE.info('Exam user has been removed: %s' % iuser.dn) else: MODULE.process( 'Cannot remove the user account %s as it is registered for the running exam "%s", as well' % (iuser.dn, parallelUsers[iuser.username])) except (ConnectionError, HTTPError) as e: MODULE.warn( 'Could not remove exam user account %s: %s' % (iuser.dn, e)) # indicate the user has been processed progress.add_steps(percentPerUser) progress.add_steps(percentPerUser) def _finished(thread, result): # mark the progress state as finished progress.info(_('finished...')) progress.finish() # running until all actions have been completed if isinstance(result, BaseException): msg = ''.join(traceback.format_exception(*thread.exc_info)) MODULE.error('Exception during exam_finish: %s' % msg) self.finished(request.id, dict(success=False)) progress.error( _('An unexpected error occurred during the preparation: %s' ) % result) else: self.finished(request.id, dict(success=True)) if project: # purge project project.purge() # remove uploaded files from cache self._cleanTmpDir() thread = notifier.threads.Simple('start_exam', _thread, _finished) thread.run()
class Instance(umcm.Base): def init(self): self.ucr = univention.config_registry.ConfigRegistry() self.ucr.load() util.install_opener(self.ucr) self.package_manager = PackageManager( info_handler=MODULE.process, step_handler=None, error_handler=MODULE.warn, lock=False, always_noninteractive=True, ) self.uu = UniventionUpdater(False) self.component_manager = util.ComponentManager(self.ucr, self.uu) # in order to set the correct locale for Application locale.setlocale(locale.LC_ALL, str(self.locale)) @sanitize(email=EmailSanitizer(required=True)) @simple_response def request_new_license(self, email): license = LICENSE.dump_data() if license is None: raise umcm.UMC_CommandError(_('Cannot parse License from LDAP')) data = {} data['email'] = email data['licence'] = license data = urllib.urlencode(data) url = 'https://license.univention.de/keyid/conversion/submit' request = urllib2.Request(url, data=data, headers={'User-agent': 'UMC/AppCenter'}) try: util.urlopen(request) except Exception as e: try: # try to parse an html error body = e.read() detail = re.search( '<span id="details">(?P<details>.*?)</span>', body).group(1) except: detail = str(e) raise umcm.UMC_CommandError( _('An error occurred while sending the request: %s') % detail) else: return True @sanitize(pattern=PatternSanitizer(default='.*')) @simple_response def query(self, pattern): LICENSE.reload() try: applications = Application.all(force_reread=True) except (urllib2.HTTPError, urllib2.URLError) as e: raise umcm.UMC_CommandError( _('Could not query App Center: %s') % e) result = [] self.package_manager.reopen_cache() for application in applications: if pattern.search(application.name): props = application.to_dict(self.package_manager) # delete larger entries for ikey in ('readmeupdate', 'licenseagreement'): if ikey in props: del props[ikey] result.append(props) return result @sanitize(application=StringSanitizer(minimum=1, required=True)) @simple_response def get(self, application): LICENSE.reload() application = Application.find(application) self.package_manager.reopen_cache() return application.to_dict(self.package_manager) @sanitize(function=ChoicesSanitizer(['install', 'uninstall', 'update'], required=True), application=StringSanitizer(minimum=1, required=True), force=BooleanSanitizer()) def invoke(self, request): function = request.options.get('function') application_id = request.options.get('application') application = Application.find(application_id) force = request.options.get('force') try: # make sure that the application cane be installed/updated can_continue = True result = { 'install': [], 'remove': [], 'broken': [], } if not application: MODULE.info('Application not found: %s' % application_id) can_continue = False elif function == 'install' and not application.can_be_installed( self.package_manager): MODULE.info('Application cannot be installed: %s' % application_id) can_continue = False elif function == 'update' and not application.can_be_updated(): MODULE.info('Application cannot be updated: %s' % application_id) can_continue = False if can_continue and function in ('install', 'update'): result = application.install_dry_run(self.package_manager, self.component_manager, remove_component=False) if result['broken'] or (result['remove'] and not force): MODULE.info('Remove component: %s' % application_id) self.component_manager.remove_app(application) self.package_manager.update() can_continue = False elif can_continue and function in ('uninstall', ) and not force: result['remove'] = application.uninstall_dry_run( self.package_manager) can_continue = False result['can_continue'] = can_continue self.finished(request.id, result) if can_continue: def _thread(module, application, function): with module.package_manager.locked(reset_status=True, set_finished=True): with module.package_manager.no_umc_restart(): if function in ('install', 'update'): # dont have to add component: already added during dry_run return application.install( module.package_manager, module.component_manager, add_component=False) else: return application.uninstall( module.package_manager, module.component_manager) def _finished(thread, result): if isinstance(result, BaseException): MODULE.warn('Exception during %s %s: %s' % (function, application_id, str(result))) thread = notifier.threads.Simple( 'invoke', notifier.Callback(_thread, self, application, function), _finished) thread.run() except LockError: # make it thread safe: another process started a package manager # this module instance already has a running package manager raise umcm.UMC_CommandError( _('Another package operation is in progress')) @simple_response def app_center_app_license(self, application): application = Application.find(application) if not application or not application.get('licensefile'): raise umcm.UMC_CommandError( _('No license file available for application: %s') % (application.id)) # open the license file and replace line breaks with BR-tags fp = util.urlopen(application.get('licensefile')) txt = ''.join(fp.readlines()).strip() txt = txt.replace('\n\n\n', '\n<br>\n<br>\n<br>\n') txt = txt.replace('\n\n', '\n<br>\n<br>\n') return txt @simple_response def packages_sections(self): """ fills the 'sections' combobox in the search form """ sections = set() for package in self.package_manager.packages(): sections.add(package.section) return sorted(sections) @sanitize(pattern=PatternSanitizer(required=True)) @simple_response def packages_query(self, pattern, section='all', key='package'): """ Query to fill the grid. Structure is fixed here. """ result = [] for package in self.package_manager.packages(): if section == 'all' or package.section == section: toshow = False if pattern.pattern == '^.*$': toshow = True elif key == 'package' and pattern.search(package.name): toshow = True elif key == 'description' and pattern.search( package.candidate.raw_description): toshow = True if toshow: result.append(self._package_to_dict(package, full=False)) return result @simple_response def packages_get(self, package): """ retrieves full properties of one package """ package = self.package_manager.get_package(package) if package is not None: return self._package_to_dict(package, full=True) else: # TODO: 404? return {} @sanitize(function=MappingSanitizer( { 'install': 'install', 'upgrade': 'install', 'uninstall': 'remove', }, required=True), packages=ListSanitizer(StringSanitizer(minimum=1), required=True)) @simple_response def packages_invoke_dry_run(self, packages, function): packages = self.package_manager.get_packages(packages) kwargs = {'install': [], 'remove': [], 'dry_run': True} if function == 'install': kwargs['install'] = packages else: kwargs['remove'] = packages return dict( zip(['install', 'remove', 'broken'], self.package_manager.mark(**kwargs))) @sanitize(function=MappingSanitizer( { 'install': 'install', 'upgrade': 'install', 'uninstall': 'remove', }, required=True), packages=ListSanitizer(StringSanitizer(minimum=1), required=True)) def packages_invoke(self, request): """ executes an installer action """ packages = request.options.get('packages') function = request.options.get('function') try: with self.package_manager.locked(reset_status=True): not_found = [ pkg_name for pkg_name in packages if self.package_manager.get_package(pkg_name) is None ] self.finished(request.id, {'not_found': not_found}) if not not_found: def _thread(package_manager, function, packages): with package_manager.locked(set_finished=True): with package_manager.no_umc_restart(): if function == 'install': package_manager.install(*packages) else: package_manager.uninstall(*packages) def _finished(thread, result): if isinstance(result, BaseException): MODULE.warn('Exception during %s %s: %r' % (function, packages, str(result))) thread = notifier.threads.Simple( 'invoke', notifier.Callback(_thread, self.package_manager, function, packages), _finished) thread.run() except LockError: # make it thread safe: another process started a package manager # this module instance already has a running package manager raise umcm.UMC_CommandError( _('Another package operation is in progress')) @simple_response def progress(self): timeout = 5 return self.package_manager.poll(timeout) def _package_to_dict(self, package, full): """ Helper that extracts properties from a 'apt_pkg.Package' object and stores them into a dictionary. Depending on the 'full' switch, stores only limited (for grid display) or full (for detail view) set of properties. """ installed = package.installed # may be None candidate = package.candidate result = { 'package': package.name, 'installed': package.is_installed, 'upgradable': package.is_upgradable, 'summary': candidate.summary, } # add (and translate) a combined status field # *** NOTE *** we translate it here: if we would use the Custom Formatter # of the grid then clicking on the sort header would not work. if package.is_installed: if package.is_upgradable: result['status'] = _('upgradable') else: result['status'] = _('installed') else: result['status'] = _('not installed') # additional fields needed for detail view if full: result['section'] = package.section result['priority'] = package.priority # Some fields differ depending on whether the package is installed or not: if package.is_installed: result['summary'] = installed.summary # take the current one result['description'] = installed.description result['installed_version'] = installed.version result['size'] = installed.installed_size if package.is_upgradable: result['candidate_version'] = candidate.version else: del result[ 'upgradable'] # not installed: don't show 'upgradable' at all result['description'] = candidate.description result['size'] = candidate.installed_size result['candidate_version'] = candidate.version # format size to handle bytes size = result['size'] byte_mods = ['B', 'kB', 'MB'] for byte_mod in byte_mods: if size < 10000: break size = float(size) / 1000 # MB, not MiB else: size = size * 1000 # once too often if size == int(size): format_string = '%d %s' else: format_string = '%.2f %s' result['size'] = format_string % (size, byte_mod) return result @simple_response def components_query(self): """ Returns components list for the grid in the ComponentsPage. """ # be as current as possible. self.uu.ucr_reinit() self.ucr.load() result = [] for comp in self.uu.get_all_components(): result.append(self.component_manager.component(comp)) return result @sanitize_list(StringSanitizer()) @multi_response(single_values=True) def components_get(self, iterator, component_id): # be as current as possible. self.uu.ucr_reinit() self.ucr.load() for component_id in iterator: yield self.component_manager.component(component_id) @sanitize_list(DictSanitizer({'object': advanced_components_sanitizer})) @multi_response def components_put(self, iterator, object): """Writes back one or more component definitions. """ # umc.widgets.Form wraps the real data into an array: # # [ # { # 'object' : { ... a dict with the real data .. }, # 'options': None # }, # ... more such entries ... # ] # # Current approach is to return a similarly structured array, # filled with elements, each one corresponding to one array # element of the request: # # [ # { # 'status' : a number where 0 stands for success, anything else # is an error code # 'message' : a result message # 'object' : a dict of field -> error message mapping, allows # the form to show detailed error information # }, # ... more such entries ... # ] with util.set_save_commit_load(self.ucr) as super_ucr: for object, in iterator: yield self.component_manager.put(object, super_ucr) self.package_manager.update() # do the same as components_put (update) # but dont allow adding an already existing entry components_add = sanitize_list( DictSanitizer({'object': add_components_sanitizer}))(components_put) components_add.__name__ = 'components_add' @sanitize_list(StringSanitizer()) @multi_response(single_values=True) def components_del(self, iterator, component_id): for component_id in iterator: yield self.component_manager.remove(component_id) self.package_manager.update() @multi_response def settings_get(self, iterator): # *** IMPORTANT *** Our UCR copy must always be current. This is not only # to catch up changes made via other channels (ucr command line etc), # but also to reflect the changes we have made ourselves! self.ucr.load() for _ in iterator: yield { 'maintained': self.ucr.is_true('repository/online/maintained', False), 'unmaintained': self.ucr.is_true('repository/online/unmaintained', False), 'server': self.ucr.get('repository/online/server', ''), 'prefix': self.ucr.get('repository/online/prefix', ''), } @sanitize_list( DictSanitizer({'object': basic_components_sanitizer}), min_elements=1, max_elements=1 # moduleStore with one element... ) @multi_response def settings_put(self, iterator, object): # FIXME: returns values although it should yield (multi_response) changed = False # Set values into our UCR copy. try: with util.set_save_commit_load(self.ucr) as super_ucr: for object, in iterator: for key, value in object.iteritems(): MODULE.info( " ++ Setting new value for '%s' to '%s'" % (key, value)) super_ucr.set_registry_var( '%s/%s' % (constants.ONLINE_BASE, key), value) changed = super_ucr.changed() except Exception as e: MODULE.warn(" !! Writing UCR failed: %s" % str(e)) return [{'message': str(e), 'status': constants.PUT_WRITE_ERROR}] self.package_manager.update() # Bug #24878: emit a warning if repository is not reachable try: updater = self.uu for line in updater.print_version_repositories().split('\n'): if line.strip(): break else: raise ConfigurationError() except ConfigurationError: msg = _( "There is no repository at this server (or at least none for the current UCS version)" ) MODULE.warn(" !! Updater error: %s" % msg) response = {'message': msg, 'status': constants.PUT_UPDATER_ERROR} # if nothing was committed, we want a different type of error code, # just to appropriately inform the user if changed: response['status'] = constants.PUT_UPDATER_NOREPOS return [response] except: info = sys.exc_info() emsg = '%s: %s' % info[:2] MODULE.warn(" !! Updater error [%s]: %s" % (emsg)) return [{ 'message': str(info[1]), 'status': constants.PUT_UPDATER_ERROR }] return [{'status': constants.PUT_SUCCESS}]
class Instance(SchoolBaseModule): def query(self, request): """Searches for internet filter rules requests.options = {} 'pattern' -- pattern to match within the rule name or the list of domains """ MODULE.info('internetrules.query: options: %s' % str(request.options)) pattern = request.options.get('pattern', '').lower() def _matchDomain(domains): # helper function to match pattern within the list of domains matches = [idom for idom in domains if pattern in idom.lower()] return 0 < len(matches) # filter out all rules that match the given pattern result = [ dict( name=irule.name, type=_filterTypesInv[irule.type], domains=len(irule.domains), priority=irule.priority, wlan=irule.wlan, ) for irule in rules.list() if pattern in irule.name.lower() or _matchDomain(irule.domains) ] MODULE.info('internetrules.query: results: %s' % str(result)) self.finished(request.id, result) @sanitize(StringSanitizer()) def get(self, request): """Returns the specified rules requests.options = [ <ruleName>, ... ] """ MODULE.info('internetrules.get: options: %s' % str(request.options)) result = [] # fetch all rules with the given names (we need to make sure that "name" is UTF8) names = set(iname.encode('utf8') for iname in request.options) result = [ dict( name=irule.name, type=_filterTypesInv[irule.type], domains=irule.domains, priority=irule.priority, wlan=irule.wlan, ) for irule in rules.list() if irule.name in names ] MODULE.info('internetrules.get: results: %s' % str(result)) self.finished(request.id, result) @sanitize(DictSanitizer(dict(object=StringSanitizer()), required=True)) def remove(self, request): """Removes the specified rules requests.options = [ { "object": <ruleName> }, ... ] """ MODULE.info('internetrules.remove: options: %s' % str(request.options)) result = [] # fetch all rules with the given names for ientry in request.options: iname = ientry['object'] success = False if iname: success = rules.remove(iname) result.append(dict(name=iname, success=success)) MODULE.info('internetrules.remove: results: %s' % str(result)) self.finished(request.id, result) @staticmethod def _parseRule(iprops, forceAllProperties=False): # validate types for ikey, itype in (('name', basestring), ('type', basestring), ('priority', (int, basestring)), ('wlan', bool), ('domains', list)): if ikey not in iprops: if forceAllProperties: # raise exception as the key is not present raise ValueError( _('The key "%s" has not been specified: %s') % (ikey, iprops)) continue if not isinstance(iprops[ikey], itype): typeStr = '' if isinstance(itype, tuple): typeStr = ', '.join([i.__name__ for i in itype]) else: typeStr = itype.__name__ raise ValueError( _('The key "%s" needs to be of type: %s') % (ikey, typeStr)) # validate name if 'name' in iprops and not univention.config_registry.validate_key( iprops['name'].encode('utf-8')): raise ValueError( _('Invalid rule name "%s". The name needs to be a string, the following special characters are not allowed: %s' ) % (iprops.get('name'), '!, ", §, $, %, &, (, ), [, ], {, }, =, ?, `, +, #, \', ",", ;, <, >, \\' )) # validate type if 'type' in iprops and iprops['type'] not in _filterTypes: raise ValueError(_('Filter type is unknown: %s') % iprops['type']) # validate domains if 'domains' in iprops: parsedDomains = [] for idomain in iprops['domains']: def _validValueChar(): # helper function to check for invalid characters for ichar in idomain: if ichar in univention.config_registry.backend.INVALID_VALUE_CHARS: return False return True if not isinstance(idomain, basestring) or not _validValueChar(): raise ValueError(_('Invalid domain ')) # parse domain domain = idomain if '://' not in domain: # make sure that we have a scheme defined for parsing MODULE.info( 'Adding a leading scheme for parsing of domain: %s' % idomain) domain = 'http://%s' % domain domain = urlparse(domain).hostname MODULE.info('Parsed domain: %s -> %s' % (idomain, domain)) if not domain: raise ValueError( _('The specified domain "%s" is not valid. Please specify a valid domain name, such as "wikipedia.org", "facebook.com"' ) % idomain) # add domain to list of parsed domains parsedDomains.append(domain) # save parsed domains in the dict iprops['domains'] = parsedDomains return iprops @sanitize( DictSanitizer(dict(object=DictSanitizer(dict( name=StringSanitizer(required=True), type=ChoicesSanitizer(list(_filterTypes.keys()), required=True), wlan=BooleanSanitizer(required=True), priority=IntegerSanitizer(required=True), domains=ListSanitizer(StringSanitizer(required=True), required=True), ), required=True)), required=True)) def add(self, request): """Add the specified new rules: requests.options = [ { 'object': { 'name': <str>, 'type': 'whitelist' | 'blacklist', 'priority': <int> | <str>, 'wlan': <bool>, 'domains': [<str>, ...], } }, ... ] """ # try to create all specified projects result = [] for ientry in request.options: iprops = ientry['object'] try: # make sure that the rule does not already exist irule = rules.load(iprops['name']) if irule: raise ValueError( _('A rule with the same name does already exist: %s') % iprops['name']) # parse the properties parsedProps = self._parseRule(iprops, True) # create a new rule from the user input newRule = rules.Rule( name=parsedProps['name'], type=_filterTypes[parsedProps['type']], priority=parsedProps['priority'], wlan=parsedProps['wlan'], domains=parsedProps['domains'], ) # try to save filter rule newRule.save() MODULE.info('Created new rule: %s' % newRule) # everything ok result.append(dict(name=iprops['name'], success=True)) except (ValueError, KeyError) as e: # data not valid... create error info MODULE.info( 'data for internet filter rule "%s" is not valid: %s' % (iprops.get('name'), e)) result.append( dict(name=iprops.get('name'), success=False, details=str(e))) # return the results self.finished(request.id, result) @sanitize( DictSanitizer(dict( object=DictSanitizer(dict( name=StringSanitizer(required=True), type=ChoicesSanitizer(list(_filterTypes.keys()), required=True), wlan=BooleanSanitizer(required=True), priority=IntegerSanitizer(required=True), domains=ListSanitizer(StringSanitizer(required=True), required=True), ), required=True), options=DictSanitizer(dict(name=StringSanitizer()), required=True), ), required=True)) def put(self, request): """Modify an existing rules: requests.options = [ { 'object': { 'name': <str>, # optional 'type': 'whitelist' | 'blacklist', # optional 'priority': <int>, # optional 'wlan': <bool>, # optional 'domains': [<str>, ...], # optional }, 'options': { 'name': <str> # the original name of the object } }, ... ] """ # try to create all specified projects result = [] for ientry in request.options: try: # get properties and options from entry iprops = ientry['object'] iname = None ioptions = ientry.get('options') if ioptions: iname = ioptions.get('name') if not iname: raise ValueError( _('No "name" attribute has been specified in the options.' )) # make sure that the rule already exists irule = rules.load(iname) if not irule: raise ValueError( _('The rule does not exist and cannot be modified: %s') % iprops.get('name', '')) # parse the properties self._parseRule(iprops) if iprops.get('name', iname) != iname: # name has been changed -> remove old rule and create a new one rules.remove(iname) irule.name = iprops['name'] if 'type' in iprops: # set rule type, move all domains from the previous type oldDomains = irule.domains irule.domains = [] irule.type = _filterTypes[iprops['type']] irule.domains = oldDomains if 'priority' in iprops: # set priority irule.priority = iprops['priority'] if 'wlan' in iprops: # set wlan irule.wlan = iprops['wlan'] if 'domains' in iprops: # set domains irule.domains = iprops['domains'] # try to save filter rule irule.save() MODULE.info('Saved rule: %s' % irule) # everything ok result.append(dict(name=iname, success=True)) except ValueError as e: # data not valid... create error info MODULE.info( 'data for internet filter rule "%s" is not valid: %s' % (iprops.get('name'), e)) result.append( dict(name=iprops.get('name'), success=False, details=str(e))) # return the results self.finished(request.id, result) @sanitize(school=SchoolSanitizer(required=True), pattern=StringSanitizer(default='')) @LDAP_Connection() def groups_query(self, request, ldap_user_read=None, ldap_position=None): """List all groups (classes, workgroups) and their assigned internet rule""" pattern = LDAP_Filter.forAll(request.options.get('pattern', ''), ['name', 'description']) school = request.options['school'] groups = [ x for x in Group.get_all(ldap_user_read, school, pattern) if not x.self_is_computerroom() ] internet_rules = rules.getGroupRuleName([i.name for i in groups]) name = re.compile('-%s$' % (re.escape(school)), flags=re.I) result = [{ 'name': i.get_relative_name() if hasattr(i, 'get_relative_name') else name.sub('', i.name), '$dn$': i.dn, 'rule': internet_rules.get(i.name, 'default') or _('-- Default (unrestricted) --') } for i in groups] result.sort(cmp=lambda x, y: cmp(x.lower(), y.lower()), key=lambda x: x['name']) self.finished(request.id, result) @sanitize( DictSanitizer( dict( group=StringSanitizer(required=True), rule=StringSanitizer(required=True), ))) @LDAP_Connection() def groups_assign(self, request, ldap_user_read=None, ldap_position=None): """Assigns default rules to groups: request.options = [ { 'group': <groupDN>, 'rule': <ruleName> }, ... ] """ MODULE.info('internetrules.groups_assign: options: %s' % str(request.options)) # try to load all group rules newRules = {} rmRules = [] for ientry in request.options: # make sure the group exists igrp = udm_objects.get(udm_modules.get('groups/group'), None, ldap_user_read, ldap_position, ientry['group']) if not igrp: raise UMC_Error('unknown group object') igrp.open() # check the rule name irule = ientry['rule'] if irule == '$default$': # remove the rule rmRules.append(igrp['name']) else: try: # make sure the rule name is valid self._parseRule(dict(name=irule)) except ValueError as exc: raise UMC_Error(str(exc)) # add new rule newRules[igrp['name']] = irule # assign default filter rules to groups rules.setGroupRuleName(newRules) rules.unsetGroupRuleName(rmRules) MODULE.info('internetrules.groups_assign: finished') self.finished(request.id, True)
class Instance(SchoolBaseModule): def __init__(self): SchoolBaseModule.__init__(self) self._tmpDir = None def init(self): SchoolBaseModule.init(self) # initiate paths for data distribution util.initPaths() def destroy(self): self._cleanTmpDir() def _cleanTmpDir(self): # clean up the temporary upload directory if self._tmpDir: MODULE.info('Clean up temporary directory: %s' % self._tmpDir) shutil.rmtree(self._tmpDir, ignore_errors=True) self._tmpDir = None @file_upload @sanitize(DictSanitizer(dict( filename=StringSanitizer(required=True), tmpfile=StringSanitizer(required=True), ), required=True)) def upload(self, request): # create a temporary upload directory, if it does not already exist if not self._tmpDir: self._tmpDir = tempfile.mkdtemp(prefix='ucsschool-distribution-upload-') MODULE.info('Created temporary directory: %s' % self._tmpDir) for file in request.options: filename = self.__workaround_filename_bug(file) destPath = os.path.join(self._tmpDir, filename) MODULE.info('Received file %r, saving it to %r' % (file['tmpfile'], destPath)) shutil.move(file['tmpfile'], destPath) self.finished(request.id, None) def __workaround_filename_bug(self, file): # the following code block is a heuristic to support both: fixed and unfixed Bug #37716 filename = file['filename'] try: # The UMC-Webserver decodes filename in latin-1, need to revert filename = filename.encode('ISO8859-1') except UnicodeEncodeError: # we got non-latin characters, Bug #37716 is fixed and string contains e.g. '→' filename = file['filename'].encode('UTF-8') else: # the string contains at least no non-latin1 characters try: # try if the bytes could be UTF-8 # can't fail if Bug #37716 is fixed filename.decode('UTF-8') except UnicodeDecodeError: filename = file['filename'].encode('UTF-8') # Bug #37716 was fixed MODULE.info('Detected filename %r as %r' % (file['filename'], filename)) # the code block can be removed and replaced by filename = file['filename'].encode('UTF-8') after Bug #37716 return filename @sanitize( filenames=ListSanitizer(min_elements=1), # project=StringSanitizer(allow_none=True) ) @simple_response def checkfiles(self, project, filenames): '''Checks whether the given filename has already been uploaded: request.options: { 'filenames': [ '...', ... ], project: '...' } returns: { 'filename': '...', 'sessionDuplicate': True|False, 'projectDuplicate': True|False, 'distributed': True|False } ''' # load project if project: project = util.Project.load(project) result = [] for ifile in filenames: ifile = ifile.encode('UTF-8') # check whether file has already been upload in this session iresult = dict(sessionDuplicate=False, projectDuplicate=False, distributed=False) iresult['filename'] = ifile iresult['sessionDuplicate'] = self._tmpDir is not None and os.path.exists(os.path.join(self._tmpDir, ifile)) # check whether the file exists in the specified project and whether # it has already been distributed if project: iresult['projectDuplicate'] = ifile in project.files iresult['distributed'] = ifile in project.files and not os.path.exists(os.path.join(project.cachedir, ifile)) result.append(iresult) return result @sanitize( pattern=PatternSanitizer(required=False, default='.*'), filter=ChoicesSanitizer(['all', 'private'], default='private') ) @simple_response def query(self, pattern, filter): result = [dict( # only show necessary information description=i.description, name=i.name, sender=i.sender.username, recipients=len(i.recipients), files=len(i.files), isDistributed=i.isDistributed ) for i in util.Project.list() if (pattern.match(i.name) or pattern.match(i.description)) and (filter == 'all' or compare_dn(i.sender.dn, self.user_dn)) ] return result @LDAP_Connection() def _get_sender(self, ldap_user_read=None, ldap_position=None): '''Return a User instance of the currently logged in user.''' try: user = User.from_dn(self.user_dn, None, ldap_user_read) obj = user.get_udm_object(ldap_user_read) return util.User(obj.info, dn=obj.dn) except udm_exceptions.base as exc: raise UMC_Error(_('Failed to load user information: %s') % exc) @sanitize(DictSanitizer(dict(object=DictSanitizer({}, required=True)), required=True)) def put(self, request): """Modify an existing project""" result = [self._save(entry['object'], True) for entry in request.options] self.finished(request.id, result) @sanitize(DictSanitizer(dict(object=DictSanitizer({}, required=True)), required=True)) def add(self, request): """Add a new project""" result = [self._save(entry['object'], False) for entry in request.options] self.finished(request.id, result) @LDAP_Connection() def _save(self, iprops, doUpdate=True, ldap_user_read=None, ldap_position=None): # try to open the UDM user object of the current user sender = self._get_sender() try: # remove keys that may not be set from outside for k in ('atJobNumCollect', 'atJobNumDistribute'): iprops.pop(k, None) # transform filenames into bytestrings iprops['files'] = [f.encode('UTF-8') for f in iprops.get('files', [])] # load the project or create a new one project = None orgProject = None if doUpdate: # try to load the given project orgProject = util.Project.load(iprops.get('name', '')) if not orgProject: raise UMC_Error(_('The specified project does not exist: %s') % iprops['name']) # create a new project with the updated values project = util.Project(orgProject.dict) project.update(iprops) else: # create a new project project = util.Project(iprops) # make sure that the project owner himself is modifying the project if doUpdate and not compare_dn(project.sender.dn, self.user_dn): raise UMC_Error(_('The project can only be modified by the owner himself')) # handle time settings for distribution/collection of project files for jsuffix, jprop, jname in (('distribute', 'starttime', _('Project distribution')), ('collect', 'deadline', _('Project collection'))): if '%sType' % jsuffix in iprops: # check the distribution/collection type: manual/automat jtype = (iprops['%sType' % jsuffix]).lower() if jtype == 'automatic': try: # try to parse the given time parameters strtime = '%s %s' % (iprops['%sDate' % jsuffix], iprops['%sTime' % jsuffix]) jdate = datetime.strptime(strtime, '%Y-%m-%d %H:%M') setattr(project, jprop, jdate) except ValueError: raise UMC_Error(_('Could not set date for: %s') % jname) # make sure the execution time lies sufficiently in the future if getattr(project, jprop) - datetime.now() < timedelta(minutes=1): raise UMC_Error(_('The specified time needs to lie in the future for: %s') % jname) else: # manual distribution/collection setattr(project, jprop, None) if project.starttime and project.deadline: # make sure distributing happens before collecting if project.deadline - project.starttime < timedelta(minutes=3): raise UMC_Error(_('Distributing the data needs to happen sufficiently long enough before collecting them')) if 'recipients' in iprops: # lookup the users in LDAP and save them to the project project.recipients = [util.openRecipients(idn, ldap_user_read) for idn in iprops.get('recipients', [])] project.recipients = [x for x in project.recipients if x] MODULE.info('recipients: %s' % (project.recipients,)) if not doUpdate: # set the sender (i.e., owner) of the project project.sender = sender # initiate project and validate its values project.validate() # make sure that there is no other project with the same directory name # if we add new projects if not doUpdate and project.isNameInUse(): MODULE.error('The project name is already in use: %s' % (project.name)) raise UMC_Error(_('The specified project directory name "%s" is already in use by a different project.') % (project.name)) # try to save project to disk project.save() # move new files into project directory if self._tmpDir: for ifile in project.files: isrc = os.path.join(self._tmpDir, ifile) itarget = os.path.join(project.cachedir, ifile) if os.path.exists(isrc): # mv file to cachedir shutil.move(isrc, itarget) os.chown(itarget, 0, 0) # remove files that have been marked for removal if doUpdate: for ifile in set(orgProject.files) - set(project.files): itarget = os.path.join(project.cachedir, ifile) try: os.remove(itarget) except OSError: pass # re-distribute the project in case it has already been distributed if doUpdate and project.isDistributed: usersFailed = [] project.distribute(usersFailed) if usersFailed: # not all files could be distributed MODULE.info('Failed processing the following users: %s' % usersFailed) usersStr = ', '.join([Display.user(i) for i in usersFailed]) raise UMC_Error(_('The project could not distributed to the following users: %s') % usersStr) except (IOError, OSError, UMC_Error): # TODO: catch only UMC_Error etype, exc, etraceback = sys.exc_info() # data not valid... create error info MODULE.info('data for project "%s" is not valid: %s' % (iprops.get('name'), exc)) if not doUpdate: # remove eventually created project file and cache dir for ipath in (project.projectfile, project.cachedir): if os.path.basename(ipath) not in os.listdir(util.DISTRIBUTION_DATA_PATH): # no file / directory has been created yet continue try: MODULE.info('cleaning up... removing: %s' % ipath) shutil.rmtree(ipath) except (IOError, OSError): pass raise UMC_Error, exc, etraceback self._cleanTmpDir() return {'success': True, 'name': iprops.get('name')} @sanitize(StringSanitizer(required=True)) @LDAP_Connection() def get(self, request, ldap_user_read=None, ldap_position=None): """Returns the objects for the given IDs requests.options = [ <ID>, ... ] return: [ { ... }, ... ] """ # try to load all given projects result = [] # list of all project properties (dicts) or None if project is not valid for iproject in [util.Project.load(iid) for iid in request.options]: # make sure that project could be loaded if not iproject: result.append(None) continue # make sure that only the project owner himself (or an admin) is able # to see the content of a project if request.flavor == 'teacher' and not compare_dn(iproject.sender.dn, self.user_dn): raise UMC_Error(_('Project details are only visible to the project owner himself or an administrator.'), status=403) # prepare date and time properties for distribution/collection of project files props = iproject.dict for jjob, jsuffix in ((iproject.atJobDistribute, 'distribute'), (iproject.atJobCollect, 'collect')): MODULE.info('check job: %s' % jsuffix) if not jjob: # no job is registered -> manual job distribution/collection MODULE.info('no existing job -> manual execution') props['%sType' % jsuffix] = 'manual' continue # job is registered -> prepare date and time fields MODULE.info('job nr #%d scheduled for %s -> automatic execution' % (jjob.nr, jjob.execTime)) props['%sType' % jsuffix] = 'automatic' props['%sDate' % jsuffix] = datetime.strftime(jjob.execTime, '%Y-%m-%d') props['%sTime' % jsuffix] = datetime.strftime(jjob.execTime, '%H:%M') # adjust sender / recipients properties props['sender'] = props['sender'].username recipients = [] for recip in props['recipients']: recipients.append({ 'id': recip.dn, 'label': recip.type == util.TYPE_USER and Display.user(recip.dict) or recip.name }) props['recipients'] = recipients # append final dict to result list MODULE.info('final project dict: %s' % props) result.append(props) self.finished(request.id, result) @sanitize(StringSanitizer(required=True)) def distribute(self, request): # update the sender information of the selected projects result = [] for iid in request.options: MODULE.info('Distribute project: %s' % iid) try: # make sure that project could be loaded iproject = util.Project.load(iid) if not iproject: raise IOError(_('Project "%s" could not be loaded') % iid) # make sure that only the project owner himself (or an admin) is able # to distribute a project if request.flavor == 'teacher' and not compare_dn(iproject.sender.dn, self.user_dn): raise ValueError(_('Only the owner himself or an administrator may distribute a project.')) # project was loaded successfully... try to distribute it usersFailed = [] iproject.distribute(usersFailed) # raise an error in case distribution failed for some users if usersFailed: MODULE.info('Failed processing the following users: %s' % usersFailed) usersStr = ', '.join([Display.user(i) for i in usersFailed]) raise IOError(_('The project could not distributed to the following users: %s') % usersStr) # save result result.append(dict( name=iid, success=True )) except (ValueError, IOError) as exc: result.append(dict( name=iid, success=False, details=str(exc) )) # return the results self.finished(request.id, result) @sanitize(StringSanitizer(required=True)) def collect(self, request): # try to open the UDM user object of the current user sender = self._get_sender() # update the sender information of the selected projects result = [] for iid in request.options: MODULE.info('Collect project: %s' % iid) try: # make sure that project could be loaded iproject = util.Project.load(iid) if not iproject: raise IOError(_('Project "%s" could not be loaded') % iid) # replace the projects sender with the current logged in user iproject.sender = sender # project was loaded successfully... try to distribute it dirsFailed = [] iproject.collect(dirsFailed) # raise an error in case distribution failed for some users if dirsFailed: dirsStr = ', '.join(dirsFailed) MODULE.info('Failed collecting the following dirs: %s' % dirsStr) raise IOError(_('The following user directories could not been collected: %s') % dirsStr) # save result result.append(dict( name=iid, success=True )) except (ValueError, IOError) as exc: result.append(dict( name=iid, success=False, details=str(exc) )) # return the results self.finished(request.id, result) @sanitize(StringSanitizer(required=True)) def adopt(self, request): # try to open the UDM user object of the current user sender = self._get_sender() # update the sender information of the selected projects result = [] for iid in request.options: try: # make sure that project could be loaded iproject = util.Project.load(iid) if not iproject: raise IOError(_('Project "%s" could not be loaded') % iid) # project was loaded successfully iproject.sender = sender iproject.save() except (ValueError, IOError) as exc: result.append(dict( name=iid, success=False, details=str(exc) )) # return the results self.finished(request.id, result) @sanitize(DictSanitizer(dict(object=StringSanitizer(required=True)), required=True)) def remove(self, request): """Removes the specified projects""" for iproject in [util.Project.load(ientry.get('object')) for ientry in request.options]: if not iproject: continue # make sure that only the project owner himself (or an admin) is able # to see the content of a project if request.flavor == 'teacher' and not compare_dn(iproject.sender.dn, self.user_dn): raise UMC_Error(_('Only the owner himself or an administrator may delete a project.'), status=403) # purge the project iproject.purge() self.finished(request.id, None)
class Instance(Base): def init(self): self.azure_response = None self.adconnection_alias = ucr.get(adconnection_wizard_ucrv) or None MODULE.process('adconnection_alias={!r}'.format(self.adconnection_alias)) @simple_response def query(self): fqdn = '%s.%s' % (ucr.get('hostname'), ucr.get('domainname')) return { 'initialized': AzureAuth.is_initialized(self.adconnection_alias), 'login-url': '{origin}/univention/command/office365/authorize', 'appid-url': 'https://%s/office365' % (fqdn,), 'base-url': 'https://%s/' % (fqdn,), } @file_upload @sanitize(DictSanitizer(dict( tmpfile=StringSanitizer(required=True) ), required=True)) @sanitize_body(DictSanitizer(dict( domain=StringSanitizer(required=True, minimum=1), adconnection_id=StringSanitizer(default='common'), ), required=True)) def upload(self, request): AzureAuth.uninitialize(self.adconnection_alias) try: adconnection_id = request.body.get('adconnection_id') or 'common' adconnection_id = urlparse.urlparse(adconnection_id).path.strip('/').split('/')[0] with open(request.options[0]['tmpfile']) as fd: manifest = Manifest(fd, adconnection_id, request.body['domain']) manifest.transform() except ManifestError as exc: raise UMC_Error(str(exc)) try: AzureAuth.store_manifest(manifest, self.adconnection_alias) except ADConnectionIDError: raise UMC_Error(_("Invalid federation metadata document address (e.g. https://login.microsoftonline.com/3e7d9eb4-c4a1-4cfd-893e-a8ec29e46b77/federationmetadata/2007-06/federationmetadata.xml).")) except AzureError as exc: raise UMC_Error(str(exc)) try: authorizationurl = AzureAuth.get_authorization_url(self.adconnection_alias) except AzureError as exc: raise UMC_Error(str(exc)) self.finished(request.id, { 'authorizationurl': authorizationurl, }, message=_('The manifest has been successfully uploaded.')) @allow_get_request def manifest_json(self, request): with open(AzureADConnectionHandler.get_conf_path('MANIFEST_FILE', self.adconnection_alias), 'rb') as fd: self.finished(request.id, fd.read(), mimetype='application/octet-stream') @allow_get_request def saml_setup_script(self, request): with open(SAML_SETUP_SCRIPT_PATH.format(adconnection_alias='_{}'.format(self.adconnection_alias) if self.adconnection_alias else ''), 'rb') as fd: self.finished(request.id, fd.read(), mimetype='application/octet-stream') @allow_get_request def public_signing_cert(self, request): with open(AzureADConnectionHandler.get_conf_path('SSL_CERT', self.adconnection_alias), 'rb') as fd: self.finished(request.id, fd.read(), mimetype='application/octet-stream') @allow_get_request @sanitize( id_token=StringSanitizer(), code=StringSanitizer(), session_state=StringSanitizer(), admin_consent=BooleanSanitizer(), error=StringSanitizer(), error_description=StringSanitizer() ) def authorize(self, request): self.init() # reset state in case the first attempt failed self.azure_response = {} self.azure_response.update(request.options) content = """<!DOCTYPE html> <html> <head> <title>%(title)s</title> <script type="application/javascript"> window.close(); window.top.close(); </script> </head> <body> %(content)s </body> </html> """ % { 'title': _('Office 365 Configuration finished'), 'content': _('The configuration has finished! You can now close this page and continue the configuration wizard.'), } self.finished(request.id, bytes(content), mimetype='text/html') @simple_response def state(self): options = self.azure_response if not options: return progress(message=_('Waiting for authorization to be completed.'), waiting=True) if options['id_token']: try: AzureAuth.parse_id_token(options['id_token'], self.adconnection_alias) AzureAuth.store_tokens(adconnection_alias=self.adconnection_alias, consent_given=True) aa = AzureAuth("office365", self.adconnection_alias) aa.write_saml_setup_script(self.adconnection_alias) aa.set_ucs_overview_link() aa.retrieve_access_token() # not really necessary, but it'll make sure everything worked except AzureError as exc: self.init() raise UMC_Error(str(exc)) options['id_token'] = None if self.adconnection_alias: ucrv_set = '{}{}={}'.format( adconnection_alias_ucrv, self.adconnection_alias, AzureAuth.load_azure_ids(self.adconnection_alias)['adconnection_id'] ) MODULE.process('Setting UCR {}...'.format(ucrv_set)) handler_set([ucrv_set]) return progress(message=_('Successfully authorized. Starting synchronization.')) elif options['error']: self.init() raise UMC_Error(_('Microsoft reported an error condition during authorization. It might help to reauthorize. Error message: {error}: {error_description}').format(**options)) elif AzureAuth.is_initialized(self.adconnection_alias): self.init() try: ah = AzureHandler(ucr, "wizard", self.adconnection_alias) users = ah.list_users() MODULE.process('Retrieved list of users: %r' % users) #except TokenError as exc: # return except AzureError as exc: raise UMC_Error(str(exc)) try: subprocess.call(["systemctl", "restart", "univention-directory-listener.service"]) except (EnvironmentError,): pass return progress(message=_('Successfully initialized'), finished=True) return progress(message=_('Not yet initialized.'))
class NoDoubleNameSanitizer(StringSanitizer): def _sanitize(self, value, name, further_arguments): from constants import COMPONENT_BASE ucr = univention.config_registry.ConfigRegistry() ucr.load() if '%s/%s' % (COMPONENT_BASE, value) in ucr: self.raise_validation_error( _("There already is a component with this name")) return value basic_components_sanitizer = DictSanitizer( { 'server': StringSanitizer(required=True, minimum=1), 'prefix': StringSanitizer(required=True), 'unmaintained': BooleanSanitizer(required=True), }, allow_other_keys=False, ) advanced_components_sanitizer = DictSanitizer({ 'server': StringSanitizer(), 'prefix': StringSanitizer(), 'unmaintained': BooleanSanitizer(), 'enabled': BooleanSanitizer(required=True), 'name': StringSanitizer(required=True, regex_pattern='^[A-Za-z0-9\-\_\.]+$'),
def _sanitize_dict(function, sanitized_attrs, sanitizer_parameters): defaults = {'default': {}, 'required': True, 'may_change_value': True} defaults.update(sanitizer_parameters) return _sanitize(function, DictSanitizer(sanitized_attrs, **defaults))
class Instance(umcm.Base, ProgressMixin): def init(self): os.umask(0o022) # umc umask is too restrictive for app center as it creates a lot of files in docker containers self.ucr = ucr_instance() self.update_applications_done = False util.install_opener(self.ucr) self._remote_progress = {} try: self.package_manager = PackageManager( info_handler=MODULE.process, step_handler=None, error_handler=MODULE.warn, lock=False, ) except SystemError as exc: MODULE.error(str(exc)) raise umcm.UMC_Error(str(exc), status=500) self.package_manager.set_finished() # currently not working. accepting new tasks self.uu = UniventionUpdater(False) self.component_manager = util.ComponentManager(self.ucr, self.uu) get_package_manager._package_manager = self.package_manager # in order to set the correct locale for Application locale.setlocale(locale.LC_ALL, str(self.locale)) try: log_to_logfile() except IOError: pass # connect univention.appcenter.log to the progress-method handler = ProgressInfoHandler(self.package_manager) handler.setLevel(logging.INFO) get_base_logger().addHandler(handler) percentage = ProgressPercentageHandler(self.package_manager) percentage.setLevel(logging.DEBUG) get_base_logger().getChild('actions.install.progress').addHandler(percentage) get_base_logger().getChild('actions.upgrade.progress').addHandler(percentage) get_base_logger().getChild('actions.remove.progress').addHandler(percentage) def error_handling(self, etype, exc, etraceback): error_handling(etype, exc, etraceback) return super(Instance, self).error_handling(exc, etype, etraceback) @simple_response def version(self): info = get_action('info') return info.get_compatibility() @simple_response def query(self, quick=False): if not quick: self.update_applications() self.ucr.load() reload_package_manager() list_apps = get_action('list') domain = get_action('domain') apps = list_apps.get_apps() if self.ucr.is_true('appcenter/docker', True): if not self._test_for_docker_service(): raise umcm.UMC_Error(_('The docker service is not running! The App Center will not work properly.') + ' ' + _('Make sure docker.io is installed, try starting the service with "service docker start".')) info = domain.to_dict(apps) if quick: ret = [] for app in info: if app is None: ret.append(None) else: short_info = {} for attr in ['id', 'name', 'vendor', 'maintainer', 'description', 'long_description', 'categories', 'end_of_life', 'update_available', 'logo_name', 'is_installed_anywhere', 'is_installed', 'installations']: short_info[attr] = app[attr] ret.append(short_info) return ret else: return info def update_applications(self): if self.ucr.is_true('appcenter/umc/update/always', True): update = get_action('update') try: update.call() except NetworkError as err: raise umcm.UMC_Error(str(err)) except Abort: pass Application._all_applications = None self.update_applications_done = True def _test_for_docker_service(self): if docker_bridge_network_conflict(): msg = _('A conflict between the system network settings and the docker bridge default network has been detected.') + '\n\n' msg += _('Please either configure a different network for the docker bridge by setting the UCR variable docker/daemon/default/opts/bip to a different network and restart the system,') + ' ' msg += _('or disable the docker support in the AppCenter by setting appcenter/docker to false.') raise umcm.UMC_Error(msg) if not docker_is_running(): MODULE.warn('Docker is not running! Trying to start it now...') call_process(['invoke-rc.d', 'docker', 'start']) if not docker_is_running(): return False return True @simple_response def enable_docker(self): if self._test_for_docker_service(): ucr_save({'appcenter/docker': 'enabled'}) else: raise umcm.UMC_Error(_('Unable to start the docker service!') + ' ' + _('Make sure docker.io is installed, try starting the service with "service docker start".')) @require_apps_update @require_password @simple_response(with_progress=True) def sync_ldap(self): register = get_action('register') register.call(username=self.username, password=self.password) # used in updater-umc @simple_response def get_by_component_id(self, component_id): domain = get_action('domain') if isinstance(component_id, list): requested_apps = [Apps().find_by_component_id(cid) for cid in component_id] return domain.to_dict(requested_apps) else: app = Apps().find_by_component_id(component_id) if app: return domain.to_dict([app])[0] else: raise umcm.UMC_Error(_('Could not find an application for %s') % component_id) # used in updater-umc @simple_response def app_updates(self): upgrade = get_action('upgrade') domain = get_action('domain') return domain.to_dict(list(upgrade.iter_upgradable_apps())) @sanitize(application=StringSanitizer(minimum=1, required=True)) @simple_response def get(self, application): domain = get_action('domain') app = Apps().find(application) if app is None: raise umcm.UMC_Error(_('Could not find an application for %s') % (application,)) return domain.to_dict([app])[0] @sanitize(app=AppSanitizer(required=True), values=DictSanitizer({})) @simple_response def configure(self, app, autostart, values): configure = get_action('configure') configure.call(app=app, set_vars=values, autostart=autostart) @sanitize(app=AppSanitizer(required=True), mode=ChoicesSanitizer(['start', 'stop'])) @simple_response def app_service(self, app, mode): service = get_action(mode) service.call(app=app) @sanitize(app=AppSanitizer(required=False), action=ChoicesSanitizer(['get', 'buy', 'search']), value=StringSanitizer()) @simple_response def track(self, app, action, value): send_information(action, app=app, value=value) def invoke_dry_run(self, request): request.options['only_dry_run'] = True self.invoke(request) @require_password @sanitize( host=StringSanitizer(required=True), function=ChoicesSanitizer(['install', 'update', 'uninstall'], required=True), app=StringSanitizer(required=True), force=BooleanSanitizer(), values=DictSanitizer({}) ) @simple_response(with_progress=True) def invoke_remote_docker(self, host, function, app, force, values, progress): options = {'function': function, 'app': app, 'force': force, 'values': values} client = Client(host, self.username, self.password) result = client.umc_command('appcenter/docker/invoke', options).result self._remote_progress[progress.id] = client, result['id'] @simple_response def remote_progress(self, progress_id): try: client, remote_progress_id = self._remote_progress[progress_id] except KeyError: # actually happens: before invoke_remote_docker is finished, remote_progress is already called return {} else: return client.umc_command('appcenter/docker/progress', {'progress_id': remote_progress_id}).result @require_apps_update @require_password @sanitize( function=MappingSanitizer({ 'install': 'install', 'update': 'upgrade', 'uninstall': 'remove', }, required=True), app=AppSanitizer(required=True), force=BooleanSanitizer(), values=DictSanitizer({}) ) @simple_response(with_progress=True) def invoke_docker(self, function, app, force, values, progress): if function == 'upgrade': app = Apps().find_candidate(app) serious_problems = False progress.title = _('%s: Running tests') % (app.name,) errors, warnings = app.check(function) can_continue = force # "dry_run" if errors: MODULE.process('Cannot %s %s: %r' % (function, app.id, errors)) serious_problems = True can_continue = False if warnings: MODULE.process('Warning trying to %s %s: %r' % (function, app.id, warnings)) result = { 'serious_problems': serious_problems, 'invokation_forbidden_details': errors, 'invokation_warning_details': warnings, 'can_continue': can_continue, 'software_changes_computed': False, } if can_continue: with self.locked(): kwargs = {'noninteractive': True, 'skip_checks': ['shall_have_enough_ram', 'shall_only_be_installed_in_ad_env_with_password_service', 'must_not_have_concurrent_operation']} if function == 'install': progress.title = _('Installing %s') % (app.name,) kwargs['set_vars'] = values elif function == 'uninstall': progress.title = _('Uninstalling %s') % (app.name,) elif function == 'upgrade': progress.title = _('Upgrading %s') % (app.name,) action = get_action(function) handler = UMCProgressHandler(progress) handler.setLevel(logging.INFO) action.logger.addHandler(handler) try: result['success'] = action.call(app=app, username=self.username, password=self.password, **kwargs) except AppCenterError as exc: raise umcm.UMC_Error(str(exc), result=dict( display_feedback=True, title='%s %s' % (exc.title, exc.info))) finally: action.logger.removeHandler(handler) return result @contextmanager def locked(self): try: with self.package_manager.locked(reset_status=True, set_finished=True): yield except LockError: raise umcm.UMC_Error(_('Another package operation is in progress')) @require_apps_update @require_password @sanitize( function=ChoicesSanitizer(['install', 'uninstall', 'update', 'install-schema', 'update-schema'], required=True), application=StringSanitizer(minimum=1, required=True), force=BooleanSanitizer(), host=StringSanitizer(), only_dry_run=BooleanSanitizer(), dont_remote_install=BooleanSanitizer(), values=DictSanitizer({}) ) def invoke(self, request): # ATTENTION!!!!!!! # this function has to stay compatible with the very first App Center installations (Dec 2012) # if you add new arguments that change the behaviour # you should add a new method (see invoke_dry_run) or add a function name (e.g. install-schema) # this is necessary because newer app center may talk remotely with older one # that does not understand new arguments and behaves the old way (in case of # dry_run: install application although they were asked to dry_run) host = request.options.get('host') function = request.options.get('function') send_as = function if function.startswith('install'): function = 'install' if function.startswith('update'): function = 'update' application_id = request.options.get('application') Application.all(only_local=True) # if not yet cached, cache. but use only local inis application = Application.find(application_id) if application is None: raise umcm.UMC_Error(_('Could not find an application for %s') % (application_id,)) force = request.options.get('force') only_dry_run = request.options.get('only_dry_run') dont_remote_install = request.options.get('dont_remote_install') only_master_packages = send_as.endswith('schema') MODULE.process('Try to %s (%s) %s on %s. Force? %r. Only master packages? %r. Prevent installation on other systems? %r. Only dry run? %r.' % (function, send_as, application_id, host, force, only_master_packages, dont_remote_install, only_dry_run)) # REMOTE invocation! if host and host != self.ucr.get('hostname'): try: client = Client(host, self.username, self.password) result = client.umc_command('appcenter/invoke', request.options).result except (ConnectionError, HTTPError) as exc: MODULE.error('Error during remote appcenter/invoke: %s' % (exc,)) result = { 'unreachable': [host], 'master_unreachable': True, 'serious_problems': True, 'software_changes_computed': True, # not really... } else: if result['can_continue']: def _thread_remote(_client, _package_manager): with _package_manager.locked(reset_status=True, set_finished=True): _package_manager.unlock() # not really locked locally, but busy, so "with locked()" is appropriate Application._query_remote_progress(_client, _package_manager) def _finished_remote(thread, result): if isinstance(result, BaseException): MODULE.warn('Exception during %s %s: %s' % (function, application_id, str(result))) thread = notifier.threads.Simple('invoke', notifier.Callback(_thread_remote, client, self.package_manager), _finished_remote) thread.run() self.finished(request.id, result) return # make sure that the application can be installed/updated can_continue = True delayed_can_continue = True serious_problems = False result = { 'install': [], 'remove': [], 'broken': [], 'unreachable': [], 'master_unreachable': False, 'serious_problems': False, 'hosts_info': {}, 'problems_with_hosts': False, 'serious_problems_with_hosts': False, 'invokation_forbidden_details': {}, 'invokation_warning_details': {}, 'software_changes_computed': False, } if not application: MODULE.process('Application not found: %s' % application_id) can_continue = False if can_continue and not only_master_packages: forbidden, warnings = application.check_invokation(function, self.package_manager) if forbidden: MODULE.process('Cannot %s %s: %r' % (function, application_id, forbidden)) result['invokation_forbidden_details'] = forbidden can_continue = False serious_problems = True if warnings: MODULE.process('Warning trying to %s %s: %r' % (function, application_id, forbidden)) result['invokation_warning_details'] = warnings if not force: # dont stop "immediately". # compute the package changes! delayed_can_continue = False result['serious_problems'] = serious_problems result['can_continue'] = can_continue try: if can_continue: if self._working(): # make it multi-tab safe (same session many buttons to be clicked) raise LockError() with self.package_manager.locked(reset_status=True): previously_registered_by_dry_run = False if can_continue and function in ('install', 'update'): remove_component = only_dry_run dry_run_result, previously_registered_by_dry_run = application.install_dry_run(self.package_manager, self.component_manager, remove_component=remove_component, username=self._username, password=self.password, only_master_packages=only_master_packages, dont_remote_install=dont_remote_install, function=function, force=force) result.update(dry_run_result) result['software_changes_computed'] = True serious_problems = bool(result['broken'] or result['master_unreachable'] or result['serious_problems_with_hosts']) if serious_problems or (not force and (result['unreachable'] or result['install'] or result['remove'] or result['problems_with_hosts'])): MODULE.process('Problems encountered or confirmation required. Removing component %s' % application.component_id) if not remove_component: # component was not removed automatically after dry_run if application.candidate: # operation on candidate failed. re-register original application application.register(self.component_manager, self.package_manager) else: # operation on self failed. unregister all application.unregister_all_and_register(None, self.component_manager, self.package_manager) can_continue = False elif can_continue and function in ('uninstall',) and not force: result['remove'] = application.uninstall_dry_run(self.package_manager) result['software_changes_computed'] = True can_continue = False can_continue = can_continue and delayed_can_continue and not only_dry_run result['serious_problems'] = serious_problems result['can_continue'] = can_continue if can_continue and not only_dry_run: def _thread(module, application, function): with module.package_manager.locked(set_finished=True): with module.package_manager.no_umc_restart(exclude_apache=True): if function in ('install', 'update'): # dont have to add component: already added during dry_run return application.install(module.package_manager, module.component_manager, add_component=only_master_packages, send_as=send_as, username=self._username, password=self.password, only_master_packages=only_master_packages, dont_remote_install=dont_remote_install, previously_registered_by_dry_run=previously_registered_by_dry_run) else: return application.uninstall(module.package_manager, module.component_manager, self._username, self.password) def _finished(thread, result): if isinstance(result, BaseException): MODULE.warn('Exception during %s %s: %s' % (function, application_id, str(result))) thread = notifier.threads.Simple('invoke', notifier.Callback(_thread, self, application, function), _finished) thread.run() else: self.package_manager.set_finished() # nothing to do, ready to take new commands self.finished(request.id, result) except LockError: # make it thread safe: another process started a package manager # this module instance already has a running package manager raise umcm.UMC_Error(_('Another package operation is in progress')) def keep_alive(self, request): ''' Fix for Bug #30611: UMC kills appcenter module if no request is sent for $(ucr get umc/module/timeout). this happens if a user logs out during a very long installation. this function will be run by the frontend to always have one connection open to prevent killing the module. ''' def _thread(): while not self.package_manager.progress_state._finished: time.sleep(1) def _finished(thread, result): success = not isinstance(result, BaseException) if not success: MODULE.warn('Exception during keep_alive: %s' % result) self.finished(request.id, success) thread = notifier.threads.Simple('keep_alive', notifier.Callback(_thread), _finished) thread.run() @simple_response def ping(self): return True @simple_response def buy(self, application): app = Apps().find(application) if not app or not app.shop_url: return None ret = {} ret['key_id'] = LICENSE.uuid ret['ucs_version'] = self.ucr.get('version/version') ret['app_id'] = app.id ret['app_version'] = app.version # ret['locale'] = locale.getlocale()[0] # done by frontend ret['user_count'] = None # FIXME: get users and computers from license ret['computer_count'] = None return ret @simple_response def enable_disable_app(self, application, enable=True): app = Apps().find(application) if not app: return stall = get_action('stall') stall.call(app=app, undo=enable) @simple_response def packages_sections(self): """ fills the 'sections' combobox in the search form """ sections = set() cache = apt.Cache() for package in cache: sections.add(package.section) return sorted(sections) @sanitize(pattern=PatternSanitizer(required=True)) @simple_response def packages_query(self, pattern, section='all', key='package'): """ Query to fill the grid. Structure is fixed here. """ result = [] for package in self.package_manager.packages(reopen=True): if section == 'all' or package.section == section: toshow = False if pattern.pattern == '^.*$': toshow = True elif key == 'package' and pattern.search(package.name): toshow = True elif key == 'description' and package.candidate and pattern.search(package.candidate.raw_description): toshow = True if toshow: result.append(self._package_to_dict(package, full=False)) return result @simple_response def packages_get(self, package): """ retrieves full properties of one package """ package = self.package_manager.get_package(package) if package is not None: return self._package_to_dict(package, full=True) else: # TODO: 404? return {} @sanitize( function=MappingSanitizer({ 'install': 'install', 'upgrade': 'install', 'uninstall': 'remove', }, required=True), packages=ListSanitizer(StringSanitizer(minimum=1), required=True), update=BooleanSanitizer() ) @simple_response def packages_invoke_dry_run(self, packages, function, update): if update: self.package_manager.update() packages = self.package_manager.get_packages(packages) kwargs = {'install': [], 'remove': [], 'dry_run': True} if function == 'install': kwargs['install'] = packages else: kwargs['remove'] = packages return dict(zip(['install', 'remove', 'broken'], self.package_manager.mark(**kwargs))) @sanitize( function=MappingSanitizer({ 'install': 'install', 'upgrade': 'install', 'uninstall': 'remove', }, required=True), packages=ListSanitizer(StringSanitizer(minimum=1), required=True) ) def packages_invoke(self, request): """ executes an installer action """ packages = request.options.get('packages') function = request.options.get('function') try: if self._working(): # make it multi-tab safe (same session many buttons to be clicked) raise LockError() with self.package_manager.locked(reset_status=True): not_found = [pkg_name for pkg_name in packages if self.package_manager.get_package(pkg_name) is None] self.finished(request.id, {'not_found': not_found}) if not not_found: def _thread(package_manager, function, packages): with package_manager.locked(set_finished=True): with package_manager.no_umc_restart(exclude_apache=True): if function == 'install': package_manager.install(*packages) else: package_manager.uninstall(*packages) def _finished(thread, result): if isinstance(result, BaseException): MODULE.warn('Exception during %s %s: %r' % (function, packages, str(result))) thread = notifier.threads.Simple('invoke', notifier.Callback(_thread, self.package_manager, function, packages), _finished) thread.run() else: self.package_manager.set_finished() # nothing to do, ready to take new commands except LockError: # make it thread safe: another process started a package manager # this module instance already has a running package manager raise umcm.UMC_Error(_('Another package operation is in progress')) def _working(self): return not self.package_manager.progress_state._finished @simple_response def working(self): # TODO: PackageManager needs is_idle() or something # preferably the package_manager can tell what is currently executed: # package_manager.is_working() => False or _('Installing UCC') return self._working() @simple_response def custom_progress(self): timeout = 5 return self.package_manager.poll(timeout) def _package_to_dict(self, package, full): """ Helper that extracts properties from a 'apt_pkg.Package' object and stores them into a dictionary. Depending on the 'full' switch, stores only limited (for grid display) or full (for detail view) set of properties. """ installed = package.installed # may be None found = True candidate = package.candidate found = candidate is not None if not found: candidate = NoneCandidate() result = { 'package': package.name, 'installed': package.is_installed, 'upgradable': package.is_upgradable and found, 'summary': candidate.summary, } # add (and translate) a combined status field # *** NOTE *** we translate it here: if we would use the Custom Formatter # of the grid then clicking on the sort header would not work. if package.is_installed: if package.is_upgradable: result['status'] = _('upgradable') else: result['status'] = _('installed') else: result['status'] = _('not installed') # additional fields needed for detail view if full: # Some fields differ depending on whether the package is installed or not: if package.is_installed: result['section'] = installed.section result['priority'] = installed.priority or '' result['summary'] = installed.summary # take the current one result['description'] = installed.description result['installed_version'] = installed.version result['size'] = installed.installed_size if package.is_upgradable: result['candidate_version'] = candidate.version else: del result['upgradable'] # not installed: don't show 'upgradable' at all result['section'] = candidate.section result['priority'] = candidate.priority or '' result['description'] = candidate.description result['size'] = candidate.installed_size result['candidate_version'] = candidate.version # format size to handle bytes size = result['size'] byte_mods = ['B', 'kB', 'MB'] for byte_mod in byte_mods: if size < 10000: break size = float(size) / 1000 # MB, not MiB else: size = size * 1000 # once too often if size == int(size): format_string = '%d %s' else: format_string = '%.2f %s' result['size'] = format_string % (size, byte_mod) return result @simple_response def components_query(self): """ Returns components list for the grid in the ComponentsPage. """ # be as current as possible. self.uu.ucr_reinit() self.ucr.load() result = [] for comp in self.uu.get_all_components(): result.append(self.component_manager.component(comp)) return result @sanitize_list(StringSanitizer()) @multi_response(single_values=True) def components_get(self, iterator, component_id): # be as current as possible. self.uu.ucr_reinit() self.ucr.load() for component_id in iterator: yield self.component_manager.component(component_id) @sanitize_list(DictSanitizer({'object': advanced_components_sanitizer})) @multi_response def components_put(self, iterator, object): """Writes back one or more component definitions. """ # umc.widgets.Form wraps the real data into an array: # # [ # { # 'object' : { ... a dict with the real data .. }, # 'options': None # }, # ... more such entries ... # ] # # Current approach is to return a similarly structured array, # filled with elements, each one corresponding to one array # element of the request: # # [ # { # 'status' : a number where 0 stands for success, anything else # is an error code # 'message' : a result message # 'object' : a dict of field -> error message mapping, allows # the form to show detailed error information # }, # ... more such entries ... # ] with util.set_save_commit_load(self.ucr) as super_ucr: for object, in iterator: yield self.component_manager.put(object, super_ucr) self.package_manager.update() # do the same as components_put (update) # but dont allow adding an already existing entry components_add = sanitize_list(DictSanitizer({'object': add_components_sanitizer}))(components_put) components_add.__name__ = 'components_add' @sanitize_list(StringSanitizer()) @multi_response(single_values=True) def components_del(self, iterator, component_id): for component_id in iterator: yield self.component_manager.remove(component_id) self.package_manager.update() @multi_response def settings_get(self, iterator): # *** IMPORTANT *** Our UCR copy must always be current. This is not only # to catch up changes made via other channels (ucr command line etc), # but also to reflect the changes we have made ourselves! self.ucr.load() for _ in iterator: yield { 'unmaintained': self.ucr.is_true('repository/online/unmaintained', False), 'server': self.ucr.get('repository/online/server', ''), 'prefix': self.ucr.get('repository/online/prefix', ''), } @sanitize_list( DictSanitizer({'object': basic_components_sanitizer}), min_elements=1, max_elements=1 # moduleStore with one element... ) @multi_response def settings_put(self, iterator, object): # FIXME: returns values although it should yield (multi_response) changed = False # Set values into our UCR copy. try: with util.set_save_commit_load(self.ucr) as super_ucr: for object, in iterator: for key, value in object.iteritems(): MODULE.info(" ++ Setting new value for '%s' to '%s'" % (key, value)) super_ucr.set_registry_var('%s/%s' % (constants.ONLINE_BASE, key), value) changed = super_ucr.changed() except Exception as e: MODULE.warn(" !! Writing UCR failed: %s" % str(e)) return [{'message': str(e), 'status': constants.PUT_WRITE_ERROR}] self.package_manager.update() # Bug #24878: emit a warning if repository is not reachable try: updater = self.uu for line in updater.print_version_repositories().split('\n'): if line.strip(): break else: raise ConfigurationError() except ConfigurationError: msg = _("There is no repository at this server (or at least none for the current UCS version)") MODULE.warn(" !! Updater error: %s" % msg) response = {'message': msg, 'status': constants.PUT_UPDATER_ERROR} # if nothing was committed, we want a different type of error code, # just to appropriately inform the user if changed: response['status'] = constants.PUT_UPDATER_NOREPOS return [response] except: info = sys.exc_info() emsg = '%s: %s' % info[:2] MODULE.warn(" !! Updater error [%s]: %s" % (emsg)) return [{'message': str(info[1]), 'status': constants.PUT_UPDATER_ERROR}] return [{'status': constants.PUT_SUCCESS}]