예제 #1
0
class Instance(SchoolBaseModule):
    @sanitize(
        **{
            'school': SchoolSanitizer(required=True),
            'class': StringSanitizer(required=True),  # allow_none=True
            'pattern': StringSanitizer(required=True),
        })
    @LDAP_Connection()
    def query(self, request, ldap_user_read=None, ldap_position=None):
        """Searches for students"""

        klass = request.options.get('class')
        if klass in (None, 'None'):
            klass = None
        result = [{
            'id': usr.dn,
            'name': Display.user(usr),
            'passwordexpiry': usr.get('passwordexpiry', '')
        } for usr in self._users(ldap_user_read,
                                 request.options['school'],
                                 group=klass,
                                 user_type=request.flavor,
                                 pattern=request.options.get('pattern', ''))]
        self.finished(request.id, result)

    @sanitize(
        userDN=StringSanitizer(required=True),
        newPassword=StringSanitizer(required=True, minimum=1),
        nextLogin=BooleanSanitizer(default=True),
    )
    @LDAP_Connection(USER_WRITE)
    def password_reset(self, request, ldap_user_write=None):
        '''Reset the password of the user'''
        userdn = request.options['userDN']
        pwdChangeNextLogin = request.options['nextLogin']
        newPassword = request.options['newPassword']

        try:
            user = User.from_dn(
                userdn, None, ldap_user_write).get_udm_object(ldap_user_write)
            user['password'] = newPassword
            user['overridePWHistory'] = '1'
            user['locked'] = 'none'
            user['pwdChangeNextLogin'] = '******' if pwdChangeNextLogin else '0'
            user.modify()
            self.finished(request.id, True)
        except udm_exceptions.permissionDenied as exc:
            MODULE.process('dn=%r' % (userdn, ))
            MODULE.process('exception=%s' % (type(exc), ))
            raise UMC_Error(_('permission denied'))
        except udm_exceptions.base as exc:
            MODULE.process('dn=%r' % (userdn, ))
            MODULE.process('exception=%s' % (type(exc), ))
            MODULE.process('exception=%s' % (exc.message, ))
            raise UMC_Error('%s' % (get_exception_msg(exc)))
class Instance(Base):
    def init(self):
        # this initialization method is called when the
        # module process is started and the configuration from the
        # UMC server is completed
        pass

    def configuration(self, request):
        """Returns a directionary of initial values for the form."""
        self.finished(
            request.id, {
                'sender': self.username + '@example.com',
                'subject': 'Test mail from PACKAGENAME',
                'recipient': '*****@*****.**'
            })

    @sanitize(
        sender=StringSanitizer(required=True),
        recipient=StringSanitizer(required=True),
        subject=StringSanitizer(required=True),
        message=StringSanitizer(required=True),
    )
    def send(self, request):
        def _send_thread(sender, recipient, subject, message):
            MODULE.info('sending mail: thread running')

            # FIXME: contains header injection
            msg = u'From: ' + sender + u'\r\n'
            msg += u'To: ' + recipient + u'\r\n'
            msg += u'Subject: %s\r\n' % subject
            msg += u'\r\n'
            msg += message + u'\r\n'
            msg += u'\r\n'

            msg = msg.encode('latin1')

            server = smtplib.SMTP('localhost')
            server.set_debuglevel(0)
            server.sendmail(sender, recipient, msg)
            server.quit()
            return True

        func = notifier.Callback(_send_thread, request.options['sender'],
                                 request.options['recipient'],
                                 request.options['subject'],
                                 request.options['message'])
        MODULE.info('sending mail: starting thread')
        cb = notifier.Callback(self.thread_finished_callback, request)
        thread = notifier.threads.Simple('mailing', func, cb)
        thread.run()
예제 #3
0
	def __init__(self, **kwargs):
		"""A LDAP attribute name.
			must at least be 1 character long.

			This sanitizer prevents LDAP search filter injections in the attribute name.

			TODO: in theory we should only allow existing attributes for the request object(/object type)
		"""
		args = dict(
			minimum=1,
			regex_pattern=r'^[\w\d\-;]+$'
		)
		args.update(kwargs)
		StringSanitizer.__init__(self, **args)
class Instance(Base):
    def init(self):
        # this initialization method is called when the
        # module process is started and the configuration from the
        # UMC server is completed
        pass

    def configuration(self, request):
        """Returns a directionary of initial values for the form."""
        self.finished(
            request.id, {
                'sender': self.username + '@example.com',
                'subject': 'Test mail from PACKAGENAME',
                'recipient': '*****@*****.**'
            })

    @sanitize(
        sender=StringSanitizer(required=True),
        recipient=StringSanitizer(required=True),
        subject=StringSanitizer(required=True),
        message=StringSanitizer(required=True),
    )
    def send(self, request):
        def _send_thread(sender, recipient, subject, message):
            MODULE.info('sending mail: thread running')

            msg = MIMENonMultipart('text', 'plain', charset='utf-8')
            cs = email.charset.Charset("utf-8")
            cs.body_encoding = email.charset.QP
            msg["Subject"] = subject
            msg["From"] = sender
            msg["To"] = recipient
            msg.set_payload(message, charset=cs)

            server = smtplib.SMTP('localhost')
            server.set_debuglevel(0)
            server.sendmail(sender, recipient, msg.as_string())
            server.quit()
            return True

        func = notifier.Callback(_send_thread, request.options['sender'],
                                 request.options['recipient'],
                                 request.options['subject'],
                                 request.options['message'])
        MODULE.info('sending mail: starting thread')
        cb = notifier.Callback(self.thread_finished_callback, request)
        thread = notifier.threads.Simple('mailing', func, cb)
        thread.run()
예제 #5
0
class Instance(umcm.Base):
	@sanitize(action=ChoicesSanitizer(['reboot', 'halt'], required=True),
		message=StringSanitizer(required=True))
	def reboot(self, request):
		message = None
		if request.options['action'] == 'halt':
			do = 'h'
			target = _('The system is going down for system halt NOW '
			           'with following message: ')
		elif request.options['action'] == 'reboot':
			do = 'r'
			target = _('The system is going down for reboot NOW '
			           'with following message: ')

		unicode_message = '%s%s' % (target, request.options['message'])
		message = unicode_message.encode('utf-8')
		subprocess.call(('/usr/bin/logger', '-f', '/var/log/syslog',
		                 '-t', 'UMC', message))
		shutdown_failed = subprocess.call(('/sbin/shutdown', '-%s' %do,
		                                   'now', message))
		if shutdown_failed:
			message = _('System could not reboot/shutdown')
			request.status = MODULE_ERR
		else:
			request.status = SUCCESS

		self.finished(request.id, None, message)
예제 #6
0
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())
예제 #7
0
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(Base, ProgressMixin):
	OPTION_MAPPING = (
		('LDAP_Host', 'connector/ad/ldap/host', ''),
		('LDAP_Base', 'connector/ad/ldap/base', ''),
		('LDAP_BindDN', 'connector/ad/ldap/binddn', ''),
		('KerberosDomain', 'connector/ad/mapping/kerberosdomain', ''),
		('PollSleep', 'connector/ad/poll/sleep', 5),
		('RetryRejected', 'connector/ad/retryrejected', 10),
		('DebugLevel', 'connector/debug/level', 2),
		('DebugFunction', 'connector/debug/function', False),
		('MappingSyncMode', 'connector/ad/mapping/syncmode', 'sync'),
		('MappingGroupLanguage', 'connector/ad/mapping/group/language', 'de')
	)

	def init(self):
		self.__update_status()

	def state(self, request):
		"""Retrieve current status of the Active Directory connection configuration and the service

		options: {}

		return: { 'configured' : (True|False), 'certificate' : (True|False), 'running' : (True|False) }
		"""

		self.__update_status()
		self.finished(request.id, {
			'ssl_enabled': self.status_ssl,
			'password_sync_enabled': self.status_password_sync,
			'running': self.status_running,
			'certificate': self.status_certificate,
			'mode_admember': self.status_mode_admember,
			'mode_adconnector': self.status_mode_adconnector,
			'configured': self.status_mode_adconnector or self.status_mode_admember,
			'server_role': ucr.get('server/role'),
		})

	def load(self, request):
		"""Retrieve current status of the Active Directory connection configuration and the service

		options: {}

		return: { <all AD connector UCR variables> }
		"""

		result = {}
		for option, var, default in Instance.OPTION_MAPPING:
			result[option] = ucr.get(var, default)

		pwd_file = ucr.get('connector/ad/ldap/bindpw')
		result['passwordExists'] = bool(pwd_file and os.path.exists(pwd_file))

		self.finished(request.id, result)

	@sanitize(LDAP_Host=StringSanitizer(required=True))
	def adconnector_save(self, request):
		"""Saves the Active Directory connection configuration

		options:
			Host_IP: IP address of the AD server
			LDAP_Host: hostname of the AD server
			LDAP_Base: LDAP base of the AD server
			LDAP_BindDN: LDAP DN to use for authentication
			KerberosDomain: kerberos domain
			PollSleep: time in seconds between polls
			RetryRejected: how many time to retry a synchronisation
			MappingSyncMode: synchronisation mode
			MappingGroupLanguage: language of the AD server

		return: { 'success' : (True|False), 'message' : <details> }
		"""

		self.required_options(request, 'Host_IP')
		self.required_options(request, *[x[0] for x in Instance.OPTION_MAPPING if x[2] == ''])

		for umckey, ucrkey, default in Instance.OPTION_MAPPING:
			val = request.options.get(umckey, default)
			if val:
				if isinstance(val, bool):
					val = val and 'yes' or 'no'
				MODULE.info('Setting %s=%s' % (ucrkey, val))
				univention.config_registry.handler_set([u'%s=%s' % (ucrkey, val)])

		ucr.load()
		if ucr.get('connector/ad/ldap/ldaps'):
			MODULE.info('Unsetting connector/ad/ldap/ldaps')
			univention.config_registry.handler_unset([u'connector/ad/ldap/ldaps'])
		if ucr.get('connector/ad/ldap/port') == '636':
			MODULE.info('Setting ldap port to 389')
			univention.config_registry.handler_set([u'connector/ad/ldap/port=389'])

		if not request.options.get('LDAP_Password') in (None, '', DO_NOT_CHANGE_PWD):
			fn = ucr.get('connector/ad/ldap/bindpw', FN_BINDPW)
			try:
				fd = open(fn, 'w')
				fd.write(request.options.get('LDAP_Password'))
				fd.close()
				os.chmod(fn, 0600)
				os.chown(fn, 0, 0)
				univention.config_registry.handler_set([u'connector/ad/ldap/bindpw=%s' % fn])
			except Exception, e:
				MODULE.info('Saving bind password failed (filename=%(fn)s ; exception=%(exception)s)' % {'fn': fn, 'exception': str(e.__class__)})
				self.finished(request.id, {'success': False, 'message': _('Saving bind password failed (filename=%(fn)s ; exception=%(exception)s)') % {'fn': fn, 'exception': str(e.__class__)}})
				return

		ssldir = '/etc/univention/ssl/%s' % request.options.get('LDAP_Host')
		if not os.path.exists(ssldir):
			self._create_certificate(request)
			return

		# enter a static host entry such that the AD server's FQDN can be resolved
		univention.config_registry.handler_set([u'hosts/static/%(Host_IP)s=%(LDAP_Host)s' % request.options])

		# check for SSL support on AD side
		if admember.server_supports_ssl(server=request.options.get('LDAP_Host')):
			MODULE.process('Enabling SSL...')
			admember.enable_ssl()
		else:
			MODULE.warn('SSL is not supported')
			admember.disable_ssl()

		# UCR variables are set, and now we can try to guess the language of
		# the AD domain
		ad_lang = guess_ad_domain_language()
		univention.config_registry.handler_set([u'connector/ad/mapping/group/language=%s' % ad_lang])

		self.finished(request.id, {'success': True, 'message': _('Active Directory connection settings have been saved.')})
		self.status_running = self.__is_process_running('*python*univention/connector/ad/main.py*')
		self.status_mode_admember = admember.is_localhost_in_admember_mode(ucr)
		self.status_mode_adconnector = admember.is_localhost_in_adconnector_mode(ucr)

	def __is_process_running(self, command):
		for proc in psutil.process_iter():
			try:
				cmdline = proc.cmdline() if callable(proc.cmdline) else proc.cmdline
			except psutil.NoSuchProcess:
				continue
			if cmdline and fnmatch.fnmatch(' '.join(cmdline), command):
				return True
		return False

	@sanitize(
		username=StringSanitizer(required=True),
		password=StringSanitizer(required=True),
		ad_server_address=StringSanitizer(required=True),
		mode=StringSanitizer(default='admember'),
	)
	@simple_response
	def check_domain(self, username, password, ad_server_address, mode):
		ad_domain_info = {}
		try:
			if mode == 'admember':
				admember.check_server_role()
			ad_domain_info = admember.lookup_adds_dc(ad_server_address)

			ad_server_ip = ad_domain_info['DC IP']
			if mode == 'admember':
				admember.check_domain(ad_domain_info)
class Instance(Base):
    def init(self):
        self.progress_state = Progress()

    @simple_response
    def dpkg_locked(self):
        """Do not execute join scripts when dpkg is running (e.g. via
		App Center)
		"""
        return self._dpkg_locked()

    def _dpkg_locked(self):
        fd = apt_pkg.get_lock('/var/lib/dpkg/lock')
        if fd == -1:
            return True
        else:
            os.close(fd)
            return False

    @simple_response
    def query(self):
        """collects status about join scripts"""

        # unjoined system?
        if not self._joined:
            return []

        # List all join scripts
        files = {}
        for fname in os.listdir(INSTDIR):
            match = RE_JOINFILE.match(fname)
            if match:
                entry = match.groupdict()
                entry['configured'] = True
                entry['status'] = '1:%s' % (entry['prio'])
                files[entry['name']] = entry

        # check for unconfigured scripts
        process = subprocess.Popen(['/usr/sbin/univention-check-join-status'],
                                   shell=False,
                                   stdout=subprocess.PIPE)
        stdout, stderr = process.communicate()
        if process.returncode == 0:
            return files.values()

        for line in stdout.splitlines():
            # is there a general error?
            match = RE_ERROR.match(line)
            if match and not line.startswith(
                    'Error: Not all install files configured'):
                raise UMC_Error(_('Error: %s') % match.groups()[0])

            # unconfigured script
            match = RE_NOT_CONFIGURED.match(line)
            if match:
                name = match.groups()[0]
                if name not in files:
                    # The joinscripts does not exists in the filesystem or has a invalid name
                    MODULE.error(
                        'not existing join script or join script with invalid name mentioned in status file: %r'
                        % (name, ))
                    continue
                files[name]['configured'] = False
                files[name]['status'] = '0:%s' % (files[name]['prio'], )

        return files.values()

    @simple_response
    def joined(self):
        return self._joined

    @simple_response
    def progress(self):
        return self.progress_state.poll()

    @simple_response
    def running(self):
        """ returns true if a join script is running. """
        return self._running

    @simple_response
    def master(self):
        """ returns the hostname of the domaincontroller master as fqdn """
        return get_master_dns_lookup()

    @property
    def _joined(self):
        return os.path.exists('/var/univention-join/joined')

    @property
    def _running(self):
        return os.path.exists(LOCKFILE)

    def _lock(self):
        try:
            open(LOCKFILE, 'a').close()
        except (IOError, OSError) as ex:
            MODULE.warn('_lock: %s' % (ex))

    def _unlock(self):
        try:
            if self._running:
                os.unlink(LOCKFILE)
        except (IOError, OSError) as ex:
            MODULE.warn('_unlock: %s' % (ex))

    def __del__(self):
        self._unlock()

    # TODO __finalize__?

    @simple_response
    def logview(self):
        """Returns the last 2MB of the join.log file"""
        with open(LOGFILE, 'rb') as fd:
            return fd.read(2097152).decode('utf-8', 'replace')

    @sanitize(
        username=StringSanitizer(required=True, minimum=1),
        password=StringSanitizer(required=True, minimum=1),
        hostname=HostSanitizer(required=True, regex_pattern=RE_HOSTNAME),
    )
    def join(self, request):
        username, password, hostname = (request.options['username'],
                                        request.options['password'],
                                        request.options['hostname'])

        # Check if already a join process is running
        if self._running:
            raise UMC_Error(_('A join process is already running.'))

        # check for valid server role
        if ucr.get('server/role') == 'domaincontroller_master':
            raise UMC_Error(
                _('Invalid server role! A master domain controller can not be joined.'
                  ))

        # check for dpkg lock
        if self._dpkg_locked():
            raise UMC_Error(
                _('Currently, software is being installed or uninstalled. Join scripts should not be run right now.'
                  ))

        def _thread():
            self.progress_state.reset()
            self.progress_state.component = _('Domain join')
            self._lock()
            return system_join(
                hostname,
                username,
                password,
                info_handler=self.progress_state.info_handler,
                step_handler=self.progress_state.add_steps,
                error_handler=self.progress_state.error_handler,
                component_handler=self.progress_state.component_handler,
                critical_handler=self.progress_state.critical_handler,
            )

        def _finished(thread, result):
            MODULE.info('Finished joining')
            self._unlock()
            self.progress_state.info = _('finished...')
            self.progress_state.finish()
            if isinstance(result, BaseException):
                msg = '%s\n%s: %s\n' % (
                    ''.join(traceback.format_tb(thread.exc_info[2])),
                    thread.exc_info[0].__name__, str(thread.exc_info[1]))
                MODULE.warn('Exception during domain join: %s' % msg)
                self.progress_state.error_handler(
                    _('An unexpected error occurred: %s') % result)

        # launch thread
        thread = notifier.threads.Simple('join', _thread, _finished)
        thread.run()

        request.status = 202
        self.finished(request.id, True)

    @sanitize(username=StringSanitizer(required=False, minimum=1),
              password=StringSanitizer(required=False, minimum=1),
              scripts=ListSanitizer(required=True, min_elements=1),
              force=BooleanSanitizer(default=False))
    def run(self, request):
        """runs the given join scripts"""

        # Check if already a join process is running
        if self._running:
            raise UMC_Error(_('A join process is already running.'))

        # check for dpkg lock
        if self._dpkg_locked():
            raise UMC_Error(
                _('Currently, software is being installed or uninstalled. Join scripts should not be run right now.'
                  ))

        scripts, username, password, force = (request.options['scripts'],
                                              request.options.get('username'),
                                              request.options.get('password'),
                                              request.options.get(
                                                  'force', False))

        # sort scripts
        scripts.sort(key=lambda i: int(re.match('^(\d+)', i).group()))

        def _thread():
            # reset progress state and lock against other join processes
            self.progress_state.reset()
            self.progress_state.component = _('Authentication')
            self._lock()
            return run_join_scripts(
                scripts,
                force,
                username,
                password,
                info_handler=self.progress_state.info_handler,
                step_handler=self.progress_state.add_steps,
                error_handler=self.progress_state.error_handler,
                component_handler=self.progress_state.component_handler,
                critical_handler=self.progress_state.critical_handler,
            )

        def _finished(thread, result):
            MODULE.info('Finished running join scripts')
            self._unlock()
            self.progress_state.info = _('finished...')
            self.progress_state.finish()
            if isinstance(result, BaseException):
                msg = '%s\n%s: %s\n' % (
                    ''.join(traceback.format_tb(thread.exc_info[2])),
                    thread.exc_info[0].__name__, str(thread.exc_info[1]))
                MODULE.warn('Exception during running join scripts: %s' % msg)
                self.progress_state.error_handler(
                    _('An unexpected error occurred: %s') % result)

        # launch thread
        thread = notifier.threads.Simple('join', _thread, _finished)
        thread.run()

        # finish request
        request.status = 202
        self.finished(request.id, True)
class Instance(umcm.Base):
    def __init__(self):
        umcm.Base.__init__(self)
        self.mem_regex = re.compile('([0-9]*) kB')

    def _call(self, command):
        try:
            process = subprocess.Popen(command,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE)
            (
                stdoutdata,
                stderrdata,
            ) = process.communicate()
            return (
                process.returncode,
                stdoutdata.decode('UTF-8'),
                stderrdata.decode('UTF-8'),
            )
        except OSError:
            return (
                True,
                None,
                None,
            )

    @simple_response
    def get_general_info(self):
        DMIDECODE = '/usr/sbin/dmidecode'
        MANUFACTURER_CMD = (
            DMIDECODE,
            '-s',
            'system-manufacturer',
        )
        MODEL_CMD = (
            DMIDECODE,
            '-s',
            'system-product-name',
        )

        stdout_list = []
        for command in (
                MANUFACTURER_CMD,
                MODEL_CMD,
        ):
            (
                exitcode,
                stdout,
                stderr,
            ) = self._call(command)
            if exitcode:
                MODULE.error('Command %r failed: %s %r %r' %
                             (command, exitcode, stdout, stderr))
                raise UMC_Error(_('Failed to execute command'))
            else:
                stdout = stdout[:-1]  # remove newline character
                stdout_list.append(stdout)
        result = {}
        result['manufacturer'] = stdout_list[0]
        result['model'] = stdout_list[1]
        return result

    @sanitize(
        manufacturer=StringSanitizer(required=True),
        model=StringSanitizer(required=True),
        comment=StringSanitizer(required=True),
        ticket=StringSanitizer(required=False, default=''),
    )
    @simple_response
    def get_system_info(self, manufacturer, model, comment, ticket=''):
        SYSTEM_INFO_CMD = (
            '/usr/bin/univention-system-info',
            '-m',
            manufacturer,
            '-t',
            model,
            '-c',
            comment,
            '-s',
            ticket,
            '-u',
        )

        (
            exitcode,
            stdout,
            stderr,
        ) = self._call(SYSTEM_INFO_CMD)
        if exitcode:
            MODULE.error('Execution of univention-system-info failed: %s' %
                         (stdout, ))
            raise UMC_Error('Execution of univention-system-info failed')

        result = {}
        for line in stdout.splitlines():
            try:
                info, value = line.split(':', 1)
                result[info] = value
            except ValueError:
                pass
        if result.get('mem'):
            match = self.mem_regex.match(result['mem'])
            if match:
                try:
                    converted_mem = (float(match.groups()[0]) / 1048576)
                    result['mem'] = '%.2f GB' % converted_mem
                    result['mem'] = result['mem'].replace('.', ',')
                except (IndexError, ValueError):
                    pass
        result.pop('Temp', None)  # remove unnecessary entry
        return result

    @simple_response
    def get_mail_info(self):
        ucr.load()
        ADDRESS_VALUE = ucr.get('umc/sysinfo/mail/address',
                                '*****@*****.**')
        SUBJECT_VALUE = ucr.get('umc/sysinfo/mail/subject',
                                'Univention System Info')

        url = urlunparse(('mailto', '', ADDRESS_VALUE, '',
                          urlencode({
                              'subject': SUBJECT_VALUE,
                          }), ''))
        result = {}
        result['url'] = url.replace('+', '%20')
        return result

    @sanitize(archive=StringSanitizer(required=True))
    @simple_response
    def upload_archive(self, archive):
        ucr.load()
        url = ucr.get(
            'umc/sysinfo/upload/url',
            'https://forge.univention.org/cgi-bin/system-info-upload.py')

        SYSINFO_PATH = '/usr/share/univention-system-info/archives/'
        path = os.path.abspath(os.path.join(SYSINFO_PATH, archive))
        if not path.startswith(SYSINFO_PATH):
            raise UMC_Error('Archive path invalid.')

        with open(os.path.join(SYSINFO_PATH, archive), 'rb') as fd:
            try:
                response = requests.post(url, files={
                    'filename': fd,
                })
                response.raise_for_status()
            except requests.exceptions.RequestException as exc:
                raise UMC_Error('Archive upload failed: %s' % (exc, ))
            answer = response.text
        if answer.startswith('ERROR:'):
            raise UMC_Error(answer)

    @sanitize(traceback=StringSanitizer(),
              remark=StringSanitizer(),
              email=StringSanitizer())
    @simple_response
    def upload_traceback(self, traceback, remark, email):
        ucr.load()
        ucs_version = '%(version/version)s-%(version/patchlevel)s errata%(version/erratalevel)s' % ucr
        if ucr.get('appcenter/apps/ucsschool/version'):
            ucs_version = '%s - UCS@school %s' % (
                ucs_version, ucr['appcenter/apps/ucsschool/version'])
        # anonymised id of localhost
        uuid_system = ucr.get('uuid/system', '')
        url = ucr.get(
            'umc/sysinfo/traceback/url',
            'https://forge.univention.org/cgi-bin/system-info-traceback.py')
        MODULE.process('Sending %s to %s' % (traceback, url))
        request_data = {
            'traceback': traceback,
            'remark': remark,
            'email': email,
            'ucs_version': ucs_version,
            'uuid_system': uuid_system,
            'uuid_license': ucr.get('uuid/license', ''),
            'server_role': ucr.get('server/role'),
        }
        try:
            response = requests.post(url, data=request_data)
            response.raise_for_status()
        except requests.exceptions.RequestException as exc:
            raise UMC_Error('Sending traceback failed: %s' % (exc, ))
예제 #12
0
class Instance(SchoolBaseModule, SchoolImport):

	def init(self):
		super(Instance, self).init()
		add_module_logger_to_schoollib()

	@simple_response
	def is_singlemaster(self):
		return ucr.is_true('ucsschool/singlemaster', False)

	@sanitize(
		schooldc=StringSanitizer(required=True, regex_pattern=re.compile(r'^[a-zA-Z](([a-zA-Z0-9-_]*)([a-zA-Z0-9]$))?$')),
		schoolou=StringSanitizer(required=True, regex_pattern=re.compile(r'^[a-zA-Z0-9](([a-zA-Z0-9_]*)([a-zA-Z0-9]$))?$')),
	)
	@simple_response
	def move_dc(self, schooldc, schoolou):
		params = ['--dcname', schooldc, '--ou', schoolou]
		return_code, stdout = self._run_script(SchoolImport.MOVE_DC_SCRIPT, params, True)
		return {'success': return_code == 0, 'message': stdout}

	@simple_response
	def computer_types(self):
		ret = []
		computer_types = [WindowsComputer, MacComputer, IPComputer]
		try:
			import univention.admin.handlers.computers.ucc as ucc
			del ucc
		except ImportError:
			pass
		else:
			computer_types.insert(1, UCCComputer)
		for computer_type in computer_types:
			ret.append({'id': computer_type._meta.udm_module_short, 'label': computer_type.type_name})
		return ret

	@response
	@LDAP_Connection()
	def share_servers(self, request, ldap_user_read=None):
		# udm/syntax/choices UCSSchool_Server_DN
		ret = [{'id': '', 'label': ''}]
		for module in ['computers/domaincontroller_master', 'computers/domaincontroller_backup', 'computers/domaincontroller_slave', 'computers/memberserver']:
			for obj in udm_modules.lookup(module, None, ldap_user_read, scope='sub'):
				obj.open()
				ret.append({'id': obj.dn, 'label': obj.info.get('fqdn', obj.info['name'])})
		return ret

	@sanitize_object(**{
		'$dn$': DNSanitizer(required=True),
	})
	@response
	@LDAP_Connection()
	def _get_obj(self, request, ldap_user_read=None):
		ret = []
		for obj in iter_objects_in_request(request, ldap_user_read, True):
			MODULE.process('Getting %r' % (obj))
			obj = obj.from_dn(obj.old_dn, obj.school, ldap_user_read)
			ret.append(obj.to_dict())
		return ret

	@response
	@LDAP_Connection(USER_READ, USER_WRITE, ADMIN_WRITE)
	def _create_obj(self, request, ldap_user_read=None, ldap_user_write=None, ldap_admin_write=None):
		# Bug #44641: workaround with security implications!
		if ucr.is_true('ucsschool/wizards/schoolwizards/workaround/admin-connection'):
			ldap_user_write = ldap_admin_write

		ret = []
		for obj in iter_objects_in_request(request, ldap_user_write):
			MODULE.process('Creating %r' % (obj,))
			obj.validate(ldap_user_read)
			if obj.errors:
				ret.append({'result': {'message': obj.get_error_msg()}})
				MODULE.process('Validation failed %r' % (ret[-1],))
				continue
			try:
				if obj.create(ldap_user_write, validate=False):
					ret.append(True)
				else:
					ret.append({'result': {'message': _('"%s" already exists!') % obj.name}})
			except uldapBaseException as exc:
				ret.append({'result': {'message': get_exception_msg(exc)}})
				MODULE.process('Creation failed %r' % (ret[-1],))
		return ret

	@sanitize_object(**{
		'$dn$': DNSanitizer(required=True),
	})
	@response
	@LDAP_Connection(USER_READ, USER_WRITE, ADMIN_WRITE)
	def _modify_obj(self, request, ldap_user_read=None, ldap_user_write=None, ldap_admin_write=None):
		# Bug #44641: workaround with security implications!
		if ucr.is_true('ucsschool/wizards/schoolwizards/workaround/admin-connection'):
			ldap_user_write = ldap_admin_write

		ret = []
		for obj in iter_objects_in_request(request, ldap_user_write, True):
			MODULE.process('Modifying %r' % (obj))
			obj.validate(ldap_user_read)
			if obj.errors:
				ret.append({'result': {'message': obj.get_error_msg()}})
				continue
			try:
				obj.modify(ldap_user_write, validate=False)
			except uldapBaseException as exc:
				ret.append({'result': {'message': get_exception_msg(exc)}})
			else:
				ret.append(True)  # no changes? who cares?
		return ret

	@sanitize_object(**{
		'$dn$': DNSanitizer(required=True),
	})
	@response
	@LDAP_Connection(USER_READ, USER_WRITE, ADMIN_WRITE)
	def _delete_obj(self, request, ldap_user_read=None, ldap_user_write=None, ldap_admin_write=None):
		# Bug #44641: workaround with security implications!
		if ucr.is_true('ucsschool/wizards/schoolwizards/workaround/admin-connection'):
			ldap_user_write = ldap_admin_write

		ret = []
		for obj in iter_objects_in_request(request, ldap_user_write, True):
			obj.name = obj.get_name_from_dn(obj.old_dn)
			MODULE.process('Deleting %r' % (obj))
			if obj.remove(ldap_user_write):
				ret.append(True)
			else:
				ret.append({'result': {'message': _('"%s" does not exist!') % obj.name}})
		return ret

	def _get_all(self, klass, school, filter_str, lo):
		if school:
			schools = [School.cache(school)]
		else:
			schools = School.from_binddn(lo)
		objs = []
		for school in schools:
			try:
				objs.extend(klass.get_all(lo, school.name, filter_str=filter_str, easy_filter=True))
			except noObject as exc:
				MODULE.error('Could not get all objects of %r: %r' % (klass.__name__, exc))
		return [obj.to_dict() for obj in objs]

	@sanitize(
		school=StringSanitizer(required=True),
		type=ChoicesSanitizer(['all'] + USER_TYPES.keys(), required=True),
		filter=StringSanitizer(default=''),
	)
	@response
	@LDAP_Connection()
	def get_users(self, request, ldap_user_read=None):
		school = request.options['school']
		user_class = USER_TYPES.get(request.options['type'], User)
		return self._get_all(user_class, school, request.options.get('filter'), ldap_user_read)

	get_user = _get_obj
	modify_user = _modify_obj
	create_user = _create_obj

	@sanitize_object(**{
		'remove_from_school': SchoolSanitizer(required=True),
		'$dn$': DNSanitizer(required=True),
	})
	@response
	@LDAP_Connection(USER_READ, USER_WRITE, ADMIN_WRITE)
	def delete_user(self, request, ldap_user_read=None, ldap_user_write=None, ldap_admin_write=None):
		# Bug #44641: workaround with security implications!
		if ucr.is_true('ucsschool/wizards/schoolwizards/workaround/admin-connection'):
			ldap_user_write = ldap_admin_write

		ret = []
		for obj_props in request.options:
			obj_props = obj_props['object']
			try:
				obj = User.from_dn(obj_props['$dn$'], None, ldap_user_write)
			except noObject:
				raise UMC_Error(_('The %s %r does not exists or might have been removed in the meanwhile.') % (getattr(User, 'type_name', None) or User.__name__, User.get_name_from_dn(obj_props['$dn$'])))
			school = obj_props['remove_from_school']
			success = obj.remove_from_school(school, ldap_user_write)
			# obj.old_dn is None when the ucsschool lib has deleted the user after the last school was removed from it
			if success and obj.old_dn is not None:
				success = obj.modify(ldap_user_write)
			if not success:
				success = {'result': {'message': _('Failed to remove user from school.')}}
			ret.append(success)
		return ret

	@sanitize(
		school=StringSanitizer(required=True),
		type=ChoicesSanitizer(['all'] + COMPUTER_TYPES.keys(), required=True),
		filter=StringSanitizer(default=''),
	)
	@response
	@LDAP_Connection()
	def get_computers(self, request, ldap_user_read=None):
		school = request.options['school']
		computer_class = COMPUTER_TYPES.get(request.options['type'], SchoolComputer)
		return self._get_all(computer_class, school, request.options.get('filter'), ldap_user_read)

	get_computer = _get_obj
	modify_computer = _modify_obj
	create_computer = _create_obj
	delete_computer = _delete_obj

	@sanitize(
		school=StringSanitizer(required=True),
		filter=StringSanitizer(default=''),
	)
	@response
	@LDAP_Connection()
	def get_classes(self, request, ldap_user_read=None):
		school = request.options['school']
		return self._get_all(SchoolClass, school, request.options.get('filter'), ldap_user_read)

	get_class = _get_obj
	modify_class = _modify_obj
	create_class = _create_obj
	delete_class = _delete_obj

	@response
	@LDAP_Connection()
	def get_schools(self, request, ldap_user_read=None):
		schools = School.get_all(ldap_user_read, filter_str=request.options.get('filter'), easy_filter=True)
		return [school.to_dict() for school in schools]

	get_school = _get_obj
	modify_school = _modify_obj
	create_school = _create_obj
	delete_school = _delete_obj
예제 #13
0
class Instance(Base, ProgressMixin):
	OPTION_MAPPING = (
		('LDAP_Host', 'connector/ad/ldap/host', ''),
		('LDAP_Base', 'connector/ad/ldap/base', ''),
		('LDAP_BindDN', 'connector/ad/ldap/binddn', ''),
		('KerberosDomain', 'connector/ad/mapping/kerberosdomain', ''),
		('PollSleep', 'connector/ad/poll/sleep', 5),
		('RetryRejected', 'connector/ad/retryrejected', 10),
		('DebugLevel', 'connector/debug/level', 2),
		('DebugFunction', 'connector/debug/function', False),
		('MappingSyncMode', 'connector/ad/mapping/syncmode', 'sync'),
		('MappingGroupLanguage', 'connector/ad/mapping/group/language', 'de')
	)

	def init(self):
		self.__update_status()

	def state(self, request):
		"""Retrieve current status of the Active Directory connection configuration and the service

		options: {}

		return: { 'configured' : (True|False), 'certificate' : (True|False), 'running' : (True|False) }
		"""

		self.__update_status()
		self.finished(request.id, {
			'ssl_enabled': self.status_ssl,
			'password_sync_enabled': self.status_password_sync,
			'running': self.status_running,
			'certificate': self.status_certificate,
			'mode_admember': self.status_mode_admember,
			'mode_adconnector': self.status_mode_adconnector,
			'configured': self.status_mode_adconnector or self.status_mode_admember,
			'server_role': ucr.get('server/role'),
		})

	def load(self, request):
		"""Retrieve current status of the Active Directory connection configuration and the service

		options: {}

		return: { <all AD connector UCR variables> }
		"""

		result = {}
		for option, var, default in Instance.OPTION_MAPPING:
			result[option] = ucr.get(var, default)

		pwd_file = ucr.get('connector/ad/ldap/bindpw')
		result['passwordExists'] = bool(pwd_file and os.path.exists(pwd_file))

		self.finished(request.id, result)

	@sanitize(
		LDAP_Host=StringSanitizer(required=True),
		Host_IP=StringSanitizer(required=True),
		LDAP_Base=StringSanitizer(required=True),
		LDAP_BindDN=StringSanitizer(required=True),
		KerberosDomain=StringSanitizer(required=True),
	)
	def adconnector_save(self, request):
		"""Saves the Active Directory connection configuration

		options:
			Host_IP: IP address of the AD server
			LDAP_Host: hostname of the AD server
			LDAP_Base: LDAP base of the AD server
			LDAP_BindDN: LDAP DN to use for authentication
			KerberosDomain: kerberos domain
			PollSleep: time in seconds between polls
			RetryRejected: how many time to retry a synchronisation
			MappingSyncMode: synchronisation mode
			MappingGroupLanguage: language of the AD server

		return: { 'success' : (True|False), 'message' : <details> }
		"""

		for umckey, ucrkey, default in Instance.OPTION_MAPPING:
			val = request.options.get(umckey, default)
			if val:
				if isinstance(val, bool):
					val = 'yes' if val else 'no'
				MODULE.info('Setting %s=%s' % (ucrkey, val))
				univention.config_registry.handler_set([u'%s=%s' % (ucrkey, val)])

		ucr.load()
		if ucr.get('connector/ad/ldap/ldaps'):
			MODULE.info('Unsetting connector/ad/ldap/ldaps')
			univention.config_registry.handler_unset([u'connector/ad/ldap/ldaps'])
		if ucr.get('connector/ad/ldap/port') == '636':
			MODULE.info('Setting ldap port to 389')
			univention.config_registry.handler_set([u'connector/ad/ldap/port=389'])

		if not request.options.get('LDAP_Password') in (None, '', DO_NOT_CHANGE_PWD):
			fn = ucr.get('connector/ad/ldap/bindpw', FN_BINDPW)
			try:
				with open(fn, 'w') as fd:
					fd.write(request.options.get('LDAP_Password'))
				os.chmod(fn, 0o600)
				os.chown(fn, 0, 0)
				univention.config_registry.handler_set([u'connector/ad/ldap/bindpw=%s' % fn])
			except Exception as e:
				MODULE.info('Saving bind password failed (filename=%(fn)s ; exception=%(exception)s)' % {'fn': fn, 'exception': str(e.__class__)})
				self.finished(request.id, {'success': False, 'message': _('Saving bind password failed (filename=%(fn)s ; exception=%(exception)s)') % {'fn': fn, 'exception': str(e.__class__)}})
				return

		ssldir = '/etc/univention/ssl/%s' % request.options.get('LDAP_Host')
		if not os.path.exists(ssldir):
			self._create_certificate(request)
			return

		# enter a static host entry such that the AD server's FQDN can be resolved
		univention.config_registry.handler_set([u'hosts/static/%(Host_IP)s=%(LDAP_Host)s' % request.options])

		# check for SSL support on AD side
		if admember.server_supports_ssl(server=request.options.get('LDAP_Host')):
			MODULE.process('Enabling SSL...')
			admember.enable_ssl()
		else:
			MODULE.warn('SSL is not supported')
			admember.disable_ssl()

		# UCR variables are set, and now we can try to guess the language of
		# the AD domain
		ad_lang = guess_ad_domain_language()
		univention.config_registry.handler_set([u'connector/ad/mapping/group/language=%s' % ad_lang])

		self.finished(request.id, {'success': True, 'message': _('Active Directory connection settings have been saved.')})

	def _create_certificate(self, request):
		ssldir = '/etc/univention/ssl/%s' % request.options.get('LDAP_Host')

		def _return(pid, status, buffer, request):
			if not os.path.exists(ssldir):
				MODULE.error('Creation of certificate failed (%s)' % ssldir)
				self.finished(request.id, {'success': False, 'message': _('Creation of certificate failed (%s)') % ssldir})
			self.finished(request.id, {'success': True, 'message': _('Active Directory connection settings have been saved and a new certificate for the Active Directory server has been created.')})

		cmd = '/usr/sbin/univention-certificate new -name %s' % pipes.quote(request.options['LDAP_Host'])
		MODULE.info('Creating new SSL certificate: %s' % cmd)
		proc = notifier.popen.Shell(cmd, stdout=True)
		cb = notifier.Callback(_return, request)
		proc.signal_connect('finished', cb)
		proc.start()

	@file_upload
	def upload_certificate(self, request):
		def _return(pid, status, bufstdout, bufstderr, request, fn):
			bufstdout, bufstderr = bufstdout.decode('UTF-8', 'replace'), bufstderr.decode('UTF-8', 'replace')
			success = True
			if status == 0:
				message = _('Certificate has been uploaded successfully.')
				MODULE.info('Certificate has been uploaded successfully. status=%s\nSTDOUT:\n%s\n\nSTDERR:\n%s' % (status, '\n'.join(bufstdout), '\n'.join(bufstderr)))
				try:
					self._enable_ssl_and_test_connection(fn)
				except UMC_Error:
					message = _('Could not establish connection. Either the certificate is wrong, the Active Directory server is unreachable or it does not support SSL.')
					success = False
			else:
				success = False
				message = _('Certificate upload or conversion failed.')
				MODULE.process('Certificate upload or conversion failed. status=%s\nSTDOUT:\n%s\n\nSTDERR:\n%s' % (status, '\n'.join(bufstdout), '\n'.join(bufstderr)))

			self.finished(request.id, [{'success': success, 'message': message}])

		upload = request.options[0]['tmpfile']
		now = time.strftime('%Y%m%d_%H%M%S', time.localtime())
		fn = '/etc/univention/connector/ad/ad_cert_%s.pem' % now
		cmd = '/usr/bin/openssl x509 -inform der -outform pem -in %s -out %s 2>&1' % (pipes.quote(upload), fn)

		MODULE.info('Converting certificate into correct format: %s' % cmd)
		proc = notifier.popen.Shell(cmd, stdout=True, stderr=True)
		cb = notifier.Callback(_return, request, fn)
		proc.signal_connect('finished', cb)
		proc.start()

	@sanitize(
		action=ChoicesSanitizer(['start', 'stop'], required=True),
	)
	def service(self, request):
		MODULE.info('State: options=%s' % request.options)

		self.__update_status()
		action = request.options['action']

		MODULE.info('State: action=%s  status_running=%s' % (action, self.status_running))

		message = None
		if self.status_running and action == 'start':
			message = _('Active Directory Connector is already running. Nothing to do.')
		elif not self.status_running and action == 'stop':
			message = _('Active Directory Connector is already stopped. Nothing to do.')

		if message is not None:
			self.finished(request.id, {'success': True, 'message': message})
			return

		def _run_it(action):
			return subprocess.call(('service', 'univention-ad-connector', action))

		def _return(thread, result, request):
			success = not result
			if result:
				message = _('Switching running state of Active Directory Connector failed.')
				MODULE.info('Switching running state of Active Directory Connector failed. exitcode=%s' % result)
			else:
				if request.options.get('action') == 'start':
					message = _('Active Directory connection service has been started.')
				else:
					message = _('Active Directory connection service has been stopped.')

			self.finished(request.id, {'success': success, 'message': message})

		cb = notifier.Callback(_return, request)
		func = notifier.Callback(_run_it, action)
		thread = notifier.threads.Simple('service', func, cb)  # TODO: use async notifier.Popen instead
		thread.run()

	def __update_status(self):
		ucr.load()
		fn = ucr.get('connector/ad/ldap/certificate')
		self.status_ssl = ucr.is_true('connector/ad/ldap/ssl')
		self.status_password_sync = ucr.is_true('connector/ad/mapping/user/password/kinit')
		self.status_certificate = bool(fn and os.path.exists(fn))
		self.status_running = self.__is_process_running('*python*univention/connector/ad/main.py*')
		self.status_mode_admember = admember.is_localhost_in_admember_mode(ucr)
		self.status_mode_adconnector = admember.is_localhost_in_adconnector_mode(ucr)

	def __is_process_running(self, command):
		for proc in psutil.process_iter():
			try:
				cmdline = proc.cmdline() if callable(proc.cmdline) else proc.cmdline
			except psutil.NoSuchProcess:
				continue
			if cmdline and fnmatch.fnmatch(' '.join(cmdline), command):
				return True
		return False

	@sanitize(
		username=StringSanitizer(required=True),
		password=StringSanitizer(required=True),
		ad_server_address=StringSanitizer(required=True),
		mode=StringSanitizer(default='admember'),
	)
	@simple_response
	def check_domain(self, username, password, ad_server_address, mode):
		ad_domain_info = {}
		try:
			if mode == 'admember':
				admember.check_server_role()
			ad_domain_info = admember.lookup_adds_dc(ad_server_address)

			ad_server_ip = ad_domain_info['DC IP']
			if mode == 'admember':
				admember.check_domain(ad_domain_info)
			admember.check_connection(ad_domain_info, username, password)
			admember.check_ad_account(ad_domain_info, username, password)
		except admember.invalidUCSServerRole as exc:  # check_server_role()
			MODULE.warn('Failure: %s' % exc)
			raise UMC_Error(_('The AD member mode can only be configured on a DC master server.'))
		except admember.failedADConnect as exc:  # lookup_adds_dc()
			MODULE.warn('Failure: %s' % exc)
			raise UMC_Error(_('Could not connect to AD Server %s. Please verify that the specified address is correct. (%s)') % (ad_server_address, 'check_domain: %s' % (exc,)))
		except admember.domainnameMismatch as exc:  # check_domain()
			MODULE.warn('Failure: %s' % exc)
			raise UMC_Error(_('The domain name of the AD Server (%(ad_domain)s) does not match the local UCS domain name (%(ucs_domain)s). For the AD member mode, it is necessary to setup a UCS system with the same domain name as the AD Server.') % {'ad_domain': ad_domain_info.get("Domain"), 'ucs_domain': ucr['domainname']})
		except admember.connectionFailed as exc:  # check_connection()
			MODULE.warn('Failure: %s' % exc)
			raise UMC_Error(_('Could not connect to AD Server %s. Please verify that username and password are correct. (Details:\n%s)') % (ad_domain_info.get('DC DNS Name'), exc))
		except admember.notDomainAdminInAD as exc:  # check_ad_account()
			MODULE.warn('Failure: %s' % exc)
			raise UMC_Error(_('The given user is not member of the Domain Admins group in Active Directory. This is a requirement for the Active Directory domain join.'))

		# final info dict that is returned... replace spaces in the keys with '_'
		MODULE.info('Preparing info dict...')
		info = dict([(key.replace(' ', '_'), value) for key, value in ad_domain_info.items()])
		info['ssl_supported'] = admember.server_supports_ssl(ad_server_ip)
		# try to get binddn
		info['LDAP_BindDN'] = get_ad_binddn_from_name(info['LDAP_Base'], ad_server_ip, username, password)
		MODULE.info(str(info))
		return info

	@sanitize(
		username=StringSanitizer(required=True),
		password=StringSanitizer(required=True),
		ad_server_address=StringSanitizer(required=True),
	)
	@simple_response(with_progress=True)
	def admember_join(self, username, password, ad_server_address, progress):
		progress.title = _('Joining UCS into Active Directory domain')
		progress.total = 100.0
		progress.warnings = []
		overall_success = False
		MODULE.process(progress.title)

		def _progress(steps, msg):
			progress.current = float(steps)
			progress.message = msg
			MODULE.process(msg)
			time.sleep(0.2)

		def _err(exc=None, msg=None):

			exc_str = ''
			if exc is not None:
				exc_str = str(exc) or exc.__doc__  # if no message, take the doc string
				exc_class_name = exc.__class__.__name__
				MODULE.error('Join process failed [%s]: %s' % (exc_class_name, exc_str))

			if msg:
				MODULE.error(msg)
			else:
				msg = _('An unexpected error occurred: %s') % exc_str

			progress.finish_with_result({
				'success': False,
				'error': msg,
				'warnings': progress.warnings,
			})

		ad_domain_info = {}
		try:
			admember.check_server_role()
			ad_domain_info = admember.lookup_adds_dc(ad_server_address)
			ad_server_ip = ad_domain_info['DC IP']

			_progress(5, _('Configuring time synchronization...'))
			admember.time_sync(ad_server_ip)
			admember.set_timeserver(ad_server_ip)

			_progress(10, _('Configuring DNS server...'))
			admember.set_nameserver([ad_server_ip])
			admember.prepare_ucr_settings()

			_progress(15, _('Configuring Kerberos settings...'))
			admember.disable_local_heimdal()
			admember.disable_local_samba4()

			_progress(20, _('Configuring reverse DNS settings...'))
			admember.prepare_dns_reverse_settings(ad_domain_info)

			_progress(25, _('Configuring software components...'))

			_step_offset = 30.0
			_nsteps = 35.0

			def _step_handler(step):
				MODULE.process('Package manager progress: %.1f' % step)
				progress.current = (step / 100.0) * _nsteps + _step_offset

			def _err_handler(err):
				MODULE.warn(err)
				progress.warnings.append(err)

			success = admember.remove_install_univention_samba(info_handler=MODULE.process, error_handler=_err_handler, step_handler=_step_handler)
			if not success:
				raise RuntimeError(_('An error occurred while installing necessary software components.'))

			_progress(65, _('Configuring synchronization from AD...'))
			admember.prepare_connector_settings(username, password, ad_domain_info)
			admember.disable_ssl()

			_progress(70, _('Renaming well known SID objects...'))
			admember.rename_well_known_sid_objects(username, password)

			_progress(75, _('Configuring Administrator account...'))
			admember.prepare_administrator(username, password)

			_progress(80, _('Running Samba join script...'))
			admember.run_samba_join_script(username, password)

			_progress(85, _('Configuring DNS entries...'))
			admember.add_domaincontroller_srv_record_in_ad(ad_server_ip, username, password)
			admember.add_host_record_in_ad(uid=username, bindpw=password, sso=True)

			admember.make_deleted_objects_readable_for_this_machine(username, password)
			admember.synchronize_account_position(ad_domain_info, username, password)

			_progress(90, _('Starting Active Directory connection service...'))
			admember.start_service('univention-ad-connector')

			_progress(95, _('Registering LDAP service entry...'))
			admember.add_admember_service_to_localhost()

			overall_success = True
			_progress(100, _('Join has been finished successfully.'))

		# error handling...
		except admember.invalidUCSServerRole as exc:
			_err(exc, _('The AD member mode can only be configured on a DC master server.'))
		except admember.failedADConnect as exc:
			_err(exc, _('Could not connect to AD Server %s. Please verify that the specified address is correct. (%s)') % (ad_domain_info.get('DC DNS Name'), 'admember_join: %s' % (exc,)))
		except admember.domainnameMismatch as exc:
			_err(exc, _('The domain name of the AD Server (%(ad_domain)s) does not match the local UCS domain name (%(ucs_domain)s). For the AD member mode, it is necessary to setup a UCS system with the same domain name as the AD Server.') % {'ad_domain': ad_domain_info["Domain"], 'ucs_domain': ucr['domainname']})
		except admember.connectionFailed as exc:
			_err(exc, _('Could not connect to AD Server %s. Please verify that username and password are correct. (Details:\n%s)') % (ad_domain_info.get('DC DNS Name'), exc))
		except admember.failedToSetAdministratorPassword as exc:
			_err(exc, _('Failed to set the password of the UCS Administrator to the Active Directory Administrator password.'))
		except admember.failedToCreateAdministratorAccount as exc:
			_err(exc, _('Failed to create the Administrator account in UCS.'))
		except admember.sambaSidNotSetForAdministratorAccount as exc:
			_err(exc, _('The sambaSID could not set for the Administrator account in UCS.'))
		except admember.failedToSearchForWellKnownSid as exc:
			_err(exc, _('Failed to search for the well known SID.'))
		except admember.failedToAddAdministratorAccountToDomainAdmins as exc:
			_err(exc, _('Failed to add the Administrator account to the Domain Admins group.'))
		except admember.timeSyncronizationFailed as exc:
			_err(exc, _('Could not synchronize the time between the UCS system and the Active Directory domain controller: %s') % exc)
		except RuntimeError as exc:
			_err(exc)
		except Exception as exc:
			# catch all other errors that are unlikely to occur
			_err(exc)
			MODULE.error('Traceback:\n%s' % traceback.format_exc())

		if not overall_success:
			_progress(100, _('Join has been finished with errors.'))
			admember.revert_ucr_settings()
			admember.revert_connector_settings()

		if hasattr(progress, 'result'):
			# some error probably occurred -> return the result in the progress
			return progress.result

		return {'success': success}

	def _enable_ssl_and_test_connection(self, certificate_fname=None):
		with ucr_rollback(ucr, ['connector/ad/ldap/ssl', 'connector/ad/ldap/certificate']):
			if certificate_fname:
				univention.config_registry.handler_set([u'connector/ad/ldap/certificate=%s' % certificate_fname])
			server = ucr.get('connector/ad/ldap/host')
			if server:
				success = False
				if admember.server_supports_ssl(server):
					admember.enable_ssl()
					try:
						success = test_connection()
					except ADNotAvailable:
						success = False
				if not success:
					raise UMC_Error(_('Could not establish an encrypted connection. Either "%r" is not reachable or does not support encryption.') % server)
			else:
				MODULE.warn('connector is not configured yet, cannot test connection')

	@simple_response
	def enable_ssl(self):
		self._enable_ssl_and_test_connection()
		return subprocess.call(['service', 'univention-ad-connector', 'restart'])

	@simple_response
	def password_sync_service(self, enable=True):
		# kinit=true  -> do not sync passwords, but use Kerberos authentication
		# kinit=false -> sync passwords
		value = str(not enable).lower()
		univention.config_registry.handler_set(['connector/ad/mapping/user/password/kinit=%s' % value])
		return subprocess.call(['service', 'univention-ad-connector', 'restart'])

	@simple_response
	def check_dcmaster_srv_rec(self):
		result = bool(admember.get_domaincontroller_srv_record(ucr.get('domainname')))
		return {'success': result}
예제 #14
0
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)
예제 #15
0
class Instance(Base, ProgressMixin):
    def __init__(self, *args, **kwargs):
        Base.__init__(self, *args, **kwargs)
        ProgressMixin.__init__(self)
        self._finishedLock = threading.Lock()
        self._finishedResult = True
        self._progressParser = util.ProgressParser()
        self.__keep_alive_request = None
        self._net_apply_running = 0
        # reset umask to default
        os.umask(0o022)

    def init(self):
        os.putenv('LANG', str(self.locale))
        _locale.setlocale(_locale.LC_ALL, str(self.locale))
        if not util.is_system_joined():
            self._preload_city_data()

    def _preload_city_data(self):
        util.get_city_data()
        util.get_country_data()

    def _get_localized_label(self, label_dict):
        # return the correctly loca
        return label_dict.get(self.locale.language) or label_dict.get(
            'en', '') or label_dict.get('', '')

    def ping(self, request):
        if request.options.get('keep_alive'):
            self.__keep_alive_request = request
            return
        self.finished(request.id, None)

    @simple_response
    def close_browser(self):
        try:
            with open('/var/cache/univention-system-setup/browser.pid',
                      'rb') as fd:
                pid = int(fd.readline().strip())
                process = psutil.Process(pid)
                process.kill()
                return True
        except IOError as exc:
            MODULE.warn('cannot open browser PID file: %s' % (exc, ))
        except ValueError as exc:
            MODULE.error('browser PID is not a number: %s' % (exc, ))
        except psutil.NoSuchProcess as exc:
            MODULE.error('cannot kill process with PID: %s' % (exc, ))
        return False

    @simple_response
    def load(self):
        '''Return a dict with all necessary values for system-setup read from the current
		status of the system.'''
        return util.load_values(self.locale.language)

    @simple_response
    def save_keymap(self, layout=None):
        '''Set the systems x-keymap according to
		request.options[keymap]'''

        # Don't set in debian installer mode
        if ucr.is_true('system/setup/boot/installer'):
            return True

        if layout:
            subprocess.call(
                ['/usr/bin/setxkbmap', '-display', ':0', '-layout', layout])
        return True

    def save(self, request):
        '''Reconfigures the system according to the values specified in the dict given as
		option named "values".'''

        # get new values
        values = request.options.get('values', {})
        run_hooks = request.options.get('run_hooks', False)

        script_args = []
        if run_hooks:
            # create a status file that indicates that save has been triggered
            util.create_status_file()

            # enforce particular arguments for setup scripts
            script_args = [
                '--appliance-mode', '--force-recreate', '--demo-mode'
            ]

        def _thread(request, obj):
            # acquire the lock until the scripts have been executed
            self._finishedResult = False
            obj._finishedLock.acquire()
            try:
                subfolders = {
                    'network': ['30_net'],
                    'certificate': ['40_ssl'],
                    'languages': ['15_keyboard', '20_language', '35_timezone'],
                }.get(request.flavor)

                self._progressParser.reset(subfolders)

                if request.flavor == 'setup':
                    # adjust progress fractions for setup wizard with pre-configurred settings
                    fractions = self._progressParser.fractions
                    fractions['05_role/10role'] = 0
                    fractions['10_basis/12domainname'] = 0
                    fractions['10_basis/14ldap_basis'] = 0
                    fractions['90_postjoin/10admember'] = 0
                    self._progressParser.calculateFractions()

                MODULE.info('saving profile values')
                util.write_profile(values)

                if not values:
                    MODULE.error('No property "values" given for save().')
                    return False

                # in case of changes of the IP address, restart UMC server and web server
                # for this we ignore changes of virtual or non-default devices
                # ... no need to restart the UMC server if cleanup scripts are run anyway
                restart = False
                if not run_hooks:
                    MODULE.info('Check whether ip addresses have been changed')
                    for ikey, ival in values.iteritems():
                        if RE_IPV4.match(ikey) or RE_IPV6_DEFAULT.match(
                                ikey) or RE_SSL.match(ikey):
                            restart = True
                            break
                    MODULE.info('Restart servers: %s' % restart)

                # on a joined system or on a basesystem, we can run the setup scripts
                MODULE.info('runnning system setup scripts (flavor %r)' %
                            (request.flavor, ))

                util.run_scripts(self._progressParser,
                                 restart,
                                 subfolders,
                                 lang=str(self.locale),
                                 args=script_args)

                # run cleanup scripts and appliance hooks if needed
                if run_hooks:
                    util.cleanup(with_appliance_hooks=True)

                # done :)
                self._finishedResult = True
                return True
            finally:
                obj._finishedLock.release()

        def _finished(thread, result):
            if self.__keep_alive_request:
                self.finished(self.__keep_alive_request.id, None)
                self.__keep_alive_request = None

            if isinstance(result, BaseException):
                msg = ''.join(thread.trace + traceback.format_exception_only(
                    *thread.exc_info[:2]))
                MODULE.warn('Exception during saving the settings: %s' %
                            (msg, ))
                self._progressParser.current.errors.append(
                    _('Encountered unexpected error during setup process: %s')
                    % result)
                self._progressParser.current.critical = True
                self._finishedResult = True

        thread = notifier.threads.Simple(
            'save', notifier.Callback(_thread, request, self), _finished)
        thread.run()
        self.finished(request.id, None)

    @simple_response
    def join(self, values=None, dcname=None, username=None, password=None):
        '''Join and reconfigure the system according to the values specified in the dict given as
		option named "values".'''

        # get old and new values
        orgValues = util.load_values()
        values = values or {}

        # determine new system role
        oldrole = orgValues.get('server/role', '')
        newrole = values.get('server/role', oldrole)

        # create a status file that indicates that save has been triggered
        util.create_status_file()

        def _thread(obj, username, password):
            # acquire the lock until the scripts have been executed
            self._finishedResult = False
            obj._finishedLock.acquire()
            try:
                self._progressParser.reset()

                # write the profile file and run setup scripts
                util.auto_complete_values_for_join(values)

                # on unjoined DC master the nameserver must be set to the external nameserver
                if newrole == 'domaincontroller_master' and not orgValues.get(
                        'joined'):
                    for i in range(1, 4):
                        # overwrite these values only if they are set, because the UMC module
                        # will save only changed values
                        if values.get('dns/forwarder%d' % i):
                            values['nameserver%d' % i] = values.get(
                                'dns/forwarder%d' % i)

                MODULE.info('saving profile values')
                util.write_profile(values)

                # unjoined DC master (that is not being converted to a basesystem) -> run the join script
                MODULE.info('runnning system setup join script')
                util.run_joinscript(self._progressParser,
                                    values,
                                    username,
                                    password,
                                    dcname,
                                    lang=str(self.locale))

                # done :)
                self._finishedResult = True

                return True
            finally:
                obj._finishedLock.release()

        def _finished(thread, result):
            if self.__keep_alive_request:
                self.finished(self.__keep_alive_request.id, None)
                self.__keep_alive_request = None

            if isinstance(result, BaseException):
                msg = ''.join(thread.trace + traceback.format_exception_only(
                    *thread.exc_info[:2]))
                MODULE.warn('Exception during saving the settings: %s' %
                            (msg, ))
                self._progressParser.current.errors.append(
                    _('Encountered unexpected error during setup process: %s')
                    % (result, ))
                self._progressParser.current.critical = True
                self._finishedResult = True

        thread = notifier.threads.Simple(
            'join', notifier.Callback(_thread, self, username, password),
            _finished)
        thread.run()
        return

    def check_finished(self, request):
        '''Check whether the join/setup scripts are finished. This method implements a long
		polling request, i.e., the request is only finished at the moment when all scripts
		have been executed or due to a timeout. If it returns because of the timeout, a new
		try can be started.'''
        def _thread(request, obj):
            def progress_info(state, **kwargs):
                info = {
                    'component': state.fractionName,
                    'info': state.message,
                    'errors': state.errors,
                    'critical': state.critical,
                    'steps': state.percentage
                }
                info.update(kwargs)
                MODULE.info(
                    'Progress state: %(steps).1f%% - %(component)s - %(info)s'
                    % info)
                return info

            # acquire the lock in order to wait for the join/setup scripts to finish
            # do this for 30 sec and then return anyway
            SLEEP_TIME = 0.200
            WAIT_TIME = 30
            ntries = WAIT_TIME / SLEEP_TIME
            while not obj._finishedLock.acquire(False):
                if ntries <= 0 or self._progressParser.changed and self._progressParser.current:
                    state = self._progressParser.current
                    return progress_info(state, finished=False)
                time.sleep(SLEEP_TIME)
                ntries -= 1

            obj._finishedLock.release()

            # scripts are done, return final result
            # return all errors that we gathered throughout the setup
            state = self._progressParser.current
            return progress_info(state, finished=obj._finishedResult)

        thread = notifier.threads.Simple(
            'check_finished', notifier.Callback(_thread, request, self),
            notifier.Callback(self.thread_finished_callback, request))
        thread.run()

    @simple_response(with_flavor=True)
    def validate(self, values=None, flavor=None):
        '''Validate the specified values given in the dict as option named "values".
		Return a dict (with variable names as key) of dicts with the structure:
		{ "valid": True/False, "message": "..." }'''

        # init variables
        messages = []
        values = values or {}
        orgValues = util.load_values()
        is_wizard_mode = flavor == 'wizard'

        # determine new system role
        newrole = values.get('server/role', orgValues.get('server/role', ''))
        ad_member = values.get('ad/member', orgValues.get('ad/member', ''))

        # mix original and new values
        allValues = copy.copy(values)
        for ikey, ival in orgValues.iteritems():
            if ikey not in allValues:
                allValues[ikey] = ival

        # helper functions
        # TODO: 'valid' is not correctly evaluated in frontend
        # i.e. if valid you may continue without getting message
        def _check(key, check, message, critical=True):
            if key not in values:
                return
            if not check(values[key]):
                messages.append({
                    'message': message,
                    'valid': not critical,
                    'key': key
                })

        def _append(key, message):
            MODULE.warn('Validation failed for key %s: %s' % (key, message))
            messages.append({'key': key, 'valid': False, 'message': message})

        # host and domain name
        packages = set(values.get('components', []))
        _check(
            'hostname', util.is_hostname,
            _('The hostname or the hostname part of the fully qualified domain name is invalid. Please go back to the host setting and make sure, that the hostname only contains letter (a-zA-Z) and digits (0-9).'
              ))

        hostname_length_critical = ad_member or 'univention-samba' in packages or 'univention-samba4' in packages
        appliance_str = _('the UCS system')
        if ucr['umc/web/appliance/name']:
            appliance_str = _('the %s appliance') % (
                ucr['umc/web/appliance/name'], )
        hostname_length_message = _(
            'A valid NetBIOS name can not be longer than 13 characters. If Samba is installed, the hostname should be shortened.'
        ) if hostname_length_critical else _(
            'The hostname %s is longer than 13 characters. It will not be possible to install an Active Directory compatible Domaincontroller (Samba 4) or UCS@school. The hostname cannot be changed after the installation of %s. It is recommended to shorten the hostname to maximal 13 characters.'
        ) % (
            values.get('hostname', ''),
            appliance_str,
        )
        _check('hostname',
               lambda x: len(x) <= 13,
               hostname_length_message,
               critical=hostname_length_critical)

        _check(
            'domainname', util.is_domainname,
            _("Please enter a valid fully qualified domain name (e.g. host.example.com)."
              ))
        hostname = allValues.get('hostname', '')
        domainname = allValues.get('domainname', '')
        if hostname or domainname:
            if len('%s%s' % (hostname, domainname)) >= 63:
                _append(
                    'domainname',
                    _('The length of fully qualified domain name is greater than 63 characters.'
                      ))
            if hostname == domainname.split('.')[0]:
                _append('domainname', _("Hostname is equal to domain name."))
        if is_wizard_mode and not util.is_system_joined():
            if newrole == 'domaincontroller_master' and not values.get(
                    'domainname'):
                _append(
                    'domainname',
                    _("No fully qualified domain name has been specified for the system."
                      ))
            elif not values.get('hostname'):
                _append('hostname',
                        _("No hostname has been specified for the system."))

        # windows domain
        _check(
            'windows/domain', lambda x: x == x.upper(),
            _("The windows domain name can only consist of upper case characters."
              ))
        _check(
            'windows/domain', lambda x: len(x) <= 15,
            _("The windows domain name cannot be longer than 15 characters."))
        _check('windows/domain', util.is_windowsdomainname,
               _("The windows domain name is not valid."))

        # LDAP base
        _check(
            'ldap/base', util.is_ldap_base,
            _("The LDAP base may neither contain blanks nor any special characters. Its structure needs to consist of at least two relative distinguished names (RDN) which may only use the attribute tags 'dc', 'cn', 'c', 'o', or 'l' (e.g., dc=test,dc=net)."
              ))

        # root password
        _check(
            'root_password', lambda x: len(x) >= 8,
            _("The root password is too short. For security reasons, your password must contain at least 8 characters."
              ))
        _check('root_password', util.is_ascii,
               _("The root password may only contain ascii characters."))

        # ssl + email
        labels = {
            'ssl/country': _('Country'),
            'ssl/state': _('State'),
            'ssl/locality': _('Location'),
            'ssl/organization': _('Organization'),
            'organization': _('Organization'),
            'ssl/organizationalunit': _('Business unit'),
            'ssl/email': _('Email address'),
            'email_address': _('Email address'),
            'ssl/common': _('Common name for the root SSL certificate'),
        }
        for maxlenth, keys in [
            (2, ('ssl/country', )), (128, (
                'ssl/state',
                'ssl/locality',
            )),
            (64, ('organization', 'ssl/organization', 'ssl/organizationalunit',
                  'ssl/email', 'email_address', 'ssl/common'))
        ]:
            for ikey in keys:
                _check(
                    ikey, lambda x: len(x) <= maxlenth,
                    _('The following value is too long, only %(max)s characters allowed: %(name)s'
                      ) % {
                          'max': maxlenth,
                          'name': labels[ikey]
                      })

        for ikey in ('ssl/country', 'ssl/state', 'ssl/locality',
                     'ssl/organization', 'ssl/organizationalunit', 'ssl/email',
                     'ssl/common'):
            for table in (stringprep.in_table_c21_c22, stringprep.in_table_a1,
                          stringprep.in_table_c8, stringprep.in_table_c3,
                          stringprep.in_table_c4, stringprep.in_table_c5,
                          lambda c: c == u'\ufffd'):
                _check(
                    ikey, lambda x: not any(map(table, unicode(x))),
                    _('The value for %s contains invalid characters.') %
                    (labels[ikey], ))

        _check('ssl/country', lambda x: len(x) == 2,
               _('Country must be a country code consisting of 2 characters.'))
        for ikey in ['ssl/email', 'email_address']:
            _check(ikey, lambda x: x.find('@') > 0,
                   _("Please enter a valid email address"))

        # net
        try:
            interfaces = network.Interfaces()
            interfaces.from_dict(allValues.get('interfaces', {}))
            interfaces.check_consistency()
        except network.DeviceError as exc:
            _append('interfaces', str(exc))

        # validate the primary network interface
        _check('interfaces/primary', lambda x: not x or x in interfaces,
               _('The primary network device must exist.'))

        # check nameservers
        for ikey, iname in [('nameserver[1-3]', _('Domain name server')),
                            ('dns/forwarder[1-3]', _('External name server'))]:
            reg = re.compile('^(%s)$' % ikey)
            for jkey, jval in values.iteritems():
                if reg.match(jkey):
                    if not values.get(jkey):
                        # allow empty value
                        continue
                    _check(
                        jkey, util.is_ipaddr,
                        _('The specified IP address (%(name)s) is not valid: %(value)s'
                          ) % {
                              'name': iname,
                              'value': jval
                          })

        if is_wizard_mode and not util.is_system_joined() and (
                newrole not in ['domaincontroller_master', 'basesystem']
                or ad_member):
            if all(nameserver in values and not values[nameserver]
                   for nameserver in ('nameserver1', 'nameserver2',
                                      'nameserver3')):
                # 'nameserver1'-key exists → widget is displayed → = not in UCS/debian installer mode
                if not any(interface.ip4dynamic or interface.ip6dynamic
                           for interface in interfaces.values()):
                    _append('nameserver1',
                            _('A domain name server needs to be specified.'))
                    # _append('nameserver1', _('At least one domain name server needs to be given if DHCP or SLAAC is not specified.'))

            # see whether the domain can be determined automatically
            ucr.load()
            guessed_domain = None
            for obj in [values, ucr]:
                for nameserver in ('nameserver1', 'nameserver2',
                                   'nameserver3'):
                    nameserver = obj.get(nameserver)
                    if nameserver:
                        guessed_domain = None
                        if obj.get('ad/member') and obj.get('ad/address'):
                            try:
                                ad_domain_info = lookup_adds_dc(
                                    obj.get('ad/address'),
                                    ucr={'nameserver1': nameserver})
                            except failedADConnect:
                                pass
                            else:
                                guessed_domain = ad_domain_info['Domain']
                        else:
                            guessed_domain = util.get_ucs_domain(nameserver)
                        if guessed_domain:
                            differing_domain_name = values.get(
                                'domainname') and values['domainname'].lower(
                                ) != guessed_domain.lower()
                            if differing_domain_name:
                                _append(
                                    'domainname',
                                    _('The specified domain name is different to the %s domain name found via the configured DNS server: %s'
                                      ) % (
                                          _('Active Directory')
                                          if ad_member else _('UCS'),
                                          guessed_domain,
                                      ))
                            else:
                                # communicate guessed domainname to frontend
                                messages.append({
                                    'valid': True,
                                    'key': 'domainname',
                                    'value': guessed_domain,
                                })
                            break
                if guessed_domain:
                    break
            if not guessed_domain:
                if not values.get('domainname'):
                    _append(
                        'domainname',
                        _('Cannot automatically determine the domain. Please specify the server\'s fully qualified domain name.'
                          ))

                if values.get('nameserver1') and values.get('start/join'):
                    _append(
                        'nameserver1',
                        _('The specified nameserver %s is not part of a valid UCS domain.'
                          ) % (values['nameserver1'], ))

        # check gateways
        if values.get('gateway'):  # allow empty value
            _check(
                'gateway', util.is_ipv4addr,
                _('The specified gateway IPv4 address is not valid: %s') %
                values.get('gateway'))
        if values.get('ipv6/gateway'):  # allow empty value
            _check(
                'ipv6/gateway', util.is_ipv6addr,
                _('The specified gateway IPv6 address is not valid: %s') %
                values.get('ipv6/gateway'))

        # proxy
        _check(
            'proxy/http', util.is_proxy,
            _('The specified proxy address is not valid (e.g., http://10.201.1.1:8080): %s'
              ) % allValues.get('proxy/http', ''))

        # software checks
        if 'univention-virtual-machine-manager-node-kvm' in packages and 'univention-virtual-machine-manager-node-xen' in packages:
            _append(
                'components',
                _('It is not possible to install KVM and XEN components on one system. Please select only one of these components.'
                  ))
        if 'univention-samba' in packages and 'univention-samba4' in packages:
            _append(
                'components',
                _('It is not possible to install Samba 3 and Samba 4 on one system. Please select only one of these components.'
                  ))

        return messages

    @sanitize(pattern=PatternSanitizer(default='.*',
                                       required=True,
                                       add_asterisks=True))
    @simple_response
    def lang_locales(self, pattern, category='language_en'):
        '''Return a list of all available locales.'''
        return util.get_available_locales(pattern, category)

    def lang_timezones(self, request):
        '''Return a list of all available time zones.'''
        try:
            file = open('/usr/share/univention-system-setup/locale/timezone')
        except EnvironmentError:
            MODULE.error(
                'Cannot find locale data for timezones in /usr/share/univention-system-setup/locale'
            )
            self.finished(request.id, None)
            return

        timezones = [i.strip('\n') for i in file if not i.startswith('#')]

        self.finished(request.id, timezones)

    @simple_response
    def lang_keyboard_model(self):
        '''Return a list of all available keyboard models.'''

        tree = lxml.etree.parse(open('/usr/share/X11/xkb/rules/base.xml'))
        models = tree.xpath("//model")

        model_result = [{
            'label':
            i18nXKeyboard.translate(
                model.xpath('./configItem/description')[0].text),
            'id':
            model.xpath('./configItem/name')[0].text
        } for model in models]

        return model_result

    @simple_response
    def lang_keyboard_layout(self):
        '''Return a list of all available keyboard layouts.'''

        tree = lxml.etree.parse(open('/usr/share/X11/xkb/rules/base.xml'))
        layouts = tree.xpath("//layout")

        layout_result = [{
            'label':
            i18nXKeyboard.translate(
                layout.xpath('./configItem/description')[0].text),
            'id':
            layout.xpath('./configItem/name')[0].text,
            'language':
            layout.xpath('./configItem/shortDescription')[0].text,
            'countries':
            ':'.join([
                icountry.text
                for icountry in layout.xpath('./configItem/countryList/*')
            ]),
        } for layout in layouts]

        return layout_result

    @sanitize(keyboardlayout=StringSanitizer(default='us'))
    @simple_response
    def lang_keyboard_variante(self, keyboardlayout):
        '''Return a list of all available keyboard variantes.'''

        variante_result = []
        tree = lxml.etree.parse(open('/usr/share/X11/xkb/rules/base.xml'))
        layouts = tree.xpath("//layout")

        for layout in layouts:
            layoutID = layout.xpath("./configItem/name")[0].text
            if layoutID != keyboardlayout:
                continue
            variants = layout.xpath("./variantList/variant")
            variante_result += [{
                'label':
                i18nXKeyboard.translate(
                    variant.xpath('./configItem/description')[0].text),
                'id':
                variant.xpath('./configItem/name')[0].text
            } for variant in variants]

        variante_result.insert(0, {'label': '', 'id': ''})

        return variante_result

    def lang_countrycodes(self, request):
        '''Return a list of all countries with their two letter chcountry codes.'''
        country_data = util.get_country_data()
        countries = [{
            'id':
            icountry,
            'label':
            self._get_localized_label(idata.get('label', {})),
        } for icountry, idata in country_data.iteritems()
                     if idata.get('label')]

        # add the value from ucr value to the list
        # this is required because invalid values will be unset in frontend
        # Bug #26409
        tmpUCR = univention.config_registry.ConfigRegistry()
        tmpUCR.load()
        ssl_country = tmpUCR.get('ssl/country')
        if ssl_country not in [i['id'] for i in countries]:
            countries.append({'label': ssl_country, 'id': ssl_country})

        self.finished(request.id, countries)

    def net_apply(self, request):
        if self._net_apply_running > 0:
            # do not start another process applying the network settings
            return False

        values = request.options.get('values', {})
        demo_mode = request.options.get('demo_mode', False)

        def _thread(obj):
            obj._net_apply_running += 1
            MODULE.process('Applying network settings')
            with util.written_profile(values):
                util.run_networkscrips(demo_mode)

        def _finished(thread, result):
            self._net_apply_running -= 1
            self.finished(request.id, True)

        thread = notifier.threads.Simple('net_apply',
                                         notifier.Callback(_thread, self),
                                         _finished)
        thread.run()

    @simple_response
    def net_apply_check_finished(self):
        if self._net_apply_running > 0:
            # raise an error if net_apply command is still running...
            # this allows long polling on the client side (poll until successful request)
            raise RequestTimeout()
        return self._net_apply_running == 0

    @simple_response
    def net_interfaces(self):
        '''Return a list of all available network interfaces.'''
        return [idev['name'] for idev in util.detect_interfaces()]

    # workaround: use with_progress to make the method threaded
    @simple_response(with_progress=True)
    def net_dhclient(self, interface, timeout=10):
        '''Request a DHCP address. Expects as options a dict containing the key
		"interface" and optionally the key "timeout" (in seconds).'''
        return util.dhclient(interface, timeout)

    @sanitize(locale=StringSanitizer(default='en_US'))
    @simple_response
    def reset_locale(self, locale):
        locale = Locale(locale)
        locale.codeset = self.locale.codeset
        MODULE.info('Switching language to: %s' % locale)
        os.putenv('LANG', str(self.locale))
        try:
            _locale.setlocale(_locale.LC_ALL, str(locale))
        except _locale.Error:
            MODULE.warn(
                'Locale %s is not supported, using fallback locale "C" instead.'
                % locale)
            _locale.setlocale(_locale.LC_ALL, 'C')
        self.locale = locale

        # dynamically change the translation methods
        _translation.set_language(str(self.locale))
        i18nXKeyboard.set_language(str(self.locale))
        network._translation.set_language(str(self.locale))
        AppCache().clear_cache()

    @sanitize(pattern=StringSanitizer(),
              max_results=IntegerSanitizer(minimum=1, default=5))
    @simple_response
    def find_city(self, pattern, max_results):
        pattern = pattern.decode(self.locale.codeset).lower()
        MODULE.info('pattern: %s' % pattern)
        if not pattern:
            return []

        # for the given pattern, find matching cities
        city_data = util.get_city_data()
        matches = []
        for icity in city_data:
            match = None
            for jlabel in icity.get('label', {}).itervalues():
                label = jlabel.decode(self.locale.codeset).lower()
                if pattern in label:
                    # matching score is the overlap if the search pattern and the matched text
                    # (as fraction between 0 and 1)
                    match_score = len(pattern) / float(len(label))
                    if match and match_score < match['match_score']:
                        # just keep the best match of a city
                        continue
                    if match_score > 0.1:
                        # found a match with more than 10% overlap :)
                        match = icity.copy()
                        match['match'] = jlabel
                        match['match_score'] = match_score
            if match:
                matches.append(match)
        MODULE.info('Search for pattern "%s" with %s matches' %
                    (pattern, len(matches)))
        if not matches:
            return None

        # add additional score w.r.t. the population size of the city
        # such that the largest city gains additional 0.4 on top
        max_population = max([imatch['population'] for imatch in matches])
        weighted_inv_max_population = 0.6 / float(max_population)
        for imatch in matches:
            imatch['final_score'] = imatch[
                'match_score'] + weighted_inv_max_population * imatch[
                    'population']

        # sort matches...
        matches.sort(key=lambda x: x['final_score'], reverse=True)
        MODULE.info('Top 5 matches: %s' % json.dumps(matches[:5], indent=2))
        matches = matches[:max_results]

        # add additional information about keyboard layout, time zone etc. and
        # get the correct localized labels
        country_data = util.get_country_data()
        for imatch in matches:
            match_country = country_data.get(imatch.get('country'))
            if match_country:
                imatch.update(util.get_random_nameserver(match_country))
                imatch.update(
                    dict(
                        default_lang=match_country.get('default_lang'),
                        country_label=self._get_localized_label(
                            match_country.get('label', {})),
                        label=self._get_localized_label(imatch.get('label'))
                        or imatch.get('match'),
                    ))

        return matches

    @simple_response
    def apps_query(self):
        return util.get_apps(True)

    @simple_response
    def check_domain(self, role, nameserver):
        result = {}
        if role == 'ad':
            try:
                ad_domain_info = lookup_adds_dc(nameserver)
                dc = ad_domain_info['DC DNS Name']
                if dc:
                    result['dc_name'] = dc
                    domain = ad_domain_info['Domain']
                    result['domain'] = domain
                    result['ucs_master'] = util.is_ucs_domain(
                        nameserver, domain)
                    ucs_master_fqdn = util.resolve_domaincontroller_master_srv_record(
                        nameserver, domain)
                    result['ucs_master_fqdn'] = ucs_master_fqdn
                    result['ucs_master_reachable'] = util.is_ssh_reachable(
                        ucs_master_fqdn)
            except (failedADConnect, connectionFailed) as exc:
                MODULE.warn('ADDS DC lookup failed: %s' % (exc, ))
        elif role == 'nonmaster':
            domain = util.get_ucs_domain(nameserver)
            if domain:
                fqdn = util.resolve_domaincontroller_master_srv_record(
                    nameserver, domain)
            else:
                fqdn = util.get_fqdn(nameserver)
            if fqdn:
                result['dc_name'] = fqdn
                domain = '.'.join(fqdn.split('.')[1:])
                result['ucs_master'] = util.is_ucs_domain(nameserver, domain)
        return result

    @simple_response
    def check_domain_join_information(self, domain_check_role, role, dns,
                                      nameserver, address, username, password):
        result = {}
        if domain_check_role == 'ad':
            domain = util.check_credentials_ad(nameserver, address, username,
                                               password)
            result['domain'] = domain
            if dns:  # "dns" means we don't want to replace the existing DC Master
                ucs_master_fqdn = util.resolve_domaincontroller_master_srv_record(
                    nameserver, domain)
                if ucs_master_fqdn:
                    # if we found a _domaincontroller_master._tcp SRV record the system will be a DC Backup/Slave/Member.
                    # We need to check the credentials of this system, too, so we ensure that the System is reachable via SSH.
                    # Otherwise the join will fail with strange error like "ping to ..." failed.
                    result.update(
                        receive_domaincontroller_master_information(
                            False, nameserver, ucs_master_fqdn, username,
                            password))
                    set_role_and_check_if_join_will_work(
                        role, ucs_master_fqdn, username, password)
        elif domain_check_role == 'nonmaster':
            result.update(
                receive_domaincontroller_master_information(
                    dns, nameserver, address, username, password))
            set_role_and_check_if_join_will_work(role, address, username,
                                                 password)
        # master? basesystem? no domain check necessary
        return result

    @simple_response
    def check_school_information(self, hostname, address, username, password):
        return check_for_school_domain(hostname, address, username, password)

    @simple_response
    def check_repository_accessibility(self):
        return get_unreachable_repository_servers()

    @simple_response
    def check_uid(self, uid, role, address, username, password):
        return check_if_uid_is_available(uid, role, address, username,
                                         password)
예제 #16
0
class Instance(Base, Nodes, Profiles, Storages, Domains, Snapshots, Cloud,
               Targethosts):
    """
	UMC functions for UVMM handling.
	"""
    def __init__(self):
        Base.__init__(self)
        Storages.__init__(self)
        self.uvmm = UVMM_RequestBroker()

    def init(self):
        """
		Initialize UVMM UMC module instance.
		"""
        self.read_profiles()

    def process_uvmm_response(self, request, callback=None):
        return Callback(self._process_uvmm_response, request, callback)

    def _process_uvmm_response(self, thread, result, request, callback=None):
        # this is a notifier thread callback. If this raises an exception the whole module process crashes!
        if isinstance(result, BaseException):
            self.thread_finished_callback(thread, result, request)
            return

        success, data = result
        MODULE.info('Got result from UVMMd: success: %s, data: %r' %
                    (success, data))
        if not success:
            try:
                raise UMC_Error(str(data), status=500)
            except UMC_Error as result:
                thread._exc_info = sys.exc_info()
                self.thread_finished_callback(thread, result, request)
            return

        if callback:
            try:
                data = callback(data)
            except BaseException as result:
                thread._exc_info = sys.exc_info()
                self.thread_finished_callback(thread, result, request)
                return

        self.finished(request.id, data)

    @sanitize(
        type=ChoicesSanitizer(
            ['group', 'node', 'domain', 'cloud', 'instance', 'targethost'],
            required=True),
        nodePattern=StringSanitizer(required=True),
        domainPattern=StringSanitizer(required=False),
    )
    def query(self, request):
        """
		Meta query function for groups, nodes and domains.

		return: {
			'success': (True|False),
			'message': <details>
			}
		"""
        def group_root(request):
            self.finished(request.id, [{
                'id': 'default',
                'label': _('Physical servers'),
                'type': 'group',
                'icon': 'uvmm-group',
            }])

        method = {
            'node': self.node_query,
            'domain': self.domain_query,
            'cloud': self.cloud_query,
            'instance': self.instance_query,
            'group': group_root,
            'targethost': self.targethost_query,
        }[request.options['type']]
        return method(request)

    def group_query(self, request):
        """
		Get server groups.
		"""
        self.uvmm.send('GROUP_LIST', self.process_uvmm_response(request))
예제 #17
0
class Instance(Base):

	def init(self):
		self._finishedLock = threading.Lock()
		self._errors = []
		self.progress_state = Progress()
		self._installation_started = False
		self.package_manager = PackageManager(
			info_handler=self.progress_state.info_handler,
			step_handler=self.progress_state.step_handler,
			error_handler=self.progress_state.error_handler,
			lock=False,
			always_noninteractive=True,
		)
		self.package_manager.set_max_steps(100.0)
		self.original_certificate_file = None

	def error_handling(self, etype, exc, etraceback):
		if isinstance(exc, SchoolInstallerError):
			# restore the original certificate... this is done at any error before the system join
			self.restore_original_certificate()

	def get_samba_version(self):
		'''Returns 3 or 4 for Samba4 or Samba3 installation, respectively, and returns None otherwise.'''
		if self.package_manager.is_installed('univention-samba4'):
			return 4
		elif self.package_manager.is_installed('univention-samba'):
			return 3
		return None

	def get_school_environment(self):
		'''Returns 'singlemaster', 'multiserver', or None'''
		if self.package_manager.is_installed('ucs-school-singlemaster'):
			return 'singlemaster'
		elif self.package_manager.is_installed('ucs-school-slave') or self.package_manager.is_installed('ucs-school-nonedu-slave') or self.package_manager.is_installed('ucs-school-master'):
			return 'multiserver'
		return None

	def get_school_version(self):
		return ucr.get('appcenter/apps/ucsschool/version')

	@simple_response
	def query(self, **kwargs):
		"""Returns a dictionary of initial values for the form."""
		ucr.load()

		return {
			'server_role': ucr.get('server/role'),
			'joined': os.path.exists('/var/univention-join/joined'),
			'samba': self.get_samba_version(),
			'school_environment': self.get_school_environment(),
			'guessed_master': get_master_dns_lookup(),
			'hostname': ucr.get('hostname'),
		}

	@simple_response
	def progress(self):
		if not self._installation_started:
			if self._installation_started is False:
				self.progress_state.finish()
				self.progress_state.error_handler('Critical: There is no current installation running. Maybe the previous process died?')
			self._installation_started = False
		return self.progress_state.poll()

	@simple_response
	def get_metainfo(self):
		"""Queries the specified DC Master for metainformation about the UCS@school environment"""
		master = ucr.get('ldap/master') or get_master_dns_lookup()
		if not master:
			return
		return self._umc_master(self.username, self.password, master, 'schoolinstaller/get/metainfo/master')

	@sanitize(
		master=HostSanitizer(required=True, regex_pattern=RE_HOSTNAME),  # TODO: add regex error message; Bug #42955
		username=StringSanitizer(required=True, minimum=1),
		password=StringSanitizer(required=True, minimum=1),
		school=StringSanitizer(required=True, regex_pattern=RE_OU),  # TODO: add regex error message
	)
	@simple_response
	def get_schoolinfo(self, username, password, master, school):
		"""Queries the specified DC Master for information about the specified school"""
		return self._umc_master(username, password, master, 'schoolinstaller/get/schoolinfo/master', {'school': school})

	@sanitize(
		school=StringSanitizer(required=True, regex_pattern=RE_OU),  # TODO: add regex error message
	)
	@simple_response
	def get_schoolinfo_master(self, school):
		"""
		Fetches LDAP information from master about specified OU.
		This function assumes that the given arguments have already been validated!
		"""

		school_name = school
		try:
			lo, po = get_machine_connection(write=True)
			school = School.from_dn(School(name=school_name).dn, None, lo)
		except noObject:
			exists = False
			class_share_server = None
			home_share_server = None
			educational_slaves = []
			administrative_slaves = []
		except ldap.SERVER_DOWN:
			raise  # handled via UMC
		except ldap.LDAPError as exc:
			MODULE.warn('LDAP error during receiving school info: %s' % (exc,))
			raise UMC_Error(_('The LDAP connection to the master system failed.'))
		else:
			exists = True
			class_share_server = school.class_share_file_server
			home_share_server = school.home_share_file_server
			educational_slaves = [SchoolDCSlave.from_dn(dn, None, lo).name for dn in school.educational_servers]
			administrative_slaves = [SchoolDCSlave.from_dn(dn, None, lo).name for dn in school.administrative_servers]

		return {
			'exists': exists,
			'school': school_name,
			'classShareServer': class_share_server,
			'homeShareServer': home_share_server,
			'educational_slaves': educational_slaves,
			'administrative_slaves': administrative_slaves,
		}

	@simple_response
	def get_metainfo_master(self):
		"""Returns information about the UCS@school Installation on the DC Master."""
		return {
			'samba': self.get_samba_version(),
			'school_environment': self.get_school_environment(),
			'school_version': self.get_school_version(),
		}

	def _umc_master(self, username, password, master, uri, data=None):
		try:
			return umc(username, password, master, uri, data).result
		except Forbidden:
			raise SchoolInstallerError(_('Make sure ucs-school-umc-installer is installed on the DC Master and all join scripts are executed.'))
		except (ConnectionError, HTTPError) as exc:
			raise SchoolInstallerError(_('Could not connect to the DC Master %s: %s') % (master, exc))  # TODO: set status, message, result

	@sanitize(
		username=StringSanitizer(required=True),
		password=StringSanitizer(required=True),
		master=HostSanitizer(required=True, regex_pattern=RE_HOSTNAME_OR_EMPTY),
		schoolOU=StringSanitizer(required=True, regex_pattern=RE_OU_OR_EMPTY),
		setup=ChoicesSanitizer(['multiserver', 'singlemaster']),
		server_type=ChoicesSanitizer(['educational', 'administrative']),
		nameEduServer=StringSanitizer(regex_pattern=RE_HOSTNAME_OR_EMPTY),  # javascript wizard page always passes value to backend, even if empty
	)
	def install(self, request):
		# get all arguments
		username = request.options.get('username')
		password = request.options.get('password')
		master = request.options.get('master')
		school_ou = request.options.get('schoolOU')
		educational_slave = request.options.get('nameEduServer')
		ou_display_name = request.options.get('OUdisplayname', school_ou)  # use school OU name as fallback
		server_type = request.options.get('server_type')
		setup = request.options.get('setup')
		server_role = ucr.get('server/role')
		joined = os.path.exists('/var/univention-join/joined')

		if self._installation_started:
			raise ValueError('The installation was started twice. This should not have happened.')

		if server_role != 'domaincontroller_slave':
			# use the credentials of the currently authenticated user on a master/backup system
			self.require_password()
			username = self.username
			password = self.password
			master = '%s.%s' % (ucr.get('hostname'), ucr.get('domainname'))
		if server_role == 'domaincontroller_backup':
			master = ucr.get('ldap/master')

		self.original_certificate_file = None

		# check for valid school OU
		if ((setup == 'singlemaster' and server_role == 'domaincontroller_master') or server_role == 'domaincontroller_slave') and not RE_OU.match(school_ou):
			raise SchoolInstallerError(_('The specified school OU is not valid.'))

		# check for valid server role
		if server_role not in ('domaincontroller_master', 'domaincontroller_backup', 'domaincontroller_slave'):
			raise SchoolInstallerError(_('Invalid server role! UCS@school can only be installed on the system roles master domain controller, backup domain controller, or slave domain controller.'))

		if server_role == 'domaincontroller_slave' and not server_type:
			raise SchoolInstallerError(_('Server type has to be set for domain controller slave'))

		if server_role == 'domaincontroller_slave' and server_type == 'administrative' and not educational_slave:
			raise SchoolInstallerError(_('The name of an educational server has to be specified if the system shall be configured as administrative server.'))

		if server_role == 'domaincontroller_slave' and server_type == 'administrative' and educational_slave.lower() == ucr.get('hostname').lower():
			raise SchoolInstallerError(_('The name of the educational server may not be equal to the name of the administrative slave.'))

		if server_role == 'domaincontroller_slave':
			# on slave systems, download the certificate from the master in order
			# to be able to build up secure connections
			self.original_certificate_file = self.retrieve_root_certificate(master)

		if server_role != 'domaincontroller_master':
			# check for a compatible environment on the DC master

			masterinfo = self._umc_master(username, password, master, 'schoolinstaller/get/metainfo')
			school_environment = masterinfo['school_environment']
			master_samba_version = masterinfo['samba']
			if not school_environment:
				raise SchoolInstallerError(_('Please install UCS@school on the master domain controller system. Cannot proceed installation on this system.'))
			if master_samba_version == 3:
				raise SchoolInstallerError(_('This UCS domain uses Samba 3 which is no longer supported by UCS@school. Please update all domain systems to samba 4 to be able to continue.'))
			if server_role == 'domaincontroller_slave' and school_environment != 'multiserver':
				raise SchoolInstallerError(_('The master domain controller is not configured for a UCS@school multi server environment. Cannot proceed installation on this system.'))
			if server_role == 'domaincontroller_backup' and school_environment != setup:
				raise SchoolInstallerError(_('The UCS@school master domain controller needs to be configured similarly to this backup system. Please choose the correct environment type for this system.'))
			if server_role == 'domaincontroller_backup' and not joined:
				raise SchoolInstallerError(_('In order to install UCS@school on a backup domain controller, the system needs to be joined first.'))

		# everything ok, try to acquire the lock for the package installation
		lock_aquired = self.package_manager.lock(raise_on_fail=False)
		if not lock_aquired:
			MODULE.warn('Could not aquire lock for package manager')
			raise SchoolInstallerError(_('Cannot get lock for installation process. Another package manager seems to block the operation.'))

		# see which packages we need to install
		MODULE.process('performing UCS@school installation')
		packages_to_install = []
		installed_samba_version = self.get_samba_version()
		if installed_samba_version == 3:
			raise SchoolInstallerError(_('This UCS domain uses Samba 3 which is no longer supported by UCS@school. Please update all domain systems to samba 4 to be able to continue.'))
		if server_role == 'domaincontroller_slave':
			# slave
			packages_to_install.extend(['univention-samba4', 'univention-s4-connector'])
			if server_type == 'educational':
				packages_to_install.append('ucs-school-slave')
			else:
				packages_to_install.append('ucs-school-nonedu-slave')
		else:  # master or backup
			if setup == 'singlemaster':
				if installed_samba_version:
					pass  # do not install samba a second time
				else:  # otherwise install samba4
					packages_to_install.extend(['univention-samba4', 'univention-s4-connector'])
				packages_to_install.append('ucs-school-singlemaster')
			elif setup == 'multiserver':
				packages_to_install.append('ucs-school-master')
			else:
				raise SchoolInstallerError(_('Invalid UCS@school configuration.'))
		MODULE.info('Packages to be installed: %s' % ', '.join(packages_to_install))

		# reset the current installation progress
		steps = 100  # installation -> 100
		if server_role != 'domaincontroller_backup' and not (server_role == 'domaincontroller_master' and setup == 'multiserver'):
			steps += 10  # create_ou -> 10
		if server_role == 'domaincontroller_slave':
			steps += 10  # move_slave_into_ou -> 10
		steps += 100  # system_join -> 100 steps

		self._installation_started = True
		progress_state = self.progress_state
		progress_state.reset(steps)
		progress_state.component = _('Installation of UCS@school packages')
		self.package_manager.reset_status()

		def _thread(_self, packages):
			MODULE.process('Starting package installation')
			with _self.package_manager.locked(reset_status=True, set_finished=True):
				with _self.package_manager.no_umc_restart(exclude_apache=True):
					_self.package_manager.update()
					if not _self.package_manager.install(*packages):
						raise SchoolInstallerError(_('Failed to install packages.'))

			if server_role != 'domaincontroller_backup' and not (server_role == 'domaincontroller_master' and setup == 'multiserver'):
				# create the school OU (not on backup and not on master w/multi server environment)
				MODULE.info('Starting creation of LDAP school OU structure...')
				progress_state.component = _('Creation of LDAP school structure')
				progress_state.info = ''
				try:
					if server_role == 'domaincontroller_slave':
						_educational_slave = ucr.get('hostname') if server_type == 'educational' else educational_slave
						administrative_slave = None if server_type == 'educational' else ucr.get('hostname')
						create_ou_remote(master, username, password, school_ou, ou_display_name, _educational_slave, administrative_slave)
					elif server_role == 'domaincontroller_master':
						create_ou_local(school_ou, ou_display_name)
				except SchoolInstallerError as exc:
					MODULE.error(str(exc))
					raise SchoolInstallerError(_(
						'The UCS@school software packages have been installed, however, a school OU could not be created and consequently a re-join of the system has not been performed. '
						'Please create a new school OU structure using the UMC module "Add school" on the master and perform a domain join on this machine via the UMC module "Domain join".'
					))

				progress_state.add_steps(10)

			if server_role == 'domaincontroller_slave':
				# make sure that the slave is correctly moved below its OU
				MODULE.info('Trying to move the slave entry in the right OU structure...')
				result = umc(username, password, master, 'schoolwizards/schools/move_dc', {'schooldc': ucr.get('hostname'), 'schoolou': school_ou}, 'schoolwizards/schools').result
				if not result.get('success'):
					MODULE.warn('Could not successfully move the slave DC into its correct OU structure:\n%s' % result.get('message'))
					raise SchoolInstallerError(_('Validating the LDAP school OU structure failed. It seems that the current slave system has already been assigned to a different school or that the specified school OU name is already in use.'))

			# system join on a slave system
			progress_state.component = _('Domain join')
			if server_role == 'domaincontroller_slave':
				progress_state.info = _('Preparing domain join...')
				MODULE.process('Starting system join...')
			else:  # run join scripts on DC backup/master
				progress_state.info = _('Executing join scripts...')
				MODULE.process('Running join scripts...')
			system_join(
				username, password,
				info_handler=self.progress_state.info_handler,
				step_handler=self.progress_state.add_steps,
				error_handler=self.progress_state.error_handler,
			)

		def _finished(thread, result):
			MODULE.info('Finished installation')
			progress_state.finish()
			progress_state.info = _('finished...')
			self._installation_started = None
			if isinstance(result, SchoolInstallerError):
				MODULE.warn('Error during installation: %s' % (result,))
				self.restore_original_certificate()
				progress_state.error_handler(str(result))
			elif isinstance(result, BaseException):
				self.restore_original_certificate()
				msg = ''.join(traceback.format_exception(*thread.exc_info))
				MODULE.error('Exception during installation: %s' % msg)
				progress_state.error_handler(_('An unexpected error occurred during installation: %s') % result)

		thread = notifier.threads.Simple('ucsschool-install', notifier.Callback(_thread, self, packages_to_install), notifier.Callback(_finished))
		thread.run()
		self.finished(request.id, None)

	def retrieve_root_certificate(self, master):
		'''On a slave system, download the root certificate from the specified master
		and install it on the system. In this way it can be ensured that secure
		connections can be performed even though the system has not been joined yet.
		Returns the renamed original file if it has been renamed. Otherwise None is returned.'''
		if ucr.get('server/role') != 'domaincontroller_slave':
			# only do this on a slave system
			return

		# make sure the directory exists
		if not os.path.exists(os.path.dirname(CERTIFICATE_PATH)):
			os.makedirs(os.path.dirname(CERTIFICATE_PATH))

		original_certificate_file = None
		# download the certificate from the DC master
		certificate_uri = 'http://%s/ucs-root-ca.crt' % (master,)
		MODULE.info('Downloading root certificate from: %s' % (master,))
		try:
			certificate_file, headers = urllib.urlretrieve(certificate_uri)

			if not filecmp.cmp(CERTIFICATE_PATH, certificate_file):
				# we need to update the certificate file...
				# save the original file first and make sure we do not override any existing file
				count = 1
				original_certificate_file = CERTIFICATE_PATH + '.orig'
				while os.path.exists(original_certificate_file):
					count += 1
					original_certificate_file = CERTIFICATE_PATH + '.orig%s' % count
				os.rename(CERTIFICATE_PATH, original_certificate_file)
				MODULE.info('Backing up old root certificate as: %s' % original_certificate_file)

				# place the downloaded certificate at the original position
				os.rename(certificate_file, CERTIFICATE_PATH)
				os.chmod(CERTIFICATE_PATH, 0o644)
		except EnvironmentError as exc:
			# print warning and ignore error
			MODULE.warn('Could not download root certificate [%s], error ignored: %s' % (certificate_uri, exc))
			self.original_certificate_file = original_certificate_file
			self.restore_original_certificate()

		return original_certificate_file

	def restore_original_certificate(self):
		# try to restore the original certificate file
		if self.original_certificate_file and os.path.exists(self.original_certificate_file):
			try:
				MODULE.info('Restoring original root certificate.')
				os.rename(self.original_certificate_file, CERTIFICATE_PATH)
			except EnvironmentError as exc:
				MODULE.warn('Could not restore original root certificate: %s' % (exc,))
			self.original_certificate_file = None
예제 #18
0
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))
예제 #19
0
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
예제 #20
0
class Instance(SchoolBaseModule):
    ATJOB_KEY = 'UMC-computerroom'

    def init(self):
        SchoolBaseModule.init(self)
        ComputerSanitizer.instance = self
        self._italc = ITALC_Manager()
        self._random = Random()
        self._random.seed()
        self._lessons = SchoolLessons()
        self._ruleEndAt = None
        self._load_plugins()

    def _load_plugins(self):
        self._plugins = {}
        for module in os.listdir(
                os.path.join(os.path.dirname(__file__), 'plugins')):
            if module.endswith('.py'):
                try:
                    module = importlib.import_module(
                        'univention.management.console.modules.computerroom.plugins.%s'
                        % (module[:-3], ))
                except ImportError:
                    MODULE.error(traceback.format_exc())
                for name, plugin in inspect.getmembers(module,
                                                       inspect.isclass):
                    MODULE.info('Loading plugin %r from module %r' % (
                        plugin,
                        module,
                    ))
                    if not name.startswith(
                            '_') and plugin is not Plugin and issubclass(
                                plugin, Plugin):
                        try:
                            plugin = plugin(self, self._italc)
                        except Exception:
                            MODULE.error(traceback.format_exc())
                        else:
                            self._plugins[plugin.name] = plugin

    def destroy(self):
        '''Remove lock file when UMC module exists'''
        MODULE.info('Cleaning up')
        if self._italc.room:
            # do not remove lock file during exam mode
            info = _readRoomInfo(self._italc.roomDN)
            MODULE.info('room info: %s' % info)
            if info and not info.get('exam'):
                MODULE.info('Removing lock file for room %s (%s)' %
                            (self._italc.room, self._italc.roomDN))
                _freeRoom(self._italc.roomDN, self.user_dn)
        _exit(0)

    def lessons(self, request):
        """Returns a list of school lessons. Lessons in the past are filtered out"""
        current = self._lessons.current
        if current is None:
            current = self._lessons.previous

        if current:
            lessons = [
                x for x in self._lessons.lessons if x.begin >= current.begin
            ]
        else:
            lessons = self._lessons.lessons
        self.finished(request.id, [x.name for x in lessons])

    def internetrules(self, request):
        """Returns a list of available internet rules"""
        self.finished(request.id, [x.name for x in internetrules.list()])

    @sanitize(room=DNSanitizer(required=True))
    @LDAP_Connection()
    def room_acquire(self, request, ldap_user_read=None):
        """Acquires the specified computerroom"""
        roomDN = request.options['room']

        success = True
        message = 'OK'

        # match the corresponding school OU
        try:
            room = ComputerRoom.from_dn(roomDN, None, ldap_user_read)
            school = room.school
        except udm_exceptions.noObject:
            success = False
            message = 'UNKNOWN_ROOM'
        else:
            # set room and school
            if self._italc.school != school:
                self._italc.school = school
            if self._italc.room != roomDN:
                try:
                    self._italc.room = roomDN
                except ITALC_Error:
                    success = False
                    message = 'EMPTY_ROOM'

        # update the room info file
        if success:
            _updateRoomInfo(roomDN, user=self.user_dn)
            if not compare_dn(_getRoomOwner(roomDN), self.user_dn):
                success = False
                message = 'ALREADY_LOCKED'

        info = dict()
        if success:
            info = _readRoomInfo(roomDN)
        self.finished(
            request.id,
            dict(success=success,
                 message=message,
                 info=dict(
                     exam=info.get('exam'),
                     examDescription=info.get('examDescription'),
                     examEndTime=info.get('examEndTime'),
                     room=info.get('room'),
                     user=info.get('user'),
                 )))

    @sanitize(school=SchoolSanitizer(required=True))
    @LDAP_Connection()
    def rooms(self, request, ldap_user_read=None):
        """Returns a list of all available rooms"""
        rooms = []
        try:
            all_rooms = ComputerRoom.get_all(ldap_user_read,
                                             request.options['school'])
        except udm_exceptions.noObject:
            all_rooms = []

        for room in all_rooms:
            room_info = _readRoomInfo(room.dn)
            user_dn = room_info.get('user')

            locked = user_dn and not compare_dn(user_dn, self.user_dn) and (
                'pid' in room_info or 'exam' in room_info)
            if locked:
                try:
                    # open the corresponding UDM object to get a displayable user name
                    user_dn = Display.user(
                        User.from_dn(
                            user_dn, None,
                            ldap_user_read).get_udm_object(ldap_user_read))
                except udm_exceptions.base as exc:
                    MODULE.warn(
                        'Cannot open LDAP information for user %r: %s' %
                        (user_dn, exc))

            rooms.append({
                'id': room.dn,
                'label': room.get_relative_name(),
                'user': user_dn,
                'locked': locked,
                'exam': room_info.get('exam'),
                'examDescription': room_info.get('examDescription'),
                'examEndTime': room_info.get('examEndTime'),
            })

        self.finished(request.id, rooms)

    @sanitize(ipaddress=ListSanitizer(required=True,
                                      sanitizer=IPAddressSanitizer(),
                                      min_elements=1,
                                      max_elements=10))
    @LDAP_Connection()
    def guess_room(self, request, ldap_user_read=None):
        ipaddress = request.options['ipaddress']
        host_filter = self._get_host_filter(ipaddress)
        computers = ldap_user_read.searchDn(host_filter)
        if computers:
            room_filter = self._get_room_filter(computers)
            for school in School.get_all(ldap_user_read):
                school = school.name
                for room in ComputerRoom.get_all(ldap_user_read, school,
                                                 room_filter):
                    self.finished(request.id, dict(school=school,
                                                   room=room.dn))
                    return
        self.finished(request.id, dict(school=None, room=None))

    def _get_room_filter(self, computers):
        return '(|(%s))' % ')('.join(
            filter_format('uniqueMember=%s', (computer, ))
            for computer in computers)

    def _get_host_filter(self, ipaddresses):
        records = {4: 'aRecord=%s', 6: 'aAAARecord=%s'}
        return '(|(%s))' % ')('.join(
            filter_format(records[ipaddress.version], (ipaddress.exploded, ))
            for ipaddress in ipaddresses)

    def _checkRoomAccess(self):
        if not self._italc.room:
            return  # no room has been selected so far

        # make sure that we run the current room session
        userDN = _getRoomOwner(self._italc.roomDN)
        if userDN and not compare_dn(userDN, self.user_dn):
            raise UMC_Error(
                _('A different user is already running a computer room session.'
                  ))

    @LDAP_Connection()
    def query(self, request, ldap_user_read=None):
        """Searches for entries. This is not allowed if the room could not be acquired."""

        if not self._italc.school or not self._italc.room:
            raise UMC_Error('no room selected')

        if request.options.get('reload', False):
            self._italc.room = self._italc.room  # believe me that makes sense :)

        result = [computer.dict for computer in self._italc.values()]
        result.sort(key=lambda c: c['id'])

        MODULE.info('computerroom.query: result: %s' % (result, ))
        self.finished(request.id, result)

    @LDAP_Connection()
    def update(self, request, ldap_user_read=None):
        """Returns an update for the computers in the selected
		room. Just attributes that have changed since the last call will
		be included in the result
		"""

        if not self._italc.school or not self._italc.room:
            raise UMC_Error('no room selected')

        computers = [
            computer.dict for computer in self._italc.values()
            if computer.hasChanged
        ]
        info = _readRoomInfo(self._italc.roomDN)
        result = {
            'computers': computers,
            'room_info': info,
            'locked': info.get('user', self.user_dn) != self.user_dn,
            'user': self.user_dn,
        }

        if result['locked'] and 'pid' in info:
            result['user'] = info['user']
            # somebody else acquired the room, the room is locked
            try:
                # open the corresponding UDM object to get a displayable user name
                result['user'] = Display.user(
                    User.from_dn(
                        result['user'], None,
                        ldap_user_read).get_udm_object(ldap_user_read))
            except udm_exceptions.base as exc:
                # could not oben the LDAP object, show the DN
                MODULE.warn('Cannot open LDAP information for user %r: %s' %
                            (result['user'], exc))

        # settings info
        if self._ruleEndAt is not None:
            diff = self._positiveTimeDiff()
            if diff is not None:
                result['settingEndsIn'] = diff.seconds / 60

        MODULE.info('Update: result: %s' % str(result))
        self.finished(request.id, result)

    def _positiveTimeDiff(self):
        now = datetime.datetime.now()
        end = datetime.datetime.now()
        end = end.replace(hour=self._ruleEndAt.hour,
                          minute=self._ruleEndAt.minute)
        if now > end:
            return None
        return end - now

    @check_room_access
    @sanitize(
        computer=ComputerSanitizer(required=True),
        device=ChoicesSanitizer(['screen', 'input'], required=True),
        lock=BooleanSanitizer(required=True),
    )
    @simple_response
    def lock(self, computer, device, lock):
        """Lock or Unlock the screen or input of a specific computer"""

        MODULE.warn('Locking device %s' % (device, ))
        if device == 'screen':
            computer.lockScreen(lock)
        else:
            computer.lockInput(lock)

    @allow_get_request
    @check_room_access
    @sanitize(
        computer=ComputerSanitizer(required=True), )
    @prevent_ucc
    def screenshot(self, request):
        """Returns a JPEG image containing a screenshot of the given computer."""

        computer = request.options['computer']
        tmpfile = computer.screenshot
        if computer.hide_screenshot:
            filename = FN_SCREENSHOT_DENIED
        elif tmpfile is None:
            filename = FN_SCREENSHOT_NOTREADY
        else:
            filename = tmpfile.name

        MODULE.info('screenshot(%s): hide screenshot = %r' %
                    (computer.name, computer.hide_screenshot))
        try:
            with open(filename, 'rb') as fd:
                response = fd.read()
        except EnvironmentError as exc:
            MODULE.error('Unable to load screenshot file %r: %s' %
                         (filename, exc))
        try:
            if tmpfile:
                os.unlink(tmpfile.name)
        except EnvironmentError as exc:
            MODULE.error('Unable to remove temporary screenshot file %r: %s' %
                         (tmpfile.name, exc))

        self.finished(request.id, response, mimetype='image/jpeg')

    @check_room_access
    @sanitize(
        computer=ComputerSanitizer(required=True), )
    def vnc(self, request):
        """Returns a ultraVNC file for the given computer."""

        # check whether VNC is enabled
        if ucr.is_false('ucsschool/umc/computerroom/ultravnc/enabled', True):
            raise UMC_Error('VNC is disabled')

        try:
            with open('/usr/share/ucs-school-umc-computerroom/ultravnc.vnc',
                      'rb') as fd:
                content = fd.read()
        except (IOError, OSError):
            raise UMC_Error('VNC template file does not exists')

        port = ucr.get('ucsschool/umc/computerroom/vnc/port', '11100')
        hostname = request.options['computer'].ipAddress

        response = content.replace('@%@HOSTNAME@%@',
                                   hostname).replace('@%@PORT@%@', port)
        self.finished(request.id, response, mimetype='application/x-vnc')

    @simple_response
    def settings_get(self):
        """Return the current settings for a room"""

        if not self._italc.school or not self._italc.room:
            raise UMC_Error('no room selected')

        ucr.load()
        rule = ucr.get('proxy/filter/room/%s/rule' % self._italc.room, 'none')
        if rule == self._username:
            rule = 'custom'
        shareMode = ucr.get('samba/sharemode/room/%s' % self._italc.room,
                            'all')
        # load custom rule:
        key_prefix = 'proxy/filter/setting-user/%s/domain/whitelisted/' % self._username
        custom_rules = []
        for key in ucr:
            if key.startswith(key_prefix):
                custom_rules.append(ucr[key])

        printMode = ucr.get('samba/printmode/room/%s' % self._italc.room,
                            'default')
        # find AT jobs for the room and execute it to remove current settings
        jobs = atjobs.list(extended=True)
        for job in jobs:
            if job.comments.get(Instance.ATJOB_KEY, False) == self._italc.room:
                if job.execTime >= datetime.datetime.now():
                    self._ruleEndAt = job.execTime
                break
        else:
            self._ruleEndAt = None

        if rule == 'none' and shareMode == 'all' and printMode == 'default':
            self._ruleEndAt = None

        # find end of lesson
        period = self._lessons.current
        if period is None:
            if self._lessons.next:  # between two lessons
                period = self._lessons.next.end
            else:  # school is out ... 1 hour should be good (FIXME: configurable?)
                period = datetime.datetime.now() + datetime.timedelta(hours=1)
                period = period.time()
        else:
            period = period.end

        if self._ruleEndAt:
            time = self._ruleEndAt.time()
            for lesson in self._lessons.lessons:
                if time == lesson.begin:
                    period = lesson
                    break

        return {
            'internetRule': rule,
            'customRule': '\n'.join(custom_rules),
            'shareMode': shareMode,
            'printMode': printMode,
            'period': str(period),
        }

    @check_room_access
    @simple_response
    def finish_exam(self):
        """Finish the exam in the current room"""
        self._settings_set(printMode='default',
                           internetRule='none',
                           shareMode='all',
                           customRule='')
        _updateRoomInfo(self._italc.roomDN,
                        exam=None,
                        examDescription=None,
                        examEndTime=None)

    @sanitize(
        room=DNSanitizer(required=True),
        exam=StringSanitizer(required=True),
        examDescription=StringSanitizer(required=True),
        examEndTime=StringSanitizer(required=True),
    )
    @check_room_access
    @simple_response
    def start_exam(self, room, exam, examDescription, examEndTime):
        """Start an exam in the current room"""
        info = _readRoomInfo(room)
        if info.get('exam'):
            raise UMC_Error(
                _('In this room an exam is currently already written. Please select another room.'
                  ))

        _updateRoomInfo(self._italc.roomDN,
                        exam=exam,
                        examDescription=examDescription,
                        examEndTime=examEndTime)

    @sanitize(
        printMode=ChoicesSanitizer(['none', 'all', 'default'], required=True),
        internetRule=StringSanitizer(required=True),
        shareMode=ChoicesSanitizer(['home', 'all'], required=True),
        period=PeriodSanitizer(default='00:00', required=False),
        customRule=StringSanitizer(allow_none=True, required=False),
    )
    @check_room_access
    @simple_response
    def settings_set(self,
                     printMode,
                     internetRule,
                     shareMode,
                     period=None,
                     customRule=None):
        return self._settings_set(printMode, internetRule, shareMode, period,
                                  customRule)

    def _settings_set(self,
                      printMode,
                      internetRule,
                      shareMode,
                      period=None,
                      customRule=None):
        """Defines settings for a room"""

        if not self._italc.school or not self._italc.room:
            raise UMC_Error('no room selected')

        # find AT jobs for the room and execute it to remove current settings
        jobs = atjobs.list(extended=True)
        for job in jobs:
            if job.comments.get(Instance.ATJOB_KEY, False) == self._italc.room:
                job.rm()
                subprocess.call(shlex.split(job.command))

        roomInfo = _readRoomInfo(self._italc.roomDN)
        in_exam_mode = roomInfo.get('exam')

        # for the exam mode, remove current settings before setting new ones
        if in_exam_mode and roomInfo.get('cmd'):
            MODULE.info('unsetting room settings for exam (%s): %s' %
                        (roomInfo['exam'], roomInfo['cmd']))
            try:
                subprocess.call(shlex.split(roomInfo['cmd']))
            except (OSError, IOError):
                MODULE.warn(
                    'Failed to reinitialize current room settings: %s' %
                    roomInfo['cmd'])
            _updateRoomInfo(self._italc.roomDN, cmd=None)

        # reset to defaults. No atjob is necessary.
        if internetRule == 'none' and shareMode == 'all' and printMode == 'default':
            self._ruleEndAt = None
            self.reset_smb_connections()
            self.reload_cups()
            return

        # collect new settings
        vset = {}
        vappend = {}
        vunset = []
        vunset_now = []
        vextract = []
        hosts = self._italc.ipAddresses(students_only=True)

        # print mode
        if printMode in ('none', 'all'):
            vextract.append('samba/printmode/hosts/%s' % printMode)
            vappend[vextract[-1]] = hosts
            vextract.append('cups/printmode/hosts/%s' % printMode)
            vappend[vextract[-1]] = hosts
            vunset.append('samba/printmode/room/%s' % self._italc.room)
            vset[vunset[-1]] = printMode
        else:
            vunset_now.append('samba/printmode/room/%s' % self._italc.room)

        # share mode
        if shareMode == 'home':
            vunset.append('samba/sharemode/room/%s' % self._italc.room)
            vset[vunset[-1]] = shareMode
            vextract.append('samba/othershares/hosts/deny')
            vappend[vextract[-1]] = hosts
            vextract.append('samba/share/Marktplatz/hosts/deny')
            vappend[vextract[-1]] = hosts
        else:
            vunset_now.append('samba/sharemode/room/%s' % self._italc.room)

        # internet rule
        if internetRule != 'none':
            vextract.append('proxy/filter/room/%s/ip' % self._italc.room)
            vappend[vextract[-1]] = hosts
            if internetRule == 'custom':
                # remove old rules
                i = 1
                while True:
                    var = 'proxy/filter/setting-user/%s/domain/whitelisted/%d' % (
                        self._username, i)
                    if var in ucr:
                        vunset_now.append(var)
                        i += 1
                    else:
                        break
                vunset.append('proxy/filter/room/%s/rule' % self._italc.room)
                vset[vunset[-1]] = self._username
                vset['proxy/filter/setting-user/%s/filtertype' %
                     self._username] = 'whitelist-block'
                i = 1
                for domain in (customRule or '').split('\n'):
                    MODULE.info('Setting whitelist entry for domain %s' %
                                domain)
                    if not domain:
                        continue
                    parsed = urlparse.urlsplit(domain)
                    MODULE.info('Setting whitelist entry for domain %s' %
                                str(parsed))
                    if parsed.netloc:
                        vset[
                            'proxy/filter/setting-user/%s/domain/whitelisted/%d'
                            % (self._username, i)] = parsed.netloc
                        i += 1
                    elif parsed.path:
                        vset[
                            'proxy/filter/setting-user/%s/domain/whitelisted/%d'
                            % (self._username, i)] = parsed.path
                        i += 1
            else:
                vunset.append('proxy/filter/room/%s/rule' % self._italc.room)
                vset[vunset[-1]] = internetRule
        else:
            vunset_now.append('proxy/filter/room/%s/ip' % self._italc.room)
            vunset_now.append('proxy/filter/room/%s/rule' % self._italc.room)
        # write configuration
        # remove old values
        handler_unset(vunset_now)

        # append values
        ucr.load()
        MODULE.info('Merging UCR variables')
        for key, value in vappend.items():
            if ucr.get(key):
                old = set(ucr[key].split(' '))
                MODULE.info('Old value: %s' % old)
            else:
                old = set()
                MODULE.info('Old value empty')
            new = set(value)
            MODULE.info('New value: %s' % new)
            new = old.union(new)
            MODULE.info('Merged value of %s: %s' % (key, new))
            if not new:
                MODULE.info('Unset variable %s' % key)
                vunset.append(key)
            else:
                vset[key] = ' '.join(new)

        # Workaround for bug 30450:
        # if samba/printmode/hosts/none is not set but samba/printmode/hosts/all then all other hosts
        # are unable to print on samba shares. Solution: set empty value for .../none if no host is on deny list.
        varname = 'samba/printmode/hosts/none'
        if varname not in vset:
            ucr.load()
            if not ucr.get(varname):
                vset[varname] = '""'
        else:
            # remove empty items ('""') in list
            vset[varname] = ' '.join(
                [x for x in vset[varname].split(' ') if x != '""'])
        if varname in vunset:
            vunset.remove(varname)

        # set values
        ucr_vars = sorted('%s=%s' % x for x in vset.items())
        MODULE.info('Writing room rules: %s' % '\n'.join(ucr_vars))
        handler_set(ucr_vars)

        # create at job to remove settings
        unset_vars = ['-r %s' % quote(x) for x in vunset]
        MODULE.info('Will remove: %s' % ' '.join(unset_vars))
        extract_vars = ['-e %s' % quote(x) for x in vextract]
        MODULE.info('Will extract: %s' % ' '.join(extract_vars))

        cmd = '/usr/share/ucs-school-umc-computerroom/ucs-school-deactivate-rules %s %s %s' % (
            ' '.join(unset_vars), ' '.join(extract_vars), ' '.join(
                quote(x) for x in hosts))
        MODULE.info('command for reinitialization is: %s' % (cmd, ))

        if in_exam_mode:
            # Command for the exam mode to be executed manually when changing the settings again...
            _updateRoomInfo(self._italc.roomDN, cmd=cmd)
        else:
            starttime = datetime.datetime.now()
            MODULE.info('Now: %s' % starttime)
            MODULE.info('Endtime: %s' % period)
            starttime = starttime.replace(hour=period.hour,
                                          minute=period.minute,
                                          second=0,
                                          microsecond=0)
            while starttime < datetime.datetime.now(
            ):  # prevent problems due to intra-day limit
                starttime += datetime.timedelta(days=1)

            # AT job for the normal case
            MODULE.info('Remove settings at %s' % (starttime, ))
            atjobs.add(cmd, starttime, {Instance.ATJOB_KEY: self._italc.room})
            self._ruleEndAt = starttime

        self.reset_smb_connections()
        self.reload_cups()

    def reload_cups(self):
        if os.path.exists('/etc/init.d/cups'):
            MODULE.info('Reloading cups')
            if subprocess.call(['/etc/init.d/cups', 'reload']) != 0:
                MODULE.error(
                    'Failed to reload cups! Printer settings not applied.')

    def reset_smb_connections(self):
        smbstatus = SMB_Status()
        italc_users = [x.lower() for x in self._italc.users if x]
        MODULE.info('iTALC users: %s' % ', '.join(italc_users))
        for process in smbstatus:
            MODULE.info('SMB process: %s' % str(process))
            if process.username and process.username.lower() in italc_users:
                MODULE.info('Kill SMB process %s' % process.pid)
                os.kill(int(process.pid), signal.SIGTERM)

    @sanitize(
        server=StringSanitizer(required=True), )
    @check_room_access
    def demo_start(self, request):
        """Starts a presentation mode"""
        self._italc.startDemo(request.options['server'], True)
        self.finished(request.id, True)

    @check_room_access
    def demo_stop(self, request):
        """Stops a presentation mode"""

        self._italc.stopDemo()
        self.finished(request.id, True)

    @sanitize(
        state=ChoicesSanitizer(['poweroff', 'poweron', 'restart']),
        computer=ComputerSanitizer(required=True),
    )
    @check_room_access
    @prevent_ucc(
        condition=lambda request: request.options['state'] != 'poweron')
    @simple_response
    def computer_state(self, computer, state):
        """Stops, starts or restarts a computer"""

        if state == 'poweroff':
            computer.powerOff()
        elif state == 'poweron':
            computer.powerOn()
        elif state == 'restart':
            computer.restart()
        return True

    @check_room_access
    @sanitize(
        computer=ComputerSanitizer(required=True), )
    @prevent_ucc
    @simple_response
    def user_logout(self, computer):
        """Log out the user at the given computer"""

        computer.logOut()
        return True

    @simple_response
    def plugins_load(self):
        plugins = {'buttons': []}
        for plugin in self._plugins.values():
            plugins['buttons'].append(plugin.button())
        return plugins

    @check_room_access
    @sanitize(
        plugin=StringSanitizer(required=True),
        computer=StringSanitizer(required=True),
    )
    def plugins_execute(self, request):
        plugin = self._plugins.get(request.options['plugin'])
        if not plugin:
            raise UMC_Error('Plugin not found.')
        result = plugin.execute(request.options['computer'])
        self.finished(request.id, result)
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) -> None:
        MODULE.info("Initializing 'updater' module (PID = %d)" % (getpid(), ))
        self._current_job = ''
        self._logfile_start_line = 0
        self._serial_file = Watched_File(COMPONENTS_SERIAL_FILE)
        try:
            self.uu = UniventionUpdater(False)
        except Exception as exc:  # FIXME: let it raise
            self.uu = None
            MODULE.error("init() ERROR: %s" % (exc, ))

    @simple_response
    def query_maintenance_information(self) -> Dict[str, Any]:
        ret = self._maintenance_information()
        ret.update(self._last_update())
        return ret

    def _last_update(self) -> Dict[str, Any]:
        status_file = '/var/lib/univention-updater/univention-updater.status'
        ret = {
            'last_update_failed': False,
            'last_update_version': None
        }  # type: Dict[str, Any]
        try:
            fstat = stat(status_file)
            mtime = datetime.fromtimestamp(fstat.st_mtime)
            delta = datetime.now() - mtime
            if delta.days != 0:  # no fresh failure
                return ret

            with open(status_file) as fd:
                info = dict(line.strip().split('=', 1)  # type: ignore
                            for line in fd)  # type: Dict[str, str]

            ret['last_update_failed'] = info.get('status') == 'FAILED'
            if ret['last_update_failed']:
                ret['last_update_version'] = info.get('next_version')
        except (ValueError, EnvironmentError) as exc:
            MODULE.error(str(exc))

        return ret

    def _maintenance_information(self) -> Dict[str, Any]:
        default = {'show_warning': False}
        if not self.uu:
            return default

        ucr.load()
        if ucr.is_true('license/extended_maintenance/disable_warning'):
            return default

        version = self.uu.current_version
        for _ver, data in self.uu.get_releases(version, version):
            status = data.get('status', 'unmaintained')

            maintenance_extended = status == 'extended'
            show_warning = maintenance_extended or status != 'maintained'

            return {
                'ucs_version': str(version),
                'show_warning': show_warning,
                'maintenance_extended': maintenance_extended,
                'base_dn': ucr.get('license/base'),
            }

        return default

    @simple_response
    def query_releases(self) -> List[Dict[str, str]]:
        """
		Returns a list of system releases suitable for the
		corresponding ComboBox
		"""

        # be as current as possible.
        self.uu.ucr_reinit()
        ucr.load()

        appliance_mode = ucr.is_true('server/appliance')

        available_versions, blocking_components = self.uu.get_all_available_release_updates(
        )
        result = [{
            'id': str(rel),
            'label': 'UCS %s' % (rel, )
        } for rel in available_versions]
        #
        # appliance_mode=no ; blocking_comp=no  → add "latest version"
        # appliance_mode=no ; blocking_comp=yes →  no "latest version"
        # appliance_mode=yes; blocking_comp=no  → add "latest version"
        # appliance_mode=yes; blocking_comp=yes → add "latest version"
        #
        if result and (appliance_mode or not blocking_components):
            # UniventionUpdater returns available version in ascending order, so
            # the last returned entry is the one to be flagged as 'latest' if there's
            # no blocking component.
            result[-1]['label'] = '%s (%s)' % (result[-1]['label'],
                                               _('latest version'))

        return result

    @sanitize(hooks=ListSanitizer(StringSanitizer(minimum=1), required=True))
    def call_hooks(self, request: Request) -> None:
        """
		Calls the specified hooks and returns data given back by each hook
		"""
        def _thread(request: Request) -> Dict[str, Any]:
            result = {}
            hookmanager = HookManager(
                HOOK_DIRECTORY)  # , raise_exceptions=False

            hooknames = request.options.get('hooks')
            MODULE.info('requested hooks: %s' % hooknames)
            for hookname in hooknames:
                MODULE.info('calling hook %s' % hookname)
                result[hookname] = hookmanager.call_hook(hookname)

            MODULE.info('result: %r' % (result, ))
            return result

        thread = notifier.threads.Simple(
            'call_hooks', notifier.Callback(_thread, request),
            notifier.Callback(self.thread_finished_callback, request))
        thread.run()

    @simple_response
    def updates_check(self) -> Dict[str, List[Tuple[str, str]]]:
        """
		Returns the list of packages to be updated/installed
		by a dist-upgrade.
		"""
        install = []
        update = []
        remove = []

        apt = Cache(memonly=True)
        apt.update()
        apt.open()
        apt.clear()
        apt.upgrade(dist_upgrade=True)
        for pkg in apt.get_changes():
            if pkg.marked_install:
                install.append((pkg.name, pkg.candidate.version))
            if pkg.marked_upgrade:
                update.append((pkg.name, pkg.candidate.version))
            if pkg.marked_delete:
                remove.append((pkg.name, pkg.installed.version))

        return dict(
            update=sorted(update),
            install=sorted(install),
            remove=sorted(remove),
        )

    @simple_response
    def updates_available(self) -> bool:
        """
		Asks if there are package updates available. (don't get confused
		by the name of the UniventionUpdater function that is called here.)
		This is a separate call since it can take an amount of time, thus
		being invoked by a separate button (and not in the background)
		"""
        ucr.load()
        try:
            # be as current as possible.
            what = 'reinitializing UniventionUpdater'
            self.uu.ucr_reinit()

            what = 'checking update availability'
            new, upgrade, removed = self.uu.component_update_get_packages()
            return any((new, upgrade, removed))
        except Exception as ex:
            typ = str(type(ex)).strip('<>')
            msg = '[while %s] [%s] %s' % (what, typ, str(ex))
            MODULE.error(msg)
        return False

    def status(self, request: Request) -> None:  # TODO: remove unneeded things
        """One call for all single-value variables."""

        result = {}  # type: Dict[str, Any]
        ucr.load()

        try:
            result['erratalevel'] = int(ucr.get('version/erratalevel', 0))
        except ValueError:
            result['erratalevel'] = 0

        result['appliance_mode'] = ucr.is_true('server/appliance')
        result['timestamp'] = int(time())
        result['reboot_required'] = ucr.is_true('update/reboot/required',
                                                False)

        try:
            # be as current as possible.
            what = 'reinitializing UniventionUpdater'
            self.uu.ucr_reinit()

            what = 'getting UCS version'
            result['ucs_version'] = str(self.uu.current_version)

            # if nothing is returned -> convert to empty string.
            what = 'querying available release updates'
            try:
                ver = self.uu.release_update_available(errorsto='exception')
                result[
                    'release_update_available'] = '' if ver is None else str(
                        ver)
            except RequiredComponentError as exc:
                result['release_update_available'] = exc.version

            what = 'querying update-blocking components'
            blocking_components = self.uu.get_all_available_release_updates(
            )[1] or set()
            # check apps
            if result['release_update_available']:
                try:
                    from univention.appcenter.actions import get_action
                    update_check = get_action('update-check')
                    if update_check:
                        blocking_apps = update_check.get_blocking_apps(
                            ucs_version=result['release_update_available'])
                        if blocking_apps:
                            blocking_components.update(set(blocking_apps))
                except (ImportError, ValueError):
                    # the new univention.appcenter package is not installed.
                    # Cannot be a dependency as the app center depends on updater...
                    raise UMC_Error(
                        _('Error checking if installed apps are available for next UCS version.'
                          ))

            result['release_update_blocking_components'] = ' '.join(
                blocking_components)

            # Component counts are now part of the general 'status' data.
            what = "counting components"
            components = [
                bool(comp) for comp in self.uu.get_components(all=True)
            ]
            result['components'] = len(components)
            result['enabled'] = sum(components)

            # HACK: the 'Updates' form polls on the serial file
            #       to refresh itself. Including the serial value
            #       into the form helps us to have a dependent field
            #       that can trigger the refresh of the "Releases"
            #       combobox and the 'package updates available' field.
            result['serial'] = self._serial_file.timestamp()

        except Exception as exc:  # FIXME: don't catch everything
            raise UMC_Error("%s %s %s" % (
                _('Error contacting the update server. Please check your proxy or firewall settings, if any. Or it may be a problem with your configured DNS server.'
                  ),
                _('This is the error message:'),
                exc,
            ),
                            traceback=format_exc())

        self.finished(request.id, [result])

    @simple_response
    def running(self) -> str:
        """
		Returns the id (key into INSTALLERS) of a currently
		running job, or the empty string if nothing is running.
		"""
        return self.__which_job_is_running()

    @sanitize(
        job=ChoicesSanitizer(list(INSTALLERS) + [''], required=True),
        count=IntegerSanitizer(default=0),
    )
    @simple_response
    def updater_log_file(self, job: str,
                         count: int) -> Union[None, float, List[str]]:
        """
		returns the content of the log file associated with
		the job.

		:param job: Job name.
		:param count: has the same meaning as already known:
			<0 ...... return timestamp of file (for polling)
			0 ....... return whole file as a string list
			>0 ...... ignore this many lines, return the rest of the file

		.. note::
			As soon as we have looked for a running job at least once,
			we know the job key and can associate it here.

		TODO: honor a given 'job' argument
		"""
        job = self._current_job or job

        if not job:
            return None

        fname = INSTALLERS[job]['logfile']
        if count < 0:
            try:
                return stat(fname).st_ctime
            except EnvironmentError:
                return 0

        # don't read complete file if we have an 'ignore' count
        count += self._logfile_start_line
        return self._logview(fname, -count)

    def _logview(self, fname: str, count: int) -> List[str]:
        """
		Contains all functions needed to view or 'tail' an arbitrary text file.

		:param count: can have different values:
			< 0 ... ignore this many lines, return the rest of the file
			0 ..... return the whole file, split into lines.
			> 0 ... return the last 'count' lines of the file. (a.k.a. tail -n <count>)
		"""
        lines = []
        try:
            with open(fname, 'rb') as fd:
                for line in fd:
                    if (count < 0):
                        count += 1
                    else:
                        lines.append(line.rstrip().decode('utf-8', 'replace'))
                        if (count > 0) and (len(lines) > count):
                            lines.pop(0)
        except EnvironmentError:
            pass
        return lines

    @sanitize(
        job=ChoicesSanitizer(INSTALLERS, required=True), )
    @simple_response
    def updater_job_status(
            self, job: str) -> Dict[str, Any]:  # TODO: remove this completely
        """Returns the status of the current/last update even if the job is not running anymore."""
        result = {}  # type: Dict[str, Any]
        try:
            with open(INSTALLERS[job]['statusfile'], 'r') as fd:
                for line in fd:
                    fields = line.strip().split('=')
                    if len(fields) == 2:
                        result['_%s_' % fields[0]] = fields[1]
        except EnvironmentError:
            pass

        result['running'] = '' != self.__which_job_is_running()
        return result

    @sanitize(
        job=ChoicesSanitizer(INSTALLERS, required=True),
        detail=StringSanitizer(r'^[A-Za-z0-9\.\- ]*$'),
    )
    @simple_response
    def run_installer(self, job: str, detail: str = '') -> Dict[str, int]:
        """
		This is the function that invokes any kind of installer. Arguments accepted:

		:param job: ..... the main thing to do. can be one of:
			'release' ...... perform a release update
			'distupgrade' .. update all currently installed packages (distupgrade)

		:param detail: ....... an argument that specifies the subject of the installer:
			for 'release' .... the target release number,
			for all other subjects: detail has no meaning.
		"""

        MODULE.info("Starting function %r" % (job, ))
        self._current_job = job
        spec = INSTALLERS[job]

        # remember initial lines of logfile before starting update to not show it in the frontend
        logfile = spec['logfile']
        try:
            with open(logfile, 'rb') as fd:
                self._logfile_start_line = sum(1 for line in fd)
        except EnvironmentError:
            pass

        command = spec['command']
        if '%' in command:
            command = command % (pipes.quote(detail).translate({
                0: None,
                10: None,
                13: None
            }), )
        MODULE.info("Creating job: %r" % (command, ))
        command = '''
%s
%s < /dev/null
%s''' % (spec["prejob"], command, spec["postjob"])
        atjobs.add(command, comments=dict(lines=self._logfile_start_line))

        return {'status': 0}

    def __which_job_is_running(self) -> str:
        # first check running at jobs
        for atjob in atjobs.list(True):
            for job, inst in INSTALLERS.items():
                cmd = inst['command'].split('%')[0]
                if cmd in atjob.command:
                    self._current_job = job
                    try:
                        self._logfile_start_line = int(
                            atjob.comments.get('lines', 0))
                    except ValueError:
                        pass
                    return job
        # no atjob found, parse process list (if univention-upgrade was started via CLI)
        commands = [
            ('/usr/share/univention-updater/univention-updater-umc-dist-upgrade',
             'distupgrade'),
            ('/usr/share/univention-updater/univention-updater', 'release'),
            ('/usr/sbin/univention-upgrade', 'distupgrade'
             )  # we don't know if it is a dist-upgrade or a release upgrade
        ]
        for cmd, job in commands:
            for process in psutil.process_iter():
                try:
                    cmdline = process.cmdline() if callable(
                        process.cmdline) else process.cmdline
                except psutil.NoSuchProcess:
                    pass

                if cmd in cmdline:
                    self._current_job = job
                    self._logfile_start_line = 0
                    return job
        return ''
예제 #23
0

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),
		return app


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=r'^[A-Za-z0-9\-\_\.]+$'),
	'description': StringSanitizer(),
	'username': StringSanitizer(),
예제 #25
0
class Instance(umcm.Base):

	def __init__(self):
		umcm.Base.__init__(self)
		self.mem_regex = re.compile('([0-9]*) kB')

	def _call(self, command):
		try:
			process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
			(stdoutdata, stderrdata, ) = process.communicate()
			return (process.returncode, stdoutdata, stderrdata, )
		except OSError:
			return (True, None, None, )

	def get_general_info(self, request):
		DMIDECODE = '/usr/sbin/dmidecode'
		MANUFACTURER_CMD = (DMIDECODE, '-s', 'system-manufacturer', )
		MODEL_CMD = (DMIDECODE, '-s', 'system-product-name', )

		stdout_list = []
		for command in (MANUFACTURER_CMD, MODEL_CMD, ):
			(exitcode, stdout, stderr, ) = self._call(command)
			if exitcode:
				message = _('Failed to execute command')
				request.status = MODULE_ERR
				self.finished(request.id, None, message)
				return
			else:
				stdout = stdout[:-1]  # remove newline character
				stdout_list.append(stdout)
		result = {}
		result['manufacturer'] = stdout_list[0]
		result['model'] = stdout_list[1]

		request.status = SUCCESS
		self.finished(request.id, result)

	def get_system_info(self, request):
		MANUFACTURER = request.options['manufacturer'].encode('utf-8')
		MODEL = request.options['model'].encode('utf-8')
		COMMENT = request.options['comment'].encode('utf-8')
		SYSTEM_INFO_CMD = ('/usr/bin/univention-system-info',
			'-m', MANUFACTURER,
			'-t', MODEL,
			'-c', COMMENT,
			'-s', request.options.get('ticket', ''),
			'-u', )

		(exitcode, stdout, stderr, ) = self._call(SYSTEM_INFO_CMD)
		if exitcode:
			MODULE.error('Execution of univention-system-info failed: %s' % stdout)
			result = None
			request.status = MODULE_ERR
		else:
			result = {}
			for line in stdout.splitlines():
				try:
					info, value = line.split(':', 1)
					result[info] = value
				except ValueError:
					pass
			if result.get('mem'):
				match = self.mem_regex.match(result['mem'])
				if match:
					try:
						converted_mem = (float(match.groups()[0]) / 1048576)
						result['mem'] = '%.2f GB' % converted_mem
						result['mem'] = result['mem'].replace('.', ',')
					except (IndexError, ValueError):
						pass
			result.pop('Temp', None)  # remove unnecessary entry
			request.status = SUCCESS

		self.finished(request.id, result)

	@simple_response
	def get_mail_info(self):
		ucr.load()
		ADDRESS_VALUE = ucr.get('umc/sysinfo/mail/address', '*****@*****.**')
		SUBJECT_VALUE = ucr.get('umc/sysinfo/mail/subject', 'Univention System Info')

		url = urlunparse(('mailto', '', ADDRESS_VALUE, '', urlencode({'subject': SUBJECT_VALUE, }), ''))
		result = {}
		result['url'] = url.replace('+', '%20')
		return result

	@sanitize(archive=StringSanitizer(required=True))
	@simple_response
	def upload_archive(self, archive):
		ucr.load()
		url = ucr.get('umc/sysinfo/upload/url', 'https://forge.univention.org/cgi-bin/system-info-upload.py')

		SYSINFO_PATH = '/usr/share/univention-system-info/archives/'
		path = os.path.abspath(os.path.join(SYSINFO_PATH, archive))
		if not path.startswith(SYSINFO_PATH):
			raise UMC_Error('Archive path invalid.')

		fd = open(os.path.join(SYSINFO_PATH, archive), 'r')
		data = {'filename': fd, }
		req = urllib2.Request(url, data, {})
		try:
			u = urllib2.urlopen(req)
			answer = u.read()
		except (urllib2.HTTPError, urllib2.URLError, httplib.HTTPException) as exc:
			raise UMC_Error('Archive upload failed: %s' % (exc,))
		else:
			if answer.startswith('ERROR:'):
				raise UMC_Error(answer)

	@sanitize(traceback=StringSanitizer(), remark=StringSanitizer(), email=StringSanitizer())
	@simple_response
	def upload_traceback(self, traceback, remark, email):
		ucr.load()
		ucs_version = '{0}-{1} errata{2} ({3})'.format(ucr.get('version/version', ''), ucr.get('version/patchlevel', ''), ucr.get('version/erratalevel', '0'), ucr.get('version/releasename', ''))
		if ucr.get('appcenter/apps/ucsschool/version'):
			ucs_version = '%s - UCS@school %s' % (ucs_version, ucr['appcenter/apps/ucsschool/version'])
		# anonymised id of localhost
		uuid_system = ucr.get('uuid/system', '')
		url = ucr.get('umc/sysinfo/traceback/url', 'https://forge.univention.org/cgi-bin/system-info-traceback.py')
		MODULE.process('Sending %s to %s' % (traceback, url))
		request_data = {
			'traceback': traceback,
			'remark': remark,
			'email': email,
			'ucs_version': ucs_version,
			'uuid_system': uuid_system,
			'uuid_license': ucr.get('uuid/license', ''),
			'server_role': ucr.get('server/role'),
		}
		request = urllib2.Request(url, request_data)
		urllib2.urlopen(request)
예제 #26
0
class Instance(Base):
	# list of dummy entries
	entries = map(lambda x: {'id': str(uuid.uuid4()), 'name': x[0], 'color': x[1]}, [
		['Zackary Cavaco', 'Blue'],
		['Shon Hodermarsky', 'Green'],
		['Jude Nachtrieb', 'Green'],
		['Najarian', 'Blue'],
		['Oswaldo Lefeld', 'Blue'],
		['Vannessa Kopatz', 'Orange'],
		['Marcellus Hoga', 'Orange'],
		['Violette Connerty', 'Orange'],
		['Lucina Jeanquart', 'Blue'],
		['Mose Maslonka', 'Green'],
		['Emmie Dezayas', 'Green'],
		['Douglass Glaubke', 'Green'],
		['Deeann Delilli', 'Blue'],
		['Janett Cooch', 'Orange'],
		['Ike Collozo', 'Orange'],
		['Tamala Pecatoste', 'Orange'],
		['Shakira Cottillion', 'Blue'],
		['Colopy', 'Blue'],
		['Vivan Noggles', 'Green'],
		['Shawnda Hamalak', 'Blue'],
	])

	def init(self):
		# this initialization method is called when the module process is created
		pass

	def colors(self, request):
		"""Returns a list of all existing colors."""
		MODULE.info('MODULEID.colors: options: %s' % str(request.options))
		allColors = set(map(lambda x: x['color'], Instance.entries))
		allColors = map(lambda x: {'id': x, 'label': x}, allColors)
		allColors.append({'id': 'None', 'label': _('All colors')})
		MODULE.info('MODULEID.colors: result: %s' % str(allColors))
		self.finished(request.id, allColors)

	def query(self, request):
		"""Searches for entries in a dummy list

		requests.options = {}
		'name' -- search pattern for name (default: '')
		'color' -- color to match, 'None' for all colors (default: 'None')

		return: [ { 'id' : <unique identifier>, 'name' : <display name>, 'color' : <name of favorite color> }, ... ]
		"""
		MODULE.info('MODULEID.query: options: %s' % str(request.options))
		color = request.options.get('color', 'None')
		pattern = request.options.get('name', '')
		result = filter(lambda x: (color == 'None' or color == x['color']) and x['name'].find(pattern) >= 0, Instance.entries)
		MODULE.info('MODULEID.query: results: %s' % str(result))
		self.finished(request.id, result)

	@sanitize(StringSanitizer())
	def get(self, request):
		"""Returns the objects for the given IDs

		requests.options = [ <ID>, ... ]

		return: [ { 'id' : <unique identifier>, 'name' : <display name>, 'color' : <name of favorite color> }, ... ]
		"""
		MODULE.info('MODULEID.get: options: %s' % str(request.options))
		ids = set(request.options)
		result = filter(lambda x: x['id'] in ids, Instance.entries)
		MODULE.info('MODULEID.get: results: %s' % str(result))
		self.finished(request.id, result)
class Instance(Base):
    @sanitize(pattern=PatternSanitizer(default='.*'))
    @simple_response
    def query(self, pattern):
        ucr.load()
        srvs = ServiceInfo()

        lang = _.im_self.locale.language
        if lang in (None, 'C'):
            lang = 'en'

        result = []
        for name, srv in srvs.services.items():
            key = srv.get('start_type', '%s/autostart' % (name, ))
            entry = {
                'service':
                name,
                'description':
                srv.get('description[%s]' % (lang, ), srv.get('description')),
                'autostart':
                ucr.get(key, 'yes'),
                'isRunning':
                srv.running,
            }
            if entry['autostart'] not in ('yes', 'no', 'manually'):
                entry['autostart'] = 'yes'
            for value in entry.values():
                if pattern.match(str(value)):
                    result.append(entry)
                    break
        return result

    @sanitize(StringSanitizer(required=True))
    def start(self, request):
        func = notifier.Callback(self._change_services, request.options,
                                 'start')
        thread = notifier.threads.Simple(
            'services', func,
            notifier.Callback(self.thread_finished_callback, request))
        thread.run()

    @sanitize(StringSanitizer(required=True))
    def stop(self, request):
        func = notifier.Callback(self._change_services, request.options,
                                 'stop')
        thread = notifier.threads.Simple(
            'services', func,
            notifier.Callback(self.thread_finished_callback, request))
        thread.run()

    @sanitize(StringSanitizer(required=True))
    def restart(self, request):
        func = notifier.Callback(self._change_services, request.options,
                                 'restart')
        thread = notifier.threads.Simple(
            'services', func,
            notifier.Callback(self.thread_finished_callback, request))
        thread.run()

    def _change_services(self, services, action):
        error_messages = []
        srvs = ServiceInfo()
        for srv in services:
            service = srvs.get_service(srv)
            try:
                getattr(service, action)()
            except ServiceError as exc:
                MODULE.warn('Error during %s of %s: %s' % (action, srv, exc))
                error_messages.append(
                    '%s\n%s' %
                    ({
                        'start': _('Starting the service %s failed:'),
                        'stop': _('Stopping the service %s failed:'),
                        'restart': _('Restarting the service %s failed:'),
                    }[action] % srv, exc))

        if error_messages:
            raise UMC_Error('\n\n'.join(error_messages))
        return {'success': True}

    @sanitize(StringSanitizer(required=True))
    def start_auto(self, request):
        self._change_start_type(request.options, 'yes')
        self.finished(request.id, {'success': True},
                      _('Successfully changed start type'))

    @sanitize(StringSanitizer(required=True))
    def start_manual(self, request):
        self._change_start_type(request.options, 'manually')
        self.finished(request.id, {'success': True},
                      _('Successfully changed start type'))

    @sanitize(StringSanitizer(required=True))
    def start_never(self, request):
        self._change_start_type(request.options, 'no')
        self.finished(request.id, {'success': True},
                      _('Successfully changed start type'))

    def _change_start_type(self, service_names, start_type):
        service_info = ServiceInfo()
        services = [(service_name, service_info.services[service_name])
                    for service_name in service_names
                    if service_name in service_info.services]
        values = [
            '%s=%s' % (service.get('start_type', '%s/autostart' %
                                   (service_name, )), start_type)
            for service_name, service in services
        ]
        univention.config_registry.handler_set(values)
        failed = [x for x in service_names if not service_info.services.get(x)]
        if failed:
            raise UMC_Error(
                '%s %s' %
                (_('Could not change start type of the following services:'),
                 ', '.join(failed)))
class Instance(Base):
    def init(self):
        MODULE.info("Initializing 'updater' module (PID = %d)" % (getpid(), ))
        self._current_job = ''
        self._logfile_start_line = 0
        self._serial_file = Watched_File(COMPONENTS_SERIAL_FILE)
        self._updates_serial = Watched_Files(UPDATE_SERIAL_FILES)
        try:
            self.uu = UniventionUpdater(False)
        except Exception as exc:  # FIXME: let it raise
            MODULE.error("init() ERROR: %s" % (exc, ))

    @simple_response
    def query_maintenance_information(self):
        ucr.load()
        if ucr.is_true('license/extended_maintenance/disable_warning'):
            return {'show_warning': False}
        version = self.uu.get_ucs_version()
        try:
            url = 'http://updates.software-univention.de/download/ucs-maintenance/{}.yaml'.format(
                version)
            response = requests.get(url, timeout=10)
            if not response.ok:
                response.raise_for_status()
            status = yaml.load(response.content)
            if not isinstance(status, dict):
                raise yaml.YAMLError(repr(status))
            # the yaml file contains for maintained either false, true or extended as value.
            # yaml.load converts true and false into booleans but extended into string.
            _maintained_status = status.get('maintained')
            maintenance_extended = _maintained_status == 'extended'
            show_warning = maintenance_extended or not _maintained_status
        except yaml.YAMLError as exc:
            MODULE.error('The YAML format is malformed: %s' % (exc, ))
            return {'show_warning': False}
        except requests.exceptions.RequestException as exc:
            MODULE.error("Querying maintenance information failed: %s" %
                         (exc, ))
            return {'show_warning': False}

        return {
            'ucs_version': version,
            'show_warning': show_warning,
            'maintenance_extended': maintenance_extended,
            'base_dn': ucr.get('license/base')
        }

    @simple_response
    def poll(self):
        return True

    @simple_response
    def query_releases(self):
        """
		Returns a list of system releases suitable for the
		corresponding ComboBox
		"""

        # be as current as possible.
        self.uu.ucr_reinit()
        ucr.load()

        appliance_mode = ucr.is_true('server/appliance')

        available_versions, blocking_components = self.uu.get_all_available_release_updates(
        )
        result = [{
            'id': rel,
            'label': 'UCS %s' % (rel, )
        } for rel in available_versions]
        #
        # appliance_mode=no ; blocking_comp=no  → add "latest version"
        # appliance_mode=no ; blocking_comp=yes →  no "latest version"
        # appliance_mode=yes; blocking_comp=no  → add "latest version"
        # appliance_mode=yes; blocking_comp=yes → add "latest version"
        #
        if result and (appliance_mode or not blocking_components):
            # UniventionUpdater returns available version in ascending order, so
            # the last returned entry is the one to be flagged as 'latest' if there's
            # no blocking component.
            result[-1]['label'] = '%s (%s)' % (result[-1]['label'],
                                               _('latest version'))

        return result

    def call_hooks(self, request):
        """
		Calls the specified hooks and returns data given back by each hook
		"""
        def _thread(request):
            result = {}
            hookmanager = univention.hooks.HookManager(
                HOOK_DIRECTORY)  # , raise_exceptions=False

            hooknames = request.options.get('hooks')
            MODULE.info('requested hooks: %s' % hooknames)
            for hookname in hooknames:
                MODULE.info('calling hook %s' % hookname)
                result[hookname] = hookmanager.call_hook(hookname)
            MODULE.info('result: %r' % (result, ))

            return result

        thread = notifier.threads.Simple(
            'call_hooks', notifier.Callback(_thread, request),
            notifier.Callback(self.thread_finished_callback, request))
        thread.run()

    @simple_response
    def updates_serial(self):
        """
		Watches the three sources.list snippets for changes
		"""
        result = self._updates_serial.timestamp()
        MODULE.info(" -> Serial for UPDATES is '%s'" % result)
        return result

    @simple_response
    def updates_check(self):
        """
		Returns the list of packages to be updated/installed
		by a dist-upgrade.
		"""
        p0 = subprocess.Popen(['LC_ALL=C apt-get update'],
                              stdout=subprocess.PIPE,
                              stderr=subprocess.PIPE,
                              shell=True)
        (stdout, stderr) = p0.communicate()

        p1 = subprocess.Popen(['LC_ALL=C apt-get -u dist-upgrade -s'],
                              stdout=subprocess.PIPE,
                              stderr=subprocess.PIPE,
                              shell=True)
        (stdout, stderr) = p1.communicate()

        install = []
        update = []
        remove = []
        for line in stdout.split('\n'):
            # upgrade:
            #   Inst univention-updater [3.1.1-5] (3.1.1-6.408.200810311159 192.168.0.10)
            # inst:
            #   Inst mc (1:4.6.1-6.12.200710211124 oxae-update.open-xchange.com)
            #
            # *** FIX ***   the above example lines ignore the fact that there's
            #               some extra text (occasionally) after the last closing
            #               parenthesis. Until now, I've seen only a pair of empty
            #               brackets [], but who knows...
            match = re.search('^Inst (\S+)\s+(.*?)\s*\((\S+)\s.*\)', line)
            if match:
                pkg = match.group(1)
                old = match.group(2)
                ver = match.group(3)
                if old:
                    update.append([pkg, ver])
                else:
                    install.append([pkg, ver])
            elif line.startswith('Remv '):
                l = line.split(' ')
                pkg = l[1]
                # i18n: The package version is unknown.
                ver = _('unknown')
                if len(l) > 2:
                    ver = l[2].replace('[', '').replace(']', '')
                remove.append([pkg, ver])

        return dict(
            update=sorted(update),
            install=sorted(install),
            remove=sorted(remove),
        )

    @simple_response
    def updates_available(self):
        """
		Asks if there are package updates available. (don't get confused
		by the name of the UniventionUpdater function that is called here.)
		This is a seperate call since it can take an amount of time, thus
		being invoked by a seperate button (and not in the background)
		"""
        ucr.load()
        try:
            # be as current as possible.
            what = 'reinitializing UniventionUpdater'
            self.uu.ucr_reinit()

            what = 'checking update availability'
            return self.uu.component_update_available()
        except Exception as ex:
            typ = str(type(ex)).strip('<>')
            msg = '[while %s] [%s] %s' % (what, typ, str(ex))
            MODULE.error(msg)
        return False

    def status(self, request):  # TODO: remove unneeded things
        """One call for all single-value variables."""

        result = {}
        ucr.load()

        try:
            result['erratalevel'] = int(ucr.get('version/erratalevel', 0))
        except ValueError:
            result['erratalevel'] = 0

        result['appliance_mode'] = ucr.is_true('server/appliance')
        result['easy_mode'] = ucr.is_true('update/umc/updateprocess/easy',
                                          False)
        result['timestamp'] = int(time())
        result['reboot_required'] = ucr.is_true('update/reboot/required',
                                                False)

        try:
            # be as current as possible.
            what = 'reinitializing UniventionUpdater'
            self.uu.ucr_reinit()

            what = 'getting UCS version'
            result['ucs_version'] = self.uu.get_ucs_version()

            # if nothing is returned -> convert to empty string.
            what = 'querying available release updates'
            try:
                result[
                    'release_update_available'] = self.uu.release_update_available(
                        errorsto='exception')
            except RequiredComponentError as exc:
                result['release_update_available'] = exc.version
            if result['release_update_available'] is None:
                result['release_update_available'] = ''

            what = 'querying update-blocking components'
            blocking_components = self.uu.get_all_available_release_updates(
            )[1]
            result['release_update_blocking_components'] = ' '.join(
                blocking_components or [])

            what = "querying availability for easy mode"

            if result['easy_mode']:
                # updates/available should reflect the need for an update
                easy_update_available = ucr.is_true('update/available', False)
                # but dont rely on ucr! update/available is set during univention-upgrade --check
                # but when was the last time this was run?

                # release update
                easy_update_available = easy_update_available or result[
                    'release_update_available']
                # if no update seems necessary perform a real (expensive) check nonetheless
                easy_update_available = easy_update_available or self.uu.component_update_available(
                )
                result['easy_update_available'] = bool(easy_update_available)
            else:
                result['easy_update_available'] = False

            # Component counts are now part of the general 'status' data.
            what = "counting components"
            c_count = 0
            e_count = 0
            for comp in self.uu.get_all_components():
                c_count = c_count + 1
                if ucr.is_true('repository/online/component/%s' % (comp, ),
                               False):
                    e_count = e_count + 1
            result['components'] = c_count
            result['enabled'] = e_count

            # HACK: the 'Updates' form polls on the serial file
            #       to refresh itself. Including the serial value
            #       into the form helps us to have a dependent field
            #       that can trigger the refresh of the "Releases"
            #       combobox and the 'package updates available' field.
            result['serial'] = self._serial_file.timestamp()

        except Exception as exc:  # FIXME: don't catch everything
            typ = str(type(exc)).strip('<>')
            msg = '[while %s] [%s] %s' % (what, typ, exc)
            result['message'] = msg
            result['status'] = 1
            MODULE.error(msg)

        self.finished(request.id, [result])

    @simple_response
    def reboot(self):
        """
		Reboots the computer. Simply invokes /sbin/reboot in the background
		and returns success to the caller. The caller is prepared for
		connection loss.
		"""
        subprocess.call(['/sbin/reboot'])
        return True

    @simple_response
    def running(self):
        """
		Returns the id (key into INSTALLERS) of a currently
		running job, or the empty string if nothing is running.
		"""
        return self.__which_job_is_running()

    @sanitize(
        job=ChoicesSanitizer(INSTALLERS.keys() + [''], required=True),
        count=IntegerSanitizer(default=0),
    )
    @simple_response
    def updater_log_file(self, job, count):
        """
		returns the content of the log file associated with
		the job.

		Argument 'count' has the same meaning as already known:
		<0 ...... return timestamp of file (for polling)
		0 ....... return whole file as a string list
		>0 ...... ignore this many lines, return the rest of the file

		*** NOTE *** As soon as we have looked for a running job at least once,
			we know the job key and can associate it here.

		TODO: honor a given 'job' argument
		"""
        job = self._current_job or job

        if not job:
            return

        fname = INSTALLERS[job]['logfile']
        if count < 0:
            try:
                return stat(fname)[9]
            except (IOError, OSError):
                return 0

        # don't read complete file if we have an 'ignore' count
        count += self._logfile_start_line
        return self._logview(fname, -count)

    def _logview(self, fname, count):
        """
		Contains all functions needed to view or 'tail' an arbitrary text file.
		Argument 'count' can have different values:
		< 0 ... ignore this many lines, return the rest of the file
		0 ..... return the whole file, split into lines.
		> 0 ... return the last 'count' lines of the file. (a.k.a. tail -n <count>)
		"""
        lines = []
        try:
            with open(fname, 'rb') as fd:
                for line in fd:
                    if (count < 0):
                        count += 1
                    else:
                        lines.append(line.rstrip().decode('utf-8', 'replace'))
                        if (count > 0) and (len(lines) > count):
                            lines.pop(0)
        except (IOError, OSError):
            pass
        return lines

    @sanitize(
        job=ChoicesSanitizer(INSTALLERS.keys(), required=True), )
    @simple_response
    def updater_job_status(self, job):  # TODO: remove this completely
        """Returns the status of the current/last update even if the job is not running anymore."""
        result = {}
        try:
            with open(INSTALLERS[job]['statusfile'], 'rb') as fd:
                for line in fd:
                    fields = line.strip().split('=')
                    if len(fields) == 2:
                        result['_%s_' % fields[0]] = fields[1]
        except (IOError, OSError):
            pass

        result['running'] = '' != self.__which_job_is_running()
        return result

    @sanitize(
        job=ChoicesSanitizer(INSTALLERS.keys(), required=True),
        detail=StringSanitizer(r'^[A-Za-z0-9\.\- ]*$'),
    )
    @simple_response
    def run_installer(self, job, detail=''):
        """
		This is the function that invokes any kind of installer. Arguments accepted:
		job ..... the main thing to do. can be one of:
			'release' ...... perform a release update
			'distupgrade' .. update all currently installed packages (distupgrade)
			'check' ........ check what would be done for 'update' ... do we need this?
		detail ....... an argument that specifies the subject of the installer:
			for 'release' .... the target release number,
			for all other subjects: detail has no meaning.
		"""

        MODULE.info("Starting function %r" % (job, ))
        self._current_job = job

        # remember initial lines of logfile before starting update to not show it in the frontend
        logfile = INSTALLERS[job]['logfile']
        try:
            with open(logfile, 'rb') as fd:
                self._logfile_start_line = sum(1 for line in fd)
        except (IOError, OSError):
            pass

        command = INSTALLERS[job]['command']
        if '%' in command:
            command = command % (pipes.quote(detail).replace('\n', '').replace(
                '\r', '').replace('\x00', ''), )

        MODULE.info("Creating job: %r" % (command, ))
        command = '''
/usr/share/univention-updater/disable-apache2-umc
%s < /dev/null
/usr/share/univention-updater/enable-apache2-umc --no-restart''' % (command, )
        atjobs.add(command, comments=dict(lines=self._logfile_start_line))

        return {'status': 0}

    def __which_job_is_running(self):
        # first check running at jobs
        for atjob in atjobs.list(True):
            for job, inst in INSTALLERS.iteritems():
                cmd = inst['command'].split('%')[0]
                if cmd in atjob.command:
                    self._current_job = job
                    try:
                        self._logfile_start_line = int(
                            atjob.comments.get('lines', 0))
                    except ValueError:
                        pass
                    return job
        # no atjob found, parse process list (if univention-upgrade was started via CLI)
        commands = [
            ('/usr/share/univention-updater/univention-updater-umc-dist-upgrade',
             'distupgrade'),
            ('/usr/share/univention-updater/univention-updater', 'release'),
            ('/usr/sbin/univention-upgrade', 'distupgrade'
             )  # we don't know if it is a dist-upgrade or a release upgrade
        ]
        for cmd, job in commands:
            for process in psutil.process_iter():
                try:
                    cmdline = process.cmdline() if callable(
                        process.cmdline) else process.cmdline
                except psutil.NoSuchProcess:
                    pass

                if cmd in cmdline:
                    self._current_job = job
                    self._logfile_start_line = 0
                    return job
        return ''
예제 #29
0
class Commands(object):

	@sanitize(
		partitionDevice=StringSanitizer(required=True),
		filter=PatternSanitizer(default='.*'),
	)
	def users_query(self, request):
		partitionDevice = request.options['partitionDevice']
		self._check_error(request, partitionDevice)

		callback = notifier.Callback(self._users_query, request.id, partitionDevice, request)
		tools.repquota(request.options['partitionDevice'], callback)

	def _users_query(self, pid, status, callbackResult, id, partition, request):
		'''This function is invoked when a repquota process has died and
		there is output to parse that is restructured as UMC Dialog'''
		# general information
		devs = fstab.File()
		devs.find(spec=partition)

		# skip header
		header = 0
		try:
			while not callbackResult[header].startswith('----'):
				header += 1
		except IndexError:
			pass
		quotas = tools.repquota_parse(partition, callbackResult[header + 1:])
		result = [q for q in quotas if request.options['filter'].match(q['user'])]
		self.finished(request.id, result)

	@sanitize(
		partitionDevice=StringSanitizer(required=True),
		user=StringSanitizer(required=True),
		sizeLimitSoft=LimitSanitizer(default=0, required=True),
		sizeLimitHard=LimitSanitizer(default=0, required=True),
		fileLimitSoft=LimitSanitizer(default=0, required=True),
		fileLimitHard=LimitSanitizer(default=0, required=True),
	)
	def users_set(self, request):
		def _thread(request):
			partition = request.options['partitionDevice']
			user = request.options['user']
			if isinstance(user, unicode):
				user = user.encode('utf-8')

			size_soft = request.options['sizeLimitSoft']
			size_hard = request.options['sizeLimitHard']
			file_soft = request.options['fileLimitSoft']
			file_hard = request.options['fileLimitHard']
			self._check_error(request, partition)

			if tools.setquota(partition, user, tools.byte2block(size_soft), tools.byte2block(size_hard), file_soft, file_hard):
				raise UMC_Error(_('Failed to modify quota settings for user %(user)s on partition %(partition)s.') % {'user': user, 'partition': partition})

			return {'objects': [], 'success': True}

		thread = notifier.threads.Simple('Set', notifier.Callback(_thread, request), notifier.Callback(self.thread_finished_callback, request))
		thread.run()

	def users_remove(self, request):
		def _thread(request):
			partitions = []
			success = True
			objects = []

			# Determine different partitions
			for obj in request.options:
				partitions.append(obj['object'].split('@')[-1])
			for partition in set(partitions):
				self._check_error(request, partition)

			# Remove user quota
			for obj in request.options:
				(unicode_user, partition) = obj['object'].split('@', 1)
				user = unicode_user.encode('utf-8')
				if tools.setquota(partition, user, 0, 0, 0, 0):
					objects.append({'id': obj['object'], 'success': False})
					success = False
				else:
					objects.append({'id': obj['object'], 'success': True})

			return {'objects': objects, 'success': success}

		thread = notifier.threads.Simple('Remove', notifier.Callback(_thread, request), notifier.Callback(self.thread_finished_callback, request))
		thread.run()

	def _check_error(self, request, partition_name):  # TODO
		try:
			fs = fstab.File()
			mt = mtab.File()
		except IOError as error:
			MODULE.error('Could not open %s' % error.filename)
			raise ValueError(_('Could not open %s') % error.filename)

		partition = fs.find(spec=partition_name)
		if partition:
			mounted_partition = mt.get(partition.spec)
			if mounted_partition:
				if 'usrquota' not in mounted_partition.options and 'usrjquota=aquota.user' not in mounted_partition.options:
					raise UMC_Error(_('The following partition is mounted without quota support: %s') % partition_name)
			else:
				raise UMC_Error(_('The following partition is currently not mounted: %s') % partition_name)
		else:
			raise UMC_Error(_('No partition found (%s)') % partition_name)
예제 #30
0
class Instance(Base):

	def init(self):
		if not ucr.is_true("umc/self-service/enabled"):
			raise UMC_Error(_('The password reset service is disabled via configuration registry.'), status=503)

		if not IS_SELFSERVICE_MASTER:
			return

		self._usersmod = None
		self.groupmod = None

		self.db = TokenDB(MODULE)
		self.conn = self.db.conn
		atexit.register(self.db.close_db)
		if not self.db.table_exists():
			self.db.create_table()

		def ucr_try_int(variable, default):
			try:
				return int(ucr.get(variable, default))
			except ValueError:
				MODULE.error('UCR variables %s is not a number, using default: %s' % (variable, default))
				return default

		self.token_validity_period = ucr_try_int("umc/self-service/passwordreset/token_validity_period", 3600)
		self.send_plugins = get_sending_plugins(MODULE.process)
		self.password_reset_plugins = {k: v for k, v in self.send_plugins.items() if v.message_application() == 'password_reset'}
		self.memcache = pylibmc.Client([MEMCACHED_SOCKET], binary=True)

		limit_total_minute = ucr_try_int("umc/self-service/passwordreset/limit/total/minute", 0)
		limit_total_hour = ucr_try_int("umc/self-service/passwordreset/limit/total/hour", 0)
		limit_total_day = ucr_try_int("umc/self-service/passwordreset/limit/total/day", 0)
		self.limit_user_minute = ucr_try_int("umc/self-service/passwordreset/limit/per_user/minute", 0)
		self.limit_user_hour = ucr_try_int("umc/self-service/passwordreset/limit/per_user/hour", 0)
		self.limit_user_day = ucr_try_int("umc/self-service/passwordreset/limit/per_user/day", 0)

		self.total_limits = [
			("t:c_minute", 60, limit_total_minute),
			("t:c_hour", 3600, limit_total_hour),
			("t:c_day", 86400, limit_total_day)
		]

	@property
	def usersmod(self):
		if not self._usersmod:
			univention.admin.modules.update()
			self._usersmod = univention.admin.modules.get('users/user')
			if not self._usersmod.initialized:
				lo, po = get_machine_connection()
				univention.admin.modules.init(lo, po, self._usersmod)
		return self._usersmod

	@forward_to_master
	@prevent_denial_of_service
	@sanitize(
		username=StringSanitizer(required=True, minimum=1),
		password=StringSanitizer(required=True, minimum=1))
	@simple_response
	def get_contact(self, username, password):
		"""
		Get users contact data.

		:return: list of dicts with users contact data
		"""
		if ucr.is_false('umc/self-service/protect-account/backend/enabled'):
			msg = _('The account protection was disabled via the Univention Configuration Registry.')
			MODULE.error('get_contact(): {}'.format(msg))
			raise UMC_Error(msg)
		dn, username = self.auth(username, password)
		if self.is_blacklisted(username, 'passwordreset'):
			raise ServiceForbidden()

		user = self.get_udm_user(username=username)
		if not self.password_reset_plugins:
			raise ServiceForbidden()

		return [{
			"id": p.send_method(),
			"label": p.send_method_label(),
			"value": user[p.udm_property]
		} for p in self.password_reset_plugins.values() if p.udm_property in user]

	@forward_to_master
	@sanitize(
		username=StringSanitizer(required=True, minimum=1),
		password=StringSanitizer(required=True, minimum=1))
	@simple_response
	def get_user_attributes(self, username, password):
		dn, username = self.auth(username, password)
		if self.is_blacklisted(username, 'profiledata'):
			raise ServiceForbidden()

		user = self.get_udm_user_by_dn(dn)
		user.set_defaults = True
		user.set_default_values()
		properties = user.info.copy()
		widget_descriptions = [
			dict(wd, value=properties.get(wd['id'])) for wd in self._get_user_attributes_descriptions()
			if user.has_property(wd['id'])
		]
		# TODO make layout configurable via ucr ?
		layout = [wd['id'] for wd in widget_descriptions]

		return {
			'widget_descriptions': widget_descriptions,
			'layout': layout,
		}

	@forward_to_master
	@simple_response
	def get_user_attributes_descriptions(self):
		return self._get_user_attributes_descriptions()

	def _get_user_attributes_descriptions(self):
		user_attributes = [attr.strip() for attr in ucr.get('self-service/udm_attributes', '').split(',')]

		widget_descriptions = []
		label_overwrites = {
			'jpegPhoto': _('Your picture')
		}
		for propname in user_attributes:
			if propname == 'password':
				continue
			prop = self.usersmod.property_descriptions.get(propname)
			if not prop:
				continue
			widget_description = {
				'id': propname,
				'label': label_overwrites.get(propname, prop.short_description),
				'description': prop.long_description,
				'syntax': prop.syntax.name,
				'size': prop.size or prop.syntax.size,
				'required': bool(prop.required),
				'editable': bool(prop.may_change),
				'readonly': not bool(prop.editable),
				'multivalue': bool(prop.multivalue),
			}
			widget_description.update(widget(prop.syntax, widget_description))
			if 'udm' in widget_description['type']:
				continue
			if 'dynamicValues' in widget_description:
				continue
			widget_descriptions.append(widget_description)
		return widget_descriptions

	@forward_to_master
	@simple_response
	def get_registration_attributes(self):
		ucr.load()
		property_ids = ['PasswordRecoveryEmail', 'password']
		for id_ in [attr.strip() for attr in ucr.get('umc/self-service/account-registration/udm_attributes', '').split(',') if attr.strip()]:
			if id_ not in property_ids:
				property_ids.append(id_)
		lo, po = get_machine_connection()
		users_mod = UDM_Module('users/user', True, lo, po)
		properties = {prop['id']: prop for prop in users_mod.properties(None)}
		not_existing = set(property_ids) - set(properties.keys())
		properties = {k: v for (k, v) in properties.items() if 'dynamicValues' not in v and 'udm' not in v['type']}  # filter out not supported props
		not_supported = set(property_ids) - set(properties.keys()) - not_existing
		if 'PasswordRecoveryEmail' in properties:
			properties['PasswordRecoveryEmail']['label'] = _('Email')
			properties['PasswordRecoveryEmail']['description'] = ''
		self._update_required_attr_of_props_for_registration(properties)
		properties = [properties[id_] for id_ in property_ids if id_ in properties]
		if not_existing:
			MODULE.warn("get_registration_attributes(): the following attributes defined by umc/self-service/account-registration/udm_attributes do not exist on users/user: {}".format(", ".join(not_existing)))
		if not_supported:
			MODULE.warn("get_registration_attributes(): the following attributes defined by umc/self-service/account-registration/udm_attributes are not supported: {}".format(", ".join(not_supported)))
		return {
			'widget_descriptions': properties,
			'layout': [prop['id'] for prop in properties],
		}

	def _update_required_attr_of_props_for_registration(self, properties):
		for k in properties.keys():
			if isinstance(properties[k], dict):
				properties[k]['required'] = False
			else:
				properties[k].required = False
		required_ids = set(['PasswordRecoveryEmail', 'password'] + [attr.strip() for attr in ucr.get('umc/self-service/account-registration/udm_attributes/required', '').split(',') if attr.strip()])
		for id_ in required_ids:
			if id_ in properties:
				if isinstance(properties[id_], dict):
					properties[id_]['required'] = True
				else:
					properties[id_].required = True

	@forward_to_master
	@sanitize(
		username=StringSanitizer(required=True, minimum=1),
		password=StringSanitizer(required=True, minimum=1))
	@simple_response
	def validate_user_attributes(self, username, password, attributes):
		dn, username = self.auth(username, password)
		if self.is_blacklisted(username, 'profiledata'):
			raise ServiceForbidden()
		return self._validate_user_attributes(attributes)

	def _validate_user_attributes(self, attributes, map_properties_func=None):
		res = {}
		properties = self.usersmod.property_descriptions
		if map_properties_func:
			properties = properties.copy()
			map_properties_func(properties)
		for propname, value in attributes.items():
			prop = properties.get(propname)
			if not prop:
				continue

			isValid = True
			message = ''
			if prop.multivalue and isinstance(value, (tuple, list)):
				isValid = []
				message = []
				for ival in value:
					_isValid = True
					_message = ''
					try:
						prop.syntax.parse(ival)
					except (udm_errors.valueError, udm_errors.valueInvalidSyntax) as e:
						_isValid = False
						_message = str(e)
					finally:
						isValid.append(_isValid)
						message.append(_message)
			else:
				try:
					prop.syntax.parse(value)
				except (udm_errors.valueError, udm_errors.valueInvalidSyntax) as e:
					isValid = False
					message = str(e)

			if prop.required and not value:
				isValid = False
				message = _('This value is required')
			res[propname] = {
				'isValid': isValid,
				'message': message,
			}
		return res

	@forward_to_master
	@sanitize(
		username=StringSanitizer(required=True, minimum=1),
		password=StringSanitizer(required=True, minimum=1))
	@simple_response
	def set_user_attributes(self, username, password, attributes):
		dn, username = self.auth(username, password)
		if self.is_blacklisted(username, 'profiledata'):
			raise ServiceForbidden()

		user_attributes = [attr.strip() for attr in ucr.get('self-service/udm_attributes', '').split(',')]
		lo, po = get_user_connection(binddn=dn, bindpw=password)
		user = self.usersmod.object(None, lo, po, dn)
		user.open()
		for propname, value in attributes.items():
			if propname in user_attributes and user.has_property(propname):
				user[propname] = value
		try:
			user.modify()
		except udm_errors.base as exc:
			MODULE.error('set_user_attributes(): modifying the user failed: %s' % (traceback.format_exc(),))
			raise UMC_Error(_('The attributes could not be saved: %s') % (UDM_Error(exc)))
		return _("Successfully changed your profile data.")

	@forward_to_master
	@simple_response
	def create_self_registered_account(self, attributes):
		MODULE.info('create_self_registered_account(): attributes: {}'.format(attributes))
		ucr.load()
		if ucr.is_false('umc/self-service/account-registration/backend/enabled', True):
			msg = _('The account registration was disabled via the Univention Configuration Registry.')
			MODULE.error('create_self_registered_account(): {}'.format(msg))
			raise UMC_Error(msg)
		# filter out attributes that are not valid to set
		allowed_to_set = set(['PasswordRecoveryEmail', 'password'] + [attr.strip() for attr in ucr.get('umc/self-service/account-registration/udm_attributes', '').split(',') if attr.strip()])
		attributes = {k: v for (k, v) in attributes.items() if k in allowed_to_set}
		# validate attributes
		res = self._validate_user_attributes(attributes, self._update_required_attr_of_props_for_registration)
		# check username taken
		if 'username' in attributes:
			try:
				UDM.machine().version(2).get('users/user').get_by_id(attributes['username'])
			except NoObject:
				pass
			else:
				res['username'] = {
					'isValid': False,
					'message': _('The username is already taken'),
				}
		invalid = {k: v for (k, v) in res.items() if not (all(v['isValid']) if isinstance(v['isValid'], list) else v['isValid'])}
		if len(invalid):
			return {
				'success': False,
				'failType': 'INVALID_ATTRIBUTES',
				'data': invalid,
			}

		# check for missing required attributes from umc/self-service/account-registration/udm_attributes/required
		required_attrs = [attr.strip() for attr in ucr.get('umc/self-service/account-registration/udm_attributes/required', '').split(',') if attr.strip()]
		not_found = [attr for attr in required_attrs if attr not in attributes]
		if not_found:
			msg = _('The account could not be created:\nInformation provided is not sufficient. The following properties are missing:\n%s') % ('\n'.join(not_found),)
			MODULE.error('create_self_registered_account(): {}'.format(msg))
			raise UMC_Error(msg)

		univention.admin.modules.update()
		lo, po = get_admin_connection()

		# get usertemplate
		template_dn = ucr.get('umc/self-service/account-registration/usertemplate', '')
		usertemplate = None
		if template_dn:
			usertemplate_mod = univention.admin.modules.get('settings/usertemplate')
			univention.admin.modules.init(lo, po, usertemplate_mod, None, True)
			try:
				usertemplate = usertemplate_mod.object(None, lo, None, template_dn)
			except udm_errors.noObject:
				msg = _('The user template "{}" set by the "umc/self-service/account-registration/usertemplate" UCR variable does not exist. A user account can not be created. Please contact your system administrator.'.format(template_dn))
				MODULE.error('create_self_registered_account(): {}'.format(msg))
				raise UMC_Error(msg)

		# init user module with template
		usersmod = univention.admin.modules.get('users/user')
		univention.admin.modules.init(lo, po, usersmod, usertemplate, True)

		# get user container
		udm = UDM.machine().version(2)
		user_position = univention.admin.uldap.position(po.getBase())
		container_dn = ucr.get('umc/self-service/account-registration/usercontainer', None)
		if container_dn:
			try:
				container = udm.obj_by_dn(container_dn)
			except NoObject:
				msg = _('The container "{}" set by the "umc/self-service/account-registration/usercontainer" UCR variable does not exist. A user account can not be created. Please contact your system administrator.'.format(container_dn))
				MODULE.error('create_self_registered_account(): {}'.format(msg))
				raise UMC_Error(msg)
			else:
				user_position.setDn(container.dn)
		else:
			for dn in usersmod.object.get_default_containers(lo):
				try:
					container = udm.obj_by_dn(dn)
				except NoObject:
					pass
				else:
					user_position.setDn(container.dn)
					break

		# create user
		attributes['PasswordRecoveryEmailVerified'] = 'FALSE'
		attributes['RegisteredThroughSelfService'] = 'TRUE'
		new_user = usersmod.object(None, lo, user_position)
		new_user.open()
		for key, value in attributes.items():
			if key in new_user and value:
				new_user[key] = value
		try:
			new_user.create()
		except univention.admin.uexceptions.base as exc:
			MODULE.error('create_self_registered_account(): could not create user: %s' % (traceback.format_exc(),))
			return {
				'success': False,
				'failType': 'CREATION_FAILED',
				'data': _('The account could not be created:\n%s') % UDM_Error(exc),
			}
		finally:
			# TODO cleanup
			# reinit user module without template.
			# This has to be done since the modules are singletons?
			univention.admin.modules.update()
			self._usersmod = None
			#  univention.admin.modules.init(lo, po, usersmod, None, True)
		try:
			self.send_message(new_user['username'], 'verify_email', new_user['PasswordRecoveryEmail'], raise_on_success=False)
		except Exception:
			verify_token_successfully_send = False
		else:
			verify_token_successfully_send = True
		return {
			'success': True,
			'verifyTokenSuccessfullySend': verify_token_successfully_send,
			'data': {
				'username': new_user['username'],
				'email': new_user['PasswordRecoveryEmail'],
			}
		}

	@forward_to_master
	@prevent_denial_of_service
	@sanitize(
		username=StringSanitizer(required=True))
	@simple_response
	def send_verification_token(self, username):
		MODULE.info("send_verification_token(): username: {}".format(username))
		ucr.load()
		if ucr.is_false('umc/self-service/account-verification/backend/enabled', True):
			msg = _('The account verification was disabled via the Univention Configuration Registry.')
			MODULE.error('send_verification_token(): {}'.format(msg))
			raise UMC_Error(msg)
		invalid_information = {
			'success': False,
			'failType': 'INVALID_INFORMATION'
		}
		users_mod = UDM.machine().version(2).get('users/user')
		try:
			user = users_mod.get_by_id(username)
		except NoObject:
			return invalid_information
		try:
			email = user.props.PasswordRecoveryEmail
		except AttributeError:
			return invalid_information
		else:
			if not email:
				return invalid_information
		self.send_message(username, 'verify_email', email, raise_on_success=False)
		return {
			'success': True,
			'data': {
				'username': username,
			}
		}

	@forward_to_master
	@prevent_denial_of_service
	@sanitize(
		username=StringSanitizer(required=True),
		password=StringSanitizer(required=True),
		email=StringSanitizer(required=False),
		mobile=StringSanitizer(required=False))
	@simple_response
	def set_contact(self, username, password, email=None, mobile=None):
		if ucr.is_false('umc/self-service/protect-account/backend/enabled'):
			msg = _('The account protection was disabled via the Univention Configuration Registry.')
			MODULE.error('set_contact(): {}'.format(msg))
			raise UMC_Error(msg)
		MODULE.info("set_contact(): username: {} password: ***** email: {} mobile: {}".format(username, email, mobile))
		dn, username = self.auth(username, password)
		if self.is_blacklisted(username, 'passwordreset'):
			raise ServiceForbidden()
		try:
			return self.set_contact_data(dn, email, mobile)
		except Exception:
			raise UMC_Error(_('Changing contact data failed.'), status=500)

	@forward_to_master
	@prevent_denial_of_service
	@sanitize(
		username=StringSanitizer(required=True),
		method=StringSanitizer(required=True))
	@simple_response
	def send_token(self, username, method):
		if ucr.is_false('umc/self-service/passwordreset/backend/enabled'):
			msg = _('The password reset was disabled via the Univention Configuration Registry.')
			MODULE.error('send_token(): {}'.format(msg))
			raise UMC_Error(msg)
		MODULE.info("send_token(): username: '******' method: '{}'.".format(username, method))
		try:
			plugin = self.password_reset_plugins[method]
		except KeyError:
			MODULE.error("send_token() method '{}' not in {}.".format(method, self.password_reset_plugins.keys()))
			raise UMC_Error(_("Unknown recovery method '{}'.").format(method))

		if self.is_blacklisted(username, 'passwordreset'):
			raise MissingContactInformation()

		# check if the user has the required attribute set
		user = self.get_udm_user(username=username)
		username = user["username"]

		if len(user[plugin.udm_property]) > 0:
			# found contact info
			self.send_message(username, method, user[plugin.udm_property])

		# no contact info
		raise MissingContactInformation()

	@forward_to_master
	@prevent_denial_of_service
	@sanitize(
		token=StringSanitizer(required=True),
		username=StringSanitizer(required=True),
		method=StringSanitizer(required=True),
	)
	@simple_response
	def verify_contact(self, token, username, method):
		MODULE.info('verify_contact(): token: {} username: {} method: {}'.format(token, username, method))
		ucr.load()
		if ucr.is_false('umc/self-service/account-verification/backend/enabled', True):
			msg = _('The account verification was disabled via the Univention Configuration Registry.')
			MODULE.error('verify_contact(): {}'.format(msg))
			raise UMC_Error(msg)
		users_mod = UDM.admin().version(1).get('users/user')
		try:
			user = users_mod.get_by_id(username)
		except NoObject:
			return {
				'success': False,
				'failType': 'INVALID_INFORMATION',
			}
		next_steps = ucr.get('umc/self-service/account-verification/next-steps/%s' % self.locale.language, '')
		if not next_steps:
			next_steps = ucr.get('umc/self-service/account-verification/next-steps', '')
		plugin = self._get_send_plugin(method)
		if getattr(user.props, plugin.udm_property) == 'TRUE':  # cleanup. map property to actual boolean?
			return {
				'success': True,
				'successType': 'ALREADY_VERIFIED',
				'data': {
					'username': username,
					'nextSteps': next_steps,
				}
			}
		self._check_token(username, token, token_application=plugin.message_application())
		setattr(user.props, plugin.udm_property, 'TRUE')
		user.save()
		self.db.delete_tokens(token=token, username=username)
		return {
			'success': True,
			'successType': 'VERIFIED',
			'data': {
				'username': username,
				'nextSteps': next_steps,
			}
		}

	@forward_to_master
	@prevent_denial_of_service
	@sanitize(
		username=StringSanitizer(required=True),
		password=StringSanitizer(required=True),
	)
	@simple_response
	def deregister_account(self, username, password):
		MODULE.info("deregister_account(): username: {} password: *****".format(username))
		ucr.load()
		if ucr.is_false('umc/self-service/account-deregistration/enabled', True):
			msg = _('The account deregistration was disabled via the Univention Configuration Registry.')
			MODULE.error('deregister_account(): {}'.format(msg))
			raise UMC_Error(msg)
		dn, username = self.auth(username, password)
		if self.is_blacklisted(username, 'account-deregistration'):
			raise ServiceForbidden()
		try:
			return self._deregister_account(username)
		except Exception:
			raise UMC_Error(_('Account could not be deleted'), status=500)

	def _deregister_account(self, username):
		try:
			user = UDM.admin().version(2).get('users/user').get_by_id(username)
			user.props.DeregisteredThroughSelfService = 'TRUE'
			user.props.DeregistrationTimestamp = datetime.datetime.strftime(datetime.datetime.utcnow(), DEREGISTRATION_TIMESTAMP_FORMATTING)
			user.props.disabled = True
			user.save()
			try:
				self._notify_about_account_deregistration(user.props.username, user.props.PasswordRecoveryEmail)
			except Exception:
				MODULE.error("_deregister_account(): sending of email failed: {}".format(traceback.format_exc()))
			return
		except Exception:
			MODULE.error("_deregister_account(): {}".format(traceback.format_exc()))
			raise

	def _notify_about_account_deregistration(self, username, mail):
		if not mail:
			return
		ucr.load()
		path_ucr = ucr.get("umc/self-service/account-deregistration/email/text_file")
		if path_ucr and os.path.exists(path_ucr):
			path = path_ucr
		else:
			path = "/usr/share/univention-self-service/email_bodies/deregistration_notification_email_body.txt"
		with open(path, "r") as fp:
			txt = fp.read()
		txt = txt.format(username=username)
		msg = MIMENonMultipart('text', 'plain', charset='utf-8')
		msg["Subject"] = "Account deletion"
		msg["Date"] = formatdate(localtime=True)
		msg["From"] = ucr.get("umc/self-service/account-deregistration/email/sender_address", "Password Reset Service <noreply@{}>".format(".".join([ucr["hostname"], ucr["domainname"]])))
		msg["To"] = mail
		cs = email.charset.Charset("utf-8")
		cs.body_encoding = email.charset.QP
		msg.set_payload(txt, charset=cs)
		smtp = smtplib.SMTP(ucr.get("umc/self-service/account-deregistration/email/server", "localhost"))
		smtp.sendmail(msg["From"], msg["To"], msg.as_string())
		smtp.quit()

	@forward_to_master
	@prevent_denial_of_service
	@sanitize(
		token=StringSanitizer(required=True),
		username=StringSanitizer(required=True),
		password=StringSanitizer(required=True))  # new_password(!)
	@simple_response
	def set_password(self, token, username, password):
		MODULE.info("set_password(): username: '******'.".format(username))
		username = self.email2username(username)
		token_from_db = self._check_token(username, token)

		# token is correct and valid
		MODULE.info("Receive valid token for '{}'.".format(username))
		if self.is_blacklisted(username, 'passwordreset'):
			# this should not happen
			MODULE.error("Found token in DB for blacklisted user '{}'.".format(username))
			self.db.delete_tokens(token=token, username=username)
			raise ServiceForbidden()  # TokenNotFound() ?

		plugin = self._get_send_plugin(token_from_db['method'])
		email_verified = plugin.password_reset_verified_recovery_email()
		ret = self.udm_set_password(username, password, email_verified=email_verified)
		self.db.delete_tokens(token=token, username=username)
		if ret:
			raise UMC_Error(_("Successfully changed your password."), status=200)
		raise UMC_Error(_('Failed to change password.'), status=500)

	def _check_token(self, username, token, token_application='password_reset'):
		try:
			token_from_db = self.db.get_one(token=token, username=username)
		except MultipleTokensInDB as e:
			# this should not happen, delete all tokens, raise Exception
			# regardless of correctness of token
			MODULE.error("set_password(): {}".format(e))
			self.db.delete_tokens(token=token, username=username)
			raise TokenNotFound()

		if not token_from_db:
			# no token in DB
			MODULE.info("Token not found in DB for user '{}'.".format(username))
			raise TokenNotFound()

		if (datetime.datetime.now() - token_from_db["timestamp"]).seconds >= TOKEN_VALIDITY_TIME:
			# token is correct but expired
			MODULE.info("Receive correct but expired token for '{}'.".format(username))
			self.db.delete_tokens(token=token, username=username)
			raise TokenNotFound()

		if not self._get_send_plugin(token_from_db['method']).message_application() == token_application:
			# token is correct but should not be used for this application
			MODULE.info("Receive correct token for '{}' but it should be used for another application.".format(username))
			self.db.delete_tokens(token=token, username=username)
			raise TokenNotFound()
		return token_from_db

	@forward_to_master
	@prevent_denial_of_service
	@sanitize(username=StringSanitizer(required=True, minimum=1))
	@simple_response
	def get_reset_methods(self, username):
		if ucr.is_false('umc/self-service/passwordreset/backend/enabled'):
			msg = _('The password reset was disabled via the Univention Configuration Registry.')
			MODULE.error('get_reset_methods(): {}'.format(msg))
			raise UMC_Error(msg)
		if self.is_blacklisted(username, 'passwordreset'):
			raise NoMethodsAvailable()

		user = self.get_udm_user(username=username)
		if not self.password_reset_plugins:
			raise NoMethodsAvailable()

		# return list of method names, for all LDAP attribs user has data
		reset_methods = [{
			"id": p.send_method(),
			"label": p.send_method_label()
		} for p in self.password_reset_plugins.values() if user[p.udm_property]]
		if not reset_methods:
			raise NoMethodsAvailable()
		return reset_methods

	@staticmethod
	def create_token(length):
		# remove easily confusable characters
		chars = string.ascii_letters.replace("l", "").replace("I", "").replace("O", "") + "".join(map(str, range(2, 10)))
		rand = random.SystemRandom()
		res = ""
		for _ in range(length):
			res += rand.choice(chars)
		return res

	def send_message(self, username, method, address, raise_on_success=True):
		plugin = self._get_send_plugin(method)
		try:
			token_from_db = self.db.get_one(username=username)
		except MultipleTokensInDB as e:
			# this should not happen, delete all tokens
			MODULE.error("send_token(): {}".format(e))
			self.db.delete_tokens(username=username)
			token_from_db = None

		token = self.create_token(plugin.token_length)
		if token_from_db:
			# replace with fresh token
			MODULE.info("send_token(): Updating token for user '{}'...".format(username))
			self.db.update_token(username, method, token)
		else:
			# store a new token
			MODULE.info("send_token(): Adding new token for user '{}'...".format(username))
			self.db.insert_token(username, method, token)
		try:
			self._call_send_msg_plugin(username, method, address, token)
		except Exception:
			MODULE.error("send_token(): Error sending token with via '{method}' to '{username}'.".format(
				method=method, username=username))
			self.db.delete_tokens(username=username)
			raise
		if raise_on_success:
			raise UMC_Error(_("Successfully send token.").format(method), status=200)
		else:
			return True

	def _get_send_plugin(self, method):
		try:
			plugin = self.send_plugins[method]
			if not plugin.is_enabled:
				raise KeyError
		except KeyError:
			raise UMC_Error("Unknown send message method", status=500)
		return plugin

	def _call_send_msg_plugin(self, username, method, address, token):
		MODULE.info("send_message(): username: {} method: {} address: {}".format(username, method, address))
		plugin = self._get_send_plugin(method)

		plugin.set_data({
			"username": username,
			"address": address,
			"token": token})
		MODULE.info("send_message(): Running plugin of class {}...".format(plugin.__class__.__name__))
		try:
			plugin.send()
		except Exception as exc:
			MODULE.error('Unknown error: %s' % (traceback.format_exc(),))
			raise UMC_Error(_("Error sending token: {}").format(exc), status=500)
		return True

	@staticmethod
	@machine_connection
	def auth(username, password, ldap_connection=None, ldap_position=None):
		filter_s = filter_format("(|(uid=%s)(mailPrimaryAddress=%s))", (username, username))
		users = ldap_connection.search(filter=filter_s)
		try:
			binddn, userdict = users[0]
			get_user_connection(binddn=binddn, bindpw=password)
		except (udm_errors.authFail, IndexError):
			raise ServiceForbidden()
		return binddn, userdict["uid"][0]

	def set_contact_data(self, dn, email, mobile):
		try:
			user = self.get_udm_user_by_dn(userdn=dn, admin=True)
			old_email = user['PasswordRecoveryEmail']
			if email is not None and email.lower() != old_email.lower():
				try:
					user["PasswordRecoveryEmail"] = email
				except udm_errors.valueInvalidSyntax as err:
					raise UMC_Error(err)
				else:
					user['PasswordRecoveryEmailVerified'] = 'FALSE'
			if mobile is not None and mobile.lower() != user["PasswordRecoveryMobile"].lower():
				user["PasswordRecoveryMobile"] = mobile
			user.modify()
			verification_email_send = False
			if user['RegisteredThroughSelfService'] == 'TRUE':
				if old_email is not None and old_email.lower() != email.lower():
					self._notify_about_email_change(user['username'], old_email, email)
				if email is not None and email.lower() != old_email.lower():
					self.send_message(user['username'], 'verify_email', email, raise_on_success=False)
					verification_email_send = True
			return {
				'verificationEmailSend': verification_email_send,
				'email': email,
			}
		except Exception:
			MODULE.error("set_contact_data(): {}".format(traceback.format_exc()))
			raise

	def _notify_about_email_change(self, username, old_email, new_email):
		if not old_email:
			return
		new_email = new_email or ''
		ucr.load()
		path_ucr = ucr.get("umc/self-service/email-change-notification/email/text_file")
		if path_ucr and os.path.exists(path_ucr):
			path = path_ucr
		else:
			path = "/usr/share/univention-self-service/email_bodies/email_change_notification_email_body.txt"
		with open(path, "r") as fp:
			txt = fp.read()
		txt = txt.format(username=username, old_email=old_email, new_email=new_email)
		msg = MIMENonMultipart('text', 'plain', charset='utf-8')
		msg["Subject"] = "Account recovery email changed"
		msg["Date"] = formatdate(localtime=True)
		msg["From"] = ucr.get("umc/self-service/passwordreset/email/sender_address", "Password Reset Service <noreply@{}>".format(".".join([ucr["hostname"], ucr["domainname"]])))
		msg["To"] = old_email
		cs = email.charset.Charset("utf-8")
		cs.body_encoding = email.charset.QP
		msg.set_payload(txt, charset=cs)
		smtp = smtplib.SMTP(ucr.get("umc/self-service/passwordreset/email/server", "localhost"))
		smtp.sendmail(msg["From"], msg["To"], msg.as_string())
		smtp.quit()

	def admember_set_password(self, username, password):
		ldb_url = ucr.get('connector/ad/ldap/host')
		ldb_url = 'ldaps://%s' % (ldb_url,) if ucr.is_true('connector/ad/ldap/ldaps') else 'ldap://%s' % (ldb_url,)
		try:
			reset_username = dict(ucr)['ad/reset/username']
			with open(dict(ucr)['ad/reset/password']) as fd:
				reset_password = fd.readline().strip()
		except (EnvironmentError, KeyError):
			raise UMC_Error(_('The configuration of the password reset service is not complete. The UCR variables "ad/reset/username" and "ad/reset/password" need to be set properly. Please inform an administration.'), status=500)
		process = Popen(['samba-tool', 'user', 'setpassword', '--username', reset_username, '--password', reset_password, '--filter', filter_format('samaccountname=%s', (username,)), '--newpassword', password, '-H', ldb_url], stdout=PIPE, stderr=STDOUT)
		stdouterr = process.communicate()[0]

		if stdouterr:
			MODULE.process('samba-tool user setpassword: %s' % (stdouterr,))

		if process.returncode:
			MODULE.error("admember_set_password(): failed to set password. Return code: %s" % (process.returncode,))
			return False
		return True

	def udm_set_password(self, username, password, email_verified):
		user = self.get_udm_user(username=username, admin=True)
		if ucr.is_true('ad/member') and 'synced' in user.get('objectFlag', []):
			success = self.admember_set_password(username, password)
		else:
			user["password"] = password
			user["pwdChangeNextLogin"] = 0
			success = True
		if email_verified:
			user["PasswordRecoveryEmailVerified"] = 'TRUE'
		try:
			user.modify()
		except (udm_errors.pwToShort, udm_errors.pwQuality) as exc:
			raise UMC_Error(str(exc))
		except udm_errors.pwalreadyused as exc:
			raise UMC_Error(exc.message)
		except Exception:
			MODULE.error("udm_set_password(): failed to set password: {}".format(traceback.format_exc()))
			raise
		else:
			return success

	# TODO: decoratorize
	@machine_connection
	def is_blacklisted(self, username, feature, ldap_connection=None, ldap_position=None):
		def listize(li):
			return [x.lower() for x in map(str.strip, li.split(",")) if x]

		bl_users = listize(ucr.get("umc/self-service/{}/blacklist/users".format(feature), ""))
		bl_groups = listize(ucr.get("umc/self-service/{}/blacklist/groups".format(feature), ""))
		wh_users = listize(ucr.get("umc/self-service/{}/whitelist/users".format(feature), ""))
		wh_groups = listize(ucr.get("umc/self-service/{}/whitelist/groups".format(feature), ""))

		username = self.email2username(username)

		# user blacklist
		if username.lower() in bl_users:
			MODULE.info("is_blacklisted(username: {}, feature: {}): match in blacklisted users".format(username, feature))
			return True

		# get groups
		try:
			filter_s = filter_format("(|(uid=%s)(mailPrimaryAddress=%s))", (username, username))
			userdn = ldap_connection.search(filter=filter_s)[0][0]
			groups_dns = self.get_groups(userdn)
			for group_dn in list(groups_dns):
				groups_dns.extend(self.get_nested_groups(group_dn))
			groups_dns = list(set(groups_dns))
			gr_names = map(str.lower, self.dns_to_groupname(groups_dns))
		except IndexError:
			# no user or no group found
			return True

		# group blacklist
		if any(gr in bl_groups for gr in gr_names):
			MODULE.info("is_blacklisted(username: {}, feature: {}): match in blacklisted groups".format(username, feature))
			return True

		# if not on blacklist, check whitelists
		# user whitelist
		if username.lower() in wh_users:
			MODULE.info("is_blacklisted(username: {}, feature: {}): match in whitelisted users".format(username, feature))
			return False

		# group whitelist
		if any(gr in wh_groups for gr in gr_names):
			MODULE.info("is_blacklisted(username: {}, feature: {}): match in whitelisted groups".format(username, feature))
			return False

		# not on either black or white list -> not allowed if whitelist exists, else OK
		MODULE.info("is_blacklisted(username: {}, feature: {}): neither black nor white listed".format(username, feature))
		return bool(wh_users or wh_groups)

	def get_groups(self, userdn):
		user = self.get_udm_user_by_dn(userdn=userdn)
		groups = user["groups"]
		prim_group = user["primaryGroup"]
		if prim_group not in groups:
			groups.append(prim_group)
		return groups

	def get_nested_groups(self, groupdn):
		group = self.get_udm_group(groupdn)
		res = group["memberOf"] or []
		for ng in list(res):
			res.extend(self.get_nested_groups(ng))
		return res

	def dns_to_groupname(self, dns):
		names = list()
		for groupdn in dns:
			group = self.get_udm_group(groupdn)
			names.append(group["name"])
		return names

	def get_udm_user_by_dn(self, userdn, admin=False):
		if admin:
			lo, po = get_admin_connection()
		else:
			lo, po = get_machine_connection()
		user = self.usersmod.object(None, lo, po, userdn)
		user.open()
		return user

	def get_udm_user(self, username, admin=False):
		filter_s = filter_format('(|(uid=%s)(mailPrimaryAddress=%s))', (username, username))

		lo, po = get_machine_connection()
		dn = lo.searchDn(filter=filter_s)[0]
		return self.get_udm_user_by_dn(dn, admin=admin)

	@machine_connection
	def get_udm_group(self, groupdn, ldap_connection=None, ldap_position=None):
		# reuse module for recursive lookups by get_nested_groups()
		if not self.groupmod:
			univention.admin.modules.update()
			self.groupmod = univention.admin.modules.get("groups/group")
			univention.admin.modules.init(ldap_connection, ldap_position, self.groupmod)

		group = self.groupmod.object(None, ldap_connection, ldap_position, groupdn)
		group.open()
		return group

	@machine_connection  # TODO: overwrite StringSanitizer and do it there
	def email2username(self, email, ldap_connection=None, ldap_position=None):
		if "@" not in email:
			return email

		# cache email->username in memcache
		username = self.memcache.get("e2u:{}".format(email))
		if not username:
			mailf = filter_format("(mailPrimaryAddress=%s)", (email,))
			users = ldap_connection.search(filter=mailf)
			try:
				_, userdict = users[0]
			except IndexError:
				return email
			username = userdict["uid"][0]
			self.memcache.set("e2u:{}".format(email), username, 300)

		return username