Esempio n. 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)))
Esempio n. 2
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)
Esempio n. 3
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),
    'name':
    StringSanitizer(required=True, regex_pattern='^[A-Za-z0-9\-\_\.]+$'),
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):
        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)
Esempio n. 6
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)
Esempio n. 7
0
class Instance(umcm.Base):
    def init(self):
        self.ucr = univention.config_registry.ConfigRegistry()
        self.ucr.load()

        util.install_opener(self.ucr)

        self.package_manager = PackageManager(
            info_handler=MODULE.process,
            step_handler=None,
            error_handler=MODULE.warn,
            lock=False,
            always_noninteractive=True,
        )
        self.uu = UniventionUpdater(False)
        self.component_manager = util.ComponentManager(self.ucr, self.uu)

        # in order to set the correct locale for Application
        locale.setlocale(locale.LC_ALL, str(self.locale))

    @sanitize(email=EmailSanitizer(required=True))
    @simple_response
    def request_new_license(self, email):
        license = LICENSE.dump_data()
        if license is None:
            raise umcm.UMC_CommandError(_('Cannot parse License from LDAP'))
        data = {}
        data['email'] = email
        data['licence'] = license
        data = urllib.urlencode(data)
        url = 'https://license.univention.de/keyid/conversion/submit'
        request = urllib2.Request(url,
                                  data=data,
                                  headers={'User-agent': 'UMC/AppCenter'})
        try:
            util.urlopen(request)
        except Exception as e:
            try:
                # try to parse an html error
                body = e.read()
                detail = re.search(
                    '<span id="details">(?P<details>.*?)</span>',
                    body).group(1)
            except:
                detail = str(e)
            raise umcm.UMC_CommandError(
                _('An error occurred while sending the request: %s') % detail)
        else:
            return True

    @sanitize(pattern=PatternSanitizer(default='.*'))
    @simple_response
    def query(self, pattern):
        LICENSE.reload()
        try:
            applications = Application.all(force_reread=True)
        except (urllib2.HTTPError, urllib2.URLError) as e:
            raise umcm.UMC_CommandError(
                _('Could not query App Center: %s') % e)
        result = []
        self.package_manager.reopen_cache()
        for application in applications:
            if pattern.search(application.name):
                props = application.to_dict(self.package_manager)

                # delete larger entries
                for ikey in ('readmeupdate', 'licenseagreement'):
                    if ikey in props:
                        del props[ikey]

                result.append(props)
        return result

    @sanitize(application=StringSanitizer(minimum=1, required=True))
    @simple_response
    def get(self, application):
        LICENSE.reload()
        application = Application.find(application)
        self.package_manager.reopen_cache()
        return application.to_dict(self.package_manager)

    @sanitize(function=ChoicesSanitizer(['install', 'uninstall', 'update'],
                                        required=True),
              application=StringSanitizer(minimum=1, required=True),
              force=BooleanSanitizer())
    def invoke(self, request):
        function = request.options.get('function')
        application_id = request.options.get('application')
        application = Application.find(application_id)
        force = request.options.get('force')
        try:
            # make sure that the application cane be installed/updated
            can_continue = True
            result = {
                'install': [],
                'remove': [],
                'broken': [],
            }
            if not application:
                MODULE.info('Application not found: %s' % application_id)
                can_continue = False
            elif function == 'install' and not application.can_be_installed(
                    self.package_manager):
                MODULE.info('Application cannot be installed: %s' %
                            application_id)
                can_continue = False
            elif function == 'update' and not application.can_be_updated():
                MODULE.info('Application cannot be updated: %s' %
                            application_id)
                can_continue = False

            if can_continue and function in ('install', 'update'):
                result = application.install_dry_run(self.package_manager,
                                                     self.component_manager,
                                                     remove_component=False)
                if result['broken'] or (result['remove'] and not force):
                    MODULE.info('Remove component: %s' % application_id)
                    self.component_manager.remove_app(application)
                    self.package_manager.update()
                    can_continue = False
            elif can_continue and function in ('uninstall', ) and not force:
                result['remove'] = application.uninstall_dry_run(
                    self.package_manager)
                can_continue = False
            result['can_continue'] = can_continue
            self.finished(request.id, result)

            if can_continue:

                def _thread(module, application, function):
                    with module.package_manager.locked(reset_status=True,
                                                       set_finished=True):
                        with module.package_manager.no_umc_restart():
                            if function in ('install', 'update'):
                                # dont have to add component: already added during dry_run
                                return application.install(
                                    module.package_manager,
                                    module.component_manager,
                                    add_component=False)
                            else:
                                return application.uninstall(
                                    module.package_manager,
                                    module.component_manager)

                def _finished(thread, result):
                    if isinstance(result, BaseException):
                        MODULE.warn('Exception during %s %s: %s' %
                                    (function, application_id, str(result)))

                thread = notifier.threads.Simple(
                    'invoke',
                    notifier.Callback(_thread, self, application, function),
                    _finished)
                thread.run()
        except LockError:
            # make it thread safe: another process started a package manager
            # this module instance already has a running package manager
            raise umcm.UMC_CommandError(
                _('Another package operation is in progress'))

    @simple_response
    def app_center_app_license(self, application):
        application = Application.find(application)
        if not application or not application.get('licensefile'):
            raise umcm.UMC_CommandError(
                _('No license file available for application: %s') %
                (application.id))

        # open the license file and replace line breaks with BR-tags
        fp = util.urlopen(application.get('licensefile'))
        txt = ''.join(fp.readlines()).strip()
        txt = txt.replace('\n\n\n', '\n<br>\n<br>\n<br>\n')
        txt = txt.replace('\n\n', '\n<br>\n<br>\n')
        return txt

    @simple_response
    def packages_sections(self):
        """ fills the 'sections' combobox in the search form """

        sections = set()
        for package in self.package_manager.packages():
            sections.add(package.section)

        return sorted(sections)

    @sanitize(pattern=PatternSanitizer(required=True))
    @simple_response
    def packages_query(self, pattern, section='all', key='package'):
        """ Query to fill the grid. Structure is fixed here. """
        result = []
        for package in self.package_manager.packages():
            if section == 'all' or package.section == section:
                toshow = False
                if pattern.pattern == '^.*$':
                    toshow = True
                elif key == 'package' and pattern.search(package.name):
                    toshow = True
                elif key == 'description' and pattern.search(
                        package.candidate.raw_description):
                    toshow = True
                if toshow:
                    result.append(self._package_to_dict(package, full=False))
        return result

    @simple_response
    def packages_get(self, package):
        """ retrieves full properties of one package """

        package = self.package_manager.get_package(package)
        if package is not None:
            return self._package_to_dict(package, full=True)
        else:
            # TODO: 404?
            return {}

    @sanitize(function=MappingSanitizer(
        {
            'install': 'install',
            'upgrade': 'install',
            'uninstall': 'remove',
        },
        required=True),
              packages=ListSanitizer(StringSanitizer(minimum=1),
                                     required=True))
    @simple_response
    def packages_invoke_dry_run(self, packages, function):
        packages = self.package_manager.get_packages(packages)
        kwargs = {'install': [], 'remove': [], 'dry_run': True}
        if function == 'install':
            kwargs['install'] = packages
        else:
            kwargs['remove'] = packages
        return dict(
            zip(['install', 'remove', 'broken'],
                self.package_manager.mark(**kwargs)))

    @sanitize(function=MappingSanitizer(
        {
            'install': 'install',
            'upgrade': 'install',
            'uninstall': 'remove',
        },
        required=True),
              packages=ListSanitizer(StringSanitizer(minimum=1),
                                     required=True))
    def packages_invoke(self, request):
        """ executes an installer action """
        packages = request.options.get('packages')
        function = request.options.get('function')

        try:
            with self.package_manager.locked(reset_status=True):
                not_found = [
                    pkg_name for pkg_name in packages
                    if self.package_manager.get_package(pkg_name) is None
                ]
                self.finished(request.id, {'not_found': not_found})

                if not not_found:

                    def _thread(package_manager, function, packages):
                        with package_manager.locked(set_finished=True):
                            with package_manager.no_umc_restart():
                                if function == 'install':
                                    package_manager.install(*packages)
                                else:
                                    package_manager.uninstall(*packages)

                    def _finished(thread, result):
                        if isinstance(result, BaseException):
                            MODULE.warn('Exception during %s %s: %r' %
                                        (function, packages, str(result)))

                    thread = notifier.threads.Simple(
                        'invoke',
                        notifier.Callback(_thread, self.package_manager,
                                          function, packages), _finished)
                    thread.run()
        except LockError:
            # make it thread safe: another process started a package manager
            # this module instance already has a running package manager
            raise umcm.UMC_CommandError(
                _('Another package operation is in progress'))

    @simple_response
    def progress(self):
        timeout = 5
        return self.package_manager.poll(timeout)

    def _package_to_dict(self, package, full):
        """ Helper that extracts properties from a 'apt_pkg.Package' object
			and stores them into a dictionary. Depending on the 'full'
			switch, stores only limited (for grid display) or full
			(for detail view) set of properties.
		"""
        installed = package.installed  # may be None
        candidate = package.candidate

        result = {
            'package': package.name,
            'installed': package.is_installed,
            'upgradable': package.is_upgradable,
            'summary': candidate.summary,
        }

        # add (and translate) a combined status field
        # *** NOTE *** we translate it here: if we would use the Custom Formatter
        #		of the grid then clicking on the sort header would not work.
        if package.is_installed:
            if package.is_upgradable:
                result['status'] = _('upgradable')
            else:
                result['status'] = _('installed')
        else:
            result['status'] = _('not installed')

        # additional fields needed for detail view
        if full:
            result['section'] = package.section
            result['priority'] = package.priority
            # Some fields differ depending on whether the package is installed or not:
            if package.is_installed:
                result['summary'] = installed.summary  # take the current one
                result['description'] = installed.description
                result['installed_version'] = installed.version
                result['size'] = installed.installed_size
                if package.is_upgradable:
                    result['candidate_version'] = candidate.version
            else:
                del result[
                    'upgradable']  # not installed: don't show 'upgradable' at all
                result['description'] = candidate.description
                result['size'] = candidate.installed_size
                result['candidate_version'] = candidate.version
            # format size to handle bytes
            size = result['size']
            byte_mods = ['B', 'kB', 'MB']
            for byte_mod in byte_mods:
                if size < 10000:
                    break
                size = float(size) / 1000  # MB, not MiB
            else:
                size = size * 1000  # once too often
            if size == int(size):
                format_string = '%d %s'
            else:
                format_string = '%.2f %s'
            result['size'] = format_string % (size, byte_mod)

        return result

    @simple_response
    def components_query(self):
        """	Returns components list for the grid in the ComponentsPage.
		"""
        # be as current as possible.
        self.uu.ucr_reinit()
        self.ucr.load()

        result = []
        for comp in self.uu.get_all_components():
            result.append(self.component_manager.component(comp))
        return result

    @sanitize_list(StringSanitizer())
    @multi_response(single_values=True)
    def components_get(self, iterator, component_id):
        # be as current as possible.
        self.uu.ucr_reinit()
        self.ucr.load()
        for component_id in iterator:
            yield self.component_manager.component(component_id)

    @sanitize_list(DictSanitizer({'object': advanced_components_sanitizer}))
    @multi_response
    def components_put(self, iterator, object):
        """Writes back one or more component definitions.
		"""
        # umc.widgets.Form wraps the real data into an array:
        #
        #	[
        #		{
        #			'object' : { ... a dict with the real data .. },
        #			'options': None
        #		},
        #		... more such entries ...
        #	]
        #
        # Current approach is to return a similarly structured array,
        # filled with elements, each one corresponding to one array
        # element of the request:
        #
        #	[
        #		{
        #			'status'	:	a number where 0 stands for success, anything else
        #							is an error code
        #			'message'	:	a result message
        #			'object'	:	a dict of field -> error message mapping, allows
        #							the form to show detailed error information
        #		},
        #		... more such entries ...
        #	]
        with util.set_save_commit_load(self.ucr) as super_ucr:
            for object, in iterator:
                yield self.component_manager.put(object, super_ucr)
        self.package_manager.update()

    # do the same as components_put (update)
    # but dont allow adding an already existing entry
    components_add = sanitize_list(
        DictSanitizer({'object': add_components_sanitizer}))(components_put)
    components_add.__name__ = 'components_add'

    @sanitize_list(StringSanitizer())
    @multi_response(single_values=True)
    def components_del(self, iterator, component_id):
        for component_id in iterator:
            yield self.component_manager.remove(component_id)
        self.package_manager.update()

    @multi_response
    def settings_get(self, iterator):
        # *** IMPORTANT *** Our UCR copy must always be current. This is not only
        #	to catch up changes made via other channels (ucr command line etc),
        #	but also to reflect the changes we have made ourselves!
        self.ucr.load()

        for _ in iterator:
            yield {
                'maintained':
                self.ucr.is_true('repository/online/maintained', False),
                'unmaintained':
                self.ucr.is_true('repository/online/unmaintained', False),
                'server':
                self.ucr.get('repository/online/server', ''),
                'prefix':
                self.ucr.get('repository/online/prefix', ''),
            }

    @sanitize_list(
        DictSanitizer({'object': basic_components_sanitizer}),
        min_elements=1,
        max_elements=1  # moduleStore with one element...
    )
    @multi_response
    def settings_put(self, iterator, object):
        # FIXME: returns values although it should yield (multi_response)
        changed = False
        # Set values into our UCR copy.
        try:
            with util.set_save_commit_load(self.ucr) as super_ucr:
                for object, in iterator:
                    for key, value in object.iteritems():
                        MODULE.info(
                            "   ++ Setting new value for '%s' to '%s'" %
                            (key, value))
                        super_ucr.set_registry_var(
                            '%s/%s' % (constants.ONLINE_BASE, key), value)
                changed = super_ucr.changed()
        except Exception as e:
            MODULE.warn("   !! Writing UCR failed: %s" % str(e))
            return [{'message': str(e), 'status': constants.PUT_WRITE_ERROR}]

        self.package_manager.update()

        # Bug #24878: emit a warning if repository is not reachable
        try:
            updater = self.uu
            for line in updater.print_version_repositories().split('\n'):
                if line.strip():
                    break
            else:
                raise ConfigurationError()
        except ConfigurationError:
            msg = _(
                "There is no repository at this server (or at least none for the current UCS version)"
            )
            MODULE.warn("   !! Updater error: %s" % msg)
            response = {'message': msg, 'status': constants.PUT_UPDATER_ERROR}
            # if nothing was committed, we want a different type of error code,
            # just to appropriately inform the user
            if changed:
                response['status'] = constants.PUT_UPDATER_NOREPOS
            return [response]
        except:
            info = sys.exc_info()
            emsg = '%s: %s' % info[:2]
            MODULE.warn("   !! Updater error [%s]: %s" % (emsg))
            return [{
                'message': str(info[1]),
                'status': constants.PUT_UPDATER_ERROR
            }]
        return [{'status': constants.PUT_SUCCESS}]
Esempio n. 8
0
class Instance(SchoolBaseModule):
    def query(self, request):
        """Searches for internet filter rules
		requests.options = {}
		'pattern' -- pattern to match within the rule name or the list of domains
		"""
        MODULE.info('internetrules.query: options: %s' % str(request.options))
        pattern = request.options.get('pattern', '').lower()

        def _matchDomain(domains):
            # helper function to match pattern within the list of domains
            matches = [idom for idom in domains if pattern in idom.lower()]
            return 0 < len(matches)

        # filter out all rules that match the given pattern
        result = [
            dict(
                name=irule.name,
                type=_filterTypesInv[irule.type],
                domains=len(irule.domains),
                priority=irule.priority,
                wlan=irule.wlan,
            ) for irule in rules.list()
            if pattern in irule.name.lower() or _matchDomain(irule.domains)
        ]

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

    @sanitize(StringSanitizer())
    def get(self, request):
        """Returns the specified rules
		requests.options = [ <ruleName>, ... ]
		"""
        MODULE.info('internetrules.get: options: %s' % str(request.options))
        result = []
        # fetch all rules with the given names (we need to make sure that "name" is UTF8)
        names = set(iname.encode('utf8') for iname in request.options)
        result = [
            dict(
                name=irule.name,
                type=_filterTypesInv[irule.type],
                domains=irule.domains,
                priority=irule.priority,
                wlan=irule.wlan,
            ) for irule in rules.list() if irule.name in names
        ]

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

    @sanitize(DictSanitizer(dict(object=StringSanitizer()), required=True))
    def remove(self, request):
        """Removes the specified rules
		requests.options = [ { "object": <ruleName> }, ... ]
		"""
        MODULE.info('internetrules.remove: options: %s' % str(request.options))
        result = []
        # fetch all rules with the given names
        for ientry in request.options:
            iname = ientry['object']
            success = False
            if iname:
                success = rules.remove(iname)
            result.append(dict(name=iname, success=success))

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

    @staticmethod
    def _parseRule(iprops, forceAllProperties=False):
        # validate types
        for ikey, itype in (('name', basestring), ('type', basestring),
                            ('priority', (int, basestring)), ('wlan', bool),
                            ('domains', list)):
            if ikey not in iprops:
                if forceAllProperties:
                    # raise exception as the key is not present
                    raise ValueError(
                        _('The key "%s" has not been specified: %s') %
                        (ikey, iprops))
                continue
            if not isinstance(iprops[ikey], itype):
                typeStr = ''
                if isinstance(itype, tuple):
                    typeStr = ', '.join([i.__name__ for i in itype])
                else:
                    typeStr = itype.__name__
                raise ValueError(
                    _('The key "%s" needs to be of type: %s') %
                    (ikey, typeStr))

        # validate name
        if 'name' in iprops and not univention.config_registry.validate_key(
                iprops['name'].encode('utf-8')):
            raise ValueError(
                _('Invalid rule name "%s". The name needs to be a string, the following special characters are not allowed: %s'
                  ) %
                (iprops.get('name'),
                 '!, ", §, $, %, &, (, ), [, ], {, }, =, ?, `, +, #, \', ",", ;, <, >, \\'
                 ))

        # validate type
        if 'type' in iprops and iprops['type'] not in _filterTypes:
            raise ValueError(_('Filter type is unknown: %s') % iprops['type'])

        # validate domains
        if 'domains' in iprops:
            parsedDomains = []
            for idomain in iprops['domains']:

                def _validValueChar():
                    # helper function to check for invalid characters
                    for ichar in idomain:
                        if ichar in univention.config_registry.backend.INVALID_VALUE_CHARS:
                            return False
                    return True

                if not isinstance(idomain,
                                  basestring) or not _validValueChar():
                    raise ValueError(_('Invalid domain '))

                # parse domain
                domain = idomain
                if '://' not in domain:
                    # make sure that we have a scheme defined for parsing
                    MODULE.info(
                        'Adding a leading scheme for parsing of domain: %s' %
                        idomain)
                    domain = 'http://%s' % domain
                domain = urlparse(domain).hostname
                MODULE.info('Parsed domain: %s -> %s' % (idomain, domain))
                if not domain:
                    raise ValueError(
                        _('The specified domain "%s" is not valid. Please specify a valid domain name, such as "wikipedia.org", "facebook.com"'
                          ) % idomain)

                # add domain to list of parsed domains
                parsedDomains.append(domain)

            # save parsed domains in the dict
            iprops['domains'] = parsedDomains

        return iprops

    @sanitize(
        DictSanitizer(dict(object=DictSanitizer(dict(
            name=StringSanitizer(required=True),
            type=ChoicesSanitizer(list(_filterTypes.keys()), required=True),
            wlan=BooleanSanitizer(required=True),
            priority=IntegerSanitizer(required=True),
            domains=ListSanitizer(StringSanitizer(required=True),
                                  required=True),
        ),
                                                required=True)),
                      required=True))
    def add(self, request):
        """Add the specified new rules:
		requests.options = [ {
			'object': {
				'name': <str>,
				'type': 'whitelist' | 'blacklist',
				'priority': <int> | <str>,
				'wlan': <bool>,
				'domains': [<str>, ...],
			}
		}, ... ]
		"""

        # try to create all specified projects
        result = []
        for ientry in request.options:
            iprops = ientry['object']
            try:

                # make sure that the rule does not already exist
                irule = rules.load(iprops['name'])
                if irule:
                    raise ValueError(
                        _('A rule with the same name does already exist: %s') %
                        iprops['name'])

                # parse the properties
                parsedProps = self._parseRule(iprops, True)

                # create a new rule from the user input
                newRule = rules.Rule(
                    name=parsedProps['name'],
                    type=_filterTypes[parsedProps['type']],
                    priority=parsedProps['priority'],
                    wlan=parsedProps['wlan'],
                    domains=parsedProps['domains'],
                )

                # try to save filter rule
                newRule.save()
                MODULE.info('Created new rule: %s' % newRule)

                # everything ok
                result.append(dict(name=iprops['name'], success=True))
            except (ValueError, KeyError) as e:
                # data not valid... create error info
                MODULE.info(
                    'data for internet filter rule "%s" is not valid: %s' %
                    (iprops.get('name'), e))
                result.append(
                    dict(name=iprops.get('name'),
                         success=False,
                         details=str(e)))

        # return the results
        self.finished(request.id, result)

    @sanitize(
        DictSanitizer(dict(
            object=DictSanitizer(dict(
                name=StringSanitizer(required=True),
                type=ChoicesSanitizer(list(_filterTypes.keys()),
                                      required=True),
                wlan=BooleanSanitizer(required=True),
                priority=IntegerSanitizer(required=True),
                domains=ListSanitizer(StringSanitizer(required=True),
                                      required=True),
            ),
                                 required=True),
            options=DictSanitizer(dict(name=StringSanitizer()), required=True),
        ),
                      required=True))
    def put(self, request):
        """Modify an existing rules:
		requests.options = [ {
			'object': {
				'name': <str>, 						# optional
				'type': 'whitelist' | 'blacklist', 	# optional
				'priority': <int>, 					# optional
				'wlan': <bool>,						# optional
				'domains': [<str>, ...],  			# optional
			},
			'options': {
				'name': <str>  # the original name of the object
			}
		}, ... ]
		"""

        # try to create all specified projects
        result = []
        for ientry in request.options:
            try:
                # get properties and options from entry
                iprops = ientry['object']
                iname = None
                ioptions = ientry.get('options')
                if ioptions:
                    iname = ioptions.get('name')
                if not iname:
                    raise ValueError(
                        _('No "name" attribute has been specified in the options.'
                          ))

                # make sure that the rule already exists
                irule = rules.load(iname)
                if not irule:
                    raise ValueError(
                        _('The rule does not exist and cannot be modified: %s')
                        % iprops.get('name', ''))

                # parse the properties
                self._parseRule(iprops)

                if iprops.get('name', iname) != iname:
                    # name has been changed -> remove old rule and create a new one
                    rules.remove(iname)
                    irule.name = iprops['name']

                if 'type' in iprops:
                    # set rule type, move all domains from the previous type
                    oldDomains = irule.domains
                    irule.domains = []
                    irule.type = _filterTypes[iprops['type']]
                    irule.domains = oldDomains

                if 'priority' in iprops:
                    # set priority
                    irule.priority = iprops['priority']

                if 'wlan' in iprops:
                    # set wlan
                    irule.wlan = iprops['wlan']

                if 'domains' in iprops:
                    # set domains
                    irule.domains = iprops['domains']

                # try to save filter rule
                irule.save()
                MODULE.info('Saved rule: %s' % irule)

                # everything ok
                result.append(dict(name=iname, success=True))
            except ValueError as e:
                # data not valid... create error info
                MODULE.info(
                    'data for internet filter rule "%s" is not valid: %s' %
                    (iprops.get('name'), e))
                result.append(
                    dict(name=iprops.get('name'),
                         success=False,
                         details=str(e)))

        # return the results
        self.finished(request.id, result)

    @sanitize(school=SchoolSanitizer(required=True),
              pattern=StringSanitizer(default=''))
    @LDAP_Connection()
    def groups_query(self, request, ldap_user_read=None, ldap_position=None):
        """List all groups (classes, workgroups) and their assigned internet rule"""
        pattern = LDAP_Filter.forAll(request.options.get('pattern', ''),
                                     ['name', 'description'])
        school = request.options['school']
        groups = [
            x for x in Group.get_all(ldap_user_read, school, pattern)
            if not x.self_is_computerroom()
        ]

        internet_rules = rules.getGroupRuleName([i.name for i in groups])
        name = re.compile('-%s$' % (re.escape(school)), flags=re.I)
        result = [{
            'name':
            i.get_relative_name()
            if hasattr(i, 'get_relative_name') else name.sub('', i.name),
            '$dn$':
            i.dn,
            'rule':
            internet_rules.get(i.name, 'default')
            or _('-- Default (unrestricted) --')
        } for i in groups]
        result.sort(cmp=lambda x, y: cmp(x.lower(), y.lower()),
                    key=lambda x: x['name'])

        self.finished(request.id, result)

    @sanitize(
        DictSanitizer(
            dict(
                group=StringSanitizer(required=True),
                rule=StringSanitizer(required=True),
            )))
    @LDAP_Connection()
    def groups_assign(self, request, ldap_user_read=None, ldap_position=None):
        """Assigns default rules to groups:
		request.options = [ { 'group': <groupDN>, 'rule': <ruleName> }, ... ]
		"""
        MODULE.info('internetrules.groups_assign: options: %s' %
                    str(request.options))

        # try to load all group rules
        newRules = {}
        rmRules = []
        for ientry in request.options:
            # make sure the group exists
            igrp = udm_objects.get(udm_modules.get('groups/group'), None,
                                   ldap_user_read, ldap_position,
                                   ientry['group'])
            if not igrp:
                raise UMC_Error('unknown group object')
            igrp.open()

            # check the rule name
            irule = ientry['rule']
            if irule == '$default$':
                # remove the rule
                rmRules.append(igrp['name'])
            else:
                try:
                    # make sure the rule name is valid
                    self._parseRule(dict(name=irule))
                except ValueError as exc:
                    raise UMC_Error(str(exc))

                # add new rule
                newRules[igrp['name']] = irule

        # assign default filter rules to groups
        rules.setGroupRuleName(newRules)
        rules.unsetGroupRuleName(rmRules)

        MODULE.info('internetrules.groups_assign: finished')
        self.finished(request.id, True)
Esempio n. 9
0
class Instance(Base):

	def init(self):
		self.azure_response = None
		self.adconnection_alias = ucr.get(adconnection_wizard_ucrv) or None
		MODULE.process('adconnection_alias={!r}'.format(self.adconnection_alias))

	@simple_response
	def query(self):
		fqdn = '%s.%s' % (ucr.get('hostname'), ucr.get('domainname'))
		return {
			'initialized': AzureAuth.is_initialized(self.adconnection_alias),
			'login-url': '{origin}/univention/command/office365/authorize',
			'appid-url': 'https://%s/office365' % (fqdn,),
			'base-url': 'https://%s/' % (fqdn,),
		}

	@file_upload
	@sanitize(DictSanitizer(dict(
		tmpfile=StringSanitizer(required=True)
	), required=True))
	@sanitize_body(DictSanitizer(dict(
		domain=StringSanitizer(required=True, minimum=1),
		adconnection_id=StringSanitizer(default='common'),
	), required=True))
	def upload(self, request):
		AzureAuth.uninitialize(self.adconnection_alias)

		try:
			adconnection_id = request.body.get('adconnection_id') or 'common'
			adconnection_id = urlparse.urlparse(adconnection_id).path.strip('/').split('/')[0]
			with open(request.options[0]['tmpfile']) as fd:
				manifest = Manifest(fd, adconnection_id, request.body['domain'])
			manifest.transform()
		except ManifestError as exc:
			raise UMC_Error(str(exc))

		try:
			AzureAuth.store_manifest(manifest, self.adconnection_alias)
		except ADConnectionIDError:
			raise UMC_Error(_("Invalid federation metadata document address (e.g. https://login.microsoftonline.com/3e7d9eb4-c4a1-4cfd-893e-a8ec29e46b77/federationmetadata/2007-06/federationmetadata.xml)."))
		except AzureError as exc:
			raise UMC_Error(str(exc))

		try:
			authorizationurl = AzureAuth.get_authorization_url(self.adconnection_alias)
		except AzureError as exc:
			raise UMC_Error(str(exc))

		self.finished(request.id, {
			'authorizationurl': authorizationurl,
		}, message=_('The manifest has been successfully uploaded.'))

	@allow_get_request
	def manifest_json(self, request):
		with open(AzureADConnectionHandler.get_conf_path('MANIFEST_FILE', self.adconnection_alias), 'rb') as fd:
			self.finished(request.id, fd.read(), mimetype='application/octet-stream')

	@allow_get_request
	def saml_setup_script(self, request):
		with open(SAML_SETUP_SCRIPT_PATH.format(adconnection_alias='_{}'.format(self.adconnection_alias) if self.adconnection_alias else ''), 'rb') as fd:
			self.finished(request.id, fd.read(), mimetype='application/octet-stream')

	@allow_get_request
	def public_signing_cert(self, request):
		with open(AzureADConnectionHandler.get_conf_path('SSL_CERT', self.adconnection_alias), 'rb') as fd:
			self.finished(request.id, fd.read(), mimetype='application/octet-stream')

	@allow_get_request
	@sanitize(
		id_token=StringSanitizer(),
		code=StringSanitizer(),
		session_state=StringSanitizer(),
		admin_consent=BooleanSanitizer(),
		error=StringSanitizer(),
		error_description=StringSanitizer()
	)
	def authorize(self, request):
		self.init()  # reset state in case the first attempt failed
		self.azure_response = {}
		self.azure_response.update(request.options)
		content = """<!DOCTYPE html>
<html>
<head>
<title>%(title)s</title>
<script type="application/javascript">
window.close();
window.top.close();
</script>
</head>
<body>
%(content)s
</body>
</html>
		""" % {
			'title': _('Office 365 Configuration finished'),
			'content': _('The configuration has finished! You can now close this page and continue the configuration wizard.'),
		}
		self.finished(request.id, bytes(content), mimetype='text/html')

	@simple_response
	def state(self):
		options = self.azure_response
		if not options:
			return progress(message=_('Waiting for authorization to be completed.'), waiting=True)

		if options['id_token']:
			try:
				AzureAuth.parse_id_token(options['id_token'], self.adconnection_alias)
				AzureAuth.store_tokens(adconnection_alias=self.adconnection_alias, consent_given=True)
				aa = AzureAuth("office365", self.adconnection_alias)
				aa.write_saml_setup_script(self.adconnection_alias)
				aa.set_ucs_overview_link()
				aa.retrieve_access_token()  # not really necessary, but it'll make sure everything worked
			except AzureError as exc:
				self.init()
				raise UMC_Error(str(exc))
			options['id_token'] = None
			if self.adconnection_alias:
				ucrv_set = '{}{}={}'.format(
					adconnection_alias_ucrv,
					self.adconnection_alias,
					AzureAuth.load_azure_ids(self.adconnection_alias)['adconnection_id']
				)
				MODULE.process('Setting UCR {}...'.format(ucrv_set))
				handler_set([ucrv_set])
			return progress(message=_('Successfully authorized. Starting synchronization.'))
		elif options['error']:
			self.init()
			raise UMC_Error(_('Microsoft reported an error condition during authorization. It might help to reauthorize. Error message: {error}: {error_description}').format(**options))
		elif AzureAuth.is_initialized(self.adconnection_alias):
			self.init()
			try:
				ah = AzureHandler(ucr, "wizard", self.adconnection_alias)
				users = ah.list_users()
				MODULE.process('Retrieved list of users: %r' % users)

			#except TokenError as exc:
			#	return
			except AzureError as exc:
				raise UMC_Error(str(exc))

			try:
				subprocess.call(["systemctl", "restart", "univention-directory-listener.service"])
			except (EnvironmentError,):
				pass
			return progress(message=_('Successfully initialized'), finished=True)
		return progress(message=_('Not yet initialized.'))
Esempio n. 10
0
                _("There already is a component with this name"))
        return value


basic_components_sanitizer = DictSanitizer(
    {
        'server':
        StringSanitizer(required=True, minimum=1),
        'prefix':
        StringSanitizer(required=True),
        'maintained':
        AnySanitizer(required=True,
                     may_change_value=False,
                     further_arguments=['unmaintained']),
        'unmaintained':
        BooleanSanitizer(required=True),
    },
    allow_other_keys=False,
)

advanced_components_sanitizer = DictSanitizer({
    'server':
    StringSanitizer(),
    'prefix':
    StringSanitizer(),
    'maintained':
    BooleanSanitizer(),
    'unmaintained':
    BooleanSanitizer(),
    'enabled':
    BooleanSanitizer(required=True),
Esempio n. 11
0
class Instance(umcm.Base, ProgressMixin):

	def init(self):
		os.umask(0o022)  # umc umask is too restrictive for app center as it creates a lot of files in docker containers
		self.ucr = ucr_instance()

		self.update_applications_done = False
		util.install_opener(self.ucr)
		self._remote_progress = {}

		try:
			self.package_manager = PackageManager(
				info_handler=MODULE.process,
				step_handler=None,
				error_handler=MODULE.warn,
				lock=False,
			)
		except SystemError as exc:
			MODULE.error(str(exc))
			raise umcm.UMC_Error(str(exc), status=500)
		self.package_manager.set_finished()  # currently not working. accepting new tasks
		self.uu = UniventionUpdater(False)
		self.component_manager = util.ComponentManager(self.ucr, self.uu)
		get_package_manager._package_manager = self.package_manager

		# in order to set the correct locale for Application
		locale.setlocale(locale.LC_ALL, str(self.locale))

		try:
			log_to_logfile()
		except IOError:
			pass

		# connect univention.appcenter.log to the progress-method
		handler = ProgressInfoHandler(self.package_manager)
		handler.setLevel(logging.INFO)
		get_base_logger().addHandler(handler)

		percentage = ProgressPercentageHandler(self.package_manager)
		percentage.setLevel(logging.DEBUG)
		get_base_logger().getChild('actions.install.progress').addHandler(percentage)
		get_base_logger().getChild('actions.upgrade.progress').addHandler(percentage)
		get_base_logger().getChild('actions.remove.progress').addHandler(percentage)

	def error_handling(self, etype, exc, etraceback):
		error_handling(etype, exc, etraceback)
		return super(Instance, self).error_handling(exc, etype, etraceback)

	@simple_response
	def version(self):
		info = get_action('info')
		return info.get_compatibility()

	@simple_response
	def query(self, quick=False):
		if not quick:
			self.update_applications()
		self.ucr.load()
		reload_package_manager()
		list_apps = get_action('list')
		domain = get_action('domain')
		apps = list_apps.get_apps()
		if self.ucr.is_true('appcenter/docker', True):
			if not self._test_for_docker_service():
				raise umcm.UMC_Error(_('The docker service is not running! The App Center will not work properly.') + ' ' + _('Make sure docker.io is installed, try starting the service with "service docker start".'))
		info = domain.to_dict(apps)
		if quick:
			ret = []
			for app in info:
				if app is None:
					ret.append(None)
				else:
					short_info = {}
					for attr in ['id', 'name', 'vendor', 'maintainer', 'description', 'long_description', 'categories', 'end_of_life', 'update_available', 'logo_name', 'is_installed_anywhere', 'is_installed', 'installations']:
						short_info[attr] = app[attr]
					ret.append(short_info)
			return ret
		else:
			return info

	def update_applications(self):
		if self.ucr.is_true('appcenter/umc/update/always', True):
			update = get_action('update')
			try:
				update.call()
			except NetworkError as err:
				raise umcm.UMC_Error(str(err))
			except Abort:
				pass
			Application._all_applications = None
			self.update_applications_done = True

	def _test_for_docker_service(self):
		if docker_bridge_network_conflict():
			msg = _('A conflict between the system network settings and the docker bridge default network has been detected.') + '\n\n'
			msg += _('Please either configure a different network for the docker bridge by setting the UCR variable docker/daemon/default/opts/bip to a different network and restart the system,') + ' '
			msg += _('or disable the docker support in the AppCenter by setting appcenter/docker to false.')
			raise umcm.UMC_Error(msg)
		if not docker_is_running():
			MODULE.warn('Docker is not running! Trying to start it now...')
			call_process(['invoke-rc.d', 'docker', 'start'])
			if not docker_is_running():
				return False
		return True

	@simple_response
	def enable_docker(self):
		if self._test_for_docker_service():
			ucr_save({'appcenter/docker': 'enabled'})
		else:
			raise umcm.UMC_Error(_('Unable to start the docker service!') + ' ' + _('Make sure docker.io is installed, try starting the service with "service docker start".'))

	@require_apps_update
	@require_password
	@simple_response(with_progress=True)
	def sync_ldap(self):
		register = get_action('register')
		register.call(username=self.username, password=self.password)

	# used in updater-umc
	@simple_response
	def get_by_component_id(self, component_id):
		domain = get_action('domain')
		if isinstance(component_id, list):
			requested_apps = [Apps().find_by_component_id(cid) for cid in component_id]
			return domain.to_dict(requested_apps)
		else:
			app = Apps().find_by_component_id(component_id)
			if app:
				return domain.to_dict([app])[0]
			else:
				raise umcm.UMC_Error(_('Could not find an application for %s') % component_id)

	# used in updater-umc
	@simple_response
	def app_updates(self):
		upgrade = get_action('upgrade')
		domain = get_action('domain')
		return domain.to_dict(list(upgrade.iter_upgradable_apps()))

	@sanitize(application=StringSanitizer(minimum=1, required=True))
	@simple_response
	def get(self, application):
		domain = get_action('domain')
		app = Apps().find(application)
		if app is None:
			raise umcm.UMC_Error(_('Could not find an application for %s') % (application,))
		return domain.to_dict([app])[0]

	@sanitize(app=AppSanitizer(required=True), values=DictSanitizer({}))
	@simple_response
	def configure(self, app, autostart, values):
		configure = get_action('configure')
		configure.call(app=app, set_vars=values, autostart=autostart)

	@sanitize(app=AppSanitizer(required=True), mode=ChoicesSanitizer(['start', 'stop']))
	@simple_response
	def app_service(self, app, mode):
		service = get_action(mode)
		service.call(app=app)

	@sanitize(app=AppSanitizer(required=False), action=ChoicesSanitizer(['get', 'buy', 'search']), value=StringSanitizer())
	@simple_response
	def track(self, app, action, value):
		send_information(action, app=app, value=value)

	def invoke_dry_run(self, request):
		request.options['only_dry_run'] = True
		self.invoke(request)

	@require_password
	@sanitize(
		host=StringSanitizer(required=True),
		function=ChoicesSanitizer(['install', 'update', 'uninstall'], required=True),
		app=StringSanitizer(required=True),
		force=BooleanSanitizer(),
		values=DictSanitizer({})
	)
	@simple_response(with_progress=True)
	def invoke_remote_docker(self, host, function, app, force, values, progress):
		options = {'function': function, 'app': app, 'force': force, 'values': values}
		client = Client(host, self.username, self.password)
		result = client.umc_command('appcenter/docker/invoke', options).result
		self._remote_progress[progress.id] = client, result['id']

	@simple_response
	def remote_progress(self, progress_id):
		try:
			client, remote_progress_id = self._remote_progress[progress_id]
		except KeyError:
			# actually happens: before invoke_remote_docker is finished, remote_progress is already called
			return {}
		else:
			return client.umc_command('appcenter/docker/progress', {'progress_id': remote_progress_id}).result

	@require_apps_update
	@require_password
	@sanitize(
		function=MappingSanitizer({
			'install': 'install',
			'update': 'upgrade',
			'uninstall': 'remove',
		}, required=True),
		app=AppSanitizer(required=True),
		force=BooleanSanitizer(),
		values=DictSanitizer({})
	)
	@simple_response(with_progress=True)
	def invoke_docker(self, function, app, force, values, progress):
		if function == 'upgrade':
			app = Apps().find_candidate(app)
		serious_problems = False
		progress.title = _('%s: Running tests') % (app.name,)
		errors, warnings = app.check(function)
		can_continue = force  # "dry_run"
		if errors:
			MODULE.process('Cannot %s %s: %r' % (function, app.id, errors))
			serious_problems = True
			can_continue = False
		if warnings:
			MODULE.process('Warning trying to %s %s: %r' % (function, app.id, warnings))
		result = {
			'serious_problems': serious_problems,
			'invokation_forbidden_details': errors,
			'invokation_warning_details': warnings,
			'can_continue': can_continue,
			'software_changes_computed': False,
		}
		if can_continue:
			with self.locked():
				kwargs = {'noninteractive': True, 'skip_checks': ['shall_have_enough_ram', 'shall_only_be_installed_in_ad_env_with_password_service', 'must_not_have_concurrent_operation']}
				if function == 'install':
					progress.title = _('Installing %s') % (app.name,)
					kwargs['set_vars'] = values
				elif function == 'uninstall':
					progress.title = _('Uninstalling %s') % (app.name,)
				elif function == 'upgrade':
					progress.title = _('Upgrading %s') % (app.name,)
				action = get_action(function)
				handler = UMCProgressHandler(progress)
				handler.setLevel(logging.INFO)
				action.logger.addHandler(handler)
				try:
					result['success'] = action.call(app=app, username=self.username, password=self.password, **kwargs)
				except AppCenterError as exc:
					raise umcm.UMC_Error(str(exc), result=dict(
						display_feedback=True,
						title='%s %s' % (exc.title, exc.info)))
				finally:
					action.logger.removeHandler(handler)
		return result

	@contextmanager
	def locked(self):
		try:
			with self.package_manager.locked(reset_status=True, set_finished=True):
				yield
		except LockError:
			raise umcm.UMC_Error(_('Another package operation is in progress'))

	@require_apps_update
	@require_password
	@sanitize(
		function=ChoicesSanitizer(['install', 'uninstall', 'update', 'install-schema', 'update-schema'], required=True),
		application=StringSanitizer(minimum=1, required=True),
		force=BooleanSanitizer(),
		host=StringSanitizer(),
		only_dry_run=BooleanSanitizer(),
		dont_remote_install=BooleanSanitizer(),
		values=DictSanitizer({})
	)
	def invoke(self, request):
		# ATTENTION!!!!!!!
		# this function has to stay compatible with the very first App Center installations (Dec 2012)
		# if you add new arguments that change the behaviour
		# you should add a new method (see invoke_dry_run) or add a function name (e.g. install-schema)
		# this is necessary because newer app center may talk remotely with older one
		#   that does not understand new arguments and behaves the old way (in case of
		#   dry_run: install application although they were asked to dry_run)
		host = request.options.get('host')
		function = request.options.get('function')
		send_as = function
		if function.startswith('install'):
			function = 'install'
		if function.startswith('update'):
			function = 'update'

		application_id = request.options.get('application')
		Application.all(only_local=True)  # if not yet cached, cache. but use only local inis
		application = Application.find(application_id)
		if application is None:
			raise umcm.UMC_Error(_('Could not find an application for %s') % (application_id,))
		force = request.options.get('force')
		only_dry_run = request.options.get('only_dry_run')
		dont_remote_install = request.options.get('dont_remote_install')
		only_master_packages = send_as.endswith('schema')
		MODULE.process('Try to %s (%s) %s on %s. Force? %r. Only master packages? %r. Prevent installation on other systems? %r. Only dry run? %r.' % (function, send_as, application_id, host, force, only_master_packages, dont_remote_install, only_dry_run))

		# REMOTE invocation!
		if host and host != self.ucr.get('hostname'):
			try:
				client = Client(host, self.username, self.password)
				result = client.umc_command('appcenter/invoke', request.options).result
			except (ConnectionError, HTTPError) as exc:
				MODULE.error('Error during remote appcenter/invoke: %s' % (exc,))
				result = {
					'unreachable': [host],
					'master_unreachable': True,
					'serious_problems': True,
					'software_changes_computed': True,  # not really...
				}
			else:
				if result['can_continue']:
					def _thread_remote(_client, _package_manager):
						with _package_manager.locked(reset_status=True, set_finished=True):
							_package_manager.unlock()   # not really locked locally, but busy, so "with locked()" is appropriate
							Application._query_remote_progress(_client, _package_manager)

					def _finished_remote(thread, result):
						if isinstance(result, BaseException):
							MODULE.warn('Exception during %s %s: %s' % (function, application_id, str(result)))
					thread = notifier.threads.Simple('invoke', notifier.Callback(_thread_remote, client, self.package_manager), _finished_remote)
					thread.run()
			self.finished(request.id, result)
			return

		# make sure that the application can be installed/updated
		can_continue = True
		delayed_can_continue = True
		serious_problems = False
		result = {
			'install': [],
			'remove': [],
			'broken': [],
			'unreachable': [],
			'master_unreachable': False,
			'serious_problems': False,
			'hosts_info': {},
			'problems_with_hosts': False,
			'serious_problems_with_hosts': False,
			'invokation_forbidden_details': {},
			'invokation_warning_details': {},
			'software_changes_computed': False,
		}
		if not application:
			MODULE.process('Application not found: %s' % application_id)
			can_continue = False
		if can_continue and not only_master_packages:
			forbidden, warnings = application.check_invokation(function, self.package_manager)
			if forbidden:
				MODULE.process('Cannot %s %s: %r' % (function, application_id, forbidden))
				result['invokation_forbidden_details'] = forbidden
				can_continue = False
				serious_problems = True
			if warnings:
				MODULE.process('Warning trying to %s %s: %r' % (function, application_id, forbidden))
				result['invokation_warning_details'] = warnings
				if not force:
					# dont stop "immediately".
					#   compute the package changes!
					delayed_can_continue = False
		result['serious_problems'] = serious_problems
		result['can_continue'] = can_continue
		try:
			if can_continue:
				if self._working():
					# make it multi-tab safe (same session many buttons to be clicked)
					raise LockError()
				with self.package_manager.locked(reset_status=True):
					previously_registered_by_dry_run = False
					if can_continue and function in ('install', 'update'):
						remove_component = only_dry_run
						dry_run_result, previously_registered_by_dry_run = application.install_dry_run(self.package_manager, self.component_manager, remove_component=remove_component, username=self._username, password=self.password, only_master_packages=only_master_packages, dont_remote_install=dont_remote_install, function=function, force=force)
						result.update(dry_run_result)
						result['software_changes_computed'] = True
						serious_problems = bool(result['broken'] or result['master_unreachable'] or result['serious_problems_with_hosts'])
						if serious_problems or (not force and (result['unreachable'] or result['install'] or result['remove'] or result['problems_with_hosts'])):
							MODULE.process('Problems encountered or confirmation required. Removing component %s' % application.component_id)
							if not remove_component:
								# component was not removed automatically after dry_run
								if application.candidate:
									# operation on candidate failed. re-register original application
									application.register(self.component_manager, self.package_manager)
								else:
									# operation on self failed. unregister all
									application.unregister_all_and_register(None, self.component_manager, self.package_manager)
							can_continue = False
					elif can_continue and function in ('uninstall',) and not force:
						result['remove'] = application.uninstall_dry_run(self.package_manager)
						result['software_changes_computed'] = True
						can_continue = False
					can_continue = can_continue and delayed_can_continue and not only_dry_run
					result['serious_problems'] = serious_problems
					result['can_continue'] = can_continue

					if can_continue and not only_dry_run:
						def _thread(module, application, function):
							with module.package_manager.locked(set_finished=True):
								with module.package_manager.no_umc_restart(exclude_apache=True):
									if function in ('install', 'update'):
										# dont have to add component: already added during dry_run
										return application.install(module.package_manager, module.component_manager, add_component=only_master_packages, send_as=send_as, username=self._username, password=self.password, only_master_packages=only_master_packages, dont_remote_install=dont_remote_install, previously_registered_by_dry_run=previously_registered_by_dry_run)
									else:
										return application.uninstall(module.package_manager, module.component_manager, self._username, self.password)

						def _finished(thread, result):
							if isinstance(result, BaseException):
								MODULE.warn('Exception during %s %s: %s' % (function, application_id, str(result)))
						thread = notifier.threads.Simple('invoke', notifier.Callback(_thread, self, application, function), _finished)
						thread.run()
					else:
						self.package_manager.set_finished()  # nothing to do, ready to take new commands
			self.finished(request.id, result)
		except LockError:
			# make it thread safe: another process started a package manager
			# this module instance already has a running package manager
			raise umcm.UMC_Error(_('Another package operation is in progress'))

	def keep_alive(self, request):
		''' Fix for Bug #30611: UMC kills appcenter module
		if no request is sent for $(ucr get umc/module/timeout).
		this happens if a user logs out during a very long installation.
		this function will be run by the frontend to always have one connection open
		to prevent killing the module. '''
		def _thread():
			while not self.package_manager.progress_state._finished:
				time.sleep(1)

		def _finished(thread, result):
			success = not isinstance(result, BaseException)
			if not success:
				MODULE.warn('Exception during keep_alive: %s' % result)
			self.finished(request.id, success)
		thread = notifier.threads.Simple('keep_alive', notifier.Callback(_thread), _finished)
		thread.run()

	@simple_response
	def ping(self):
		return True

	@simple_response
	def buy(self, application):
		app = Apps().find(application)
		if not app or not app.shop_url:
			return None
		ret = {}
		ret['key_id'] = LICENSE.uuid
		ret['ucs_version'] = self.ucr.get('version/version')
		ret['app_id'] = app.id
		ret['app_version'] = app.version
		# ret['locale'] = locale.getlocale()[0] # done by frontend
		ret['user_count'] = None  # FIXME: get users and computers from license
		ret['computer_count'] = None
		return ret

	@simple_response
	def enable_disable_app(self, application, enable=True):
		app = Apps().find(application)
		if not app:
			return
		stall = get_action('stall')
		stall.call(app=app, undo=enable)

	@simple_response
	def packages_sections(self):
		""" fills the 'sections' combobox in the search form """

		sections = set()
		cache = apt.Cache()
		for package in cache:
			sections.add(package.section)

		return sorted(sections)

	@sanitize(pattern=PatternSanitizer(required=True))
	@simple_response
	def packages_query(self, pattern, section='all', key='package'):
		""" Query to fill the grid. Structure is fixed here. """
		result = []
		for package in self.package_manager.packages(reopen=True):
			if section == 'all' or package.section == section:
				toshow = False
				if pattern.pattern == '^.*$':
					toshow = True
				elif key == 'package' and pattern.search(package.name):
					toshow = True
				elif key == 'description' and package.candidate and pattern.search(package.candidate.raw_description):
					toshow = True
				if toshow:
					result.append(self._package_to_dict(package, full=False))
		return result

	@simple_response
	def packages_get(self, package):
		""" retrieves full properties of one package """

		package = self.package_manager.get_package(package)
		if package is not None:
			return self._package_to_dict(package, full=True)
		else:
			# TODO: 404?
			return {}

	@sanitize(
		function=MappingSanitizer({
			'install': 'install',
			'upgrade': 'install',
			'uninstall': 'remove',
		}, required=True),
		packages=ListSanitizer(StringSanitizer(minimum=1), required=True),
		update=BooleanSanitizer()
	)
	@simple_response
	def packages_invoke_dry_run(self, packages, function, update):
		if update:
			self.package_manager.update()
		packages = self.package_manager.get_packages(packages)
		kwargs = {'install': [], 'remove': [], 'dry_run': True}
		if function == 'install':
			kwargs['install'] = packages
		else:
			kwargs['remove'] = packages
		return dict(zip(['install', 'remove', 'broken'], self.package_manager.mark(**kwargs)))

	@sanitize(
		function=MappingSanitizer({
			'install': 'install',
			'upgrade': 'install',
			'uninstall': 'remove',
		}, required=True),
		packages=ListSanitizer(StringSanitizer(minimum=1), required=True)
	)
	def packages_invoke(self, request):
		""" executes an installer action """
		packages = request.options.get('packages')
		function = request.options.get('function')

		try:
			if self._working():
				# make it multi-tab safe (same session many buttons to be clicked)
				raise LockError()
			with self.package_manager.locked(reset_status=True):
				not_found = [pkg_name for pkg_name in packages if self.package_manager.get_package(pkg_name) is None]
				self.finished(request.id, {'not_found': not_found})

				if not not_found:
					def _thread(package_manager, function, packages):
						with package_manager.locked(set_finished=True):
							with package_manager.no_umc_restart(exclude_apache=True):
								if function == 'install':
									package_manager.install(*packages)
								else:
									package_manager.uninstall(*packages)

					def _finished(thread, result):
						if isinstance(result, BaseException):
							MODULE.warn('Exception during %s %s: %r' % (function, packages, str(result)))
					thread = notifier.threads.Simple('invoke', notifier.Callback(_thread, self.package_manager, function, packages), _finished)
					thread.run()
				else:
					self.package_manager.set_finished()  # nothing to do, ready to take new commands
		except LockError:
			# make it thread safe: another process started a package manager
			# this module instance already has a running package manager
			raise umcm.UMC_Error(_('Another package operation is in progress'))

	def _working(self):
		return not self.package_manager.progress_state._finished

	@simple_response
	def working(self):
		# TODO: PackageManager needs is_idle() or something
		#   preferably the package_manager can tell what is currently executed:
		#   package_manager.is_working() => False or _('Installing UCC')
		return self._working()

	@simple_response
	def custom_progress(self):
		timeout = 5
		return self.package_manager.poll(timeout)

	def _package_to_dict(self, package, full):
		""" Helper that extracts properties from a 'apt_pkg.Package' object
			and stores them into a dictionary. Depending on the 'full'
			switch, stores only limited (for grid display) or full
			(for detail view) set of properties.
		"""
		installed = package.installed  # may be None
		found = True
		candidate = package.candidate
		found = candidate is not None
		if not found:
			candidate = NoneCandidate()

		result = {
			'package': package.name,
			'installed': package.is_installed,
			'upgradable': package.is_upgradable and found,
			'summary': candidate.summary,
		}

		# add (and translate) a combined status field
		# *** NOTE *** we translate it here: if we would use the Custom Formatter
		#		of the grid then clicking on the sort header would not work.
		if package.is_installed:
			if package.is_upgradable:
				result['status'] = _('upgradable')
			else:
				result['status'] = _('installed')
		else:
			result['status'] = _('not installed')

		# additional fields needed for detail view
		if full:
			# Some fields differ depending on whether the package is installed or not:
			if package.is_installed:
				result['section'] = installed.section
				result['priority'] = installed.priority or ''
				result['summary'] = installed.summary   # take the current one
				result['description'] = installed.description
				result['installed_version'] = installed.version
				result['size'] = installed.installed_size
				if package.is_upgradable:
					result['candidate_version'] = candidate.version
			else:
				del result['upgradable']  # not installed: don't show 'upgradable' at all
				result['section'] = candidate.section
				result['priority'] = candidate.priority or ''
				result['description'] = candidate.description
				result['size'] = candidate.installed_size
				result['candidate_version'] = candidate.version
			# format size to handle bytes
			size = result['size']
			byte_mods = ['B', 'kB', 'MB']
			for byte_mod in byte_mods:
				if size < 10000:
					break
				size = float(size) / 1000  # MB, not MiB
			else:
				size = size * 1000  # once too often
			if size == int(size):
				format_string = '%d %s'
			else:
				format_string = '%.2f %s'
			result['size'] = format_string % (size, byte_mod)

		return result

	@simple_response
	def components_query(self):
		"""	Returns components list for the grid in the ComponentsPage.
		"""
		# be as current as possible.
		self.uu.ucr_reinit()
		self.ucr.load()

		result = []
		for comp in self.uu.get_all_components():
			result.append(self.component_manager.component(comp))
		return result

	@sanitize_list(StringSanitizer())
	@multi_response(single_values=True)
	def components_get(self, iterator, component_id):
		# be as current as possible.
		self.uu.ucr_reinit()
		self.ucr.load()
		for component_id in iterator:
			yield self.component_manager.component(component_id)

	@sanitize_list(DictSanitizer({'object': advanced_components_sanitizer}))
	@multi_response
	def components_put(self, iterator, object):
		"""Writes back one or more component definitions.
		"""
		# umc.widgets.Form wraps the real data into an array:
		#
		#	[
		#		{
		#			'object' : { ... a dict with the real data .. },
		#			'options': None
		#		},
		#		... more such entries ...
		#	]
		#
		# Current approach is to return a similarly structured array,
		# filled with elements, each one corresponding to one array
		# element of the request:
		#
		#	[
		#		{
		#			'status'	:	a number where 0 stands for success, anything else
		#							is an error code
		#			'message'	:	a result message
		#			'object'	:	a dict of field -> error message mapping, allows
		#							the form to show detailed error information
		#		},
		#		... more such entries ...
		#	]
		with util.set_save_commit_load(self.ucr) as super_ucr:
			for object, in iterator:
				yield self.component_manager.put(object, super_ucr)
		self.package_manager.update()

	# do the same as components_put (update)
	# but dont allow adding an already existing entry
	components_add = sanitize_list(DictSanitizer({'object': add_components_sanitizer}))(components_put)
	components_add.__name__ = 'components_add'

	@sanitize_list(StringSanitizer())
	@multi_response(single_values=True)
	def components_del(self, iterator, component_id):
		for component_id in iterator:
			yield self.component_manager.remove(component_id)
		self.package_manager.update()

	@multi_response
	def settings_get(self, iterator):
		# *** IMPORTANT *** Our UCR copy must always be current. This is not only
		#	to catch up changes made via other channels (ucr command line etc),
		#	but also to reflect the changes we have made ourselves!
		self.ucr.load()

		for _ in iterator:
			yield {
				'unmaintained': self.ucr.is_true('repository/online/unmaintained', False),
				'server': self.ucr.get('repository/online/server', ''),
				'prefix': self.ucr.get('repository/online/prefix', ''),
			}

	@sanitize_list(
		DictSanitizer({'object': basic_components_sanitizer}),
		min_elements=1,
		max_elements=1  # moduleStore with one element...
	)
	@multi_response
	def settings_put(self, iterator, object):
		# FIXME: returns values although it should yield (multi_response)
		changed = False
		# Set values into our UCR copy.
		try:
			with util.set_save_commit_load(self.ucr) as super_ucr:
				for object, in iterator:
					for key, value in object.iteritems():
						MODULE.info("   ++ Setting new value for '%s' to '%s'" % (key, value))
						super_ucr.set_registry_var('%s/%s' % (constants.ONLINE_BASE, key), value)
				changed = super_ucr.changed()
		except Exception as e:
			MODULE.warn("   !! Writing UCR failed: %s" % str(e))
			return [{'message': str(e), 'status': constants.PUT_WRITE_ERROR}]

		self.package_manager.update()

		# Bug #24878: emit a warning if repository is not reachable
		try:
			updater = self.uu
			for line in updater.print_version_repositories().split('\n'):
				if line.strip():
					break
			else:
				raise ConfigurationError()
		except ConfigurationError:
			msg = _("There is no repository at this server (or at least none for the current UCS version)")
			MODULE.warn("   !! Updater error: %s" % msg)
			response = {'message': msg, 'status': constants.PUT_UPDATER_ERROR}
			# if nothing was committed, we want a different type of error code,
			# just to appropriately inform the user
			if changed:
				response['status'] = constants.PUT_UPDATER_NOREPOS
			return [response]
		except:
			info = sys.exc_info()
			emsg = '%s: %s' % info[:2]
			MODULE.warn("   !! Updater error [%s]: %s" % (emsg))
			return [{'message': str(info[1]), 'status': constants.PUT_UPDATER_ERROR}]
		return [{'status': constants.PUT_SUCCESS}]