1 From 1522e384f8a9cb5e7d3e42b37aec11e5674c4436 Mon Sep 17 00:00:00 2001
2 From: Don Penney <don.penney@windriver.com>
3 Date: Thu, 2 Jan 2020 17:36:21 -0500
4 Subject: [PATCH] Migrate patch-agent to use DNF for swmgmt
6 As the smart package manager is not supported under python3, we're
7 migrating the patch-agent to use the python2 DNF libraries in
8 preparation for CentOS 8. This impacts how the patch-agent queries the
9 repos and manages installed software, but is done without changing how
10 the patch-agent and patch-controller exchange information, to ensure
11 we don't impact cross-version communication in an upgrade scenario.
13 Depends-On: https://review.opendev.org/700960
14 Change-Id: I00729a487c24ba5c182a9a2a48e2024be9260978
17 Signed-off-by: Don Penney <don.penney@windriver.com>
20 cgcs-patch/centos/cgcs-patch.spec | 17 +-
21 cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py | 625 +++++++--------------
22 .../cgcs-patch/cgcs_patch/patch_functions.py | 7 +-
23 .../cgcs_patch/tests/test_patch_agent.py | 9 +
24 cgcs-patch/cgcs-patch/pylint.rc | 5 +-
25 cgcs-patch/cgcs-patch/test-requirements.txt | 1 -
26 6 files changed, 242 insertions(+), 422 deletions(-)
28 diff --git a/cgcs-patch/centos/cgcs-patch.spec b/cgcs-patch/centos/cgcs-patch.spec
29 index f834447..4ed3f99 100644
30 --- a/cgcs-patch/centos/cgcs-patch.spec
31 +++ b/cgcs-patch/centos/cgcs-patch.spec
33 -Summary: TIS Platform Patching
34 +Summary: StarlingX Platform Patching
37 Release: %{tis_patch_ver}%{?_tis_dist}
38 @@ -16,11 +16,12 @@ BuildRequires: systemd-units
39 BuildRequires: systemd-devel
40 Requires: python-devel
41 Requires: python-crypto
42 -Requires: python-smartpm
48 -TIS Platform Patching
49 +StarlingX Platform Patching
51 %define pythonroot /usr/lib64/python2.7/site-packages
53 @@ -110,10 +111,10 @@ install -m 644 dist/*.whl $RPM_BUILD_ROOT/wheels/
54 %{buildroot}%{_sbindir}/upgrade-start-pkg-extract
57 -rm -rf $RPM_BUILD_ROOT
58 +rm -rf $RPM_BUILD_ROOT
60 %package -n cgcs-patch-controller
61 -Summary: TIS Platform Patching
62 +Summary: StarlingX Platform Patching
64 Requires: /usr/bin/env
66 @@ -123,7 +124,7 @@ Requires(post): /usr/bin/env
67 Requires(post): /bin/sh
69 %description -n cgcs-patch-controller
70 -TIS Platform Patching
71 +StarlingX Platform Patching
73 %post -n cgcs-patch-controller
74 /usr/bin/systemctl enable sw-patch-controller.service
75 @@ -131,7 +132,7 @@ TIS Platform Patching
78 %package -n cgcs-patch-agent
79 -Summary: TIS Platform Patching
80 +Summary: StarlingX Platform Patching
82 Requires: /usr/bin/env
84 @@ -139,7 +140,7 @@ Requires(post): /usr/bin/env
85 Requires(post): /bin/sh
87 %description -n cgcs-patch-agent
88 -TIS Platform Patching
89 +StarlingX Platform Patching
91 %post -n cgcs-patch-agent
92 /usr/bin/systemctl enable sw-patch-agent.service
93 diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
94 index 3abd891..d8bc375 100644
95 --- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
96 +++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
97 @@ -5,22 +5,26 @@ SPDX-License-Identifier: Apache-2.0
107 +import dnf.exceptions
110 +import dnf.transaction
114 +import libdnf.transaction
118 -import xml.etree.ElementTree as ElementTree
129 from cgcs_patch.patch_functions import configure_logging
130 -from cgcs_patch.patch_functions import parse_pkgver
131 from cgcs_patch.patch_functions import LOG
132 import cgcs_patch.config as cfg
133 from cgcs_patch.base import PatchService
134 @@ -50,19 +54,13 @@ pa = None
136 http_port_real = http_port
139 -smart_cmd = ["/usr/bin/smart"]
140 -smart_quiet = smart_cmd + ["--quiet"]
141 -smart_update = smart_quiet + ["update"]
142 -smart_newer = smart_quiet + ["newer"]
143 -smart_orphans = smart_quiet + ["query", "--orphans", "--show-format", "$name\n"]
144 -smart_query = smart_quiet + ["query"]
145 -smart_query_repos = smart_quiet + ["query", "--channel=base", "--channel=updates"]
146 -smart_install_cmd = smart_cmd + ["install", "--yes", "--explain"]
147 -smart_remove_cmd = smart_cmd + ["remove", "--yes", "--explain"]
148 -smart_query_installed = smart_quiet + ["query", "--installed", "--show-format", "$name $version\n"]
149 -smart_query_base = smart_quiet + ["query", "--channel=base", "--show-format", "$name $version\n"]
150 -smart_query_updates = smart_quiet + ["query", "--channel=updates", "--show-format", "$name $version\n"]
152 +dnf_cmd = ['/usr/bin/dnf']
153 +dnf_quiet = dnf_cmd + ['--quiet']
154 +dnf_makecache = dnf_quiet + ['makecache',
155 + '--disablerepo="*"',
156 + '--enablerepo', 'platform-base',
157 + '--enablerepo', 'platform-updates']
161 @@ -123,10 +121,6 @@ class PatchMessageHelloAgent(messages.PatchMessage):
162 def handle(self, sock, addr):
165 - # Run the smart config audit
167 - pa.timed_audit_smart_config()
170 # If a user tries to do a host-install on an unlocked node,
171 # without bypassing the lock check (either via in-service
172 @@ -289,6 +283,46 @@ class PatchMessageAgentInstallResp(messages.PatchMessage):
176 +class PatchAgentDnfTransLogCB(dnf.callback.TransactionProgress):
177 + def __init__(self):
178 + dnf.callback.TransactionProgress.__init__(self)
180 + self.log_prefix = 'dnf trans'
182 + def progress(self, package, action, ti_done, ti_total, ts_done, ts_total):
183 + if action in dnf.transaction.ACTIONS:
184 + action_str = dnf.transaction.ACTIONS[action]
185 + elif action == dnf.transaction.TRANS_POST:
186 + action_str = 'Post transaction'
188 + action_str = 'unknown(%d)' % action
190 + if ti_done is not None:
191 + # To reduce the volume of logs, only log 0% and 100%
192 + if ti_done == 0 or ti_done == ti_total:
193 + LOG.info('%s PROGRESS %s: %s %0.1f%% [%s/%s]',
194 + self.log_prefix, action_str, package,
195 + (ti_done * 100 / ti_total),
198 + LOG.info('%s PROGRESS %s: %s [%s/%s]',
199 + self.log_prefix, action_str, package, ts_done, ts_total)
201 + def filelog(self, package, action):
202 + if action in dnf.transaction.FILE_ACTIONS:
203 + msg = '%s: %s' % (dnf.transaction.FILE_ACTIONS[action], package)
205 + msg = '%s: %s' % (package, action)
206 + LOG.info('%s FILELOG %s', self.log_prefix, msg)
208 + def scriptout(self, msgs):
210 + LOG.info("%s SCRIPTOUT :\n%s", self.log_prefix, msgs)
212 + def error(self, message):
213 + LOG.error("%s ERROR: %s", self.log_prefix, message)
216 class PatchAgent(PatchService):
218 PatchService.__init__(self)
219 @@ -298,9 +332,14 @@ class PatchAgent(PatchService):
223 + self.installed_dnf = []
225 + self.to_install_dnf = []
226 + self.to_downgrade_dnf = []
228 + self.to_remove_dnf = []
229 self.missing_pkgs = []
230 + self.missing_pkgs_dnf = []
231 self.patch_op_counter = 0
232 self.node_is_patched = os.path.exists(node_is_patched_file)
233 self.node_is_patched_timestamp = 0
234 @@ -308,6 +347,7 @@ class PatchAgent(PatchService):
235 self.state = constants.PATCH_AGENT_STATE_IDLE
236 self.last_config_audit = 0
237 self.rejection_timestamp = 0
241 if os.path.exists(patch_installing_file):
242 @@ -343,289 +383,40 @@ class PatchAgent(PatchService):
243 self.listener.bind(('', self.port))
244 self.listener.listen(2) # Allow two connections, for two controllers
246 - def audit_smart_config(self):
247 - LOG.info("Auditing smart configuration")
249 - # Get the current channel config
251 - output = subprocess.check_output(smart_cmd +
252 - ["channel", "--yaml"],
253 - stderr=subprocess.STDOUT)
254 - config = yaml.load(output)
255 - except subprocess.CalledProcessError as e:
256 - LOG.exception("Failed to query channels")
257 - LOG.error("Command output: %s", e.output)
260 - LOG.exception("Failed to query channels")
263 - expected = [{'channel': 'rpmdb',
265 - 'name': 'RPM Database',
267 - {'channel': 'base',
270 - 'baseurl': "http://controller:%s/feed/rel-%s" % (http_port_real, SW_VERSION)},
271 - {'channel': 'updates',
274 - 'baseurl': "http://controller:%s/updates/rel-%s" % (http_port_real, SW_VERSION)}]
278 - for item in expected:
279 - channel = item['channel']
280 - ch_type = item['type']
281 - ch_name = item['name']
282 - ch_baseurl = item['baseurl']
284 - add_channel = False
286 - if channel in config:
287 - # Verify existing channel config
288 - if (config[channel].get('type') != ch_type or
289 - config[channel].get('name') != ch_name or
290 - config[channel].get('baseurl') != ch_baseurl):
291 - # Config is invalid
293 - LOG.warning("Invalid smart config found for %s", channel)
295 - output = subprocess.check_output(smart_cmd +
296 - ["channel", "--yes",
297 - "--remove", channel],
298 - stderr=subprocess.STDOUT)
299 - except subprocess.CalledProcessError as e:
300 - LOG.exception("Failed to configure %s channel", channel)
301 - LOG.error("Command output: %s", e.output)
304 - # Channel is missing
306 - LOG.warning("Channel %s is missing from config", channel)
309 - LOG.info("Adding channel %s", channel)
310 - cmd_args = ["channel", "--yes", "--add", channel,
311 - "type=%s" % ch_type,
312 - "name=%s" % ch_name]
313 - if ch_baseurl is not None:
314 - cmd_args += ["baseurl=%s" % ch_baseurl]
317 - output = subprocess.check_output(smart_cmd + cmd_args,
318 - stderr=subprocess.STDOUT)
319 - except subprocess.CalledProcessError as e:
320 - LOG.exception("Failed to configure %s channel", channel)
321 - LOG.error("Command output: %s", e.output)
326 - # Validate the smart config
328 - output = subprocess.check_output(smart_cmd +
329 - ["config", "--yaml"],
330 - stderr=subprocess.STDOUT)
331 - config = yaml.load(output)
332 - except subprocess.CalledProcessError as e:
333 - LOG.exception("Failed to query smart config")
334 - LOG.error("Command output: %s", e.output)
337 - LOG.exception("Failed to query smart config")
340 - # Check for the rpm-nolinktos flag
341 - nolinktos = 'rpm-nolinktos'
342 - if config.get(nolinktos) is not True:
344 - LOG.warning("Setting %s option", nolinktos)
346 - output = subprocess.check_output(smart_cmd +
347 - ["config", "--set",
348 - "%s=true" % nolinktos],
349 - stderr=subprocess.STDOUT)
350 - except subprocess.CalledProcessError as e:
351 - LOG.exception("Failed to configure %s option", nolinktos)
352 - LOG.error("Command output: %s", e.output)
357 - # Check for the rpm-check-signatures flag
358 - nosignature = 'rpm-check-signatures'
359 - if config.get(nosignature) is not False:
361 - LOG.warning("Setting %s option", nosignature)
363 - output = subprocess.check_output(smart_cmd +
364 - ["config", "--set",
365 - "%s=false" % nosignature],
366 - stderr=subprocess.STDOUT)
367 - except subprocess.CalledProcessError as e:
368 - LOG.exception("Failed to configure %s option", nosignature)
369 - LOG.error("Command output: %s", e.output)
376 - subprocess.check_output(smart_update, stderr=subprocess.STDOUT)
377 - except subprocess.CalledProcessError as e:
378 - LOG.exception("Failed to update smartpm")
379 - LOG.error("Command output: %s", e.output)
382 - # Reset the patch op counter to force a detailed query
383 - self.patch_op_counter = 0
385 - self.last_config_audit = time.time()
388 - def timed_audit_smart_config(self):
390 - if (time.time() - self.last_config_audit) > 1800:
391 - # It's been 30 minutes since the last completed audit
392 - LOG.info("Kicking timed audit")
393 - rc = self.audit_smart_config()
398 - def parse_smart_pkglist(output):
400 - for line in output.splitlines():
404 - fields = line.split()
405 - pkgname = fields[0]
406 - (version, arch) = fields[1].split('@')
408 - if pkgname not in pkglist:
409 - pkglist[pkgname] = {}
410 - pkglist[pkgname][arch] = version
411 - elif arch not in pkglist[pkgname]:
412 - pkglist[pkgname][arch] = version
413 + def pkgobjs_to_list(pkgobjs):
414 + # Transform pkgobj list to format used by patch-controller
416 + for pkg in pkgobjs:
418 + output[pkg.name] = "%s:%s-%s@%s" % (pkg.epoch, pkg.version, pkg.release, pkg.arch)
420 - stored_ver = pkglist[pkgname][arch]
422 - # The rpm.labelCompare takes version broken into 3 components
424 - # 1, if first arg is higher version
425 - # 0, if versions are same
426 - # -1, if first arg is lower version
427 - rc = rpm.labelCompare(parse_pkgver(version),
428 - parse_pkgver(stored_ver))
429 + output[pkg.name] = "%s-%s@%s" % (pkg.version, pkg.release, pkg.arch)
433 - pkglist[pkgname][arch] = version
437 + def dnf_reset_client(self):
438 + if self.dnfb is not None:
443 - def get_pkg_version(pkglist, pkg, arch):
444 - if pkg not in pkglist:
446 - if arch not in pkglist[pkg]:
448 - return pkglist[pkg][arch]
450 - def parse_smart_newer(self, output):
451 - # Skip the first two lines, which are headers
452 - for line in output.splitlines()[2:]:
456 - fields = line.split()
457 - pkgname = fields[0]
458 - installedver = fields[2]
460 + self.dnfb = dnf.Base()
461 + self.dnfb.conf.substitutions['infra'] = 'stock'
463 - self.installed[pkgname] = installedver
464 - self.to_install[pkgname] = newver
466 - def parse_smart_orphans(self, output):
467 - for pkgname in output.splitlines():
470 + # Reset default installonlypkgs list
471 + self.dnfb.conf.installonlypkgs = []
473 - highest_version = None
474 + self.dnfb.read_all_repos()
477 - query = subprocess.check_output(smart_query_repos + ["--show-format", '$version\n', pkgname])
478 - # The last non-blank version is the highest
479 - for version in query.splitlines():
482 - highest_version = version.split('@')[0]
484 - except subprocess.CalledProcessError:
485 - # Package is not in the repo
486 - highest_version = None
488 - if highest_version is None:
489 - # Package is to be removed
490 - self.to_remove.append(pkgname)
491 + # Ensure only platform repos are enabled for transaction
492 + for repo in self.dnfb.repos.all():
493 + if repo.id == 'platform-base' or repo.id == 'platform-updates':
496 - # Rollback to the highest version
497 - self.to_install[pkgname] = highest_version
500 - # Get the installed version
502 - query = subprocess.check_output(smart_query + ["--installed", "--show-format", '$version\n', pkgname])
503 - for version in query.splitlines():
506 - self.installed[pkgname] = version.split('@')[0]
508 - except subprocess.CalledProcessError:
509 - LOG.error("Failed to query installed version of %s", pkgname)
511 - self.changes = True
513 - def check_groups(self):
514 - # Get the groups file
515 - mygroup = "updates-%s" % "-".join(subfunctions)
516 - self.missing_pkgs = []
517 - installed_pkgs = []
519 - groups_url = "http://controller:%s/updates/rel-%s/comps.xml" % (http_port_real, SW_VERSION)
521 - req = requests.get(groups_url)
522 - if req.status_code != 200:
523 - LOG.error("Failed to get groups list from server")
525 - except requests.ConnectionError:
526 - LOG.error("Failed to connect to server")
529 - # Get list of installed packages
531 - query = subprocess.check_output(["rpm", "-qa", "--queryformat", "%{NAME}\n"])
532 - installed_pkgs = query.split()
533 - except subprocess.CalledProcessError:
534 - LOG.exception("Failed to query RPMs")
537 - root = ElementTree.fromstring(req.text)
539 - group_id = child.find('id')
540 - if group_id is None or group_id.text != mygroup:
543 - pkglist = child.find('packagelist')
544 - if pkglist is None:
547 - for pkg in pkglist.findall('packagereq'):
548 - if pkg.text not in installed_pkgs and pkg.text not in self.missing_pkgs:
549 - self.missing_pkgs.append(pkg.text)
550 - self.changes = True
552 + self.dnfb.fill_sack()
555 """ Check current patch state """
556 @@ -633,14 +424,15 @@ class PatchAgent(PatchService):
557 LOG.info("Failed install_uuid check. Skipping query")
560 - if not self.audit_smart_config():
561 - # Set a state to "unknown"?
563 + if self.dnfb is not None:
567 + # TODO(dpenney): Use python APIs for makecache
569 - subprocess.check_output(smart_update, stderr=subprocess.STDOUT)
570 + subprocess.check_output(dnf_makecache, stderr=subprocess.STDOUT)
571 except subprocess.CalledProcessError as e:
572 - LOG.error("Failed to update smartpm")
573 + LOG.error("Failed to run dnf makecache")
574 LOG.error("Command output: %s", e.output)
575 # Set a state to "unknown"?
577 @@ -649,78 +441,72 @@ class PatchAgent(PatchService):
578 self.query_id = random.random()
581 + self.installed_dnf = []
583 - self.to_install = {}
584 + self.to_install_dnf = []
585 + self.to_downgrade_dnf = []
587 + self.to_remove_dnf = []
588 self.missing_pkgs = []
589 + self.missing_pkgs_dnf = []
591 - # Get the repo data
592 - pkgs_installed = {}
597 - output = subprocess.check_output(smart_query_installed)
598 - pkgs_installed = self.parse_smart_pkglist(output)
599 - except subprocess.CalledProcessError as e:
600 - LOG.error("Failed to query installed pkgs: %s", e.output)
601 - # Set a state to "unknown"?
605 - output = subprocess.check_output(smart_query_base)
606 - pkgs_base = self.parse_smart_pkglist(output)
607 - except subprocess.CalledProcessError as e:
608 - LOG.error("Failed to query base pkgs: %s", e.output)
609 - # Set a state to "unknown"?
611 + self.dnf_reset_client()
614 - output = subprocess.check_output(smart_query_updates)
615 - pkgs_updates = self.parse_smart_pkglist(output)
616 - except subprocess.CalledProcessError as e:
617 - LOG.error("Failed to query patched pkgs: %s", e.output)
618 - # Set a state to "unknown"?
620 + # Get the repo data
621 + pkgs_installed = dnf.sack._rpmdb_sack(self.dnfb).query().installed() # pylint: disable=protected-access
622 + avail = self.dnfb.sack.query().available().latest()
624 - # There are four possible actions:
625 - # 1. If installed pkg is not in base or updates, remove it.
626 - # 2. If installed pkg version is higher than highest in base
627 - # or updates, downgrade it.
628 - # 3. If installed pkg version is lower than highest in updates,
630 - # 4. If pkg in grouplist is not in installed, install it.
631 + # There are three possible actions:
632 + # 1. If installed pkg is not in a repo, remove it.
633 + # 2. If installed pkg version does not match newest repo version, update it.
634 + # 3. If a package in the grouplist is not installed, install it.
636 for pkg in pkgs_installed:
637 - for arch in pkgs_installed[pkg]:
638 - installed_version = pkgs_installed[pkg][arch]
639 - updates_version = self.get_pkg_version(pkgs_updates, pkg, arch)
640 - base_version = self.get_pkg_version(pkgs_base, pkg, arch)
642 - if updates_version is None and base_version is None:
644 - self.to_remove.append(pkg)
645 - self.changes = True
647 + highest = avail.filter(name=pkg.name, arch=pkg.arch)
649 + highest_pkg = highest[0]
651 - compare_version = updates_version
652 - if compare_version is None:
653 - compare_version = base_version
655 - # Compare the installed version to what's in the repo
656 - rc = rpm.labelCompare(parse_pkgver(installed_version),
657 - parse_pkgver(compare_version))
659 - # Versions match, nothing to do.
660 + if pkg.evr_eq(highest_pkg):
663 + if pkg.evr_gt(highest_pkg):
664 + self.to_downgrade_dnf.append(highest_pkg)
666 - # Install the version from the repo
667 - self.to_install[pkg] = "@".join([compare_version, arch])
668 - self.installed[pkg] = "@".join([installed_version, arch])
669 - self.changes = True
670 + self.to_install_dnf.append(highest_pkg)
672 + self.to_remove_dnf.append(pkg)
673 + self.to_remove.append(pkg.name)
675 + self.installed_dnf.append(pkg)
676 + self.changes = True
678 # Look for new packages
679 - self.check_groups()
680 + self.dnfb.read_comps()
681 + grp_id = 'updates-%s' % '-'.join(subfunctions)
683 + for grp in self.dnfb.comps.groups_iter():
684 + if grp.id == grp_id:
689 + LOG.error("Could not find software group: %s", grp_id)
691 + for pkg in pkggrp.packages_iter():
693 + res = pkgs_installed.filter(name=pkg.name)
695 + found_pkg = avail.filter(name=pkg.name)
696 + self.missing_pkgs_dnf.append(found_pkg[0])
697 + self.missing_pkgs.append(found_pkg[0].name)
698 + self.changes = True
699 + except dnf.exceptions.PackageNotFoundError:
700 + self.missing_pkgs_dnf.append(pkg)
701 + self.missing_pkgs.append(pkg.name)
702 + self.changes = True
704 + self.installed = self.pkgobjs_to_list(self.installed_dnf)
705 + self.to_install = self.pkgobjs_to_list(self.to_install_dnf + self.to_downgrade_dnf)
707 LOG.info("Patch state query returns %s", self.changes)
708 LOG.info("Installed: %s", self.installed)
709 @@ -730,6 +516,35 @@ class PatchAgent(PatchService):
713 + def resolve_dnf_transaction(self, undo_failure=True):
714 + LOG.info("Starting to process transaction: undo_failure=%s", undo_failure)
715 + self.dnfb.resolve()
716 + self.dnfb.download_packages(self.dnfb.transaction.install_set)
718 + tid = self.dnfb.do_transaction(display=PatchAgentDnfTransLogCB())
720 + transaction_rc = True
721 + for t in self.dnfb.transaction:
722 + if t.state != libdnf.transaction.TransactionItemState_DONE:
723 + transaction_rc = False
726 + self.dnf_reset_client()
728 + if not transaction_rc:
730 + LOG.error("Failure occurred... Undoing last transaction (%s)", tid)
731 + old = self.dnfb.history.old((tid,))[0]
732 + mobj = dnf.db.history.MergedTransactionWrapper(old)
734 + self.dnfb._history_undo_operations(mobj, old.tid, True) # pylint: disable=protected-access
736 + if not self.resolve_dnf_transaction(undo_failure=False):
737 + LOG.error("Failed to undo transaction")
739 + LOG.info("Transaction complete: undo_failure=%s, success=%s", undo_failure, transaction_rc)
740 + return transaction_rc
742 def handle_install(self, verbose_to_stdout=False, disallow_insvc_patch=False):
744 # The disallow_insvc_patch parameter is set when we're installing
745 @@ -781,64 +596,54 @@ class PatchAgent(PatchService):
746 if verbose_to_stdout:
747 print("Checking for software updates...")
750 - for pkg, version in self.to_install.items():
751 - install_set.append("%s-%s" % (pkg, version))
753 - install_set += self.missing_pkgs
758 - if len(install_set) > 0:
759 + if len(self.to_install_dnf) > 0 or len(self.to_downgrade_dnf) > 0:
760 + LOG.info("Adding pkgs to installation set: %s", self.to_install)
761 + for pkg in self.to_install_dnf:
762 + self.dnfb.package_install(pkg)
764 + for pkg in self.to_downgrade_dnf:
765 + self.dnfb.package_downgrade(pkg)
769 + if len(self.missing_pkgs_dnf) > 0:
770 + LOG.info("Adding missing pkgs to installation set: %s", self.missing_pkgs)
771 + for pkg in self.missing_pkgs_dnf:
772 + self.dnfb.package_install(pkg)
775 + if len(self.to_remove_dnf) > 0:
776 + LOG.info("Adding pkgs to be removed: %s", self.to_remove)
777 + for pkg in self.to_remove_dnf:
778 + self.dnfb.package_remove(pkg)
782 + # Run the transaction set
783 + transaction_rc = False
785 - if verbose_to_stdout:
786 - print("Installing software updates...")
787 - LOG.info("Installing: %s", ", ".join(install_set))
788 - output = subprocess.check_output(smart_install_cmd + install_set, stderr=subprocess.STDOUT)
790 - for line in output.split('\n'):
791 - LOG.info("INSTALL: %s", line)
792 - if verbose_to_stdout:
793 - print("Software updated.")
794 - except subprocess.CalledProcessError as e:
795 - LOG.exception("Failed to install RPMs")
796 - LOG.error("Command output: %s", e.output)
797 + transaction_rc = self.resolve_dnf_transaction()
798 + except dnf.exceptions.DepsolveError:
799 + LOG.error("Failures resolving dependencies in transaction")
800 + except dnf.exceptions.DownloadError:
801 + LOG.error("Failures downloading in transaction")
803 + if not transaction_rc:
804 + LOG.error("Failures occurred during transaction")
806 if verbose_to_stdout:
807 print("WARNING: Software update failed.")
810 if verbose_to_stdout:
811 print("Nothing to install.")
812 LOG.info("Nothing to install")
816 - remove_set = self.to_remove
818 - if len(remove_set) > 0:
820 - if verbose_to_stdout:
821 - print("Handling patch removal...")
822 - LOG.info("Removing: %s", ", ".join(remove_set))
823 - output = subprocess.check_output(smart_remove_cmd + remove_set, stderr=subprocess.STDOUT)
825 - for line in output.split('\n'):
826 - LOG.info("REMOVE: %s", line)
827 - if verbose_to_stdout:
828 - print("Patch removal complete.")
829 - except subprocess.CalledProcessError as e:
830 - LOG.exception("Failed to remove RPMs")
831 - LOG.error("Command output: %s", e.output)
833 - if verbose_to_stdout:
834 - print("WARNING: Patch removal failed.")
836 - if verbose_to_stdout:
837 - print("Nothing to remove.")
838 - LOG.info("Nothing to remove")
842 # Update the node_is_patched flag
843 setflag(node_is_patched_file)
845 @@ -1057,7 +862,7 @@ class PatchAgent(PatchService):
849 - configure_logging()
850 + configure_logging(dnf_log=True)
854 diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py
855 index e9017f2..2ee9fce 100644
856 --- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py
857 +++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py
858 @@ -69,7 +69,7 @@ def handle_exception(exc_type, exc_value, exc_traceback):
859 sys.__excepthook__(exc_type, exc_value, exc_traceback)
862 -def configure_logging(logtofile=True, level=logging.INFO):
863 +def configure_logging(logtofile=True, level=logging.INFO, dnf_log=False):
865 my_exec = os.path.basename(sys.argv[0])
867 @@ -84,6 +84,11 @@ def configure_logging(logtofile=True, level=logging.INFO):
868 main_log_handler = logging.FileHandler(logfile)
869 main_log_handler.setFormatter(formatter)
870 LOG.addHandler(main_log_handler)
873 + dnf_logger = logging.getLogger('dnf')
874 + dnf_logger.addHandler(main_log_handler)
877 os.chmod(logfile, 0o640)
879 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
880 index bd1eef9..7e30fc5 100644
881 --- a/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_agent.py
882 +++ b/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_agent.py
883 @@ -10,6 +10,15 @@ import sys
886 sys.modules['rpm'] = mock.Mock()
887 +sys.modules['dnf'] = mock.Mock()
888 +sys.modules['dnf.callback'] = mock.Mock()
889 +sys.modules['dnf.comps'] = mock.Mock()
890 +sys.modules['dnf.exceptions'] = mock.Mock()
891 +sys.modules['dnf.rpm'] = mock.Mock()
892 +sys.modules['dnf.sack'] = mock.Mock()
893 +sys.modules['dnf.transaction'] = mock.Mock()
894 +sys.modules['libdnf'] = mock.Mock()
895 +sys.modules['libdnf.transaction'] = mock.Mock()
897 import cgcs_patch.patch_agent # noqa: E402
899 diff --git a/cgcs-patch/cgcs-patch/pylint.rc b/cgcs-patch/cgcs-patch/pylint.rc
900 index 57a9829..f511718 100644
901 --- a/cgcs-patch/cgcs-patch/pylint.rc
902 +++ b/cgcs-patch/cgcs-patch/pylint.rc
903 @@ -45,10 +45,11 @@ symbols=no
904 # no Warning level messages displayed, use"--disable=all --enable=classes
906 # W0107 unnecessary-pass
908 # W0603 global-statement
910 # W1505, deprecated-method
911 -disable=C, R, W0107, W0603, W0703, W1505
912 +disable=C, R, W0107, W0511, W0603, W0703, W1505
916 @@ -235,7 +236,7 @@ ignore-mixin-members=yes
917 # List of module names for which member attributes should not be checked
918 # (useful for modules/projects where namespaces are manipulated during runtime
919 # and thus existing member attributes cannot be deduced by static analysis
921 +ignored-modules=dnf,libdnf
923 # List of classes names for which member attributes should not be checked
924 # (useful for classes with attributes dynamically set).
925 diff --git a/cgcs-patch/cgcs-patch/test-requirements.txt b/cgcs-patch/cgcs-patch/test-requirements.txt
926 index 3f4e581..56e4806 100644
927 --- a/cgcs-patch/cgcs-patch/test-requirements.txt
928 +++ b/cgcs-patch/cgcs-patch/test-requirements.txt
929 @@ -8,4 +8,3 @@ coverage!=4.4,>=4.0 # Apache-2.0
931 stestr>=1.0.0 # Apache-2.0
932 testtools>=2.2.0 # MIT