From 44cdc55fd8151f19af556522c74095348519a95c Mon Sep 17 00:00:00 2001 From: Don Penney Date: Thu, 2 Jan 2020 17:36:21 -0500 Subject: [PATCH] Migrate patch-agent to use DNF for swmgmt As the smart package manager is not supported under python3, we're migrating the patch-agent to use the python2 DNF libraries in preparation for CentOS 8. This impacts how the patch-agent queries the repos and manages installed software, but is done without changing how the patch-agent and patch-controller exchange information, to ensure we don't impact cross-version communication in an upgrade scenario. Depends-On: https://review.opendev.org/700960 Change-Id: I00729a487c24ba5c182a9a2a48e2024be9260978 Story: 2006227 Task: 37932 Signed-off-by: Don Penney --- cgcs-patch/centos/cgcs-patch.spec | 17 +- .../cgcs-patch/cgcs_patch/patch_agent.py | 623 ++++++------------ .../cgcs-patch/cgcs_patch/patch_functions.py | 7 +- .../cgcs_patch/tests/test_patch_agent.py | 9 + cgcs-patch/cgcs-patch/pylint.rc | 5 +- cgcs-patch/cgcs-patch/test-requirements.txt | 1 - 6 files changed, 241 insertions(+), 421 deletions(-) diff --git a/cgcs-patch/centos/cgcs-patch.spec b/cgcs-patch/centos/cgcs-patch.spec index f8344478..4ed3f997 100644 --- a/cgcs-patch/centos/cgcs-patch.spec +++ b/cgcs-patch/centos/cgcs-patch.spec @@ -1,4 +1,4 @@ -Summary: TIS Platform Patching +Summary: StarlingX Platform Patching Name: cgcs-patch Version: 1.0 Release: %{tis_patch_ver}%{?_tis_dist} @@ -16,11 +16,12 @@ BuildRequires: systemd-units BuildRequires: systemd-devel Requires: python-devel Requires: python-crypto -Requires: python-smartpm +Requires: dnf +Requires: python-dnf Requires: /bin/bash %description -TIS Platform Patching +StarlingX Platform Patching %define pythonroot /usr/lib64/python2.7/site-packages @@ -110,10 +111,10 @@ install -m 644 dist/*.whl $RPM_BUILD_ROOT/wheels/ %{buildroot}%{_sbindir}/upgrade-start-pkg-extract %clean -rm -rf $RPM_BUILD_ROOT +rm -rf $RPM_BUILD_ROOT %package -n cgcs-patch-controller -Summary: TIS Platform Patching +Summary: StarlingX Platform Patching Group: base Requires: /usr/bin/env Requires: /bin/sh @@ -123,7 +124,7 @@ Requires(post): /usr/bin/env Requires(post): /bin/sh %description -n cgcs-patch-controller -TIS Platform Patching +StarlingX Platform Patching %post -n cgcs-patch-controller /usr/bin/systemctl enable sw-patch-controller.service @@ -131,7 +132,7 @@ TIS Platform Patching %package -n cgcs-patch-agent -Summary: TIS Platform Patching +Summary: StarlingX Platform Patching Group: base Requires: /usr/bin/env Requires: /bin/sh @@ -139,7 +140,7 @@ Requires(post): /usr/bin/env Requires(post): /bin/sh %description -n cgcs-patch-agent -TIS Platform Patching +StarlingX Platform Patching %post -n cgcs-patch-agent /usr/bin/systemctl enable sw-patch-agent.service diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py index 3abd8917..e4cceb8c 100644 --- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py +++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py @@ -5,22 +5,26 @@ SPDX-License-Identifier: Apache-2.0 """ -import os -import time -import socket +import dnf +import dnf.callback +import dnf.comps +import dnf.exceptions +import dnf.rpm +import dnf.sack +import dnf.transaction import json -import select -import subprocess +import libdnf.transaction +import os import random import requests -import xml.etree.ElementTree as ElementTree -import rpm -import sys -import yaml +import select import shutil +import socket +import subprocess +import sys +import time from cgcs_patch.patch_functions import configure_logging -from cgcs_patch.patch_functions import parse_pkgver from cgcs_patch.patch_functions import LOG import cgcs_patch.config as cfg from cgcs_patch.base import PatchService @@ -50,19 +54,13 @@ pa = None http_port_real = http_port -# Smart commands -smart_cmd = ["/usr/bin/smart"] -smart_quiet = smart_cmd + ["--quiet"] -smart_update = smart_quiet + ["update"] -smart_newer = smart_quiet + ["newer"] -smart_orphans = smart_quiet + ["query", "--orphans", "--show-format", "$name\n"] -smart_query = smart_quiet + ["query"] -smart_query_repos = smart_quiet + ["query", "--channel=base", "--channel=updates"] -smart_install_cmd = smart_cmd + ["install", "--yes", "--explain"] -smart_remove_cmd = smart_cmd + ["remove", "--yes", "--explain"] -smart_query_installed = smart_quiet + ["query", "--installed", "--show-format", "$name $version\n"] -smart_query_base = smart_quiet + ["query", "--channel=base", "--show-format", "$name $version\n"] -smart_query_updates = smart_quiet + ["query", "--channel=updates", "--show-format", "$name $version\n"] +# DNF commands +dnf_cmd = ['/bin/dnf'] +dnf_quiet = dnf_cmd + ['--quiet'] +dnf_makecache = dnf_quiet + ['makecache', + '--disablerepo="*"', + '--enablerepo', 'platform-base', + '--enablerepo', 'platform-updates'] def setflag(fname): @@ -123,10 +121,6 @@ class PatchMessageHelloAgent(messages.PatchMessage): def handle(self, sock, addr): # Send response - # Run the smart config audit - global pa - pa.timed_audit_smart_config() - # # If a user tries to do a host-install on an unlocked node, # without bypassing the lock check (either via in-service @@ -289,6 +283,46 @@ class PatchMessageAgentInstallResp(messages.PatchMessage): resp.send(sock) +class PatchAgentDnfTransLogCB(dnf.callback.TransactionProgress): + def __init__(self): + dnf.callback.TransactionProgress.__init__(self) + + self.log_prefix = 'dnf trans' + + def progress(self, package, action, ti_done, ti_total, ts_done, ts_total): + if action in dnf.transaction.ACTIONS: + action_str = dnf.transaction.ACTIONS[action] + elif action == dnf.transaction.TRANS_POST: + action_str = 'Post transaction' + else: + action_str = 'unknown(%d)' % action + + if ti_done is not None: + # To reduce the volume of logs, only log 0% and 100% + if ti_done == 0 or ti_done == ti_total: + LOG.info('%s PROGRESS %s: %s %0.1f%% [%s/%s]', + self.log_prefix, action_str, package, + (ti_done * 100 / ti_total), + ts_done, ts_total) + else: + LOG.info('%s PROGRESS %s: %s [%s/%s]', + self.log_prefix, action_str, package, ts_done, ts_total) + + def filelog(self, package, action): + if action in dnf.transaction.FILE_ACTIONS: + msg = '%s: %s' % (dnf.transaction.FILE_ACTIONS[action], package) + else: + msg = '%s: %s' % (package, action) + LOG.info('%s FILELOG %s', self.log_prefix, msg) + + def scriptout(self, msgs): + if msgs: + LOG.info("%s SCRIPTOUT :\n%s", self.log_prefix, msgs) + + def error(self, message): + LOG.error("%s ERROR: %s", self.log_prefix, message) + + class PatchAgent(PatchService): def __init__(self): PatchService.__init__(self) @@ -298,9 +332,14 @@ class PatchAgent(PatchService): self.listener = None self.changes = False self.installed = {} + self.installed_dnf = [] self.to_install = {} + self.to_install_dnf = [] + self.to_downgrade_dnf = [] self.to_remove = [] + self.to_remove_dnf = [] self.missing_pkgs = [] + self.missing_pkgs_dnf = [] self.patch_op_counter = 0 self.node_is_patched = os.path.exists(node_is_patched_file) self.node_is_patched_timestamp = 0 @@ -308,6 +347,7 @@ class PatchAgent(PatchService): self.state = constants.PATCH_AGENT_STATE_IDLE self.last_config_audit = 0 self.rejection_timestamp = 0 + self.dnfb = None # Check state flags if os.path.exists(patch_installing_file): @@ -343,289 +383,40 @@ class PatchAgent(PatchService): self.listener.bind(('', self.port)) self.listener.listen(2) # Allow two connections, for two controllers - def audit_smart_config(self): - LOG.info("Auditing smart configuration") - - # Get the current channel config - try: - output = subprocess.check_output(smart_cmd + - ["channel", "--yaml"], - stderr=subprocess.STDOUT) - config = yaml.load(output) - except subprocess.CalledProcessError as e: - LOG.exception("Failed to query channels") - LOG.error("Command output: %s", e.output) - return False - except Exception: - LOG.exception("Failed to query channels") - return False - - expected = [{'channel': 'rpmdb', - 'type': 'rpm-sys', - 'name': 'RPM Database', - 'baseurl': None}, - {'channel': 'base', - 'type': 'rpm-md', - 'name': 'Base', - 'baseurl': "http://controller:%s/feed/rel-%s" % (http_port_real, SW_VERSION)}, - {'channel': 'updates', - 'type': 'rpm-md', - 'name': 'Patches', - 'baseurl': "http://controller:%s/updates/rel-%s" % (http_port_real, SW_VERSION)}] - - updated = False - - for item in expected: - channel = item['channel'] - ch_type = item['type'] - ch_name = item['name'] - ch_baseurl = item['baseurl'] - - add_channel = False - - if channel in config: - # Verify existing channel config - if (config[channel].get('type') != ch_type or - config[channel].get('name') != ch_name or - config[channel].get('baseurl') != ch_baseurl): - # Config is invalid - add_channel = True - LOG.warning("Invalid smart config found for %s", channel) - try: - output = subprocess.check_output(smart_cmd + - ["channel", "--yes", - "--remove", channel], - stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - LOG.exception("Failed to configure %s channel", channel) - LOG.error("Command output: %s", e.output) - return False - else: - # Channel is missing - add_channel = True - LOG.warning("Channel %s is missing from config", channel) - - if add_channel: - LOG.info("Adding channel %s", channel) - cmd_args = ["channel", "--yes", "--add", channel, - "type=%s" % ch_type, - "name=%s" % ch_name] - if ch_baseurl is not None: - cmd_args += ["baseurl=%s" % ch_baseurl] - - try: - output = subprocess.check_output(smart_cmd + cmd_args, - stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - LOG.exception("Failed to configure %s channel", channel) - LOG.error("Command output: %s", e.output) - return False - - updated = True - - # Validate the smart config - try: - output = subprocess.check_output(smart_cmd + - ["config", "--yaml"], - stderr=subprocess.STDOUT) - config = yaml.load(output) - except subprocess.CalledProcessError as e: - LOG.exception("Failed to query smart config") - LOG.error("Command output: %s", e.output) - return False - except Exception: - LOG.exception("Failed to query smart config") - return False - - # Check for the rpm-nolinktos flag - nolinktos = 'rpm-nolinktos' - if config.get(nolinktos) is not True: - # Set the flag - LOG.warning("Setting %s option", nolinktos) - try: - output = subprocess.check_output(smart_cmd + - ["config", "--set", - "%s=true" % nolinktos], - stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - LOG.exception("Failed to configure %s option", nolinktos) - LOG.error("Command output: %s", e.output) - return False - - updated = True - - # Check for the rpm-check-signatures flag - nosignature = 'rpm-check-signatures' - if config.get(nosignature) is not False: - # Set the flag - LOG.warning("Setting %s option", nosignature) - try: - output = subprocess.check_output(smart_cmd + - ["config", "--set", - "%s=false" % nosignature], - stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - LOG.exception("Failed to configure %s option", nosignature) - LOG.error("Command output: %s", e.output) - return False - - updated = True - - if updated: - try: - subprocess.check_output(smart_update, stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - LOG.exception("Failed to update smartpm") - LOG.error("Command output: %s", e.output) - return False - - # Reset the patch op counter to force a detailed query - self.patch_op_counter = 0 - - self.last_config_audit = time.time() - return True - - def timed_audit_smart_config(self): - rc = True - if (time.time() - self.last_config_audit) > 1800: - # It's been 30 minutes since the last completed audit - LOG.info("Kicking timed audit") - rc = self.audit_smart_config() - - return rc - @staticmethod - def parse_smart_pkglist(output): - pkglist = {} - for line in output.splitlines(): - if line == '': - continue - - fields = line.split() - pkgname = fields[0] - (version, arch) = fields[1].split('@') - - if pkgname not in pkglist: - pkglist[pkgname] = {} - pkglist[pkgname][arch] = version - elif arch not in pkglist[pkgname]: - pkglist[pkgname][arch] = version + def pkgobjs_to_list(pkgobjs): + # Transform pkgobj list to format used by patch-controller + output = {} + for pkg in pkgobjs: + if pkg.epoch != 0: + output[pkg.name] = "%s:%s-%s@%s" % (pkg.epoch, pkg.version, pkg.release, pkg.arch) else: - stored_ver = pkglist[pkgname][arch] + output[pkg.name] = "%s-%s@%s" % (pkg.version, pkg.release, pkg.arch) - # The rpm.labelCompare takes version broken into 3 components - # It returns: - # 1, if first arg is higher version - # 0, if versions are same - # -1, if first arg is lower version - rc = rpm.labelCompare(parse_pkgver(version), - parse_pkgver(stored_ver)) + return output - if rc > 0: - # Update version - pkglist[pkgname][arch] = version + def dnf_reset_client(self): + if self.dnfb is not None: + self.dnfb.close() + self.dnfb = None - return pkglist + self.dnfb = dnf.Base() + self.dnfb.conf.substitutions['infra'] = 'stock' - @staticmethod - def get_pkg_version(pkglist, pkg, arch): - if pkg not in pkglist: - return None - if arch not in pkglist[pkg]: - return None - return pkglist[pkg][arch] + # Reset default installonlypkgs list + self.dnfb.conf.installonlypkgs = [] - def parse_smart_newer(self, output): - # Skip the first two lines, which are headers - for line in output.splitlines()[2:]: - if line == '': - continue + self.dnfb.read_all_repos() - fields = line.split() - pkgname = fields[0] - installedver = fields[2] - newver = fields[5] - - self.installed[pkgname] = installedver - self.to_install[pkgname] = newver - - def parse_smart_orphans(self, output): - for pkgname in output.splitlines(): - if pkgname == '': - continue - - highest_version = None - - try: - query = subprocess.check_output(smart_query_repos + ["--show-format", '$version\n', pkgname]) - # The last non-blank version is the highest - for version in query.splitlines(): - if version == '': - continue - highest_version = version.split('@')[0] - - except subprocess.CalledProcessError: - # Package is not in the repo - highest_version = None - - if highest_version is None: - # Package is to be removed - self.to_remove.append(pkgname) + # Ensure only platform repos are enabled for transaction + for repo in self.dnfb.repos.all(): + if repo.id == 'platform-base' or repo.id == 'platform-updates': + repo.enable() else: - # Rollback to the highest version - self.to_install[pkgname] = highest_version + repo.disable() - # Get the installed version - try: - query = subprocess.check_output(smart_query + ["--installed", "--show-format", '$version\n', pkgname]) - for version in query.splitlines(): - if version == '': - continue - self.installed[pkgname] = version.split('@')[0] - break - except subprocess.CalledProcessError: - LOG.error("Failed to query installed version of %s", pkgname) - - self.changes = True - - def check_groups(self): - # Get the groups file - mygroup = "updates-%s" % "-".join(subfunctions) - self.missing_pkgs = [] - installed_pkgs = [] - - groups_url = "http://controller:%s/updates/rel-%s/comps.xml" % (http_port_real, SW_VERSION) - try: - req = requests.get(groups_url) - if req.status_code != 200: - LOG.error("Failed to get groups list from server") - return False - except requests.ConnectionError: - LOG.error("Failed to connect to server") - return False - - # Get list of installed packages - try: - query = subprocess.check_output(["rpm", "-qa", "--queryformat", "%{NAME}\n"]) - installed_pkgs = query.split() - except subprocess.CalledProcessError: - LOG.exception("Failed to query RPMs") - return False - - root = ElementTree.fromstring(req.text) - for child in root: - group_id = child.find('id') - if group_id is None or group_id.text != mygroup: - continue - - pkglist = child.find('packagelist') - if pkglist is None: - continue - - for pkg in pkglist.findall('packagereq'): - if pkg.text not in installed_pkgs and pkg.text not in self.missing_pkgs: - self.missing_pkgs.append(pkg.text) - self.changes = True + # Read repo info + self.dnfb.fill_sack() def query(self): """ Check current patch state """ @@ -633,14 +424,15 @@ class PatchAgent(PatchService): LOG.info("Failed install_uuid check. Skipping query") return False - if not self.audit_smart_config(): - # Set a state to "unknown"? - return False + if self.dnfb is not None: + self.dnfb.close() + self.dnfb = None + # TODO(dpenney): Use python APIs for makecache try: - subprocess.check_output(smart_update, stderr=subprocess.STDOUT) + subprocess.check_output(dnf_makecache, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: - LOG.error("Failed to update smartpm") + LOG.error("Failed to run dnf makecache") LOG.error("Command output: %s", e.output) # Set a state to "unknown"? return False @@ -649,78 +441,72 @@ class PatchAgent(PatchService): self.query_id = random.random() self.changes = False + self.installed_dnf = [] self.installed = {} - self.to_install = {} + self.to_install_dnf = [] + self.to_downgrade_dnf = [] self.to_remove = [] + self.to_remove_dnf = [] self.missing_pkgs = [] + self.missing_pkgs_dnf = [] + + self.dnf_reset_client() # Get the repo data - pkgs_installed = {} - pkgs_base = {} - pkgs_updates = {} + pkgs_installed = dnf.sack._rpmdb_sack(self.dnfb).query().installed() # pylint: disable=protected-access + avail = self.dnfb.sack.query().available().latest() - try: - output = subprocess.check_output(smart_query_installed) - pkgs_installed = self.parse_smart_pkglist(output) - except subprocess.CalledProcessError as e: - LOG.error("Failed to query installed pkgs: %s", e.output) - # Set a state to "unknown"? - return False - - try: - output = subprocess.check_output(smart_query_base) - pkgs_base = self.parse_smart_pkglist(output) - except subprocess.CalledProcessError as e: - LOG.error("Failed to query base pkgs: %s", e.output) - # Set a state to "unknown"? - return False - - try: - output = subprocess.check_output(smart_query_updates) - pkgs_updates = self.parse_smart_pkglist(output) - except subprocess.CalledProcessError as e: - LOG.error("Failed to query patched pkgs: %s", e.output) - # Set a state to "unknown"? - return False - - # There are four possible actions: - # 1. If installed pkg is not in base or updates, remove it. - # 2. If installed pkg version is higher than highest in base - # or updates, downgrade it. - # 3. If installed pkg version is lower than highest in updates, - # upgrade it. - # 4. If pkg in grouplist is not in installed, install it. + # There are three possible actions: + # 1. If installed pkg is not in a repo, remove it. + # 2. If installed pkg version does not match newest repo version, update it. + # 3. If a package in the grouplist is not installed, install it. for pkg in pkgs_installed: - for arch in pkgs_installed[pkg]: - installed_version = pkgs_installed[pkg][arch] - updates_version = self.get_pkg_version(pkgs_updates, pkg, arch) - base_version = self.get_pkg_version(pkgs_base, pkg, arch) + highest = avail.filter(name=pkg.name, arch=pkg.arch) + if highest: + highest_pkg = highest[0] - if updates_version is None and base_version is None: - # Remove it - self.to_remove.append(pkg) - self.changes = True + if pkg.evr_eq(highest_pkg): continue - compare_version = updates_version - if compare_version is None: - compare_version = base_version - - # Compare the installed version to what's in the repo - rc = rpm.labelCompare(parse_pkgver(installed_version), - parse_pkgver(compare_version)) - if rc == 0: - # Versions match, nothing to do. - continue + if pkg.evr_gt(highest_pkg): + self.to_downgrade_dnf.append(highest_pkg) else: - # Install the version from the repo - self.to_install[pkg] = "@".join([compare_version, arch]) - self.installed[pkg] = "@".join([installed_version, arch]) - self.changes = True + self.to_install_dnf.append(highest_pkg) + else: + self.to_remove_dnf.append(pkg) + self.to_remove.append(pkg.name) + + self.installed_dnf.append(pkg) + self.changes = True # Look for new packages - self.check_groups() + self.dnfb.read_comps() + grp_id = 'updates-%s' % '-'.join(subfunctions) + pkggrp = None + for grp in self.dnfb.comps.groups_iter(): + if grp.id == grp_id: + pkggrp = grp + break + + if pkggrp is None: + LOG.error("Could not find software group: %s", grp_id) + + for pkg in pkggrp.packages_iter(): + try: + res = pkgs_installed.filter(name=pkg.name) + if len(res) == 0: + found_pkg = avail.filter(name=pkg.name) + self.missing_pkgs_dnf.append(found_pkg[0]) + self.missing_pkgs.append(found_pkg[0].name) + self.changes = True + except dnf.exceptions.PackageNotFoundError: + self.missing_pkgs_dnf.append(pkg) + self.missing_pkgs.append(pkg.name) + self.changes = True + + self.installed = self.pkgobjs_to_list(self.installed_dnf) + self.to_install = self.pkgobjs_to_list(self.to_install_dnf + self.to_downgrade_dnf) LOG.info("Patch state query returns %s", self.changes) LOG.info("Installed: %s", self.installed) @@ -730,6 +516,35 @@ class PatchAgent(PatchService): return True + def resolve_dnf_transaction(self, undo_failure=True): + LOG.info("Starting to process transaction: undo_failure=%s", undo_failure) + self.dnfb.resolve() + self.dnfb.download_packages(self.dnfb.transaction.install_set) + + tid = self.dnfb.do_transaction(display=PatchAgentDnfTransLogCB()) + + transaction_rc = True + for t in self.dnfb.transaction: + if t.state != libdnf.transaction.TransactionItemState_DONE: + transaction_rc = False + break + + self.dnf_reset_client() + + if not transaction_rc: + if undo_failure: + LOG.error("Failure occurred... Undoing last transaction (%s)", tid) + old = self.dnfb.history.old((tid,))[0] + mobj = dnf.db.history.MergedTransactionWrapper(old) + + self.dnfb._history_undo_operations(mobj, old.tid, True) # pylint: disable=protected-access + + if not self.resolve_dnf_transaction(undo_failure=False): + LOG.error("Failed to undo transaction") + + LOG.info("Transaction complete: undo_failure=%s, success=%s", undo_failure, transaction_rc) + return transaction_rc + def handle_install(self, verbose_to_stdout=False, disallow_insvc_patch=False): # # The disallow_insvc_patch parameter is set when we're installing @@ -781,64 +596,54 @@ class PatchAgent(PatchService): if verbose_to_stdout: print("Checking for software updates...") self.query() - install_set = [] - for pkg, version in self.to_install.items(): - install_set.append("%s-%s" % (pkg, version)) - - install_set += self.missing_pkgs changed = False rc = True - if len(install_set) > 0: + if len(self.to_install_dnf) > 0 or len(self.to_downgrade_dnf) > 0: + LOG.info("Adding pkgs to installation set: %s", self.to_install) + for pkg in self.to_install_dnf: + self.dnfb.package_install(pkg) + + for pkg in self.to_downgrade_dnf: + self.dnfb.package_downgrade(pkg) + + changed = True + + if len(self.missing_pkgs_dnf) > 0: + LOG.info("Adding missing pkgs to installation set: %s", self.missing_pkgs) + for pkg in self.missing_pkgs_dnf: + self.dnfb.package_install(pkg) + changed = True + + if len(self.to_remove_dnf) > 0: + LOG.info("Adding pkgs to be removed: %s", self.to_remove) + for pkg in self.to_remove_dnf: + self.dnfb.package_remove(pkg) + changed = True + + if changed: + # Run the transaction set + transaction_rc = False try: - if verbose_to_stdout: - print("Installing software updates...") - LOG.info("Installing: %s", ", ".join(install_set)) - output = subprocess.check_output(smart_install_cmd + install_set, stderr=subprocess.STDOUT) - changed = True - for line in output.split('\n'): - LOG.info("INSTALL: %s", line) - if verbose_to_stdout: - print("Software updated.") - except subprocess.CalledProcessError as e: - LOG.exception("Failed to install RPMs") - LOG.error("Command output: %s", e.output) + transaction_rc = self.resolve_dnf_transaction() + except dnf.exceptions.DepsolveError: + LOG.error("Failures resolving dependencies in transaction") + except dnf.exceptions.DownloadError: + LOG.error("Failures downloading in transaction") + + if not transaction_rc: + LOG.error("Failures occurred during transaction") rc = False if verbose_to_stdout: print("WARNING: Software update failed.") + else: if verbose_to_stdout: print("Nothing to install.") LOG.info("Nothing to install") - if rc: - self.query() - remove_set = self.to_remove - - if len(remove_set) > 0: - try: - if verbose_to_stdout: - print("Handling patch removal...") - LOG.info("Removing: %s", ", ".join(remove_set)) - output = subprocess.check_output(smart_remove_cmd + remove_set, stderr=subprocess.STDOUT) - changed = True - for line in output.split('\n'): - LOG.info("REMOVE: %s", line) - if verbose_to_stdout: - print("Patch removal complete.") - except subprocess.CalledProcessError as e: - LOG.exception("Failed to remove RPMs") - LOG.error("Command output: %s", e.output) - rc = False - if verbose_to_stdout: - print("WARNING: Patch removal failed.") - else: - if verbose_to_stdout: - print("Nothing to remove.") - LOG.info("Nothing to remove") - - if changed: + if changed and rc: # Update the node_is_patched flag setflag(node_is_patched_file) @@ -1057,7 +862,7 @@ class PatchAgent(PatchService): def main(): global pa - configure_logging() + configure_logging(dnf_log=True) cfg.read_config() diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py index 5866e8b7..d76eba59 100644 --- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py +++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py @@ -69,7 +69,7 @@ def handle_exception(exc_type, exc_value, exc_traceback): sys.__excepthook__(exc_type, exc_value, exc_traceback) -def configure_logging(logtofile=True, level=logging.INFO): +def configure_logging(logtofile=True, level=logging.INFO, dnf_log=False): if logtofile: my_exec = os.path.basename(sys.argv[0]) @@ -84,6 +84,11 @@ def configure_logging(logtofile=True, level=logging.INFO): main_log_handler = logging.FileHandler(logfile) main_log_handler.setFormatter(formatter) LOG.addHandler(main_log_handler) + + if dnf_log: + dnf_logger = logging.getLogger('dnf') + dnf_logger.addHandler(main_log_handler) + try: os.chmod(logfile, 0o640) except Exception: diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_agent.py b/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_agent.py index bd1eef9b..7e30fc50 100644 --- a/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_agent.py +++ b/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_agent.py @@ -10,6 +10,15 @@ import sys import testtools sys.modules['rpm'] = mock.Mock() +sys.modules['dnf'] = mock.Mock() +sys.modules['dnf.callback'] = mock.Mock() +sys.modules['dnf.comps'] = mock.Mock() +sys.modules['dnf.exceptions'] = mock.Mock() +sys.modules['dnf.rpm'] = mock.Mock() +sys.modules['dnf.sack'] = mock.Mock() +sys.modules['dnf.transaction'] = mock.Mock() +sys.modules['libdnf'] = mock.Mock() +sys.modules['libdnf.transaction'] = mock.Mock() import cgcs_patch.patch_agent # noqa: E402 diff --git a/cgcs-patch/cgcs-patch/pylint.rc b/cgcs-patch/cgcs-patch/pylint.rc index 57a9829d..f5117184 100644 --- a/cgcs-patch/cgcs-patch/pylint.rc +++ b/cgcs-patch/cgcs-patch/pylint.rc @@ -45,10 +45,11 @@ symbols=no # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" # W0107 unnecessary-pass +# W0511 fixme # W0603 global-statement # W0703 broad-except # W1505, deprecated-method -disable=C, R, W0107, W0603, W0703, W1505 +disable=C, R, W0107, W0511, W0603, W0703, W1505 [REPORTS] @@ -235,7 +236,7 @@ ignore-mixin-members=yes # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis -ignored-modules= +ignored-modules=dnf,libdnf # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). diff --git a/cgcs-patch/cgcs-patch/test-requirements.txt b/cgcs-patch/cgcs-patch/test-requirements.txt index dadd6c56..85bf419f 100644 --- a/cgcs-patch/cgcs-patch/test-requirements.txt +++ b/cgcs-patch/cgcs-patch/test-requirements.txt @@ -8,4 +8,3 @@ coverage!=4.4,>=4.0 # Apache-2.0 mock>=2.0.0 # BSD stestr>=1.0.0 # Apache-2.0 testtools>=2.2.0 # MIT -