1 diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
2 index 3abd891..d8bc375 100644
3 --- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
4 +++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
5 @@ -5,22 +5,26 @@ SPDX-License-Identifier: Apache-2.0
15 +import dnf.exceptions
18 +import dnf.transaction
22 +import libdnf.transaction
26 -import xml.etree.ElementTree as ElementTree
37 from cgcs_patch.patch_functions import configure_logging
38 -from cgcs_patch.patch_functions import parse_pkgver
39 from cgcs_patch.patch_functions import LOG
40 import cgcs_patch.config as cfg
41 from cgcs_patch.base import PatchService
42 @@ -50,19 +54,13 @@ pa = None
44 http_port_real = http_port
47 -smart_cmd = ["/usr/bin/smart"]
48 -smart_quiet = smart_cmd + ["--quiet"]
49 -smart_update = smart_quiet + ["update"]
50 -smart_newer = smart_quiet + ["newer"]
51 -smart_orphans = smart_quiet + ["query", "--orphans", "--show-format", "$name\n"]
52 -smart_query = smart_quiet + ["query"]
53 -smart_query_repos = smart_quiet + ["query", "--channel=base", "--channel=updates"]
54 -smart_install_cmd = smart_cmd + ["install", "--yes", "--explain"]
55 -smart_remove_cmd = smart_cmd + ["remove", "--yes", "--explain"]
56 -smart_query_installed = smart_quiet + ["query", "--installed", "--show-format", "$name $version\n"]
57 -smart_query_base = smart_quiet + ["query", "--channel=base", "--show-format", "$name $version\n"]
58 -smart_query_updates = smart_quiet + ["query", "--channel=updates", "--show-format", "$name $version\n"]
60 +dnf_cmd = ['/usr/bin/dnf']
61 +dnf_quiet = dnf_cmd + ['--quiet']
62 +dnf_makecache = dnf_quiet + ['makecache',
63 + '--disablerepo="*"',
64 + '--enablerepo', 'platform-base',
65 + '--enablerepo', 'platform-updates']
69 @@ -123,10 +121,6 @@ class PatchMessageHelloAgent(messages.PatchMessage):
70 def handle(self, sock, addr):
73 - # Run the smart config audit
75 - pa.timed_audit_smart_config()
78 # If a user tries to do a host-install on an unlocked node,
79 # without bypassing the lock check (either via in-service
80 @@ -289,6 +283,46 @@ class PatchMessageAgentInstallResp(messages.PatchMessage):
84 +class PatchAgentDnfTransLogCB(dnf.callback.TransactionProgress):
86 + dnf.callback.TransactionProgress.__init__(self)
88 + self.log_prefix = 'dnf trans'
90 + def progress(self, package, action, ti_done, ti_total, ts_done, ts_total):
91 + if action in dnf.transaction.ACTIONS:
92 + action_str = dnf.transaction.ACTIONS[action]
93 + elif action == dnf.transaction.TRANS_POST:
94 + action_str = 'Post transaction'
96 + action_str = 'unknown(%d)' % action
98 + if ti_done is not None:
99 + # To reduce the volume of logs, only log 0% and 100%
100 + if ti_done == 0 or ti_done == ti_total:
101 + LOG.info('%s PROGRESS %s: %s %0.1f%% [%s/%s]',
102 + self.log_prefix, action_str, package,
103 + (ti_done * 100 / ti_total),
106 + LOG.info('%s PROGRESS %s: %s [%s/%s]',
107 + self.log_prefix, action_str, package, ts_done, ts_total)
109 + def filelog(self, package, action):
110 + if action in dnf.transaction.FILE_ACTIONS:
111 + msg = '%s: %s' % (dnf.transaction.FILE_ACTIONS[action], package)
113 + msg = '%s: %s' % (package, action)
114 + LOG.info('%s FILELOG %s', self.log_prefix, msg)
116 + def scriptout(self, msgs):
118 + LOG.info("%s SCRIPTOUT :\n%s", self.log_prefix, msgs)
120 + def error(self, message):
121 + LOG.error("%s ERROR: %s", self.log_prefix, message)
124 class PatchAgent(PatchService):
126 PatchService.__init__(self)
127 @@ -298,9 +332,14 @@ class PatchAgent(PatchService):
131 + self.installed_dnf = []
133 + self.to_install_dnf = []
134 + self.to_downgrade_dnf = []
136 + self.to_remove_dnf = []
137 self.missing_pkgs = []
138 + self.missing_pkgs_dnf = []
139 self.patch_op_counter = 0
140 self.node_is_patched = os.path.exists(node_is_patched_file)
141 self.node_is_patched_timestamp = 0
142 @@ -308,6 +347,7 @@ class PatchAgent(PatchService):
143 self.state = constants.PATCH_AGENT_STATE_IDLE
144 self.last_config_audit = 0
145 self.rejection_timestamp = 0
149 if os.path.exists(patch_installing_file):
150 @@ -343,289 +383,40 @@ class PatchAgent(PatchService):
151 self.listener.bind(('', self.port))
152 self.listener.listen(2) # Allow two connections, for two controllers
154 - def audit_smart_config(self):
155 - LOG.info("Auditing smart configuration")
157 - # Get the current channel config
159 - output = subprocess.check_output(smart_cmd +
160 - ["channel", "--yaml"],
161 - stderr=subprocess.STDOUT)
162 - config = yaml.load(output)
163 - except subprocess.CalledProcessError as e:
164 - LOG.exception("Failed to query channels")
165 - LOG.error("Command output: %s", e.output)
168 - LOG.exception("Failed to query channels")
171 - expected = [{'channel': 'rpmdb',
173 - 'name': 'RPM Database',
175 - {'channel': 'base',
178 - 'baseurl': "http://controller:%s/feed/rel-%s" % (http_port_real, SW_VERSION)},
179 - {'channel': 'updates',
182 - 'baseurl': "http://controller:%s/updates/rel-%s" % (http_port_real, SW_VERSION)}]
186 - for item in expected:
187 - channel = item['channel']
188 - ch_type = item['type']
189 - ch_name = item['name']
190 - ch_baseurl = item['baseurl']
192 - add_channel = False
194 - if channel in config:
195 - # Verify existing channel config
196 - if (config[channel].get('type') != ch_type or
197 - config[channel].get('name') != ch_name or
198 - config[channel].get('baseurl') != ch_baseurl):
199 - # Config is invalid
201 - LOG.warning("Invalid smart config found for %s", channel)
203 - output = subprocess.check_output(smart_cmd +
204 - ["channel", "--yes",
205 - "--remove", channel],
206 - stderr=subprocess.STDOUT)
207 - except subprocess.CalledProcessError as e:
208 - LOG.exception("Failed to configure %s channel", channel)
209 - LOG.error("Command output: %s", e.output)
212 - # Channel is missing
214 - LOG.warning("Channel %s is missing from config", channel)
217 - LOG.info("Adding channel %s", channel)
218 - cmd_args = ["channel", "--yes", "--add", channel,
219 - "type=%s" % ch_type,
220 - "name=%s" % ch_name]
221 - if ch_baseurl is not None:
222 - cmd_args += ["baseurl=%s" % ch_baseurl]
225 - output = subprocess.check_output(smart_cmd + cmd_args,
226 - stderr=subprocess.STDOUT)
227 - except subprocess.CalledProcessError as e:
228 - LOG.exception("Failed to configure %s channel", channel)
229 - LOG.error("Command output: %s", e.output)
234 - # Validate the smart config
236 - output = subprocess.check_output(smart_cmd +
237 - ["config", "--yaml"],
238 - stderr=subprocess.STDOUT)
239 - config = yaml.load(output)
240 - except subprocess.CalledProcessError as e:
241 - LOG.exception("Failed to query smart config")
242 - LOG.error("Command output: %s", e.output)
245 - LOG.exception("Failed to query smart config")
248 - # Check for the rpm-nolinktos flag
249 - nolinktos = 'rpm-nolinktos'
250 - if config.get(nolinktos) is not True:
252 - LOG.warning("Setting %s option", nolinktos)
254 - output = subprocess.check_output(smart_cmd +
255 - ["config", "--set",
256 - "%s=true" % nolinktos],
257 - stderr=subprocess.STDOUT)
258 - except subprocess.CalledProcessError as e:
259 - LOG.exception("Failed to configure %s option", nolinktos)
260 - LOG.error("Command output: %s", e.output)
265 - # Check for the rpm-check-signatures flag
266 - nosignature = 'rpm-check-signatures'
267 - if config.get(nosignature) is not False:
269 - LOG.warning("Setting %s option", nosignature)
271 - output = subprocess.check_output(smart_cmd +
272 - ["config", "--set",
273 - "%s=false" % nosignature],
274 - stderr=subprocess.STDOUT)
275 - except subprocess.CalledProcessError as e:
276 - LOG.exception("Failed to configure %s option", nosignature)
277 - LOG.error("Command output: %s", e.output)
284 - subprocess.check_output(smart_update, stderr=subprocess.STDOUT)
285 - except subprocess.CalledProcessError as e:
286 - LOG.exception("Failed to update smartpm")
287 - LOG.error("Command output: %s", e.output)
290 - # Reset the patch op counter to force a detailed query
291 - self.patch_op_counter = 0
293 - self.last_config_audit = time.time()
296 - def timed_audit_smart_config(self):
298 - if (time.time() - self.last_config_audit) > 1800:
299 - # It's been 30 minutes since the last completed audit
300 - LOG.info("Kicking timed audit")
301 - rc = self.audit_smart_config()
306 - def parse_smart_pkglist(output):
308 - for line in output.splitlines():
312 - fields = line.split()
313 - pkgname = fields[0]
314 - (version, arch) = fields[1].split('@')
316 - if pkgname not in pkglist:
317 - pkglist[pkgname] = {}
318 - pkglist[pkgname][arch] = version
319 - elif arch not in pkglist[pkgname]:
320 - pkglist[pkgname][arch] = version
321 + def pkgobjs_to_list(pkgobjs):
322 + # Transform pkgobj list to format used by patch-controller
324 + for pkg in pkgobjs:
326 + output[pkg.name] = "%s:%s-%s@%s" % (pkg.epoch, pkg.version, pkg.release, pkg.arch)
328 - stored_ver = pkglist[pkgname][arch]
330 - # The rpm.labelCompare takes version broken into 3 components
332 - # 1, if first arg is higher version
333 - # 0, if versions are same
334 - # -1, if first arg is lower version
335 - rc = rpm.labelCompare(parse_pkgver(version),
336 - parse_pkgver(stored_ver))
337 + output[pkg.name] = "%s-%s@%s" % (pkg.version, pkg.release, pkg.arch)
341 - pkglist[pkgname][arch] = version
345 + def dnf_reset_client(self):
346 + if self.dnfb is not None:
351 - def get_pkg_version(pkglist, pkg, arch):
352 - if pkg not in pkglist:
354 - if arch not in pkglist[pkg]:
356 - return pkglist[pkg][arch]
358 - def parse_smart_newer(self, output):
359 - # Skip the first two lines, which are headers
360 - for line in output.splitlines()[2:]:
364 - fields = line.split()
365 - pkgname = fields[0]
366 - installedver = fields[2]
368 + self.dnfb = dnf.Base()
369 + self.dnfb.conf.substitutions['infra'] = 'stock'
371 - self.installed[pkgname] = installedver
372 - self.to_install[pkgname] = newver
374 - def parse_smart_orphans(self, output):
375 - for pkgname in output.splitlines():
378 + # Reset default installonlypkgs list
379 + self.dnfb.conf.installonlypkgs = []
381 - highest_version = None
382 + self.dnfb.read_all_repos()
385 - query = subprocess.check_output(smart_query_repos + ["--show-format", '$version\n', pkgname])
386 - # The last non-blank version is the highest
387 - for version in query.splitlines():
390 - highest_version = version.split('@')[0]
392 - except subprocess.CalledProcessError:
393 - # Package is not in the repo
394 - highest_version = None
396 - if highest_version is None:
397 - # Package is to be removed
398 - self.to_remove.append(pkgname)
399 + # Ensure only platform repos are enabled for transaction
400 + for repo in self.dnfb.repos.all():
401 + if repo.id == 'platform-base' or repo.id == 'platform-updates':
404 - # Rollback to the highest version
405 - self.to_install[pkgname] = highest_version
408 - # Get the installed version
410 - query = subprocess.check_output(smart_query + ["--installed", "--show-format", '$version\n', pkgname])
411 - for version in query.splitlines():
414 - self.installed[pkgname] = version.split('@')[0]
416 - except subprocess.CalledProcessError:
417 - LOG.error("Failed to query installed version of %s", pkgname)
419 - self.changes = True
421 - def check_groups(self):
422 - # Get the groups file
423 - mygroup = "updates-%s" % "-".join(subfunctions)
424 - self.missing_pkgs = []
425 - installed_pkgs = []
427 - groups_url = "http://controller:%s/updates/rel-%s/comps.xml" % (http_port_real, SW_VERSION)
429 - req = requests.get(groups_url)
430 - if req.status_code != 200:
431 - LOG.error("Failed to get groups list from server")
433 - except requests.ConnectionError:
434 - LOG.error("Failed to connect to server")
437 - # Get list of installed packages
439 - query = subprocess.check_output(["rpm", "-qa", "--queryformat", "%{NAME}\n"])
440 - installed_pkgs = query.split()
441 - except subprocess.CalledProcessError:
442 - LOG.exception("Failed to query RPMs")
445 - root = ElementTree.fromstring(req.text)
447 - group_id = child.find('id')
448 - if group_id is None or group_id.text != mygroup:
451 - pkglist = child.find('packagelist')
452 - if pkglist is None:
455 - for pkg in pkglist.findall('packagereq'):
456 - if pkg.text not in installed_pkgs and pkg.text not in self.missing_pkgs:
457 - self.missing_pkgs.append(pkg.text)
458 - self.changes = True
460 + self.dnfb.fill_sack()
463 """ Check current patch state """
464 @@ -633,14 +424,15 @@ class PatchAgent(PatchService):
465 LOG.info("Failed install_uuid check. Skipping query")
468 - if not self.audit_smart_config():
469 - # Set a state to "unknown"?
471 + if self.dnfb is not None:
475 + # TODO(dpenney): Use python APIs for makecache
477 - subprocess.check_output(smart_update, stderr=subprocess.STDOUT)
478 + subprocess.check_output(dnf_makecache, stderr=subprocess.STDOUT)
479 except subprocess.CalledProcessError as e:
480 - LOG.error("Failed to update smartpm")
481 + LOG.error("Failed to run dnf makecache")
482 LOG.error("Command output: %s", e.output)
483 # Set a state to "unknown"?
485 @@ -649,78 +441,72 @@ class PatchAgent(PatchService):
486 self.query_id = random.random()
489 + self.installed_dnf = []
491 - self.to_install = {}
492 + self.to_install_dnf = []
493 + self.to_downgrade_dnf = []
495 + self.to_remove_dnf = []
496 self.missing_pkgs = []
497 + self.missing_pkgs_dnf = []
499 - # Get the repo data
500 - pkgs_installed = {}
505 - output = subprocess.check_output(smart_query_installed)
506 - pkgs_installed = self.parse_smart_pkglist(output)
507 - except subprocess.CalledProcessError as e:
508 - LOG.error("Failed to query installed pkgs: %s", e.output)
509 - # Set a state to "unknown"?
513 - output = subprocess.check_output(smart_query_base)
514 - pkgs_base = self.parse_smart_pkglist(output)
515 - except subprocess.CalledProcessError as e:
516 - LOG.error("Failed to query base pkgs: %s", e.output)
517 - # Set a state to "unknown"?
519 + self.dnf_reset_client()
522 - output = subprocess.check_output(smart_query_updates)
523 - pkgs_updates = self.parse_smart_pkglist(output)
524 - except subprocess.CalledProcessError as e:
525 - LOG.error("Failed to query patched pkgs: %s", e.output)
526 - # Set a state to "unknown"?
528 + # Get the repo data
529 + pkgs_installed = dnf.sack._rpmdb_sack(self.dnfb).query().installed() # pylint: disable=protected-access
530 + avail = self.dnfb.sack.query().available().latest()
532 - # There are four possible actions:
533 - # 1. If installed pkg is not in base or updates, remove it.
534 - # 2. If installed pkg version is higher than highest in base
535 - # or updates, downgrade it.
536 - # 3. If installed pkg version is lower than highest in updates,
538 - # 4. If pkg in grouplist is not in installed, install it.
539 + # There are three possible actions:
540 + # 1. If installed pkg is not in a repo, remove it.
541 + # 2. If installed pkg version does not match newest repo version, update it.
542 + # 3. If a package in the grouplist is not installed, install it.
544 for pkg in pkgs_installed:
545 - for arch in pkgs_installed[pkg]:
546 - installed_version = pkgs_installed[pkg][arch]
547 - updates_version = self.get_pkg_version(pkgs_updates, pkg, arch)
548 - base_version = self.get_pkg_version(pkgs_base, pkg, arch)
550 - if updates_version is None and base_version is None:
552 - self.to_remove.append(pkg)
553 - self.changes = True
555 + highest = avail.filter(name=pkg.name, arch=pkg.arch)
557 + highest_pkg = highest[0]
559 - compare_version = updates_version
560 - if compare_version is None:
561 - compare_version = base_version
563 - # Compare the installed version to what's in the repo
564 - rc = rpm.labelCompare(parse_pkgver(installed_version),
565 - parse_pkgver(compare_version))
567 - # Versions match, nothing to do.
568 + if pkg.evr_eq(highest_pkg):
571 + if pkg.evr_gt(highest_pkg):
572 + self.to_downgrade_dnf.append(highest_pkg)
574 - # Install the version from the repo
575 - self.to_install[pkg] = "@".join([compare_version, arch])
576 - self.installed[pkg] = "@".join([installed_version, arch])
577 - self.changes = True
578 + self.to_install_dnf.append(highest_pkg)
580 + self.to_remove_dnf.append(pkg)
581 + self.to_remove.append(pkg.name)
583 + self.installed_dnf.append(pkg)
584 + self.changes = True
586 # Look for new packages
587 - self.check_groups()
588 + self.dnfb.read_comps()
589 + grp_id = 'updates-%s' % '-'.join(subfunctions)
591 + for grp in self.dnfb.comps.groups_iter():
592 + if grp.id == grp_id:
597 + LOG.error("Could not find software group: %s", grp_id)
599 + for pkg in pkggrp.packages_iter():
601 + res = pkgs_installed.filter(name=pkg.name)
603 + found_pkg = avail.filter(name=pkg.name)
604 + self.missing_pkgs_dnf.append(found_pkg[0])
605 + self.missing_pkgs.append(found_pkg[0].name)
606 + self.changes = True
607 + except dnf.exceptions.PackageNotFoundError:
608 + self.missing_pkgs_dnf.append(pkg)
609 + self.missing_pkgs.append(pkg.name)
610 + self.changes = True
612 + self.installed = self.pkgobjs_to_list(self.installed_dnf)
613 + self.to_install = self.pkgobjs_to_list(self.to_install_dnf + self.to_downgrade_dnf)
615 LOG.info("Patch state query returns %s", self.changes)
616 LOG.info("Installed: %s", self.installed)
617 @@ -730,6 +516,35 @@ class PatchAgent(PatchService):
621 + def resolve_dnf_transaction(self, undo_failure=True):
622 + LOG.info("Starting to process transaction: undo_failure=%s", undo_failure)
623 + self.dnfb.resolve()
624 + self.dnfb.download_packages(self.dnfb.transaction.install_set)
626 + tid = self.dnfb.do_transaction(display=PatchAgentDnfTransLogCB())
628 + transaction_rc = True
629 + for t in self.dnfb.transaction:
630 + if t.state != libdnf.transaction.TransactionItemState_DONE:
631 + transaction_rc = False
634 + self.dnf_reset_client()
636 + if not transaction_rc:
638 + LOG.error("Failure occurred... Undoing last transaction (%s)", tid)
639 + old = self.dnfb.history.old((tid,))[0]
640 + mobj = dnf.db.history.MergedTransactionWrapper(old)
642 + self.dnfb._history_undo_operations(mobj, old.tid, True) # pylint: disable=protected-access
644 + if not self.resolve_dnf_transaction(undo_failure=False):
645 + LOG.error("Failed to undo transaction")
647 + LOG.info("Transaction complete: undo_failure=%s, success=%s", undo_failure, transaction_rc)
648 + return transaction_rc
650 def handle_install(self, verbose_to_stdout=False, disallow_insvc_patch=False):
652 # The disallow_insvc_patch parameter is set when we're installing
653 @@ -781,64 +596,54 @@ class PatchAgent(PatchService):
654 if verbose_to_stdout:
655 print("Checking for software updates...")
658 - for pkg, version in self.to_install.items():
659 - install_set.append("%s-%s" % (pkg, version))
661 - install_set += self.missing_pkgs
666 - if len(install_set) > 0:
667 + if len(self.to_install_dnf) > 0 or len(self.to_downgrade_dnf) > 0:
668 + LOG.info("Adding pkgs to installation set: %s", self.to_install)
669 + for pkg in self.to_install_dnf:
670 + self.dnfb.package_install(pkg)
672 + for pkg in self.to_downgrade_dnf:
673 + self.dnfb.package_downgrade(pkg)
677 + if len(self.missing_pkgs_dnf) > 0:
678 + LOG.info("Adding missing pkgs to installation set: %s", self.missing_pkgs)
679 + for pkg in self.missing_pkgs_dnf:
680 + self.dnfb.package_install(pkg)
683 + if len(self.to_remove_dnf) > 0:
684 + LOG.info("Adding pkgs to be removed: %s", self.to_remove)
685 + for pkg in self.to_remove_dnf:
686 + self.dnfb.package_remove(pkg)
690 + # Run the transaction set
691 + transaction_rc = False
693 - if verbose_to_stdout:
694 - print("Installing software updates...")
695 - LOG.info("Installing: %s", ", ".join(install_set))
696 - output = subprocess.check_output(smart_install_cmd + install_set, stderr=subprocess.STDOUT)
698 - for line in output.split('\n'):
699 - LOG.info("INSTALL: %s", line)
700 - if verbose_to_stdout:
701 - print("Software updated.")
702 - except subprocess.CalledProcessError as e:
703 - LOG.exception("Failed to install RPMs")
704 - LOG.error("Command output: %s", e.output)
705 + transaction_rc = self.resolve_dnf_transaction()
706 + except dnf.exceptions.DepsolveError:
707 + LOG.error("Failures resolving dependencies in transaction")
708 + except dnf.exceptions.DownloadError:
709 + LOG.error("Failures downloading in transaction")
711 + if not transaction_rc:
712 + LOG.error("Failures occurred during transaction")
714 if verbose_to_stdout:
715 print("WARNING: Software update failed.")
718 if verbose_to_stdout:
719 print("Nothing to install.")
720 LOG.info("Nothing to install")
724 - remove_set = self.to_remove
726 - if len(remove_set) > 0:
728 - if verbose_to_stdout:
729 - print("Handling patch removal...")
730 - LOG.info("Removing: %s", ", ".join(remove_set))
731 - output = subprocess.check_output(smart_remove_cmd + remove_set, stderr=subprocess.STDOUT)
733 - for line in output.split('\n'):
734 - LOG.info("REMOVE: %s", line)
735 - if verbose_to_stdout:
736 - print("Patch removal complete.")
737 - except subprocess.CalledProcessError as e:
738 - LOG.exception("Failed to remove RPMs")
739 - LOG.error("Command output: %s", e.output)
741 - if verbose_to_stdout:
742 - print("WARNING: Patch removal failed.")
744 - if verbose_to_stdout:
745 - print("Nothing to remove.")
746 - LOG.info("Nothing to remove")
750 # Update the node_is_patched flag
751 setflag(node_is_patched_file)
753 @@ -1057,7 +862,7 @@ class PatchAgent(PatchService):
757 - configure_logging()
758 + configure_logging(dnf_log=True)
762 diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py
763 index e9017f2..2ee9fce 100644
764 --- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py
765 +++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py
766 @@ -69,7 +69,7 @@ def handle_exception(exc_type, exc_value, exc_traceback):
767 sys.__excepthook__(exc_type, exc_value, exc_traceback)
770 -def configure_logging(logtofile=True, level=logging.INFO):
771 +def configure_logging(logtofile=True, level=logging.INFO, dnf_log=False):
773 my_exec = os.path.basename(sys.argv[0])
775 @@ -84,6 +84,11 @@ def configure_logging(logtofile=True, level=logging.INFO):
776 main_log_handler = logging.FileHandler(logfile)
777 main_log_handler.setFormatter(formatter)
778 LOG.addHandler(main_log_handler)
781 + dnf_logger = logging.getLogger('dnf')
782 + dnf_logger.addHandler(main_log_handler)
785 os.chmod(logfile, 0o640)
787 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
788 index bd1eef9..7e30fc5 100644
789 --- a/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_agent.py
790 +++ b/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_agent.py
791 @@ -10,6 +10,15 @@ import sys
794 sys.modules['rpm'] = mock.Mock()
795 +sys.modules['dnf'] = mock.Mock()
796 +sys.modules['dnf.callback'] = mock.Mock()
797 +sys.modules['dnf.comps'] = mock.Mock()
798 +sys.modules['dnf.exceptions'] = mock.Mock()
799 +sys.modules['dnf.rpm'] = mock.Mock()
800 +sys.modules['dnf.sack'] = mock.Mock()
801 +sys.modules['dnf.transaction'] = mock.Mock()
802 +sys.modules['libdnf'] = mock.Mock()
803 +sys.modules['libdnf.transaction'] = mock.Mock()
805 import cgcs_patch.patch_agent # noqa: E402
807 diff --git a/cgcs-patch/cgcs-patch/pylint.rc b/cgcs-patch/cgcs-patch/pylint.rc
808 index 57a9829..f511718 100644
809 --- a/cgcs-patch/cgcs-patch/pylint.rc
810 +++ b/cgcs-patch/cgcs-patch/pylint.rc
811 @@ -45,10 +45,11 @@ symbols=no
812 # no Warning level messages displayed, use"--disable=all --enable=classes
814 # W0107 unnecessary-pass
816 # W0603 global-statement
818 # W1505, deprecated-method
819 -disable=C, R, W0107, W0603, W0703, W1505
820 +disable=C, R, W0107, W0511, W0603, W0703, W1505
824 @@ -235,7 +236,7 @@ ignore-mixin-members=yes
825 # List of module names for which member attributes should not be checked
826 # (useful for modules/projects where namespaces are manipulated during runtime
827 # and thus existing member attributes cannot be deduced by static analysis
829 +ignored-modules=dnf,libdnf
831 # List of classes names for which member attributes should not be checked
832 # (useful for classes with attributes dynamically set).
833 diff --git a/cgcs-patch/cgcs-patch/test-requirements.txt b/cgcs-patch/cgcs-patch/test-requirements.txt
834 index 3f4e581..56e4806 100644
835 --- a/cgcs-patch/cgcs-patch/test-requirements.txt
836 +++ b/cgcs-patch/cgcs-patch/test-requirements.txt
837 @@ -8,4 +8,3 @@ coverage!=4.4,>=4.0 # Apache-2.0
839 stestr>=1.0.0 # Apache-2.0
840 testtools>=2.2.0 # MIT