X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=blobdiff_plain;f=meta-starlingx%2Fmeta-stx-flock%2Fstx-update%2Ffiles%2F0006-Migrate-patch-agent-to-use-DNF-for-swmgmt.patch;fp=meta-starlingx%2Fmeta-stx-flock%2Fstx-update%2Ffiles%2F0006-Migrate-patch-agent-to-use-DNF-for-swmgmt.patch;h=0000000000000000000000000000000000000000;hb=6fc6934434f70595536a387ece31bc30141cafb5;hp=467f37ef9191c3ba64adb7c3b4e31af4f9db0bfe;hpb=eb1e26510491ba49de693ab3b0498edcb06be6c5;p=pti%2Frtp.git diff --git a/meta-starlingx/meta-stx-flock/stx-update/files/0006-Migrate-patch-agent-to-use-DNF-for-swmgmt.patch b/meta-starlingx/meta-stx-flock/stx-update/files/0006-Migrate-patch-agent-to-use-DNF-for-swmgmt.patch deleted file mode 100644 index 467f37e..0000000 --- a/meta-starlingx/meta-stx-flock/stx-update/files/0006-Migrate-patch-agent-to-use-DNF-for-swmgmt.patch +++ /dev/null @@ -1,841 +0,0 @@ -diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py -index 3abd891..d8bc375 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 = ['/usr/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] -- -- # 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)) -+ output[pkg.name] = "%s-%s@%s" % (pkg.version, pkg.release, pkg.arch) - -- if rc > 0: -- # Update version -- pkglist[pkgname][arch] = version -+ return output - -- return pkglist -+ def dnf_reset_client(self): -+ if self.dnfb is not None: -+ self.dnfb.close() -+ self.dnfb = None - -- @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] -- -- def parse_smart_newer(self, output): -- # Skip the first two lines, which are headers -- for line in output.splitlines()[2:]: -- if line == '': -- continue -- -- fields = line.split() -- pkgname = fields[0] -- installedver = fields[2] -- newver = fields[5] -+ self.dnfb = dnf.Base() -+ self.dnfb.conf.substitutions['infra'] = 'stock' - -- self.installed[pkgname] = installedver -- self.to_install[pkgname] = newver -- -- def parse_smart_orphans(self, output): -- for pkgname in output.splitlines(): -- if pkgname == '': -- continue -+ # Reset default installonlypkgs list -+ self.dnfb.conf.installonlypkgs = [] - -- highest_version = None -+ self.dnfb.read_all_repos() - -- 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 = [] - -- # Get the repo data -- pkgs_installed = {} -- pkgs_base = {} -- pkgs_updates = {} -- -- 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 -+ self.dnf_reset_client() - -- 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 -+ # Get the repo data -+ pkgs_installed = dnf.sack._rpmdb_sack(self.dnfb).query().installed() # pylint: disable=protected-access -+ avail = self.dnfb.sack.query().available().latest() - -- # 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) -- -- if updates_version is None and base_version is None: -- # Remove it -- self.to_remove.append(pkg) -- self.changes = True -- continue -+ highest = avail.filter(name=pkg.name, arch=pkg.arch) -+ if highest: -+ highest_pkg = highest[0] - -- 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. -+ if pkg.evr_eq(highest_pkg): - 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 e9017f2..2ee9fce 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 bd1eef9..7e30fc5 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 57a9829..f511718 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 3f4e581..56e4806 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 --