import logging import os import re import sau import sau.helpers import sau.services EIX_SYNC_PATH='/usr/bin/eix-sync' RC_SERVICE_PATH='/sbin/rc-service' SYSTEMCTL='/usr/bin/systemctl' EMERGE_PATH='/usr/bin/emerge' EQUERY_PATH='/usr/bin/equery' EMAINT_PATH='/usr/sbin/emaint' PCLEAN_PATH='/usr/bin/perl-cleaner' GRUB_MKCONFIG='/usr/sbin/grub-mkconfig' # parsing output from eix -Ttnc package_re = re.compile(r'^\[([^\]])\] ([^ ]*) \((.*)\): .*$') # parsing version information from substrings of the above slot_re = re.compile(r'^(\(~\))?([^\(]+)(\([^\)]+\))$') def identify_service_from_bin(exe): log = logging.getLogger(sau.LOGNAME) if sau.services.on_systemd(): init_script_re = re.compile(r'[^/]*(.*)\.service$') else: init_script_re = re.compile(r'/etc/init\.d/(.*)') cmd = [ EQUERY_PATH, '-Cq', 'b', exe ] ret, out, err = sau.helpers.exec_cmd(cmd) if ret != 0: raise sau.errors.UnknownServiceError("searching for owner of {} failed:".format(exe)) pkg = out.strip() cmd = [ EQUERY_PATH, '-Cq', 'f', pkg ] ret, out, err = sau.helpers.exec_cmd(cmd) if ret != 0: log.warning("listing files for package {} failed:".format(pkg)) for line in out.splitlines(): log.warning("stdout: {}".format(line)) for line in err.splitlines(): log.warning("stderr: {}".format(line)) return None matches = set() for line in out.splitlines(): match = re.match(init_script_re, line) if match: matches.add(match.group(1)) if len(matches) < 1: raise sau.errors.UnknownServiceError('Could not find any init script in package {}'.format(pkg)) elif len(matches) > 1: raise sau.errors.UnknownServiceError('Found multiple init script in package {}'.format(pkg)) else: return matches.pop() return None def restart_service(service): log = logging.getLogger(sau.LOGNAME) if sau.services.on_systemd(): cmd = [ SYSTEMCTL, 'restart', service ] else: cmd = [ RC_SERVICE_PATH, service, 'restart' ] ret, out, err = sau.helpers.exec_cmd(cmd) if ret != 0: log.warning("Restart of {} failed:".format(service)) for line in out.splitlines(): log.warning("stdout: {}".format(line)) for line in err.splitlines(): log.warning("stderr: {}".format(line)) else: log.info("restarted service {}".format(service)) def system_upgrade(): log = logging.getLogger(sau.LOGNAME) log.debug('Gentoo "system_upgrade" is done at package upgrade stage; ignoring here...') return False def _sync_portage(): log = logging.getLogger(sau.LOGNAME) if os.path.exists(EIX_SYNC_PATH): cmd = [ EIX_SYNC_PATH, '-q' ] ret, out, err = sau.helpers.exec_cmd(cmd, timeout=3600) else: cmd = [ EMERGE_PATH, '-q', '--sync' ] ret, out, err = sau.helpers.exec_cmd(cmd, timeout=3600) if ret != 0: log.error("Portage sync failed:") for line in out.splitlines(): log.error("stdout: {}".format(line)) for line in err.splitlines(): log.error("stderr: {}".format(line)) raise sau.errors.UpgradeError(f'Sync command {cmd} failed') cmd = [ EMAINT_PATH, '-f', 'all' ] ret, out, err = sau.helpers.exec_cmd(cmd, timeout=3600) if ret != 0: log.warning("emaint failed:") for line in out.splitlines(): log.warning("stdout: {}".format(line)) for line in err.splitlines(): log.warning("stderr: {}".format(line)) def is_system_package(atom, eclasses): log = logging.getLogger(sau.LOGNAME) name=re.sub(r'^[<=>]*(.*?)(?:-\d)?(?:::\w+)?$', r'\1', atom) # sys-boot/ category should probably always be considered # system-packages if name.split('/')[0] == 'sys-boot': log.debug(f"{name} is a sys-boot package") return True if eclasses is True: return True # libc-packages should be considered system-packages as they generally # requires the system to be restarted. Not sure if there is a better way # then just checking for specific packages here, but as far as I know there # are not many of them anyway... if re.search(r'^sys-libs/(glibc|musl)', name): log.debug(f"{name} is a libc package") return True if any([ x in eclasses for x in [ 'dist-kernel-utils', 'linux-mod', 'kernel-install' ] ]): log.debug(f"{name} is of system eclass (eclasses: {eclasses})") return True return False def get_eclasses(atom): log = logging.getLogger(sau.LOGNAME) eclasses = [] name=re.sub(r'^[<=>]*(.*?)(?:-\d+)?(?:::\w+)?$', r'\1', atom) test_re = re.compile(r'^\s*inherit\s+') cmd=[ EQUERY_PATH, 'w', name ] ret, out, err = sau.helpers.exec_cmd(cmd) if not ret == 0: log.warning(f'Unable to locate ebuild for {atom}') # better safe than sorry; if we don't know, let's pretend it's a system # package return True path = out.strip() if not os.path.isfile(path): log.warning(f"This path doesn't look lika a path to the ebuild for {name}: {path}") return True with open(path, 'r', encoding='utf-8') as f: for line in f.readlines(): if eclasses and eclasses[-1] == '\\': eclasses = eclasses[:-1] eclasses.extend(line.split()) if re.search(test_re, line): if re.search(test_re, line): eclasses.extend(line.split()[1:]) # Remove revisions from eclasses, hopefully makes it less messy if they get # updated eclasses = [re.sub(r'^(.*?)-r\d+', r'\1', x) for x in eclasses] return eclasses def get_dependencies(atom): cmd=[ EQUERY_PATH, '-q', 'd', '-F', '$cp', atom ] ret, out, err = sau.helpers.exec_cmd(cmd) dependencies = [l.strip() for l in out.splitlines()] return dependencies def pkg_upgrade(): log = logging.getLogger(sau.LOGNAME) conf = sau.config do_system_upgrade = conf.getboolean('default', 'do_system_upgrade', fallback=False) if conf.getboolean('default', 'do_reposync', fallback=True): _sync_portage() # [ebuild U ] media-plugins/alsa-plugins-1.1.8 [1.1.6] pretend_re = re.compile(r'^\[(?:ebuild|binary) ([^\]]*)\] ([^ ]+?)-(\d[-\.\w]*)( \[[^\]]+\])?') ignore_re = re.compile(r'^(|.*caus.* rebuilds.*|.*scheduled for merge.*|.*waiting for lock on.*)$') default_version_sens = conf.getint('default', 'version_sensitivity', fallback=1) ## Query upgradeable packages cmd = [ EMERGE_PATH, '--color', 'n', '-uDNpq', '--with-bdeps=y', '@world' ] ret, out, err = sau.helpers.exec_cmd(cmd) if not ret == 0: log.error('emerge pretend returned {}'.format(ret)) for line in out.splitlines(): log.error('stdout: {}'.format(line)) for line in err.splitlines(): log.error('stderr: {}'.format(line)) raise sau.errors.UpgradeError(f'Failed to calculate upgrade path') do_rebuild = True do_grub = False rebuild_packages = {} for line in out.splitlines(): if re.match(ignore_re, line): continue match = re.match(pretend_re, line) if not match: log.warning('No match on line: {}'.format(line)) continue status = match.group(1) name = match.group(2) new = match.group(3) old = match.group(4) if not old: continue old = old.strip(' []') sens = conf.getint('packages', name, fallback=default_version_sens) common = sau.helpers.version_diff(new, old) if sens <= common: log.info('{} -- {} -> {} configured level {} <= pkg level {}'.format(name, old, new, sens, common)) else: log.error('{} -- {} -> {} configured level {} > pkg level {}'.format(name, old, new, sens, common)) do_rebuild = False nameversion = f'{name}-{new}' eclasses = get_eclasses(nameversion) rebuild_packages[name] = eclasses for package,eclasses in rebuild_packages.items(): if is_system_package(package, eclasses): if do_system_upgrade: do_grub = True else: raise sau.errors.UpgradeError(f"System package {package} has an update, but system upgrade is disabled") if not do_rebuild: raise sau.errors.UpgradeError('Some packages require manual attention, did not upgrade') if not rebuild_packages: log.info('No packages to upgrade') return False ## Actual upgrade cmd = [ EMERGE_PATH, '--color', 'n', '-uDNq', '--with-bdeps=y', '@world' ] ret, out, err = sau.helpers.exec_cmd(cmd, timeout=72000) if ret != 0 or err: log.error('emerge returned {}'.format(ret)) for line in out.splitlines(): log.error('stdout: {}'.format(line)) for line in err.splitlines(): log.error('stderr: {}'.format(line)) raise sau.errors.UpgradeError(f'Error during upgrade') else: log.info('upgrade complete') for line in out.splitlines(): if line.startswith(' * '): log.warning(line) ## rebuild as needed do_rebuild = conf.getboolean('default', 'do_rebuilds', fallback=True) if do_rebuild: # from here on we shouldn't need to rebuild the upgraded packages again exclude_list = ' --exclude '.join(rebuild_packages.keys()).split() # Rebuild go go_packages = [] cmd = None for package,eclasses in rebuild_packages.items(): if 'go-module' in eclasses or package == 'dev-lang/go': go_packages.append(package) if 'dev-lang/go' in go_packages: log.info("Running golang-rebuild due to update of dev-lang/go") cmd = [ EMERGE_PATH, '--color', 'n', '-q', '--usepkg', 'n', '@golang-rebuild', '--exclude' ] + exclude_list elif go_packages: dependencies = [] for package in go_packages: dependencies.extend(get_dependencies(package)) dependencies = set(dependencies) upgraded = set(rebuild_packages.keys()) not_upgraded = dependencies-upgraded if not_upgraded: log.info(f'Rebuilding packages dependant of go modules {", ".join(go_packages)}') cmd = [ EMERGE_PATH, '--color', 'n', '-q', '--usepkg', 'n'] + not_upgraded if cmd: ret, out, err = sau.helpers.exec_cmd(cmd, timeout=72000) if ret != 0 or err: log.error('Rebuild of go packages returned {}'.format(ret)) for line in out.splitlines(): log.error('stdout: {}'.format(line)) for line in err.splitlines(): log.error('stderr: {}'.format(line)) raise sau.errors.UpgradeError(f'Error during go rebuild') else: log.info('go rebuild complete') for line in out.splitlines(): if line.startswith(' * '): log.warning(line) # rebuild rust if any([x in rebuild_packages for x in ('dev-lang/rust', 'dev-lang/rust-bin')]): log.info("Running rust-rebuild due to update of rust") cmd = [ EMERGE_PATH, '--color', 'n', '-q', '--usepkg', 'n', '@rust-rebuild', '--exclude' ] + exclude_list ret, out, err = sau.helpers.exec_cmd(cmd, timeout=72000) if ret != 0 or err: log.error('Rebuild of rust packages returned {}'.format(ret)) for line in out.splitlines(): log.error('stdout: {}'.format(line)) for line in err.splitlines(): log.error('stderr: {}'.format(line)) raise sau.errors.UpgradeError(f'Error during rust rebuild') else: log.info('rust rebuild complete') for line in out.splitlines(): if line.startswith(' * '): log.warning(line) # run perl-cleaner if 'dev-lang/perl' in rebuild_packages: log.info("Running perl-cleaner due to perl upgrade") cmd = [ PCLEAN_PATH, '--all', '--', '-q', '--usepkg', 'n'] ret, out, err = sau.helpers.exec_cmd(cmd, timeout=72000) if ret != 0 or err: log.error('perl-cleaner failed with code {}'.format(ret)) for line in out.splitlines(): log.error('stdout: {}'.format(line)) for line in err.splitlines(): log.error('stderr: {}'.format(line)) raise sau.errors.UpgradeError(f'Error during perl-cleaner') else: log.info('perl-cleaner complete') for line in out.splitlines(): if line.startswith(' * '): log.warning(line) # rebuild live packages cmd = [ EMERGE_PATH, '--color', 'n', '-q', '--usepkg', 'n', '@live-rebuild' ] ret, out, err = sau.helpers.exec_cmd(cmd, timeout=3600) if ret != 0 or err: log.error('live-rebuild returned {}'.format(ret)) for line in out.splitlines(): log.error('stdout: {}'.format(line)) for line in err.splitlines(): log.error('stderr: {}'.format(line)) raise sau.errors.UpgradeError(f'Error during live-rebuild') else: log.info('live-rebuild complete') for line in out.splitlines(): if line.startswith(' * '): log.warning(line) ## Depclean if conf.getboolean('default', 'do_depclean', fallback=False): cmd = [ EMERGE_PATH, '--color', 'n', '-q', '--depclean' ] ret, out, err = sau.helpers.exec_cmd(cmd, timeout=3600) if ret != 0 or err: log.error('depclean returned {}'.format(ret)) for line in out.splitlines(): log.error('stdout: {}'.format(line)) for line in err.splitlines(): log.error('stderr: {}'.format(line)) raise sau.errors.UpgradeError(f'Error during depclean') else: log.info('depclean complete') for line in out.splitlines(): if line.startswith(' * '): log.warning(line) ## Preserved rebuild cmd = [ EMERGE_PATH, '--color', 'n', '--usepkg', 'n', '-q', '@preserved-rebuild' ] ret, out, err = sau.helpers.exec_cmd(cmd, timeout=72000) if ret != 0 or err: log.error('preserved-rebuild returned {}'.format(ret)) for line in out.splitlines(): log.error('stdout: {}'.format(line)) for line in err.splitlines(): log.error('stderr: {}'.format(line)) raise sau.errors.UpgradeError(f'Error during preserved-rebuild') else: log.info('preserved-rebuild complete') for line in out.splitlines(): if line.startswith(' * '): log.warning(line) # doing grub reconfig and clean old kernels if do_grub and os.path.exists(GRUB_MKCONFIG): keep_kernels = conf.getint('default', 'keep_kernels', fallback=4) if keep_kernels < 1: log.error('keep_kernels cannot be less than one; falling back to default') keep_kernels = 4 for root, dirs, files in os.walk('/boot'): for sysfile in ['config', 'initramfs', 'System.map', 'vmlinuz', 'kernel']: match = sorted( [f for f in files if f.startswith(f'{sysfile}-')], reverse=True, key=lambda x: tuple(map(int, x.split('-')[1].split('.')[:3]))) for f in match[keep_kernels:]: log.debug(f"Removing old kernel file {f}") os.remove(os.path.join(root, f)) break cmd = [ GRUB_MKCONFIG, '-o', '/boot/grub/grub.cfg' ] ret, out, err = sau.helpers.exec_cmd(cmd) if ret != 0: log.error(f"grub-mkconfig returned {ret}:") for line in out.splitlines(): log.error('stdout: {}'.format(line)) for line in err.splitlines(): log.error('stderr: {}'.format(line)) raise sau.errors.UpgradeError(f'Failed to reconfiugre grub') else: log.info("grub reconfigured") return True