-From a6912bf7cffaade9647d8921816cc30db85630bb Mon Sep 17 00:00:00 2001
+From 80ee2e342d1854f439a1ec25c2f6a3a3625a0720 Mon Sep 17 00:00:00 2001
From: Don Penney <don.penney@windriver.com>
Date: Sun, 22 Dec 2019 22:45:18 -0500
Subject: [PATCH] Remove use of rpmUtils.miscutils from cgcs-patch
Story: 2006228
Task: 37871
Signed-off-by: Don Penney <don.penney@windriver.com>
+
---
cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py | 11 +++++------
cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py | 6 +++---
6 files changed, 40 insertions(+), 13 deletions(-)
diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
-index ed6f67e..547db52 100644
+index b95b79d..77930d7 100644
--- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
+++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
@@ -19,9 +19,8 @@ import sys
# Versions match, nothing to do.
continue
diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py
-index 60b2b14..79a6401 100644
+index 1ba8f5e..4b94a5f 100644
--- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py
+++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py
@@ -17,7 +17,7 @@ import rpm
if self.patch_data.metadata[patch_id]["repostate"] == constants.AVAILABLE:
# The RPM is not expected to be installed.
diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py
-index 211c2c9..5866e8b 100644
+index 832e4e9..281a286 100644
--- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py
+++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py
@@ -176,6 +176,24 @@ def parse_rpm_filename(filename):
import cgcs_patch.patch_agent # noqa: E402
diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_controller.py b/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_controller.py
-index 1c603b2..1db4b68 100644
+index d11623f..e2b02c0 100644
--- a/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_controller.py
+++ b/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_controller.py
@@ -10,8 +10,6 @@ import sys
+ for ver, expected in versions.items():
+ result = cgcs_patch.patch_functions.parse_pkgver(ver)
+ self.assertEqual(result, expected)
---
-2.7.4
-
--- /dev/null
+From aef3694691b14415f59aaea759d95f3ef3f1183b Mon Sep 17 00:00:00 2001
+From: Don Penney <don.penney@windriver.com>
+Date: Wed, 15 Jan 2020 23:42:41 -0500
+Subject: [PATCH] Cleanup smartpm references
+
+As smartpm is no longer used, this commit removes the remaining
+comment reference from the patching code, and deletes the unused
+smart-helper files.
+
+Change-Id: Iac557403e43c1e732eb38393bb2cfeb6fc6c3573
+Story: 2006227
+Task: 38136
+Signed-off-by: Don Penney <don.penney@windriver.com>
+
+---
+ cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py | 2 +-
+ smart-helper/LICENSE | 202 -----------------------
+ smart-helper/files/etc.rpm.platform | 11 --
+ smart-helper/files/etc.rpm.sysinfo.Dirnames | 1 -
+ 4 files changed, 1 insertion(+), 215 deletions(-)
+ delete mode 100644 smart-helper/LICENSE
+ delete mode 100644 smart-helper/files/etc.rpm.platform
+ delete mode 100644 smart-helper/files/etc.rpm.sysinfo.Dirnames
+
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py
+index 6d61204..705590c 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py
+@@ -1090,7 +1090,7 @@ def patch_install_local(debug, args):
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
+
+ # To allow patch installation to occur before configuration, we need
+- # to alias controller to localhost so that the smartpm channels work.
++ # to alias controller to localhost so that the dnf repos work.
+ # There is a HOSTALIASES feature that would be preferred here, but it
+ # unfortunately requires dnsmasq to be running, which it is not at this point.
+
+diff --git a/smart-helper/LICENSE b/smart-helper/LICENSE
+deleted file mode 100644
+index d645695..0000000
+--- a/smart-helper/LICENSE
++++ /dev/null
+@@ -1,202 +0,0 @@
+-
+- Apache License
+- Version 2.0, January 2004
+- http://www.apache.org/licenses/
+-
+- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+-
+- 1. Definitions.
+-
+- "License" shall mean the terms and conditions for use, reproduction,
+- and distribution as defined by Sections 1 through 9 of this document.
+-
+- "Licensor" shall mean the copyright owner or entity authorized by
+- the copyright owner that is granting the License.
+-
+- "Legal Entity" shall mean the union of the acting entity and all
+- other entities that control, are controlled by, or are under common
+- control with that entity. For the purposes of this definition,
+- "control" means (i) the power, direct or indirect, to cause the
+- direction or management of such entity, whether by contract or
+- otherwise, or (ii) ownership of fifty percent (50%) or more of the
+- outstanding shares, or (iii) beneficial ownership of such entity.
+-
+- "You" (or "Your") shall mean an individual or Legal Entity
+- exercising permissions granted by this License.
+-
+- "Source" form shall mean the preferred form for making modifications,
+- including but not limited to software source code, documentation
+- source, and configuration files.
+-
+- "Object" form shall mean any form resulting from mechanical
+- transformation or translation of a Source form, including but
+- not limited to compiled object code, generated documentation,
+- and conversions to other media types.
+-
+- "Work" shall mean the work of authorship, whether in Source or
+- Object form, made available under the License, as indicated by a
+- copyright notice that is included in or attached to the work
+- (an example is provided in the Appendix below).
+-
+- "Derivative Works" shall mean any work, whether in Source or Object
+- form, that is based on (or derived from) the Work and for which the
+- editorial revisions, annotations, elaborations, or other modifications
+- represent, as a whole, an original work of authorship. For the purposes
+- of this License, Derivative Works shall not include works that remain
+- separable from, or merely link (or bind by name) to the interfaces of,
+- the Work and Derivative Works thereof.
+-
+- "Contribution" shall mean any work of authorship, including
+- the original version of the Work and any modifications or additions
+- to that Work or Derivative Works thereof, that is intentionally
+- submitted to Licensor for inclusion in the Work by the copyright owner
+- or by an individual or Legal Entity authorized to submit on behalf of
+- the copyright owner. For the purposes of this definition, "submitted"
+- means any form of electronic, verbal, or written communication sent
+- to the Licensor or its representatives, including but not limited to
+- communication on electronic mailing lists, source code control systems,
+- and issue tracking systems that are managed by, or on behalf of, the
+- Licensor for the purpose of discussing and improving the Work, but
+- excluding communication that is conspicuously marked or otherwise
+- designated in writing by the copyright owner as "Not a Contribution."
+-
+- "Contributor" shall mean Licensor and any individual or Legal Entity
+- on behalf of whom a Contribution has been received by Licensor and
+- subsequently incorporated within the Work.
+-
+- 2. Grant of Copyright License. Subject to the terms and conditions of
+- this License, each Contributor hereby grants to You a perpetual,
+- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+- copyright license to reproduce, prepare Derivative Works of,
+- publicly display, publicly perform, sublicense, and distribute the
+- Work and such Derivative Works in Source or Object form.
+-
+- 3. Grant of Patent License. Subject to the terms and conditions of
+- this License, each Contributor hereby grants to You a perpetual,
+- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+- (except as stated in this section) patent license to make, have made,
+- use, offer to sell, sell, import, and otherwise transfer the Work,
+- where such license applies only to those patent claims licensable
+- by such Contributor that are necessarily infringed by their
+- Contribution(s) alone or by combination of their Contribution(s)
+- with the Work to which such Contribution(s) was submitted. If You
+- institute patent litigation against any entity (including a
+- cross-claim or counterclaim in a lawsuit) alleging that the Work
+- or a Contribution incorporated within the Work constitutes direct
+- or contributory patent infringement, then any patent licenses
+- granted to You under this License for that Work shall terminate
+- as of the date such litigation is filed.
+-
+- 4. Redistribution. You may reproduce and distribute copies of the
+- Work or Derivative Works thereof in any medium, with or without
+- modifications, and in Source or Object form, provided that You
+- meet the following conditions:
+-
+- (a) You must give any other recipients of the Work or
+- Derivative Works a copy of this License; and
+-
+- (b) You must cause any modified files to carry prominent notices
+- stating that You changed the files; and
+-
+- (c) You must retain, in the Source form of any Derivative Works
+- that You distribute, all copyright, patent, trademark, and
+- attribution notices from the Source form of the Work,
+- excluding those notices that do not pertain to any part of
+- the Derivative Works; and
+-
+- (d) If the Work includes a "NOTICE" text file as part of its
+- distribution, then any Derivative Works that You distribute must
+- include a readable copy of the attribution notices contained
+- within such NOTICE file, excluding those notices that do not
+- pertain to any part of the Derivative Works, in at least one
+- of the following places: within a NOTICE text file distributed
+- as part of the Derivative Works; within the Source form or
+- documentation, if provided along with the Derivative Works; or,
+- within a display generated by the Derivative Works, if and
+- wherever such third-party notices normally appear. The contents
+- of the NOTICE file are for informational purposes only and
+- do not modify the License. You may add Your own attribution
+- notices within Derivative Works that You distribute, alongside
+- or as an addendum to the NOTICE text from the Work, provided
+- that such additional attribution notices cannot be construed
+- as modifying the License.
+-
+- You may add Your own copyright statement to Your modifications and
+- may provide additional or different license terms and conditions
+- for use, reproduction, or distribution of Your modifications, or
+- for any such Derivative Works as a whole, provided Your use,
+- reproduction, and distribution of the Work otherwise complies with
+- the conditions stated in this License.
+-
+- 5. Submission of Contributions. Unless You explicitly state otherwise,
+- any Contribution intentionally submitted for inclusion in the Work
+- by You to the Licensor shall be under the terms and conditions of
+- this License, without any additional terms or conditions.
+- Notwithstanding the above, nothing herein shall supersede or modify
+- the terms of any separate license agreement you may have executed
+- with Licensor regarding such Contributions.
+-
+- 6. Trademarks. This License does not grant permission to use the trade
+- names, trademarks, service marks, or product names of the Licensor,
+- except as required for reasonable and customary use in describing the
+- origin of the Work and reproducing the content of the NOTICE file.
+-
+- 7. Disclaimer of Warranty. Unless required by applicable law or
+- agreed to in writing, Licensor provides the Work (and each
+- Contributor provides its Contributions) on an "AS IS" BASIS,
+- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+- implied, including, without limitation, any warranties or conditions
+- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+- PARTICULAR PURPOSE. You are solely responsible for determining the
+- appropriateness of using or redistributing the Work and assume any
+- risks associated with Your exercise of permissions under this License.
+-
+- 8. Limitation of Liability. In no event and under no legal theory,
+- whether in tort (including negligence), contract, or otherwise,
+- unless required by applicable law (such as deliberate and grossly
+- negligent acts) or agreed to in writing, shall any Contributor be
+- liable to You for damages, including any direct, indirect, special,
+- incidental, or consequential damages of any character arising as a
+- result of this License or out of the use or inability to use the
+- Work (including but not limited to damages for loss of goodwill,
+- work stoppage, computer failure or malfunction, or any and all
+- other commercial damages or losses), even if such Contributor
+- has been advised of the possibility of such damages.
+-
+- 9. Accepting Warranty or Additional Liability. While redistributing
+- the Work or Derivative Works thereof, You may choose to offer,
+- and charge a fee for, acceptance of support, warranty, indemnity,
+- or other liability obligations and/or rights consistent with this
+- License. However, in accepting such obligations, You may act only
+- on Your own behalf and on Your sole responsibility, not on behalf
+- of any other Contributor, and only if You agree to indemnify,
+- defend, and hold each Contributor harmless for any liability
+- incurred by, or claims asserted against, such Contributor by reason
+- of your accepting any such warranty or additional liability.
+-
+- END OF TERMS AND CONDITIONS
+-
+- APPENDIX: How to apply the Apache License to your work.
+-
+- To apply the Apache License to your work, attach the following
+- boilerplate notice, with the fields enclosed by brackets "[]"
+- replaced with your own identifying information. (Don't include
+- the brackets!) The text should be enclosed in the appropriate
+- comment syntax for the file format. We also recommend that a
+- file or class name and description of purpose be included on the
+- same "printed page" as the copyright notice for easier
+- identification within third-party archives.
+-
+- Copyright [yyyy] [name of copyright owner]
+-
+- Licensed under the Apache License, Version 2.0 (the "License");
+- you may not use this file except in compliance with the License.
+- You may obtain a copy of the License at
+-
+- http://www.apache.org/licenses/LICENSE-2.0
+-
+- Unless required by applicable law or agreed to in writing, software
+- distributed under the License is distributed on an "AS IS" BASIS,
+- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+- See the License for the specific language governing permissions and
+- limitations under the License.
+diff --git a/smart-helper/files/etc.rpm.platform b/smart-helper/files/etc.rpm.platform
+deleted file mode 100644
+index 5a6282c..0000000
+--- a/smart-helper/files/etc.rpm.platform
++++ /dev/null
+@@ -1,11 +0,0 @@
+-x86_64-wrs-linux
+-intel_x86_64-.*-linux
+-x86_64-.*-linux
+-noarch-.*-linux.*
+-any-.*-linux.*
+-all-.*-linux.*
+-lib32_intel_x86_64-.*-linux
+-lib32_x86-.*-linux
+-noarch-.*-linux.*
+-any-.*-linux.*
+-all-.*-linux.*
+diff --git a/smart-helper/files/etc.rpm.sysinfo.Dirnames b/smart-helper/files/etc.rpm.sysinfo.Dirnames
+deleted file mode 100644
+index b498fd4..0000000
+--- a/smart-helper/files/etc.rpm.sysinfo.Dirnames
++++ /dev/null
+@@ -1 +0,0 @@
+-/
--- /dev/null
+From de774c85653692b2a901123b5653d0e2101c5353 Mon Sep 17 00:00:00 2001
+From: Al Bailey <Al.Bailey@windriver.com>
+Date: Fri, 4 Oct 2019 12:29:03 -0500
+Subject: [PATCH] Cleaning up pylint settings for cgcs patch
+
+This also adds cgcs_make_patch folder for pylint
+
+pylint is invoked with two different pylint.rc files
+so that different codes can be suppressed for the
+two different code structures.
+
+Change-Id: I0d7a87ed435ed716a3c1ea98f5d7badfd2adac7d
+Story: 2004515
+Task: 37701
+Signed-off-by: Al Bailey <Al.Bailey@windriver.com>
+
+---
+ cgcs-patch/cgcs-patch/pylint.rc | 14 +-
+ cgcs-patch/cgcs-patch/pylint_make_patch.rc | 352 +++++++++++++++++++++++++++++
+ cgcs-patch/cgcs-patch/tox.ini | 6 +-
+ 3 files changed, 365 insertions(+), 7 deletions(-)
+ create mode 100644 cgcs-patch/cgcs-patch/pylint_make_patch.rc
+
+diff --git a/cgcs-patch/cgcs-patch/pylint.rc b/cgcs-patch/cgcs-patch/pylint.rc
+index dc20bb0..812b6b5 100644
+--- a/cgcs-patch/cgcs-patch/pylint.rc
++++ b/cgcs-patch/cgcs-patch/pylint.rc
+@@ -44,8 +44,16 @@ symbols=no
+ # --enable=similarities". If you want to run only the classes checker, but have
+ # no Warning level messages displayed, use"--disable=all --enable=classes
+ # --disable=W"
+-#disable=
+-disable=C, R, W0603, W0613, W0702, W0703, W1201
++# E1111 assignment-from-no-return
++# W0107 unnecessary-pass
++# W0603 global-statement
++# W0612 unused-variable
++# W0613 unused-argument
++# W0703 broad-except
++# W0705 duplicate-except
++# W1201 logging-not-lazy
++# W1505, deprecated-method
++disable=C, R, E1111, W0107, W0603, W0612, W0613, W0703, W0705, W1201, W1505
+
+
+ [REPORTS]
+@@ -61,7 +69,7 @@ output-format=text
+ files-output=no
+
+ # Tells whether to display a full report or only the messages
+-reports=yes
++reports=no
+
+ # Python expression which should return a note less than 10 (10 is the highest
+ # note). You have access to the variables errors warning, statement which
+diff --git a/cgcs-patch/cgcs-patch/pylint_make_patch.rc b/cgcs-patch/cgcs-patch/pylint_make_patch.rc
+new file mode 100644
+index 0000000..ef4e838
+--- /dev/null
++++ b/cgcs-patch/cgcs-patch/pylint_make_patch.rc
+@@ -0,0 +1,352 @@
++[MASTER]
++
++# Specify a configuration file.
++#rcfile=
++
++# Python code to execute, usually for sys.path manipulation such as
++# pygtk.require().
++#init-hook=
++
++# Profiled execution.
++profile=no
++
++# Add files or directories to the blacklist. They should be base names, not
++# paths.
++ignore=CVS
++
++# Pickle collected data for later comparisons.
++persistent=yes
++
++# List of plugins (as comma separated values of python modules names) to load,
++# usually to register additional checkers.
++load-plugins=
++
++# DEPRECATED
++include-ids=no
++
++# DEPRECATED
++symbols=no
++
++
++[MESSAGES CONTROL]
++
++# Enable the message, report, category or checker with the given id(s). You can
++# either give multiple identifier separated by comma (,) or put this option
++# multiple time. See also the "--disable" option for examples.
++#enable=
++
++# Disable the message, report, category or checker with the given id(s). You
++# can either give multiple identifiers separated by comma (,) or put this
++# option multiple times (only on the command line, not in the configuration
++# file where it should appear only once).You can also use "--disable=all" to
++# disable everything first and then reenable specific checks. For example, if
++# you want to run only the similarities checker, you can use "--disable=all
++# --enable=similarities". If you want to run only the classes checker, but have
++# no Warning level messages displayed, use"--disable=all --enable=classes
++# --disable=W"
++# The following are suppressed due to pylint warnings in cgcs_make_patch
++# fixme Use of fixme, todo, etc..
++# E1101 no-member
++# W0101 unreachable
++# W0104 pointless-statement
++# W0107 unnecessary-pass
++# W0212 protected-access
++# W0231 super-init-not-called
++# W0603 global-statement
++# W0612 unused-variable
++# W0613 unused-argument
++# W0622 redefined-builtin
++# W0703 broad-except
++# W1401 anomalous-backslash-in-string
++# W1505, deprecated-method
++disable=C, R, fixme, E1101,
++ W0101, W0104, W0107, W0212, W0231, W0603, W0612, W0613, W0622, W0703,
++ W1401, W1505
++
++[REPORTS]
++
++# Set the output format. Available formats are text, parseable, colorized, msvs
++# (visual studio) and html. You can also give a reporter class, eg
++# mypackage.mymodule.MyReporterClass.
++output-format=text
++
++# Put messages in a separate file for each module / package specified on the
++# command line instead of printing them on stdout. Reports (if any) will be
++# written in a file name "pylint_global.[txt|html]".
++files-output=no
++
++# Tells whether to display a full report or only the messages
++reports=no
++
++# Python expression which should return a note less than 10 (10 is the highest
++# note). You have access to the variables errors warning, statement which
++# respectively contain the number of errors / warnings messages and the total
++# number of statements analyzed. This is used by the global evaluation report
++# (RP0004).
++evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
++
++# Add a comment according to your evaluation note. This is used by the global
++# evaluation report (RP0004).
++comment=no
++
++# Template used to display messages. This is a python new-style format string
++# used to format the message information. See doc for all details
++#msg-template=
++
++
++[BASIC]
++
++# Required attributes for module, separated by a comma
++required-attributes=
++
++# List of builtins function names that should not be used, separated by a comma
++bad-functions=map,filter,apply,input,file
++
++# Good variable names which should always be accepted, separated by a comma
++good-names=i,j,k,ex,Run,_
++
++# Bad variable names which should always be refused, separated by a comma
++bad-names=foo,bar,baz,toto,tutu,tata
++
++# Colon-delimited sets of names that determine each other's naming style when
++# the name regexes allow several styles.
++name-group=
++
++# Include a hint for the correct naming format with invalid-name
++include-naming-hint=no
++
++# Regular expression matching correct function names
++function-rgx=[a-z_][a-z0-9_]{2,30}$
++
++# Naming hint for function names
++function-name-hint=[a-z_][a-z0-9_]{2,30}$
++
++# Regular expression matching correct variable names
++variable-rgx=[a-z_][a-z0-9_]{2,30}$
++
++# Naming hint for variable names
++variable-name-hint=[a-z_][a-z0-9_]{2,30}$
++
++# Regular expression matching correct constant names
++const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
++
++# Naming hint for constant names
++const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
++
++# Regular expression matching correct attribute names
++attr-rgx=[a-z_][a-z0-9_]{2,30}$
++
++# Naming hint for attribute names
++attr-name-hint=[a-z_][a-z0-9_]{2,30}$
++
++# Regular expression matching correct argument names
++argument-rgx=[a-z_][a-z0-9_]{2,30}$
++
++# Naming hint for argument names
++argument-name-hint=[a-z_][a-z0-9_]{2,30}$
++
++# Regular expression matching correct class attribute names
++class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
++
++# Naming hint for class attribute names
++class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
++
++# Regular expression matching correct inline iteration names
++inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
++
++# Naming hint for inline iteration names
++inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
++
++# Regular expression matching correct class names
++class-rgx=[A-Z_][a-zA-Z0-9]+$
++
++# Naming hint for class names
++class-name-hint=[A-Z_][a-zA-Z0-9]+$
++
++# Regular expression matching correct module names
++module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
++
++# Naming hint for module names
++module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
++
++# Regular expression matching correct method names
++method-rgx=[a-z_][a-z0-9_]{2,30}$
++
++# Naming hint for method names
++method-name-hint=[a-z_][a-z0-9_]{2,30}$
++
++# Regular expression which should only match function or class names that do
++# not require a docstring.
++no-docstring-rgx=__.*__
++
++# Minimum line length for functions/classes that require docstrings, shorter
++# ones are exempt.
++docstring-min-length=-1
++
++
++[FORMAT]
++
++# Maximum number of characters on a single line.
++max-line-length=80
++
++# Regexp for a line that is allowed to be longer than the limit.
++ignore-long-lines=^\s*(# )?<?https?://\S+>?$
++
++# Allow the body of an if to be on the same line as the test if there is no
++# else.
++single-line-if-stmt=no
++
++# List of optional constructs for which whitespace checking is disabled
++no-space-check=trailing-comma,dict-separator
++
++# Maximum number of lines in a module
++max-module-lines=1000
++
++# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
++# tab).
++indent-string=' '
++
++# Number of spaces of indent required inside a hanging or continued line.
++indent-after-paren=4
++
++
++[LOGGING]
++
++# Logging modules to check that the string format arguments are in logging
++# function parameter format
++logging-modules=logging
++
++
++[MISCELLANEOUS]
++
++# List of note tags to take in consideration, separated by a comma.
++notes=FIXME,XXX,TODO
++
++
++[SIMILARITIES]
++
++# Minimum lines number of a similarity.
++min-similarity-lines=4
++
++# Ignore comments when computing similarities.
++ignore-comments=yes
++
++# Ignore docstrings when computing similarities.
++ignore-docstrings=yes
++
++# Ignore imports when computing similarities.
++ignore-imports=no
++
++
++[TYPECHECK]
++
++# Tells whether missing members accessed in mixin class should be ignored. A
++# mixin class is detected if its name ends with "mixin" (case insensitive).
++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=
++
++# List of classes names for which member attributes should not be checked
++# (useful for classes with attributes dynamically set).
++ignored-classes=rpm,PKCS1_PSS
++
++# When zope mode is activated, add a predefined set of Zope acquired attributes
++# to generated-members.
++zope=no
++
++# List of members which are set dynamically and missed by pylint inference
++# system, and so shouldn't trigger E0201 when accessed. Python regular
++# expressions are accepted.
++generated-members=REQUEST,acl_users,aq_parent
++
++
++[VARIABLES]
++
++# Tells whether we should check for unused import in __init__ files.
++init-import=no
++
++# A regular expression matching the name of dummy variables (i.e. expectedly
++# not used).
++dummy-variables-rgx=_$|dummy
++
++# List of additional names supposed to be defined in builtins. Remember that
++# you should avoid to define new builtins when possible.
++additional-builtins=
++
++
++[CLASSES]
++
++# List of interface methods to ignore, separated by a comma. This is used for
++# instance to not check methods defines in Zope's Interface base class.
++ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
++
++# List of method names used to declare (i.e. assign) instance attributes.
++defining-attr-methods=__init__,__new__,setUp
++
++# List of valid names for the first argument in a class method.
++valid-classmethod-first-arg=cls
++
++# List of valid names for the first argument in a metaclass class method.
++valid-metaclass-classmethod-first-arg=mcs
++
++
++[DESIGN]
++
++# Maximum number of arguments for function / method
++max-args=5
++
++# Argument names that match this expression will be ignored. Default to name
++# with leading underscore
++ignored-argument-names=_.*
++
++# Maximum number of locals for function / method body
++max-locals=15
++
++# Maximum number of return / yield for function / method body
++max-returns=6
++
++# Maximum number of branch for function / method body
++max-branches=12
++
++# Maximum number of statements in function / method body
++max-statements=50
++
++# Maximum number of parents for a class (see R0901).
++max-parents=7
++
++# Maximum number of attributes for a class (see R0902).
++max-attributes=7
++
++# Minimum number of public methods for a class (see R0903).
++min-public-methods=2
++
++# Maximum number of public methods for a class (see R0904).
++max-public-methods=20
++
++
++[IMPORTS]
++
++# Deprecated modules which should not be used, separated by a comma
++deprecated-modules=regsub,TERMIOS,Bastion,rexec
++
++# Create a graph of every (i.e. internal and external) dependencies in the
++# given file (report RP0402 must not be disabled)
++import-graph=
++
++# Create a graph of external dependencies in the given file (report RP0402 must
++# not be disabled)
++ext-import-graph=
++
++# Create a graph of internal dependencies in the given file (report RP0402 must
++# not be disabled)
++int-import-graph=
++
++
++[EXCEPTIONS]
++
++# Exceptions that will emit a warning when being caught. Defaults to
++# "Exception"
++overgeneral-exceptions=Exception
+diff --git a/cgcs-patch/cgcs-patch/tox.ini b/cgcs-patch/cgcs-patch/tox.ini
+index ba9c568..88e5723 100644
+--- a/cgcs-patch/cgcs-patch/tox.ini
++++ b/cgcs-patch/cgcs-patch/tox.ini
+@@ -76,7 +76,6 @@ exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,release-tag-*
+ enable-extensions = H106 H203 H904
+ max-line-length = 120
+
+-
+ [testenv:flake8]
+ basepython = python3
+ usedevelop = False
+@@ -85,13 +84,12 @@ commands =
+ flake8 {posargs} .
+
+ [testenv:pylint]
++basepython = python3
+ deps = {[testenv]deps}
+ pylint
+-
+-basepython = python2.7
+ sitepackages = False
+-
+ commands = pylint cgcs_patch --rcfile=./pylint.rc
++ pylint cgcs_make_patch --rcfile=./pylint_make_patch.rc
+
+ [testenv:cover]
+ setenv =
--- /dev/null
+From d6675196199ddcefccba0d5d745ac4e93aaecd0f Mon Sep 17 00:00:00 2001
+From: Don Penney <don.penney@windriver.com>
+Date: Wed, 4 Dec 2019 22:26:52 -0500
+Subject: [PATCH] Address python3 pylint errors and warnings
+
+This commit addresses issues detected by the updated python3 pylint:
+- Added a return code to the report_app_dependencies function to
+satisfy the E1111 error reported.
+- Added line-specific pylint disable for unused-argument for cases
+where the inclusion of such arguments in the function signature was
+intentional.
+- Added line-specific pylint disable for the duplicate-except case
+found, as python3 has merged IOError into OSError, while these are
+separate exceptions in python2. Once we're running solely on python3,
+this duplicate exception handling can be dropped.
+
+Change-Id: I96a521288e71948f06ad0c88a12c8f475ed8bc99
+Closes-Bug: 1855180
+Signed-off-by: Don Penney <don.penney@windriver.com>
+
+---
+ cgcs-patch/cgcs-patch/cgcs_patch/api/controllers/root.py | 4 ++--
+ cgcs-patch/cgcs-patch/cgcs_patch/messages.py | 2 +-
+ cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py | 6 +++---
+ cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py | 6 +++---
+ cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py | 8 +++++---
+ cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py | 2 +-
+ cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_controller.py | 2 +-
+ cgcs-patch/cgcs-patch/pylint.rc | 6 +-----
+ 8 files changed, 17 insertions(+), 19 deletions(-)
+
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/api/controllers/root.py b/cgcs-patch/cgcs-patch/cgcs_patch/api/controllers/root.py
+index f1e0262..4c7bd7f 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/api/controllers/root.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/api/controllers/root.py
+@@ -182,7 +182,7 @@ class PatchAPIController(object):
+
+ @expose('json')
+ @expose('query_hosts.xml', content_type='application/xml')
+- def query_hosts(self, *args):
++ def query_hosts(self, *args): # pylint: disable=unused-argument
+ return dict(data=pc.query_host_cache())
+
+ @expose('json')
+@@ -197,7 +197,7 @@ class PatchAPIController(object):
+
+ @expose('json')
+ @expose('query.xml', content_type='application/xml')
+- def host_install(self, *args):
++ def host_install(self, *args): # pylint: disable=unused-argument
+ return dict(error="Deprecated: Use host_install_async")
+
+ @expose('json')
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/messages.py b/cgcs-patch/cgcs-patch/cgcs_patch/messages.py
+index a57ea28..6abc29d 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/messages.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/messages.py
+@@ -60,5 +60,5 @@ class PatchMessage(object):
+ return PATCHMSG_STR[self.msgtype]
+ return "invalid-type"
+
+- def handle(self, sock, addr):
++ def handle(self, sock, addr): # pylint: disable=unused-argument
+ LOG.info("Unhandled message type: %s" % self.msgtype)
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
+index 77930d7..547db52 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
+@@ -150,7 +150,7 @@ class PatchMessageHelloAgent(messages.PatchMessage):
+ resp = PatchMessageHelloAgentAck()
+ resp.send(sock)
+
+- def send(self, sock):
++ def send(self, sock): # pylint: disable=unused-argument
+ LOG.error("Should not get here")
+
+
+@@ -196,7 +196,7 @@ class PatchMessageQueryDetailed(messages.PatchMessage):
+ resp = PatchMessageQueryDetailedResp()
+ resp.send(sock)
+
+- def send(self, sock):
++ def send(self, sock): # pylint: disable=unused-argument
+ LOG.error("Should not get here")
+
+
+@@ -258,7 +258,7 @@ class PatchMessageAgentInstallReq(messages.PatchMessage):
+ resp.status = pa.handle_install()
+ resp.send(sock, addr)
+
+- def send(self, sock):
++ def send(self, sock): # pylint: disable=unused-argument
+ LOG.error("Should not get here")
+
+
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py
+index 705590c..af189fc 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py
+@@ -960,7 +960,7 @@ def wait_for_install_complete(agent_ip):
+ return rc
+
+
+-def host_install(debug, args):
++def host_install(debug, args): # pylint: disable=unused-argument
+ force = False
+ rc = 0
+
+@@ -1072,7 +1072,7 @@ def patch_upload_dir_req(debug, args):
+ return check_rc(req)
+
+
+-def patch_install_local(debug, args):
++def patch_install_local(debug, args): # pylint: disable=unused-argument
+ """ This function is used to trigger patch installation prior to configuration """
+ # Check to see if initial configuration has completed
+ if os.path.isfile(INITIAL_CONTROLLER_CONFIG_COMPLETE):
+@@ -1214,7 +1214,7 @@ def patch_is_available_req(args):
+ return rc
+
+
+-def patch_report_app_dependencies_req(debug, args):
++def patch_report_app_dependencies_req(debug, args): # pylint: disable=unused-argument
+ if len(args) < 2:
+ print_help()
+
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py
+index 4b94a5f..79a6401 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py
+@@ -392,7 +392,7 @@ class PatchMessageHelloAgentAck(messages.PatchMessage):
+ self.agent_state)
+ pc.hosts_lock.release()
+
+- def send(self, sock):
++ def send(self, sock): # pylint: disable=unused-argument
+ LOG.error("Should not get here")
+
+
+@@ -469,7 +469,7 @@ class PatchMessageQueryDetailedResp(messages.PatchMessage):
+ else:
+ pc.hosts_lock.release()
+
+- def send(self, sock):
++ def send(self, sock): # pylint: disable=unused-argument
+ LOG.error("Should not get here")
+
+
+@@ -525,7 +525,7 @@ class PatchMessageAgentInstallResp(messages.PatchMessage):
+ pc.hosts[addr[0]].install_reject_reason = self.reject_reason
+ pc.hosts_lock.release()
+
+- def send(self, sock):
++ def send(self, sock): # pylint: disable=unused-argument
+ LOG.error("Should not get here")
+
+
+@@ -2298,6 +2298,8 @@ class PatchController(PatchService):
+ finally:
+ self.patch_data_lock.release()
+
++ return True
++
+ def query_app_dependencies(self):
+ """
+ Query application dependencies
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py
+index 281a286..e9017f2 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py
+@@ -1253,7 +1253,7 @@ class PatchFile(object):
+ msg = "Failed during patch extraction"
+ LOG.exception(msg)
+ raise PatchFail(msg)
+- except IOError:
++ except IOError: # pylint: disable=duplicate-except
+ msg = "Failed during patch extraction"
+ LOG.exception(msg)
+ raise PatchFail(msg)
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_controller.py b/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_controller.py
+index e2b02c0..1db4b68 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_controller.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/tests/test_patch_controller.py
+@@ -17,6 +17,6 @@ import cgcs_patch.patch_controller # noqa: E402
+ class CgcsPatchControllerTestCase(testtools.TestCase):
+
+ @mock.patch('six.moves.builtins.open')
+- def test_cgcs_patch_controller_instantiate(self, mock_open):
++ def test_cgcs_patch_controller_instantiate(self, mock_open): # pylint: disable=unused-argument
+ # pylint: disable=unused-variable
+ pc = cgcs_patch.patch_controller.PatchController() # noqa: F841
+diff --git a/cgcs-patch/cgcs-patch/pylint.rc b/cgcs-patch/cgcs-patch/pylint.rc
+index 812b6b5..a2d888b 100644
+--- a/cgcs-patch/cgcs-patch/pylint.rc
++++ b/cgcs-patch/cgcs-patch/pylint.rc
+@@ -44,16 +44,12 @@ symbols=no
+ # --enable=similarities". If you want to run only the classes checker, but have
+ # no Warning level messages displayed, use"--disable=all --enable=classes
+ # --disable=W"
+-# E1111 assignment-from-no-return
+ # W0107 unnecessary-pass
+ # W0603 global-statement
+-# W0612 unused-variable
+-# W0613 unused-argument
+ # W0703 broad-except
+-# W0705 duplicate-except
+ # W1201 logging-not-lazy
+ # W1505, deprecated-method
+-disable=C, R, E1111, W0107, W0603, W0612, W0613, W0703, W0705, W1201, W1505
++disable=C, R, W0107, W0603, W0703, W1201, W1505
+
+
+ [REPORTS]
--- /dev/null
+From b206b6574a75dfc3793886529064e3d938759be8 Mon Sep 17 00:00:00 2001
+From: Don Penney <don.penney@windriver.com>
+Date: Mon, 23 Dec 2019 14:36:08 -0500
+Subject: [PATCH] Clean up pylint W1201 logging-not-lazy in cgcs-patch
+
+Change-Id: Ib461890ddf7635645d42660dc07a153e2449b09e
+Story: 2007050
+Task: 37874
+Signed-off-by: Don Penney <don.penney@windriver.com>
+
+---
+ .../cgcs-patch/cgcs_patch/api/controllers/root.py | 2 +-
+ cgcs-patch/cgcs-patch/cgcs_patch/base.py | 4 +-
+ cgcs-patch/cgcs-patch/cgcs_patch/messages.py | 2 +-
+ cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py | 76 +++++++++----------
+ .../cgcs-patch/cgcs_patch/patch_controller.py | 86 +++++++++++-----------
+ cgcs-patch/cgcs-patch/pylint.rc | 3 +-
+ 6 files changed, 86 insertions(+), 87 deletions(-)
+
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/api/controllers/root.py b/cgcs-patch/cgcs-patch/cgcs_patch/api/controllers/root.py
+index 4c7bd7f..883b58d 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/api/controllers/root.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/api/controllers/root.py
+@@ -135,7 +135,7 @@ class PatchAPIController(object):
+ def upload_dir(self, **kwargs):
+ files = []
+ for path in kwargs.values():
+- LOG.info("upload-dir: Retrieving patches from %s" % path)
++ LOG.info("upload-dir: Retrieving patches from %s", path)
+ for f in glob.glob(path + '/*.patch'):
+ if os.path.isfile(f):
+ files.append(f)
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/base.py b/cgcs-patch/cgcs-patch/cgcs_patch/base.py
+index 8e47905..e12e26c 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/base.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/base.py
+@@ -160,11 +160,11 @@ class PatchService(object):
+ if result == self.mcast_addr:
+ return
+ except subprocess.CalledProcessError as e:
+- LOG.error("Command output: %s" % e.output)
++ LOG.error("Command output: %s", e.output)
+ return
+
+ # Close the socket and set it up again
+- LOG.info("Detected missing multicast addr (%s). Reconfiguring" % self.mcast_addr)
++ LOG.info("Detected missing multicast addr (%s). Reconfiguring", self.mcast_addr)
+ while self.setup_socket() is None:
+ LOG.info("Unable to setup sockets. Waiting to retry")
+ time.sleep(5)
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/messages.py b/cgcs-patch/cgcs-patch/cgcs_patch/messages.py
+index 6abc29d..86ff99f 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/messages.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/messages.py
+@@ -61,4 +61,4 @@ class PatchMessage(object):
+ return "invalid-type"
+
+ def handle(self, sock, addr): # pylint: disable=unused-argument
+- LOG.info("Unhandled message type: %s" % self.msgtype)
++ LOG.info("Unhandled message type: %s", self.msgtype)
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
+index 547db52..3abd891 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
+@@ -70,7 +70,7 @@ def setflag(fname):
+ with open(fname, "w") as f:
+ f.write("%d\n" % os.getpid())
+ except Exception:
+- LOG.exception("Failed to update %s flag" % fname)
++ LOG.exception("Failed to update %s flag", fname)
+
+
+ def clearflag(fname):
+@@ -78,7 +78,7 @@ def clearflag(fname):
+ try:
+ os.remove(fname)
+ except Exception:
+- LOG.exception("Failed to clear %s flag" % fname)
++ LOG.exception("Failed to clear %s flag", fname)
+
+
+ def check_install_uuid():
+@@ -101,7 +101,7 @@ def check_install_uuid():
+ controller_install_uuid = str(req.text).rstrip()
+
+ if install_uuid != controller_install_uuid:
+- LOG.error("Local install_uuid=%s doesn't match controller=%s" % (install_uuid, controller_install_uuid))
++ LOG.error("Local install_uuid=%s doesn't match controller=%s", install_uuid, controller_install_uuid)
+ return False
+
+ return True
+@@ -239,7 +239,7 @@ class PatchMessageAgentInstallReq(messages.PatchMessage):
+ messages.PatchMessage.encode(self)
+
+ def handle(self, sock, addr):
+- LOG.info("Handling host install request, force=%s" % self.force)
++ LOG.info("Handling host install request, force=%s", self.force)
+ global pa
+ resp = PatchMessageAgentInstallResp()
+
+@@ -354,7 +354,7 @@ class PatchAgent(PatchService):
+ config = yaml.load(output)
+ except subprocess.CalledProcessError as e:
+ LOG.exception("Failed to query channels")
+- LOG.error("Command output: %s" % e.output)
++ LOG.error("Command output: %s", e.output)
+ return False
+ except Exception:
+ LOG.exception("Failed to query channels")
+@@ -390,23 +390,23 @@ class PatchAgent(PatchService):
+ config[channel].get('baseurl') != ch_baseurl):
+ # Config is invalid
+ add_channel = True
+- LOG.warning("Invalid smart config found for %s" % channel)
++ 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)
++ 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)
++ LOG.warning("Channel %s is missing from config", channel)
+
+ if add_channel:
+- LOG.info("Adding channel %s" % channel)
++ LOG.info("Adding channel %s", channel)
+ cmd_args = ["channel", "--yes", "--add", channel,
+ "type=%s" % ch_type,
+ "name=%s" % ch_name]
+@@ -417,8 +417,8 @@ class PatchAgent(PatchService):
+ 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)
++ LOG.exception("Failed to configure %s channel", channel)
++ LOG.error("Command output: %s", e.output)
+ return False
+
+ updated = True
+@@ -431,7 +431,7 @@ class PatchAgent(PatchService):
+ config = yaml.load(output)
+ except subprocess.CalledProcessError as e:
+ LOG.exception("Failed to query smart config")
+- LOG.error("Command output: %s" % e.output)
++ LOG.error("Command output: %s", e.output)
+ return False
+ except Exception:
+ LOG.exception("Failed to query smart config")
+@@ -441,15 +441,15 @@ class PatchAgent(PatchService):
+ nolinktos = 'rpm-nolinktos'
+ if config.get(nolinktos) is not True:
+ # Set the flag
+- LOG.warning("Setting %s option" % nolinktos)
++ 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)
++ LOG.exception("Failed to configure %s option", nolinktos)
++ LOG.error("Command output: %s", e.output)
+ return False
+
+ updated = True
+@@ -458,15 +458,15 @@ class PatchAgent(PatchService):
+ nosignature = 'rpm-check-signatures'
+ if config.get(nosignature) is not False:
+ # Set the flag
+- LOG.warning("Setting %s option" % nosignature)
++ 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)
++ LOG.exception("Failed to configure %s option", nosignature)
++ LOG.error("Command output: %s", e.output)
+ return False
+
+ updated = True
+@@ -476,7 +476,7 @@ class PatchAgent(PatchService):
+ 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)
++ LOG.error("Command output: %s", e.output)
+ return False
+
+ # Reset the patch op counter to force a detailed query
+@@ -584,7 +584,7 @@ class PatchAgent(PatchService):
+ self.installed[pkgname] = version.split('@')[0]
+ break
+ except subprocess.CalledProcessError:
+- LOG.error("Failed to query installed version of %s" % pkgname)
++ LOG.error("Failed to query installed version of %s", pkgname)
+
+ self.changes = True
+
+@@ -641,7 +641,7 @@ class PatchAgent(PatchService):
+ subprocess.check_output(smart_update, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ LOG.error("Failed to update smartpm")
+- LOG.error("Command output: %s" % e.output)
++ LOG.error("Command output: %s", e.output)
+ # Set a state to "unknown"?
+ return False
+
+@@ -663,7 +663,7 @@ class PatchAgent(PatchService):
+ 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)
++ LOG.error("Failed to query installed pkgs: %s", e.output)
+ # Set a state to "unknown"?
+ return False
+
+@@ -671,7 +671,7 @@ class PatchAgent(PatchService):
+ 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)
++ LOG.error("Failed to query base pkgs: %s", e.output)
+ # Set a state to "unknown"?
+ return False
+
+@@ -679,7 +679,7 @@ class PatchAgent(PatchService):
+ 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)
++ LOG.error("Failed to query patched pkgs: %s", e.output)
+ # Set a state to "unknown"?
+ return False
+
+@@ -722,11 +722,11 @@ class PatchAgent(PatchService):
+ # Look for new packages
+ self.check_groups()
+
+- LOG.info("Patch state query returns %s" % self.changes)
+- LOG.info("Installed: %s" % self.installed)
+- LOG.info("To install: %s" % self.to_install)
+- LOG.info("To remove: %s" % self.to_remove)
+- LOG.info("Missing: %s" % self.missing_pkgs)
++ LOG.info("Patch state query returns %s", self.changes)
++ LOG.info("Installed: %s", self.installed)
++ LOG.info("To install: %s", self.to_install)
++ LOG.info("To remove: %s", self.to_remove)
++ LOG.info("Missing: %s", self.missing_pkgs)
+
+ return True
+
+@@ -794,16 +794,16 @@ class PatchAgent(PatchService):
+ try:
+ if verbose_to_stdout:
+ print("Installing software updates...")
+- LOG.info("Installing: %s" % ", ".join(install_set))
++ 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)
++ 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)
++ LOG.error("Command output: %s", e.output)
+ rc = False
+ if verbose_to_stdout:
+ print("WARNING: Software update failed.")
+@@ -820,16 +820,16 @@ class PatchAgent(PatchService):
+ try:
+ if verbose_to_stdout:
+ print("Handling patch removal...")
+- LOG.info("Removing: %s" % ", ".join(remove_set))
++ 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)
++ 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)
++ LOG.error("Command output: %s", e.output)
+ rc = False
+ if verbose_to_stdout:
+ print("WARNING: Patch removal failed.")
+@@ -862,7 +862,7 @@ class PatchAgent(PatchService):
+ self.node_is_patched = False
+ except subprocess.CalledProcessError as e:
+ LOG.exception("In-Service patch scripts failed")
+- LOG.error("Command output: %s" % e.output)
++ LOG.error("Command output: %s", e.output)
+ # Fail the patching operation
+ rc = False
+
+@@ -1071,7 +1071,7 @@ def main():
+ # In certain cases, the lighttpd server could still be running using
+ # its default port 80, as opposed to the port configured in platform.conf
+ global http_port_real
+- LOG.info("Failed install_uuid check via http_port=%s. Trying with default port 80" % http_port_real)
++ LOG.info("Failed install_uuid check via http_port=%s. Trying with default port 80", http_port_real)
+ http_port_real = 80
+
+ pa.handle_install(verbose_to_stdout=True, disallow_insvc_patch=True)
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py
+index 79a6401..f2b24c8 100644
+--- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py
++++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py
+@@ -137,11 +137,11 @@ class AgentNeighbour(object):
+ if out_of_date != self.out_of_date or requires_reboot != self.requires_reboot:
+ self.out_of_date = out_of_date
+ self.requires_reboot = requires_reboot
+- LOG.info("Agent %s (%s) reporting out_of_date=%s, requires_reboot=%s" % (
+- self.hostname,
+- self.ip,
+- self.out_of_date,
+- self.requires_reboot))
++ LOG.info("Agent %s (%s) reporting out_of_date=%s, requires_reboot=%s",
++ self.hostname,
++ self.ip,
++ self.out_of_date,
++ self.requires_reboot)
+
+ if self.last_query_id != query_id:
+ self.last_query_id = query_id
+@@ -488,7 +488,7 @@ class PatchMessageAgentInstallReq(messages.PatchMessage):
+ LOG.error("Should not get here")
+
+ def send(self, sock):
+- LOG.info("sending install request to node: %s" % self.ip)
++ LOG.info("sending install request to node: %s", self.ip)
+ self.encode()
+ message = json.dumps(self.message)
+ sock.sendto(message, (self.ip, cfg.agent_port))
+@@ -512,7 +512,7 @@ class PatchMessageAgentInstallResp(messages.PatchMessage):
+ messages.PatchMessage.encode(self)
+
+ def handle(self, sock, addr):
+- LOG.info("Handling install resp from %s" % addr[0])
++ LOG.info("Handling install resp from %s", addr[0])
+ global pc
+ # LOG.info("Handling hello ack")
+
+@@ -551,7 +551,7 @@ class PatchMessageDropHostReq(messages.PatchMessage):
+ return
+
+ if self.ip is None:
+- LOG.error("Received PATCHMSG_DROP_HOST_REQ with no ip: %s" % json.dumps(self.data))
++ LOG.error("Received PATCHMSG_DROP_HOST_REQ with no ip: %s", json.dumps(self.data))
+ return
+
+ pc.drop_host(self.ip, sync_nbr=False)
+@@ -602,7 +602,7 @@ class PatchController(PatchService):
+ with open(app_dependency_filename, 'r') as f:
+ self.app_dependencies = json.loads(f.read())
+ except Exception:
+- LOG.exception("Failed to read app dependencies: %s" % app_dependency_filename)
++ LOG.exception("Failed to read app dependencies: %s", app_dependency_filename)
+ else:
+ self.app_dependencies = {}
+
+@@ -658,7 +658,7 @@ class PatchController(PatchService):
+ counter = config.getint('runtime', 'patch_op_counter')
+ self.patch_op_counter = counter
+
+- LOG.info("patch_op_counter is: %d" % self.patch_op_counter)
++ LOG.info("patch_op_counter is: %d", self.patch_op_counter)
+ except configparser.Error:
+ LOG.exception("Failed to read state info")
+
+@@ -679,9 +679,9 @@ class PatchController(PatchService):
+ "rsync://%s/patching/" % host_url,
+ "%s/" % patch_dir],
+ stderr=subprocess.STDOUT)
+- LOG.info("Synced to mate patching via rsync: %s" % output)
++ LOG.info("Synced to mate patching via rsync: %s", output)
+ except subprocess.CalledProcessError as e:
+- LOG.error("Failed to rsync: %s" % e.output)
++ LOG.error("Failed to rsync: %s", e.output)
+ return False
+
+ try:
+@@ -691,9 +691,9 @@ class PatchController(PatchService):
+ "rsync://%s/repo/" % host_url,
+ "%s/" % repo_root_dir],
+ stderr=subprocess.STDOUT)
+- LOG.info("Synced to mate repo via rsync: %s" % output)
++ LOG.info("Synced to mate repo via rsync: %s", output)
+ except subprocess.CalledProcessError:
+- LOG.error("Failed to rsync: %s" % output)
++ LOG.error("Failed to rsync: %s", output)
+ return False
+
+ self.read_state_file()
+@@ -710,7 +710,7 @@ class PatchController(PatchService):
+ with open(app_dependency_filename, 'r') as f:
+ self.app_dependencies = json.loads(f.read())
+ except Exception:
+- LOG.exception("Failed to read app dependencies: %s" % app_dependency_filename)
++ LOG.exception("Failed to read app dependencies: %s", app_dependency_filename)
+ else:
+ self.app_dependencies = {}
+
+@@ -757,7 +757,7 @@ class PatchController(PatchService):
+ continue
+
+ if patch_id not in self.patch_data.metadata:
+- LOG.error("Patch data missing for %s" % patch_id)
++ LOG.error("Patch data missing for %s", patch_id)
+ continue
+
+ # If the patch is on a different release than the host, skip it.
+@@ -811,7 +811,7 @@ class PatchController(PatchService):
+ continue
+
+ if patch_id not in self.patch_data.metadata:
+- LOG.error("Patch data missing for %s" % patch_id)
++ LOG.error("Patch data missing for %s", patch_id)
+ continue
+
+ if personality not in self.patch_data.metadata[patch_id]:
+@@ -835,7 +835,7 @@ class PatchController(PatchService):
+ continue
+
+ if patch_id not in self.patch_data.metadata:
+- LOG.error("Patch data missing for %s" % patch_id)
++ LOG.error("Patch data missing for %s", patch_id)
+ continue
+
+ if personality not in self.patch_data.metadata[patch_id]:
+@@ -902,10 +902,10 @@ class PatchController(PatchService):
+
+ if os.path.exists(semchk):
+ try:
+- LOG.info("Running semantic check: %s" % semchk)
++ LOG.info("Running semantic check: %s", semchk)
+ subprocess.check_output([semchk] + patch_state_args,
+ stderr=subprocess.STDOUT)
+- LOG.info("Semantic check %s passed" % semchk)
++ LOG.info("Semantic check %s passed", semchk)
+ except subprocess.CalledProcessError as e:
+ msg = "Semantic check failed for %s:\n%s" % (patch_id, e.output)
+ LOG.exception(msg)
+@@ -1158,7 +1158,7 @@ class PatchController(PatchService):
+ # Copy the RPMs. If a failure occurs, clean up copied files.
+ copied = []
+ for rpmfile in rpmlist:
+- LOG.info("Copy %s to %s" % (rpmfile, rpmlist[rpmfile]))
++ LOG.info("Copy %s to %s", rpmfile, rpmlist[rpmfile])
+ try:
+ shutil.copy(rpmfile, rpmlist[rpmfile])
+ copied.append(rpmlist[rpmfile])
+@@ -1167,7 +1167,7 @@ class PatchController(PatchService):
+ LOG.exception(msg)
+ # Clean up files
+ for filename in copied:
+- LOG.info("Cleaning up %s" % filename)
++ LOG.info("Cleaning up %s", filename)
+ os.remove(filename)
+
+ raise RpmFail(msg)
+@@ -1206,7 +1206,7 @@ class PatchController(PatchService):
+ "comps.xml",
+ rdir],
+ stderr=subprocess.STDOUT)
+- LOG.info("Repo[%s] updated:\n%s" % (ver, output))
++ LOG.info("Repo[%s] updated:\n%s", ver, output)
+ except subprocess.CalledProcessError:
+ msg = "Failed to update the repo for %s" % ver
+ LOG.exception(msg)
+@@ -1387,7 +1387,7 @@ class PatchController(PatchService):
+ "comps.xml",
+ rdir],
+ stderr=subprocess.STDOUT)
+- LOG.info("Repo[%s] updated:\n%s" % (ver, output))
++ LOG.info("Repo[%s] updated:\n%s", ver, output)
+ except subprocess.CalledProcessError:
+ msg = "Failed to update the repo for %s" % ver
+ LOG.exception(msg)
+@@ -1529,7 +1529,7 @@ class PatchController(PatchService):
+ "comps.xml",
+ repo_dir[release]],
+ stderr=subprocess.STDOUT)
+- LOG.info("Repo[%s] updated:\n%s" % (release, output))
++ LOG.info("Repo[%s] updated:\n%s", release, output)
+ except subprocess.CalledProcessError:
+ msg = "Failed to update the repo for %s" % release
+ LOG.exception(msg)
+@@ -1844,7 +1844,7 @@ class PatchController(PatchService):
+ for patch_id in sorted(patch_ids):
+ if patch_id not in self.patch_data.metadata.keys():
+ errormsg = "%s is unrecognized\n" % patch_id
+- LOG.info("patch_query_dependencies: %s" % errormsg)
++ LOG.info("patch_query_dependencies: %s", errormsg)
+ results["error"] += errormsg
+ failure = True
+ self.patch_data_lock.release()
+@@ -1892,7 +1892,7 @@ class PatchController(PatchService):
+ errormsg = "A commit cannot be performed with non-REL status patches in the system:\n"
+ for patch_id in non_rel_list:
+ errormsg += " %s\n" % patch_id
+- LOG.info("patch_commit rejected: %s" % errormsg)
++ LOG.info("patch_commit rejected: %s", errormsg)
+ results["error"] += errormsg
+ return results
+
+@@ -1901,7 +1901,7 @@ class PatchController(PatchService):
+ for patch_id in sorted(patch_ids):
+ if patch_id not in self.patch_data.metadata.keys():
+ errormsg = "%s is unrecognized\n" % patch_id
+- LOG.info("patch_commit: %s" % errormsg)
++ LOG.info("patch_commit: %s", errormsg)
+ results["error"] += errormsg
+ failure = True
+ self.patch_data_lock.release()
+@@ -1925,7 +1925,7 @@ class PatchController(PatchService):
+ errormsg = "The following patches are not applied and cannot be committed:\n"
+ for patch_id in avail_list:
+ errormsg += " %s\n" % patch_id
+- LOG.info("patch_commit rejected: %s" % errormsg)
++ LOG.info("patch_commit rejected: %s", errormsg)
+ results["error"] += errormsg
+ return results
+
+@@ -2039,7 +2039,7 @@ class PatchController(PatchService):
+ "comps.xml",
+ rdir],
+ stderr=subprocess.STDOUT)
+- LOG.info("Repo[%s] updated:\n%s" % (ver, output))
++ LOG.info("Repo[%s] updated:\n%s", ver, output)
+ except subprocess.CalledProcessError:
+ msg = "Failed to update the repo for %s" % ver
+ LOG.exception(msg)
+@@ -2100,7 +2100,7 @@ class PatchController(PatchService):
+ self.hosts_lock.release()
+ msg = "Unknown host specified: %s" % host_ip
+ msg_error += msg + "\n"
+- LOG.error("Error in host-install: " + msg)
++ LOG.error("Error in host-install: %s", msg)
+ return dict(info=msg_info, warning=msg_warning, error=msg_error)
+
+ msg = "Running host-install for %s (%s), force=%s, async_req=%s" % (host_ip, ip, force, async_req)
+@@ -2128,7 +2128,7 @@ class PatchController(PatchService):
+ # async_req install requested, so return now
+ msg = "Patch installation request sent to %s." % self.hosts[ip].hostname
+ msg_info += msg + "\n"
+- LOG.info("host-install async_req: " + msg)
++ LOG.info("host-install async_req: %s", msg)
+ return dict(info=msg_info, warning=msg_warning, error=msg_error)
+
+ # Now we wait, up to ten mins... TODO: Wait on a condition
+@@ -2141,7 +2141,7 @@ class PatchController(PatchService):
+ self.hosts_lock.release()
+ msg = "Agent expired while waiting: %s" % ip
+ msg_error += msg + "\n"
+- LOG.error("Error in host-install: " + msg)
++ LOG.error("Error in host-install: %s", msg)
+ break
+
+ if not self.hosts[ip].install_pending:
+@@ -2150,17 +2150,17 @@ class PatchController(PatchService):
+ if self.hosts[ip].install_status:
+ msg = "Patch installation was successful on %s." % self.hosts[ip].hostname
+ msg_info += msg + "\n"
+- LOG.info("host-install: " + msg)
++ LOG.info("host-install: %s", msg)
+ elif self.hosts[ip].install_reject_reason:
+ msg = "Patch installation rejected by %s. %s" % (
+ self.hosts[ip].hostname,
+ self.hosts[ip].install_reject_reason)
+ msg_error += msg + "\n"
+- LOG.error("Error in host-install: " + msg)
++ LOG.error("Error in host-install: %s", msg)
+ else:
+ msg = "Patch installation failed on %s." % self.hosts[ip].hostname
+ msg_error += msg + "\n"
+- LOG.error("Error in host-install: " + msg)
++ LOG.error("Error in host-install: %s", msg)
+
+ self.hosts_lock.release()
+ break
+@@ -2172,7 +2172,7 @@ class PatchController(PatchService):
+ if not resp_rx:
+ msg = "Timeout occurred while waiting response from %s." % ip
+ msg_error += msg + "\n"
+- LOG.error("Error in host-install: " + msg)
++ LOG.error("Error in host-install: %s", msg)
+
+ return dict(info=msg_info, warning=msg_warning, error=msg_error)
+
+@@ -2203,7 +2203,7 @@ class PatchController(PatchService):
+ self.hosts_lock.release()
+ msg = "Unknown host specified: %s" % host_ip
+ msg_error += msg + "\n"
+- LOG.error("Error in drop-host: " + msg)
++ LOG.error("Error in drop-host: %s", msg)
+ return dict(info=msg_info, warning=msg_warning, error=msg_error)
+
+ msg = "Running drop-host for %s (%s)" % (host_ip, ip)
+@@ -2272,8 +2272,8 @@ class PatchController(PatchService):
+
+ appname = kwargs.get("app")
+
+- LOG.info("Handling app dependencies report: app=%s, patch_ids=%s" %
+- (appname, ','.join(patch_ids)))
++ LOG.info("Handling app dependencies report: app=%s, patch_ids=%s",
++ appname, ','.join(patch_ids))
+
+ self.patch_data_lock.acquire()
+
+@@ -2516,7 +2516,7 @@ class PatchControllerMainThread(threading.Thread):
+ inputs = [pc.sock_in] + agent_query_conns
+ outputs = []
+
+- # LOG.info("Running select, remaining=%d" % remaining)
++ # LOG.info("Running select, remaining=%d", remaining)
+ rlist, wlist, xlist = select.select(inputs, outputs, inputs, remaining)
+
+ if (len(rlist) == 0 and
+@@ -2641,7 +2641,7 @@ class PatchControllerMainThread(threading.Thread):
+ for n in nbrs:
+ # Age out controllers after 2 minutes
+ if pc.controller_neighbours[n].get_age() >= 120:
+- LOG.info("Aging out controller %s from table" % n)
++ LOG.info("Aging out controller %s from table", n)
+ del pc.controller_neighbours[n]
+ pc.controller_neighbours_lock.release()
+
+@@ -2650,7 +2650,7 @@ class PatchControllerMainThread(threading.Thread):
+ for n in nbrs:
+ # Age out hosts after 1 hour
+ if pc.hosts[n].get_age() >= 3600:
+- LOG.info("Aging out host %s from table" % n)
++ LOG.info("Aging out host %s from table", n)
+ del pc.hosts[n]
+ for patch_id in pc.interim_state.keys():
+ if n in pc.interim_state[patch_id]:
+diff --git a/cgcs-patch/cgcs-patch/pylint.rc b/cgcs-patch/cgcs-patch/pylint.rc
+index a2d888b..57a9829 100644
+--- a/cgcs-patch/cgcs-patch/pylint.rc
++++ b/cgcs-patch/cgcs-patch/pylint.rc
+@@ -47,9 +47,8 @@ symbols=no
+ # W0107 unnecessary-pass
+ # W0603 global-statement
+ # W0703 broad-except
+-# W1201 logging-not-lazy
+ # W1505, deprecated-method
+-disable=C, R, W0107, W0603, W0703, W1201, W1505
++disable=C, R, W0107, W0603, W0703, W1505
+
+
+ [REPORTS]
--- /dev/null
+From 1522e384f8a9cb5e7d3e42b37aec11e5674c4436 Mon Sep 17 00:00:00 2001
+From: Don Penney <don.penney@windriver.com>
+Date: Thu, 2 Jan 2020 17:36:21 -0500
+Subject: [PATCH] Migrate patch-agent to use DNF for swmgmt
+
+As the smart package manager is not supported under python3, we're
+migrating the patch-agent to use the python2 DNF libraries in
+preparation for CentOS 8. This impacts how the patch-agent queries the
+repos and manages installed software, but is done without changing how
+the patch-agent and patch-controller exchange information, to ensure
+we don't impact cross-version communication in an upgrade scenario.
+
+Depends-On: https://review.opendev.org/700960
+Change-Id: I00729a487c24ba5c182a9a2a48e2024be9260978
+Story: 2006227
+Task: 37932
+Signed-off-by: Don Penney <don.penney@windriver.com>
+
+---
+ cgcs-patch/centos/cgcs-patch.spec | 17 +-
+ cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py | 625 +++++++--------------
+ .../cgcs-patch/cgcs_patch/patch_functions.py | 7 +-
+ .../cgcs_patch/tests/test_patch_agent.py | 9 +
+ cgcs-patch/cgcs-patch/pylint.rc | 5 +-
+ cgcs-patch/cgcs-patch/test-requirements.txt | 1 -
+ 6 files changed, 242 insertions(+), 422 deletions(-)
+
+diff --git a/cgcs-patch/centos/cgcs-patch.spec b/cgcs-patch/centos/cgcs-patch.spec
+index f834447..4ed3f99 100644
+--- a/cgcs-patch/centos/cgcs-patch.spec
++++ b/cgcs-patch/centos/cgcs-patch.spec
+@@ -1,4 +1,4 @@
+-Summary: TIS Platform Patching
++Summary: StarlingX Platform Patching
+ Name: cgcs-patch
+ Version: 1.0
+ Release: %{tis_patch_ver}%{?_tis_dist}
+@@ -16,11 +16,12 @@ BuildRequires: systemd-units
+ BuildRequires: systemd-devel
+ Requires: python-devel
+ Requires: python-crypto
+-Requires: python-smartpm
++Requires: dnf
++Requires: python-dnf
+ Requires: /bin/bash
+
+ %description
+-TIS Platform Patching
++StarlingX Platform Patching
+
+ %define pythonroot /usr/lib64/python2.7/site-packages
+
+@@ -110,10 +111,10 @@ install -m 644 dist/*.whl $RPM_BUILD_ROOT/wheels/
+ %{buildroot}%{_sbindir}/upgrade-start-pkg-extract
+
+ %clean
+-rm -rf $RPM_BUILD_ROOT
++rm -rf $RPM_BUILD_ROOT
+
+ %package -n cgcs-patch-controller
+-Summary: TIS Platform Patching
++Summary: StarlingX Platform Patching
+ Group: base
+ Requires: /usr/bin/env
+ Requires: /bin/sh
+@@ -123,7 +124,7 @@ Requires(post): /usr/bin/env
+ Requires(post): /bin/sh
+
+ %description -n cgcs-patch-controller
+-TIS Platform Patching
++StarlingX Platform Patching
+
+ %post -n cgcs-patch-controller
+ /usr/bin/systemctl enable sw-patch-controller.service
+@@ -131,7 +132,7 @@ TIS Platform Patching
+
+
+ %package -n cgcs-patch-agent
+-Summary: TIS Platform Patching
++Summary: StarlingX Platform Patching
+ Group: base
+ Requires: /usr/bin/env
+ Requires: /bin/sh
+@@ -139,7 +140,7 @@ Requires(post): /usr/bin/env
+ Requires(post): /bin/sh
+
+ %description -n cgcs-patch-agent
+-TIS Platform Patching
++StarlingX Platform Patching
+
+ %post -n cgcs-patch-agent
+ /usr/bin/systemctl enable sw-patch-agent.service
+diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_agent.py
+index 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
+-
SRC_URI = " \
git://opendev.org/starlingx/update.git;protocol=${PROTOCOL};rev=${SRCREV};branch=${BRANCH} \
file://0001-Remove-use-of-rpmUtils.miscutils-from-cgcs-patch.patch \
+ file://0002-Cleanup-smartpm-references.patch \
+ file://0003-Cleaning-up-pylint-settings-for-cgcs-patch.patch \
+ file://0004-Address-python3-pylint-errors-and-warnings.patch \
+ file://0005-Clean-up-pylint-W1201-logging-not-lazy-in-cgcs-patch.patch \
+ file://0006-Migrate-patch-agent-to-use-DNF-for-swmgmt.patch \
"
DEPENDS = " \