From 0f0cbce6c93b97e312cafead937b46e6b2ceaf51 Mon Sep 17 00:00:00 2001 From: wang-guangge Date: Thu, 9 Nov 2023 10:46:33 +0800 Subject: [PATCH] support kabi check --- ceres/manages/vulnerability_manage.py | 2 +- hotpatch/hotupgrade.py | 97 +++++- hotpatch/updateinfo_parse.py | 3 + hotpatch/upgrade_en.py | 413 ++++++++++++++++++++++++++ 4 files changed, 506 insertions(+), 9 deletions(-) create mode 100644 hotpatch/upgrade_en.py diff --git a/ceres/manages/vulnerability_manage.py b/ceres/manages/vulnerability_manage.py index c41a7fa..bad2dee 100644 --- a/ceres/manages/vulnerability_manage.py +++ b/ceres/manages/vulnerability_manage.py @@ -620,7 +620,7 @@ class VulnerabilityManage: Tuple[str, str] a tuple containing two elements (update result, log). """ - code, stdout, stderr = execute_shell_command(f"dnf update {rpm_name} -y") + code, stdout, stderr = execute_shell_command(f"dnf upgrade-en {rpm_name} -y") if code != CommandExitCode.SUCCEED: return TaskExecuteRes.FAIL, stderr if "Complete" not in stdout: diff --git a/hotpatch/hotupgrade.py b/hotpatch/hotupgrade.py index f61e37f..c508e07 100644 --- a/hotpatch/hotupgrade.py +++ b/hotpatch/hotupgrade.py @@ -12,16 +12,14 @@ # ******************************************************************************/ from __future__ import print_function -from time import sleep import dnf.base import dnf.exceptions import hawkey +from time import sleep from dnf.cli import commands from dnf.cli.option_parser import OptionParser - -# from dnf.cli.output import Output from dnfpluginscore import _, logger - +from .upgrade_en import UpgradeEnhanceCommand from .hot_updateinfo import HotUpdateinfoCommand from .updateinfo_parse import HotpatchUpdateInfo from .syscare import Syscare @@ -37,6 +35,9 @@ class HotupgradeCommand(dnf.cli.Command): usage = "" syscare = Syscare() hp_list = [] + is_need_accept_kernel_hp = False + is_kernel_coldpatch_installed = False + kernel_coldpatch = '' @staticmethod def set_argparser(parser): @@ -50,6 +51,13 @@ class HotupgradeCommand(dnf.cli.Command): parser.add_argument( "--takeover", default=False, action='store_true', help=_('kernel cold patch takeover operation') ) + parser.add_argument( + "-f", + dest='force', + default=False, + action='store_true', + help=_('force retain kernel rpm package if kernel kabi check fails'), + ) def configure(self): """Verify that conditions are met so that this command can run. @@ -104,17 +112,72 @@ class HotupgradeCommand(dnf.cli.Command): def run_transaction(self) -> None: """ - apply hot patches + apply hot patches, and process kabi check for kernel package rpm. Returns: None """ # syscare need a little bit time to process the installed hot patch sleep(0.5) + + is_all_kernel_hp_actived = True + # hotpatch that fail to be activated will be automatically uninstalled + target_remove_hp = [] + acceptable_hp = [] for hp in self.hp_list: - self._apply_hp(hp) - if self.opts.takeover and self.is_need_accept_kernel_hp: + status = self._apply_hp(hp) + if status: + target_remove_hp.append(hp) + if not hp.startswith('patch-kernel-'): + continue + if status: + is_all_kernel_hp_actived &= False + else: + is_all_kernel_hp_actived &= True + acceptable_hp.append(hp) + + for ts_item in self.base.transaction: + if ts_item.action not in dnf.transaction.FORWARD_ACTIONS: + continue + if str(ts_item.pkg) == self.kernel_coldpatch: + self.is_kernel_coldpatch_installed = True + + self.keep_hp_operation_atomic(is_all_kernel_hp_actived, target_remove_hp) + + if self.is_need_accept_kernel_hp and acceptable_hp: + logger.info(_('No available kernel cold patch for takeover, gonna accept available kernel hot patch.')) + for hp in acceptable_hp: self._accept_kernel_hp(hp) + def keep_hp_operation_atomic(self, is_all_kernel_hp_actived, target_remove_hp): + """ + Keep hotpatch related operation atomic. Once one kernel hotpatch is not successfully activated or + kabi check fails, uninstall the kernel coldpatch. And unsuccessfully activated hotpatch package + will be removed. + + Args: + is_all_kernel_hp_actived(bool): are all kernel related hotpatches activated + target_remove_hp(list): target remove hotpatch list + """ + upgrade_en = UpgradeEnhanceCommand(self.cli) + + if self.is_kernel_coldpatch_installed: + if not is_all_kernel_hp_actived: + logger.info(_('Gonna remove %s due to some kernel hotpatch activation failed.'), self.kernel_coldpatch) + upgrade_en.remove_rpm(str(self.kernel_coldpatch)) + self.is_need_accept_kernel_hp = False + # process kabi check + elif not upgrade_en.kabi_check(str(self.kernel_coldpatch)) and not self.opts.force: + logger.info(_('Gonna remove %s due to Kabi check failed.'), self.kernel_coldpatch) + # rebuild rpm database for processing kernel rpm remove operation + upgrade_en.rebuild_rpm_db() + upgrade_en.remove_rpm(str(self.kernel_coldpatch)) + self.is_need_accept_kernel_hp = True + + if target_remove_hp: + logger.info(_('Gonna remove unsuccessfully activated hotpatch rpm.')) + for hotpatch in target_remove_hp: + upgrade_en.remove_rpm(hotpatch) + def _apply_hp(self, hp_full_name): pkg_info = self._parse_hp_name(hp_full_name) hp_subname = self._get_hp_subname_for_syscare(pkg_info) @@ -123,6 +186,7 @@ class HotupgradeCommand(dnf.cli.Command): logger.info(_('Apply hot patch failed: %s.'), hp_subname) else: logger.info(_('Apply hot patch succeed: %s.'), hp_subname) + return status @staticmethod def _get_hp_subname_for_syscare(pkg_info: dict) -> str: @@ -394,9 +458,11 @@ class HotupgradeCommand(dnf.cli.Command): """ process takeover operation. """ + if not self.get_kernel_hp_list(): + return kernel_coldpatch = self.get_target_installed_kernel_coldpatch_of_hotpatch() - self.is_need_accept_kernel_hp = False if kernel_coldpatch: + self.kernel_coldpatch = kernel_coldpatch logger.info(_("Gonna takeover kernel cold patch: ['%s']" % kernel_coldpatch)) success = self._install_rpm_pkg([kernel_coldpatch]) if success: @@ -412,6 +478,21 @@ class HotupgradeCommand(dnf.cli.Command): ) return + def get_kernel_hp_list(self) -> list: + """ + Get kernel hp list from self.hp_list. + + Returns: + list: kernel hp list + e.g. + ['patch-kernel-5.10.0-153.12.0.92.oe2203sp2-ACC-1-1.x86_64'] + """ + kernel_hp_list = [] + for hp in self.hp_list: + if hp.startswith('patch-kernel-'): + kernel_hp_list.append(hp) + return kernel_hp_list + def get_target_installed_kernel_coldpatch_of_hotpatch(self) -> str: """ get the highest kernel cold patch of hot patch in "dnf hot-updateinfo list cves", if the corresponding diff --git a/hotpatch/updateinfo_parse.py b/hotpatch/updateinfo_parse.py index 4760378..fc39d48 100644 --- a/hotpatch/updateinfo_parse.py +++ b/hotpatch/updateinfo_parse.py @@ -322,6 +322,9 @@ class HotpatchUpdateInfo(object): cmd = ["uname", "-r"] kernel_version = '' kernel_version, return_code = cmd_output(cmd) + # 'uname -r' show the kernel version-release.arch of the current system + # [root@openEuler hotpatch]# uname -r + # 5.10.0-136.12.0.86.oe2203sp1.x86_64 if return_code != SUCCEED: return kernel_version kernel_version = kernel_version.split('\n')[0] diff --git a/hotpatch/upgrade_en.py b/hotpatch/upgrade_en.py new file mode 100644 index 0000000..266bcae --- /dev/null +++ b/hotpatch/upgrade_en.py @@ -0,0 +1,413 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +import dnf +import gzip +import subprocess +from dnfpluginscore import _ +from dnf.cli import commands +from dnf.cli.commands.upgrade import UpgradeCommand +from dnf.cli.option_parser import OptionParser + +SUCCEED = 0 +FAIL = 255 + + +def cmd_output(cmd): + try: + result = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + result.wait() + return result.stdout.read().decode('utf-8'), result.returncode + except Exception as e: + print("error: ", e) + return str(e), FAIL + + +@dnf.plugin.register_command +class UpgradeEnhanceCommand(dnf.cli.Command): + SYMVERS_FILE = "/boot/symvers-%s.gz" + + aliases = ['upgrade-en'] + summary = _( + 'upgrade with KABI(Kernel Application Binary Interface) check. If the loaded kernel modules \ + have KABI compatibility with the new version kernel rpm, the kernel modules can be installed \ + and used in the new version kernel without recompling.' + ) + + @staticmethod + def set_argparser(parser): + parser.add_argument( + 'packages', + nargs='*', + help=_('Package to upgrade'), + action=OptionParser.ParseSpecGroupFileCallback, + metavar=_('PACKAGE'), + ) + parser.add_argument( + "-f", + dest='force', + default=False, + action='store_true', + help=_('force retain kernel rpm package if kernel kabi check fails'), + ) + + def configure(self): + """Verify that conditions are met so that this command can run. + + These include that there are enabled repositories with gpg + keys, and that this command is being run by the root user. + """ + demands = self.cli.demands + demands.sack_activation = True + demands.available_repos = True + demands.resolving = True + demands.root_user = True + commands._checkGPGKey(self.base, self.cli) + if not self.opts.filenames: + commands._checkEnabledRepo(self.base) + self.upgrade_minimal = None + self.all_security = None + self.skipped_grp_specs = None + + def run(self): + self.upgrade() + + def run_transaction(self): + """ + Process kabi check for kernel rpm package installed this time. If the kernel rpm pakcgae fails kabi check, + uninstall it. + """ + for ts_item in self.base.transaction: + if ts_item.action not in dnf.transaction.FORWARD_ACTIONS: + continue + if ts_item.pkg.name == 'kernel': + kernel_pkg = str(ts_item.pkg) + success = self.kabi_check(kernel_pkg) + if not success and not self.opts.force: + print('Gonna remove %s due to kabi check failed.' % kernel_pkg) + # rebuild rpm database for processing kernel rpm remove operation + self.rebuild_rpm_db() + self.remove_rpm(kernel_pkg) + + def remove_rpm(self, pkg: str): + """ + Remove rpm package via command line. + + Args: + pkg(str): package name + e.g. + kernel-5.10.0-153.18.0.94.oe2203sp2.x86_64 + """ + remove_cmd = ["dnf", "remove", pkg, "-y"] + output, return_code = cmd_output(remove_cmd) + if return_code != SUCCEED: + print('Remove package failed: %s.' % pkg) + exit(1) + else: + print('Remove package succeed: %s.' % pkg) + # do not achieve the expected result of installing related kernel rpm + exit(1) + + def rebuild_rpm_db(self): + """ + Rebuild rpm database for processing kernel rpm remove operation. + """ + rebuilddb_cmd = ["rpm", "--rebuilddb"] + output, return_code = cmd_output(rebuilddb_cmd) + if return_code != SUCCEED: + print('Rebuild rpm database failed.') + else: + print('Rebuild rpm database succeed.') + + def kabi_check(self, pkg: str) -> bool: + """ + Process kabi check after upgrading kernel rpm. + + Args: + pkg(str): package name + e.g. + kernel-5.10.0-153.18.0.94.oe2203sp2.x86_64 + + Returns: + bool: kabi check result + """ + print("Kabi check for %s:" % pkg) + # version-release.arch + evra = pkg.split("-", 1)[1] + symvers_file = self.SYMVERS_FILE % (evra) + + target_symvers_symbol_crc_mapping, return_code = self.get_target_symvers_symbol_crc_mapping(symvers_file) + if return_code != SUCCEED: + print('[Fail] Cannot find the symvers file of %s.', pkg) + return False + module_actual_symbol_crc_mapping = self.get_module_actual_symbol_crc_mapping() + + module_different_symbol_crc_mapping = self.compare_actual_and_target_symvers_symbol_crc_mapping( + module_actual_symbol_crc_mapping, target_symvers_symbol_crc_mapping + ) + + sum_module_num = len(module_actual_symbol_crc_mapping) + fail_module_num = len(module_different_symbol_crc_mapping) + pass_module_num = sum_module_num - fail_module_num + + reminder_statement = "Here are %s loaded kernel modules in this system, %s pass, %s fail." % ( + sum_module_num, + pass_module_num, + fail_module_num, + ) + + if fail_module_num > 0: + print('[Fail] %s' % reminder_statement) + self.output_symbol_crc_difference_report(module_different_symbol_crc_mapping) + return False + + print('[Success] %s' % reminder_statement) + return True + + def output_symbol_crc_difference_report(self, module_different_symbol_crc_mapping: dict): + """ + Format the output for symbol crc difference report. + The output is as follows: + + Failed modules are as follows: + No. Module Difference + 1 upatch ipv6_chk_custom_prefix : 0x0c994af2 != 0x0c994af3 + pcmcia_reset_card : 0xe9bed965 != null + 2 crct10dif_pclmul crypto_unregister_shash: 0x60f5b0b7 != 0x0c994af3 + __fentry__ : 0xbdfb6dbb != null + """ + print('Failed modules are as follows:') + + title = ['No.', 'Module', 'Difference'] + # column width + sequence_width = len(title[0]) + module_width = len(title[1]) + symbol_width = crc_info_width = 0 + + for seq, module_name in enumerate(module_different_symbol_crc_mapping): + # the sequence starts from 1 + seq = seq + 1 + sequence_width = max(sequence_width, len(str(seq))) + different_symbol_crc_mapping = module_different_symbol_crc_mapping[module_name] + module_width = max(module_width, len(module_name)) + for symbol, crc_list in different_symbol_crc_mapping.items(): + symbol_width = max(symbol_width, len(symbol)) + crc_info = "%s != %s" % (crc_list[0], crc_list[1]) + crc_info_width = max(crc_info_width, len(crc_info)) + + # print title + print('%-*s %-*s %s' % (sequence_width, title[0], module_width, title[1], title[2])) + + for seq, module_name in enumerate(module_different_symbol_crc_mapping): + seq = seq + 1 + print('%-*s %-*s' % (sequence_width, seq, module_width, module_name), end='') + different_symbol_crc_mapping = module_different_symbol_crc_mapping[module_name] + is_first_symbol = True + for symbol, crc_list in different_symbol_crc_mapping.items(): + crc_info = "%s != %s" % (crc_list[0], crc_list[1]) + if is_first_symbol: + print(' %-*s: %s' % (symbol_width, symbol, crc_info), end='') + is_first_symbol = False + else: + print( + ' %-*s %-*s: %s' % (sequence_width + module_width, "", symbol_width, symbol, crc_info), end='' + ) + print('') + + def compare_actual_and_target_symvers_symbol_crc_mapping( + self, module_actual_symbol_crc_mapping: dict, target_symvers_symbol_crc_mapping: dict + ) -> dict: + """ + Compare the actual symbol crc mapping with the target symvers symbol crc mapping. + + Args: + module_actual_symbol_crc_mapping(dict): module actual symbol crc mapping + e.g. + { + 'upatch': { + 'ipv6_chk_custom_prefix': '0x0c994af3', + 'pcmcia_reset_card': '0xe9bed965', + } + } + + target_symvers_symbol_crc_mapping(dict): target symvers symbol crc mapping + e.g. + { + 'ipv6_chk_custom_prefix': '0x0c994af2', + 'pcmcia_reset_card': '0xe9bed965', + } + + Returns: + dict: module different symbol crc mapping + e.g. + { + 'upatch': { + 'ipv6_chk_custom_prefix': ['0x0c994af3', '0x0c994af2']. + } + } + """ + module_different_symbol_crc_mapping = dict() + for module_name, actual_symbol_crc_mapping in module_actual_symbol_crc_mapping.items(): + different_symbol_crc_mapping = dict() + for actual_symbol, actual_crc in actual_symbol_crc_mapping.items(): + if actual_symbol not in target_symvers_symbol_crc_mapping: + continue + elif target_symvers_symbol_crc_mapping[actual_symbol] != actual_symbol_crc_mapping[actual_symbol]: + different_symbol_crc_mapping[actual_symbol] = [ + actual_crc, + target_symvers_symbol_crc_mapping[actual_symbol], + ] + if not different_symbol_crc_mapping: + continue + module_different_symbol_crc_mapping[module_name] = different_symbol_crc_mapping + return module_different_symbol_crc_mapping + + def get_module_actual_symbol_crc_mapping(self) -> dict: + """ + Get the module actual symbol crc mapping of the driver modules currently being loaded in the system. + + Returns: + dict: module actual symbol crc mapping + e.g. + { + 'upatch': { + 'ipv6_chk_custom_prefix': '0x0c994af3', + 'pcmcia_reset_card': '0xe9bed965', + } + } + """ + module_actual_symbol_crc_mapping = dict() + lsmod_cmd = ["lsmod"] + # 'lsmod' shows all modules loaded in the system + # e.g. + # [root@openEuler ~]# lsmod + # Module Size Used by + # upatch 53248 0 + # nft_fib_inet 16384 1 + # nft_fib_ipv4 16384 1 nft_fib_inet + list_output, return_code = cmd_output(lsmod_cmd) + if return_code != SUCCEED: + return module_actual_symbol_crc_mapping + + content = list_output.split('\n') + for line in content[1:]: + if not line: + continue + module_name = line.split()[0] + modinfo_cmd = ['modinfo', module_name, '-n'] + # 'modinfo module_name -n' shows module path information + # e.g. + # [root@openEuler ~]# modinfo upatch -n + # /lib/modules/5.10.0-153.12.0.92.oe2203sp2.x86_64/weak-updates/syscare/upatch.ko + module_path_output, return_code = cmd_output(modinfo_cmd) + if return_code != SUCCEED: + continue + + module_path = module_path_output.split('\n')[0] + actual_symbol_crc_mapping, return_code = self.get_actual_symbol_crc_mapping(module_path) + if return_code != SUCCEED: + continue + + module_actual_symbol_crc_mapping[module_name] = actual_symbol_crc_mapping + return module_actual_symbol_crc_mapping + + def get_actual_symbol_crc_mapping(self, module_path: str) -> (dict, int): + """ + Get actual symbol crc mapping for specific module. + + Args: + module_path(str): loaded module path + + Returns: + dict, bool: actual symbol crc mapping, return code + """ + actual_symbol_crc_mapping = dict() + modprobe_cmd = ['modprobe', '--dump', module_path] + # 'modprobe --dump module_path' shows module related kabi information + # e.g. + # [root@openEuler ~]# modprobe --dump \ + # /lib/modules/5.10.0-153.12.0.92.oe2203sp2.x86_64/weak-updates/syscare/upatch.ko + # 0xe32130cf module_layout + # 0x9c4befaf kmalloc_caches + # 0xeb233a45 __kmalloc + # 0xd6ee688f vmalloc + # 0x349cba85 strchr + # 0x754d539c strlen + crc_symbol_output_lines, return_code = cmd_output(modprobe_cmd) + if return_code != SUCCEED: + return actual_symbol_crc_mapping, return_code + + crc_symbol_output = crc_symbol_output_lines.split('\n') + for crc_symbol_line in crc_symbol_output: + if not crc_symbol_line: + continue + crc_symbol_line = crc_symbol_line.split() + crc, symbol = crc_symbol_line[0], crc_symbol_line[1] + actual_symbol_crc_mapping[symbol] = crc + return actual_symbol_crc_mapping, return_code + + def get_target_symvers_symbol_crc_mapping(self, symvers_file: str) -> (dict, int): + """ + Get target symbol crc mapping from symvers file of kernel rpm package. The symvers file content is + as follows(e.g.): + + 0x0c994af3 ipv6_chk_custom_prefix vmlinux EXPORT_SYMBOL + 0xe9bed965 pcmcia_reset_card vmlinux EXPORT_SYMBOL + 0x55417264 unregister_vt_notifier vmlinux EXPORT_SYMBOL_GPL + 0x8c8905c0 set_anon_super vmlinux EXPORT_SYMBOL + 0x3ba051a9 __cleancache_invalidate_page vmlinux EXPORT_SYMBOL + + the first column is crc(Cyclic Redundancy Check), and the second column is symbol. + + Args: + symvers_file(str): symvers file path + + Returns: + dict, int: target symvers symbol crc mapping, return_code + e.g. + { + 'ipv6_chk_custom_prefix': '0x0c994af3', + 'pcmcia_reset_card': '0xe9bed965', + }, + SUCCEED + """ + symvers_symbol_crc_mapping = dict() + try: + content = gzip.open(symvers_file, 'rb') + except FileNotFoundError as e: + print("error: ", e) + return symvers_symbol_crc_mapping, FAIL + + for line in content.readlines(): + line = line.decode() + line = line.split() + crc, symbol = line[0], line[1] + symvers_symbol_crc_mapping[symbol] = crc + content.close() + return symvers_symbol_crc_mapping, SUCCEED + + def upgrade(self): + """ + Use UpgradeCommand to process the upgrade operation. + """ + upgrade = UpgradeCommand(self.cli) + upgrade.upgrade_minimal = self.upgrade_minimal + upgrade.opts = self.opts + upgrade.opts.filenames = self.opts.filenames + upgrade.opts.pkg_specs = self.opts.pkg_specs + upgrade.opts.grp_specs = self.opts.grp_specs + + upgrade.upgrade_minimal = None + upgrade.all_security = None + upgrade.skipped_grp_specs = None + + upgrade.run() -- 2.27.0