def init(self):
     MODULE.info("Initializing 'updater' module (PID = %d)" % (getpid(), ))
     self._current_job = ''
     self._logfile_start_line = 0
     self._serial_file = Watched_File(COMPONENTS_SERIAL_FILE)
     self._updates_serial = Watched_Files(UPDATE_SERIAL_FILES)
     try:
         self.uu = UniventionUpdater(False)
     except Exception as exc:  # FIXME: let it raise
         MODULE.error("init() ERROR: %s" % (exc, ))
def update_net(opt, ucr):
    # type: (Namespace, ConfigRegistry) -> Tuple[UniventionUpdater, Optional[UCS_Version]]
    dprint('Checking network repository')
    try:
        updater = UniventionUpdater()
        nextversion = updater.release_update_available(errorsto='exception')
    except RequiredComponentError:
        raise
    except ConfigurationError as ex:
        raise UpdateError('The configured repository is unavailable: %s' %
                          (ex, ),
                          errorsource='SETTINGS')

    return (updater, nextversion)
Ejemplo n.º 3
0
	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 get_updater(self):
     if self._uu is None:
         self._uu = UniventionUpdater(False)
     return self._uu
class Instance(Base):
    def init(self):
        MODULE.info("Initializing 'updater' module (PID = %d)" % (getpid(), ))
        self._current_job = ''
        self._logfile_start_line = 0
        self._serial_file = Watched_File(COMPONENTS_SERIAL_FILE)
        self._updates_serial = Watched_Files(UPDATE_SERIAL_FILES)
        try:
            self.uu = UniventionUpdater(False)
        except Exception as exc:  # FIXME: let it raise
            MODULE.error("init() ERROR: %s" % (exc, ))

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

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

    @simple_response
    def poll(self):
        return True

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

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

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

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

        return result

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

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

            return result

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

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

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

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

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

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

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

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

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

        result = {}
        ucr.load()

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

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

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

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

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

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

            what = "querying availability for easy mode"

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

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

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

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

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

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

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

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

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

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

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

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

        if not job:
            return

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

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

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

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

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

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

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

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

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

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

        return {'status': 0}

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

                if cmd in cmdline:
                    self._current_job = job
                    self._logfile_start_line = 0
                    return job
        return ''
def do_package_updates(options: Namespace, checkForUpdates: bool,
                       silent: bool) -> bool:
    interactive = not (options.noninteractive or checkForUpdates)
    updater = UniventionUpdater()
    # check if component updates are available
    dprint(silent, 'Checking for package updates: ', newline=False)
    new_packages, upgraded_packages, removed_packages = updater.component_update_get_packages(
    )
    update_available = bool(new_packages + upgraded_packages +
                            removed_packages)

    if not update_available:
        dprint(silent, 'none')
        return False

    # updates available ==> stop here in "check-mode"
    if checkForUpdates:
        dprint(silent, 'found')
        return True

    dprint(silent, 'found\n')
    if len(removed_packages) > 0:
        dprint(
            silent, 'The following packages will be REMOVED:\n %s' %
            _package_list(removed_packages))
    if len(new_packages) > 0:
        dprint(
            silent, 'The following packages will be installed:\n %s' %
            _package_list(new_packages))
    if len(upgraded_packages) > 0:
        dprint(
            silent, 'The following packages will be upgraded:\n %s' %
            _package_list(upgraded_packages))
    if interactive and not readcontinue('\nDo you want to continue [Y|n]?'):
        return False

    time.sleep(1)
    dprint(silent,
           'Starting dist-update at %s...' % (time.ctime()),
           debug=True)
    dprint(silent, 'Starting package upgrade', newline=False)

    hostname = socket.gethostname()
    context_id = write_event(UPDATE_STARTED, {'hostname': hostname})
    if context_id:
        os.environ['ADMINDIARY_CONTEXT'] = context_id
    returncode = updater.run_dist_upgrade()

    if returncode:
        dprint(silent,
               'exitcode of apt-get dist-upgrade: %s' % returncode,
               debug=True)
        dprint(
            silent,
            'ERROR: update failed. Please check /var/log/univention/updater.log\n'
        )
        update_status(status='FAILED', errorsource='UPDATE')
        write_event(UPDATE_FINISHED_FAILURE, {'hostname': hostname})
        sys.exit(1)
    dprint(silent,
           'dist-update finished at %s...' % (time.ctime()),
           debug=True)
    dprint(silent, 'done')
    write_event(
        UPDATE_FINISHED_SUCCESS, {
            'hostname':
            hostname,
            'version':
            'UCS %(version/version)s-%(version/patchlevel)s errata%(version/erratalevel)s'
            % configRegistry
        })
    time.sleep(1)
    return True
def do_release_update(options: Namespace, checkForUpdates: bool,
                      silent: bool) -> bool:
    updater = UniventionUpdater()

    # get next release update version
    dprint(silent, 'Checking for release updates: ', newline=False)
    version_next = updater.release_update_available()
    if not version_next:
        dprint(silent, 'none')
        return False
    if options.updateto and UCS_Version(
            options.updateto) < UCS_Version(version_next):
        dprint(
            silent,
            '%s is available but updater has been instructed to stop at version %s.'
            % (version_next, options.updateto))
        return False
    dprint(silent, 'found: UCS %s' % version_next)
    if checkForUpdates:
        return True

    interactive = not (options.noninteractive or checkForUpdates)
    if interactive and not readcontinue(
            'Do you want to update to %s [Y|n]?' % version_next):
        return False

    update_status(current_version=updater.current_version,
                  next_version=version_next,
                  status='RUNNING')

    dprint(silent,
           'Starting update to UCS version %s at %s...' %
           (version_next, time.ctime()),
           debug=True)
    dprint(silent, 'Starting update to UCS version %s' % (version_next))
    time.sleep(1)
    params = ['--silent']
    if options.ignoressh:
        params.append('--ignoressh')
    if options.ignoreterm:
        params.append('--ignoreterm')
    retcode = subprocess.call([
        '/usr/share/univention-updater/univention-updater', 'net',
        '--updateto',
        '%s' % (version_next)
    ] + params,
                              env=os.environ)
    if retcode:
        dprint(silent,
               'exitcode of univention-updater: %s' % retcode,
               debug=True)
        dprint(
            silent,
            'ERROR: update failed. Please check /var/log/univention/updater.log\n'
        )
        update_status(status='FAILED', errorsource='UPDATE')
        sys.exit(1)
    dprint(silent,
           'Update to UCS version %s finished at %s...' %
           (version_next, time.ctime()),
           debug=True)
    return True
class Instance(Base):
    def init(self) -> None:
        MODULE.info("Initializing 'updater' module (PID = %d)" % (getpid(), ))
        self._current_job = ''
        self._logfile_start_line = 0
        self._serial_file = Watched_File(COMPONENTS_SERIAL_FILE)
        try:
            self.uu = UniventionUpdater(False)
        except Exception as exc:  # FIXME: let it raise
            self.uu = None
            MODULE.error("init() ERROR: %s" % (exc, ))

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

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

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

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

        return ret

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

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

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

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

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

        return default

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

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

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

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

        return result

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        if not job:
            return None

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

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

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

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

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

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

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

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

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

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

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

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

        return {'status': 0}

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

                if cmd in cmdline:
                    self._current_job = job
                    self._logfile_start_line = 0
                    return job
        return ''
Ejemplo n.º 9
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}]