stx-update: migrate patch-agent to use dnf instead of smart 21/3621/1
authorJackie Huang <jackie.huang@windriver.com>
Wed, 6 May 2020 12:31:01 +0000 (20:31 +0800)
committerJackie Huang <jackie.huang@windriver.com>
Thu, 7 May 2020 12:51:48 +0000 (20:51 +0800)
smart is not availble in yocto any more and is replaced by dnf, which is
the same  situation in the later stx release on centos 8, so backport
seceral patches from later stx release to migrate patch-agent to use dnf
instead of smart.

Issue-ID: INF-84
Issue-ID: INF-91
Signed-off-by: Jackie Huang <jackie.huang@windriver.com>
Change-Id: I1a53ffae6c53a638cf14cd4354e2c5d8bf7f6440

meta-stx/recipes-core/stx-update/files/0001-Remove-use-of-rpmUtils.miscutils-from-cgcs-patch.patch
meta-stx/recipes-core/stx-update/files/0002-Cleanup-smartpm-references.patch [new file with mode: 0644]
meta-stx/recipes-core/stx-update/files/0003-Cleaning-up-pylint-settings-for-cgcs-patch.patch [new file with mode: 0644]
meta-stx/recipes-core/stx-update/files/0004-Address-python3-pylint-errors-and-warnings.patch [new file with mode: 0644]
meta-stx/recipes-core/stx-update/files/0005-Clean-up-pylint-W1201-logging-not-lazy-in-cgcs-patch.patch [new file with mode: 0644]
meta-stx/recipes-core/stx-update/files/0006-Migrate-patch-agent-to-use-DNF-for-swmgmt.patch [new file with mode: 0644]
meta-stx/recipes-core/stx-update/stx-update.bb

index 8fdfd01..fbda959 100644 (file)
@@ -1,4 +1,4 @@
-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
@@ -14,6 +14,7 @@ Change-Id: I2a04f3dbf85d62c87ca1afcf988b672aafceb642
 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 +++---
@@ -24,7 +25,7 @@ Signed-off-by: Don Penney <don.penney@windriver.com>
  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
@@ -61,7 +62,7 @@ index ed6f67e..547db52 100644
                      # 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
@@ -85,7 +86,7 @@ index 60b2b14..79a6401 100644
                      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):
@@ -127,7 +128,7 @@ index c953e4f..bd1eef9 100644
  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
@@ -168,6 +169,3 @@ index a5eb8d4..653c65a 100644
 +        for ver, expected in versions.items():
 +            result = cgcs_patch.patch_functions.parse_pkgver(ver)
 +            self.assertEqual(result, expected)
--- 
-2.7.4
-
diff --git a/meta-stx/recipes-core/stx-update/files/0002-Cleanup-smartpm-references.patch b/meta-stx/recipes-core/stx-update/files/0002-Cleanup-smartpm-references.patch
new file mode 100644 (file)
index 0000000..ac257a2
--- /dev/null
@@ -0,0 +1,269 @@
+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 @@
+-/
diff --git a/meta-stx/recipes-core/stx-update/files/0003-Cleaning-up-pylint-settings-for-cgcs-patch.patch b/meta-stx/recipes-core/stx-update/files/0003-Cleaning-up-pylint-settings-for-cgcs-patch.patch
new file mode 100644 (file)
index 0000000..b5e8c7a
--- /dev/null
@@ -0,0 +1,441 @@
+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 =
diff --git a/meta-stx/recipes-core/stx-update/files/0004-Address-python3-pylint-errors-and-warnings.patch b/meta-stx/recipes-core/stx-update/files/0004-Address-python3-pylint-errors-and-warnings.patch
new file mode 100644 (file)
index 0000000..faa2d3b
--- /dev/null
@@ -0,0 +1,213 @@
+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]
diff --git a/meta-stx/recipes-core/stx-update/files/0005-Clean-up-pylint-W1201-logging-not-lazy-in-cgcs-patch.patch b/meta-stx/recipes-core/stx-update/files/0005-Clean-up-pylint-W1201-logging-not-lazy-in-cgcs-patch.patch
new file mode 100644 (file)
index 0000000..ee81e58
--- /dev/null
@@ -0,0 +1,673 @@
+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]
diff --git a/meta-stx/recipes-core/stx-update/files/0006-Migrate-patch-agent-to-use-DNF-for-swmgmt.patch b/meta-stx/recipes-core/stx-update/files/0006-Migrate-patch-agent-to-use-DNF-for-swmgmt.patch
new file mode 100644 (file)
index 0000000..cf5351c
--- /dev/null
@@ -0,0 +1,933 @@
+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
+-
index 32697ea..a5a9df4 100644 (file)
@@ -28,6 +28,11 @@ LIC_FILES_CHKSUM = "file://LICENSE;md5=3b83ef96387f14655fc854ddc3c6bd57"
 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 = " \