+++ /dev/null
-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
--