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