add dnf hotupgrade plugin

This commit is contained in:
zhu-yuncheng 2023-03-25 15:30:56 +08:00
parent dbf2328c65
commit 3119815903
2 changed files with 411 additions and 6 deletions

View File

@ -0,0 +1,399 @@
From 20c09ffb55a630a4259b7c87cd88da2ce6f28a07 Mon Sep 17 00:00:00 2001
From: zhu-yuncheng <zhuyuncheng@huawei.com>
Date: Sat, 25 Mar 2023 14:43:20 +0800
Subject: [PATCH] add dnf hot upgrade plugin
---
hotpatch/hot-upgrade.py | 275 ++++++++++++++++++++++++++++++++++++++++
hotpatch/syscare.py | 96 ++++++++++++++
2 files changed, 371 insertions(+)
create mode 100644 hotpatch/hot-upgrade.py
create mode 100644 hotpatch/syscare.py
diff --git a/hotpatch/hot-upgrade.py b/hotpatch/hot-upgrade.py
new file mode 100644
index 0000000..7a4c3c6
--- /dev/null
+++ b/hotpatch/hot-upgrade.py
@@ -0,0 +1,275 @@
+# supplies the dnf 'diff' command.
+#
+# Copyright (C) 2018 Red Hat, Inc.
+# Written by Pavel Raiskup <praiskup@redhat.com>.
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details. You should have received a copy of the
+# GNU General Public License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
+# source code or documentation are not subject to the GNU General Public
+# License and may only be used or replicated with the express permission of
+# Red Hat, Inc.
+
+from __future__ import print_function
+
+import dnf.base
+import dnf.exceptions
+import hawkey
+from dnf.cli import commands
+from dnf.cli.option_parser import OptionParser
+from dnf.cli.output import Output
+from dnfpluginscore import _, logger
+
+from .syscare import Syscare
+from .hotpatch_updateinfo import HotpatchUpdateInfo
+
+
+@dnf.plugin.register_command
+class HotupgradeCommand(dnf.cli.Command):
+ aliases = ("hotupgrade",)
+ summary = "Hot upgrade package using hot patch."
+ usage = ""
+ syscare = Syscare()
+ hp_list = []
+
+ @staticmethod
+ def set_argparser(parser):
+ parser.add_argument('packages', nargs='*', help=_('Package to upgrade'),
+ action=OptionParser.ParseSpecGroupFileCallback,
+ metavar=_('PACKAGE'))
+
+ 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)
+
+ def run(self):
+ if self.opts.pkg_specs:
+ self.hp_list = self.opts.pkg_specs
+ elif self.opts.cves or self.opts.advisory:
+ cve_pkgs = self.get_hotpatch_based_on_cve(self.opts.cves)
+ advisory_pkgs = self.get_hotpatch_based_on_advisory(self.opts.advisory)
+ self.hp_list = cve_pkgs + advisory_pkgs
+ else:
+ raise dnf.exceptions.Error(_('No qualified rpm package name or cve/advisory id.'))
+
+ hp_target_map = self._get_available_hotpatches(self.hp_list)
+ if not hp_target_map:
+ raise dnf.exceptions.Error(_('No hot patches marked for install.'))
+
+ target_patch_map = self._get_applied_old_patch(list(hp_target_map.values()))
+ if target_patch_map:
+ self._remove_hot_patches(target_patch_map)
+ else:
+ self.syscare.save()
+ success = self._install_hot_patch(list(hp_target_map.keys()))
+ if not success:
+ output, status = self.syscare.restore()
+ if status:
+ raise dnf.exceptions.Error(_('Roll back failed.'))
+ raise dnf.exceptions.Error(_("Roll back succeed."))
+ return
+
+ def run_transaction(self) -> None:
+ """
+ apply hot patches
+ Returns:
+ None
+ """
+ logger.info(_('Applying hot patch'))
+ if not self.base.transaction:
+ for hp in self.hp_list:
+ self._apply_hp(hp)
+ return
+
+ for ts_item in self.base.transaction:
+ if ts_item.action not in dnf.transaction.FORWARD_ACTIONS:
+ continue
+ self._apply_hp(str(ts_item.pkg))
+
+ def _apply_hp(self, hp_full_name):
+ pkg_info = self._parse_hp_name(hp_full_name)
+ hp_full_name = "-".join([pkg_info["name"], pkg_info["version"], pkg_info["release"]]) \
+ + '/' + pkg_info["hp_name"]
+ output, status = self.syscare.apply(hp_full_name)
+ if status:
+ logger.info(_('Apply hot patch failed: %s.'), hp_full_name)
+ else:
+ logger.info(_('Apply hot patch succeed: %s.'), hp_full_name)
+
+ def _get_available_hotpatches(self, pkg_specs: list) -> dict:
+ """
+ check two conditions:
+ 1. the hot patch rpm package exists in repositories
+ 2. the hot patch's target package with specific version and release already installed
+ Args:
+ pkg_specs: full names of hot patches' rpm packages
+
+ Returns:
+ dict: key is available hot patches' full name, value is target package's name-version-release
+ """
+ hp_target_map = {}
+ installed_packages = self.base.sack.query().installed()
+ for pkg_spec in set(pkg_specs):
+ query = self.base.sack.query()
+ # check the package exist in repo or not
+ subj = dnf.subject.Subject(pkg_spec)
+ parsed_nevras = subj.get_nevra_possibilities(forms=[hawkey.FORM_NEVRA])
+ if len(parsed_nevras) != 1:
+ logger.info(_('Cannot parse NEVRA for package "{nevra}"').format(nevra=pkg_spec))
+ continue
+
+ parsed_nevra = parsed_nevras[0]
+ available_hp = query.available().filter(name=parsed_nevra.name, version=parsed_nevra.version,
+ release=parsed_nevra.release, arch=parsed_nevra.arch)
+ if not available_hp:
+ logger.info(_('No match for argument: %s'), self.base.output.term.bold(pkg_spec))
+ continue
+
+ # check the hot patch's target package installed or not
+ pkg_info = self._parse_hp_name(pkg_spec)
+ installed_pkg = installed_packages.filter(name=pkg_info["name"],
+ version=pkg_info["version"],
+ release=pkg_info["release"]).run()
+ if not installed_pkg:
+ logger.info(_("The hot patch's target package is not installed: %s"),
+ self.base.output.term.bold(pkg_spec))
+ continue
+
+ if len(installed_pkg) != 1:
+ logger.info(_("The hot patch '%s' has multiple target packages, please check."),
+ self.base.output.term.bold(pkg_spec))
+ continue
+ target = "-".join([pkg_info["name"], pkg_info["version"], pkg_info["release"]])
+ hp_target_map[pkg_spec] = target
+ return hp_target_map
+
+ def _get_applied_old_patch(self, targets: list):
+ """
+ get targets' applied hot patches
+ Args:
+ targets: target RPMs' name-version-release. e.g. redis-1.0-1
+
+ Returns:
+ dict: targets' applied hot patches. e.g. {'redis-1.0-1': 'redis-1.0-1/HP001'}
+ """
+ target_patch_map = {}
+ hps_info = Syscare.list()
+ for hp_info in hps_info:
+ target, hp_name = hp_info["Name"].split('/')
+ if target in targets and hp_info["Status"] != "NOT-APPLIED":
+ logger.info(_("The target package '%s' has a hotpatch '%s' applied"),
+ self.base.output.term.bold(target),
+ self.base.output.term.bold(hp_name))
+ target_patch_map[target] = hp_info["Name"]
+ return target_patch_map
+
+ def _remove_hot_patches(self, target_patch_map: dict) -> None:
+ output = Output(self.base, dnf.conf.Conf())
+ logger.info(_("Gonna remove these hot patches: %s"), list(target_patch_map.values()))
+ remove_flag = output.userconfirm()
+ if not remove_flag:
+ raise dnf.exceptions.Error(_('Operation aborted.'))
+
+ self.syscare.save()
+ for target, hp_name in target_patch_map.items():
+ logger.info(_("Remove hot patch %s."), hp_name)
+ output, status = self.syscare.remove(hp_name)
+ if status:
+ logger.info(_("Remove hot patch '%s' failed, roll back to original status."),
+ self.base.output.term.bold(hp_name))
+ output, status = self.syscare.restore()
+ if status:
+ raise dnf.exceptions.Error(_('Roll back failed.'))
+ raise dnf.exceptions.Error(_('Roll back succeed.'))
+
+ @staticmethod
+ def _parse_hp_name(hp_filename: str) -> dict:
+ """
+ parse hot patch's name, get target rpm's name, version, release and hp's name.
+ Args:
+ hp_filename: hot patch's name, in the format of
+ 'patch-{pkg_name}-{pkg_version}-{pkg_release}-{patchname}-{patch_version}-{patch_release}.rpm'
+ e.g. patch-kernel-5.10.0-60.66.0.91.oe2203-HP001-1-1.x86_64.rpm
+ pkg_name may have '-' in it, patch name cannot have '-'.
+ Returns:
+ dict: rpm info. {"name": "", "version": "", "release": "", "hp_name": ""}
+ """
+ splitted_hp_filename = hp_filename.split('-')
+ try:
+ rpm_info = {"release": splitted_hp_filename[-4], "version": splitted_hp_filename[-5],
+ "name": "-".join(splitted_hp_filename[1:-5]), "hp_name": splitted_hp_filename[-3]}
+ except IndexError as e:
+ raise dnf.exceptions.Error(_('Parse hot patch name failed. Please insert correct hot patch name.'))
+ return rpm_info
+
+ def _install_hot_patch(self, pkg_specs: list) -> bool:
+ """
+ install hot patches
+ Args:
+ pkg_specs: hot patches' full name
+
+ Returns:
+ bool
+ """
+ success = True
+ for pkg_spec in pkg_specs:
+ try:
+ self.base.install(pkg_spec)
+ except dnf.exceptions.MarkingError as e:
+ logger.info(_('No match for argument: %s.'),
+ self.base.output.term.bold(pkg_spec))
+ success = False
+ return success
+
+ def get_hotpatch_based_on_cve(self, cves: list) -> list:
+ """
+ Get the hot patches corresponding to CVEs
+ Args:
+ cves: cve id list
+
+ Returns:
+ list: list of hot patches full name. e.g.["tmp2-tss-3.1.0-3.oe2203sp1"]
+ """
+ updateinfo = HotpatchUpdateInfo(self.cli.base, self.cli)
+ hp_list = []
+ cve_hp_dict = updateinfo.get_hotpatches_from_cve(cves)
+ for cve, hp in cve_hp_dict.items():
+ if not hp:
+ logger.info(_("The cve doesn't exist: %s"), cve)
+ continue
+ hp_list += hp
+ return list(set(hp_list))
+
+ def get_hotpatch_based_on_advisory(self, advisories: list) -> list:
+ """
+ Get the hot patches corresponding to advisories
+ Args:
+ advisories: advisory id list
+
+ Returns:
+ list: list of hot patches full name. e.g.["tmp2-tss-3.1.0-3.oe2203sp1"]
+ """
+ updateinfo = HotpatchUpdateInfo(self.cli.base, self.cli)
+ hp_list = []
+ advisory_hp_dict = updateinfo.get_hotpatches_from_advisories(advisories)
+ for hp in advisory_hp_dict.values():
+ hp_list += hp
+ return list(set(hp_list))
diff --git a/hotpatch/syscare.py b/hotpatch/syscare.py
new file mode 100644
index 0000000..b24b07e
--- /dev/null
+++ b/hotpatch/syscare.py
@@ -0,0 +1,96 @@
+import subprocess
+from typing import List
+
+
+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
+
+class Syscare:
+ @classmethod
+ def list(cls, condition=None) -> List[dict]:
+ """
+ Target Name Status
+ redis-6.2.5-1.oe2203 CVE-2021-23675 ACTIVED
+ kernel-5.10.0-60.80.0.104.oe2203 modify-proc-version ACTIVED
+ """
+ cmd = ["syscare", "list"]
+ list_output, return_code = cmd_output(cmd)
+ if return_code != SUCCEED:
+ return []
+
+ content = list_output.split('\n')
+ if len(content) <= 2:
+ return []
+
+ header = content[0].split()
+ result = []
+ for item in content[1:-1]:
+ tmp = dict(zip(header, item.split()))
+ if not condition or cls.judge(tmp, condition):
+ result.append(tmp)
+ return result
+
+ @staticmethod
+ def judge(content: dict, condition: dict):
+ for key, value in condition.items():
+ if content.get(key) != value:
+ return False
+ return True
+
+ @staticmethod
+ def status(patch_name: str):
+ cmd = ["syscare", "status", patch_name]
+ output, return_code = cmd_output(cmd)
+
+ return output, return_code
+
+ @staticmethod
+ def active(patch_name: str):
+ cmd = ["syscare", "active", patch_name]
+ output, return_code = cmd_output(cmd)
+
+ return output, return_code
+
+ @staticmethod
+ def deactive(patch_name: str):
+ cmd = ["syscare", "deactive", patch_name]
+ output, return_code = cmd_output(cmd)
+
+ return output, return_code
+
+ @staticmethod
+ def remove(patch_name: str):
+ cmd = ["syscare", "remove", patch_name]
+ output, return_code = cmd_output(cmd)
+
+ return output, return_code
+
+ @staticmethod
+ def apply(patch_name: str):
+ cmd = ["syscare", "apply", patch_name]
+ output, return_code = cmd_output(cmd)
+
+ return output, return_code
+
+ @staticmethod
+ def save():
+ cmd = ["syscare", "save"]
+ output, return_code = cmd_output(cmd)
+
+ return output, return_code
+
+ @staticmethod
+ def restore():
+ cmd = ["syscare", "restore"]
+ output, return_code = cmd_output(cmd)
+
+ return output, return_code
\ No newline at end of file
--
2.30.0

View File

@ -1,12 +1,13 @@
Name: aops-apollo Name: aops-apollo
Version: v1.1.2 Version: v1.1.2
Release: 4 Release: 5
Summary: Cve management service, monitor machine vulnerabilities and provide fix functions. Summary: Cve management service, monitor machine vulnerabilities and provide fix functions.
License: MulanPSL2 License: MulanPSL2
URL: https://gitee.com/openeuler/%{name} URL: https://gitee.com/openeuler/%{name}
Source0: %{name}-%{version}.tar.gz Source0: %{name}-%{version}.tar.gz
Patch0001: 0001-fix-partial-succeed-bug.patch Patch0001: 0001-fix-partial-succeed-bug.patch
Patch0002: 0002-add-dnf-hot-patch-list-plugin.patch Patch0002: 0002-add-dnf-hot-patch-list-plugin.patch
Patch0003: 0003-add-dnf-hot-upgrade-plugin.patch
BuildRequires: python3-setuptools BuildRequires: python3-setuptools
Requires: aops-vulcanus >= v1.0.0 Requires: aops-vulcanus >= v1.0.0
@ -19,11 +20,11 @@ Provides: aops-apollo
%description %description
Cve management service, monitor machine vulnerabilities and provide fix functions. Cve management service, monitor machine vulnerabilities and provide fix functions.
%package -n aops-hotpatch-plugin %package -n dnf-hotpatch-plugin
Summary: aops hotpatch plugin Summary: dnf hotpatch plugin
Requires: python3-hawkey python3-dnf Requires: python3-hawkey python3-dnf
%description -n aops-hotpatch-plugin %description -n dnf-hotpatch-plugin
dnf hotpatch plugin, it's about hotpatch query and fix dnf hotpatch plugin, it's about hotpatch query and fix
%prep %prep
@ -37,7 +38,7 @@ dnf hotpatch plugin, it's about hotpatch query and fix
%py3_install %py3_install
#install for aops-dnf-plugin #install for aops-dnf-plugin
cp -r hotpatch %{buildroot}/%{python3_sitelib}/dnf-plugin/ cp -r hotpatch %{buildroot}/%{python3_sitelib}/dnf-plugins/
%files %files
@ -47,10 +48,15 @@ cp -r hotpatch %{buildroot}/%{python3_sitelib}/dnf-plugin/
%attr(0755,root,root) /usr/lib/systemd/system/aops-apollo.service %attr(0755,root,root) /usr/lib/systemd/system/aops-apollo.service
%{python3_sitelib}/aops_apollo*.egg-info %{python3_sitelib}/aops_apollo*.egg-info
%{python3_sitelib}/apollo/* %{python3_sitelib}/apollo/*
%{python3_sitelib}/dnf-plugin/*
%files -n dnf-hotpatch-plugin
%{python3_sitelib}/dnf-plugins/*
%changelog %changelog
* Sat Mar 54 2023 zhu-yuncheng<zhuyuncheng@huawei.com> - v1.1.2-5
- add dnf hot upgrade plugin
* Fri Mar 24 2023 wangguangge<wangguangge@huawei.com> - v1.1.2-4 * Fri Mar 24 2023 wangguangge<wangguangge@huawei.com> - v1.1.2-4
- add dnf hotpatch list plugin - add dnf hotpatch list plugin