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
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 ''