Debian: Remove dnf calls from sw-patch
Debian uses ostree for software patching. Centos uses rpm and dnf for software patching. The dnf API to track rpms is being removed from the debian code. Calls to ostree interaction may be added later. Note: One dnf call remains in upgrade-start-pkg-extract This has been left, as upgrade for Debian is still being investigated. This change eliminates sw-patch-agent service failures caused by calling dnf and therefore also eliminates the 200.006 major alarm raised due to service failures. Test Plan: PASS: Build / Boot / Bootstrap / Unlock AIO-SX PASS: Upload / Apply a patch on AIO-SX PASS: reboot after applying a patch and verify no alarms or excessive error logs for patching. PASS: Delete a patch on AIO-SX Depends-On: https://review.opendev.org/c/starlingx/update/+/840721 Story: 2009969 Task: 45242 Co-Authored-By: Jessica Castelino <jessica.castelino@windriver.com> Signed-off-by: Al Bailey <al.bailey@windriver.com> Change-Id: I823b45efb90b4106fc8b684ee49453e2354e9315
This commit is contained in:
parent
77a7383ab7
commit
1a41856642
|
@ -1,19 +1,10 @@
|
|||
"""
|
||||
Copyright (c) 2014-2019 Wind River Systems, Inc.
|
||||
Copyright (c) 2014-2022 Wind River Systems, Inc.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
|
||||
import dnf
|
||||
import dnf.callback
|
||||
import dnf.comps
|
||||
import dnf.exceptions
|
||||
import dnf.rpm
|
||||
import dnf.sack
|
||||
import dnf.transaction
|
||||
import json
|
||||
import libdnf.transaction
|
||||
import os
|
||||
import random
|
||||
import requests
|
||||
|
@ -54,14 +45,6 @@ pa = None
|
|||
|
||||
http_port_real = http_port
|
||||
|
||||
# DNF commands
|
||||
dnf_cmd = ['/bin/dnf']
|
||||
dnf_quiet = dnf_cmd + ['--quiet']
|
||||
dnf_makecache = dnf_quiet + ['makecache',
|
||||
'--disablerepo="*"',
|
||||
'--enablerepo', 'platform-base',
|
||||
'--enablerepo', 'platform-updates']
|
||||
|
||||
|
||||
def setflag(fname):
|
||||
try:
|
||||
|
@ -284,46 +267,6 @@ 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)
|
||||
|
@ -333,14 +276,9 @@ 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.duplicated_pkgs = {}
|
||||
self.patch_op_counter = 0
|
||||
self.node_is_patched = os.path.exists(node_is_patched_file)
|
||||
|
@ -349,7 +287,6 @@ class PatchAgent(PatchService):
|
|||
self.state = constants.PATCH_AGENT_STATE_IDLE
|
||||
self.last_config_audit = 0
|
||||
self.rejection_timestamp = 0
|
||||
self.dnfb = None
|
||||
self.last_repo_revision = None
|
||||
|
||||
# Check state flags
|
||||
|
@ -405,28 +342,13 @@ class PatchAgent(PatchService):
|
|||
|
||||
return output
|
||||
|
||||
def dnf_reset_client(self):
|
||||
if self.dnfb is not None:
|
||||
self.dnfb.close()
|
||||
self.dnfb = None
|
||||
|
||||
self.dnfb = dnf.Base()
|
||||
self.dnfb.conf.substitutions['infra'] = 'stock'
|
||||
|
||||
# Reset default installonlypkgs list
|
||||
self.dnfb.conf.installonlypkgs = []
|
||||
|
||||
self.dnfb.read_all_repos()
|
||||
|
||||
# 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:
|
||||
repo.disable()
|
||||
|
||||
# Read repo info
|
||||
self.dnfb.fill_sack()
|
||||
def getOSTreeRevision(self, feed_type):
|
||||
""" Get the ostree revision for a particular feed """
|
||||
# todo(abailey): is a software version parameter required to support upgrade?
|
||||
# Probably this method will invoke something like:
|
||||
# ostree pull --commit-metadata-only --depth=1
|
||||
LOG.info("Querying OSTree Revision from %s", feed_type)
|
||||
return "UNDER CONSTRUCTION"
|
||||
|
||||
def query(self, check_revision=False):
|
||||
""" Check current patch state """
|
||||
|
@ -434,42 +356,12 @@ class PatchAgent(PatchService):
|
|||
LOG.info("Failed install_uuid check. Skipping query")
|
||||
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(dnf_makecache, stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
LOG.error("Failed to run dnf makecache")
|
||||
LOG.error("Command output: %s", e.output)
|
||||
# Set a state to "unknown"?
|
||||
return False
|
||||
|
||||
self.dnf_reset_client()
|
||||
current_repo_revision = self.dnfb.repos['platform-updates']._repo.getRevision() # pylint: disable=protected-access
|
||||
|
||||
if check_revision and self.last_repo_revision is not None:
|
||||
# We're expecting the revision to be updated.
|
||||
# If it's not, we ended up getting a cached repomd query.
|
||||
if current_repo_revision == self.last_repo_revision:
|
||||
LOG.info("makecache returned same revision as previous (%s). Retry after one second",
|
||||
current_repo_revision)
|
||||
time.sleep(1)
|
||||
try:
|
||||
subprocess.check_output(dnf_makecache, stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as e:
|
||||
LOG.error("Failed to run dnf makecache")
|
||||
LOG.error("Command output: %s", e.output)
|
||||
# Set a state to "unknown"?
|
||||
return False
|
||||
|
||||
self.dnf_reset_client()
|
||||
current_repo_revision = self.dnfb.repos['platform-updates']._repo.getRevision() # pylint: disable=protected-access
|
||||
if current_repo_revision != self.last_repo_revision:
|
||||
LOG.info("Stale repo revision id corrected with retry. New id: %s",
|
||||
current_repo_revision)
|
||||
current_repo_revision = self.getOSTreeRevision('platform-updates')
|
||||
if check_revision:
|
||||
# The revision is a SHA for ostree
|
||||
# todo(jcasteli): do we need check_revision
|
||||
# since there is no caching or retry code
|
||||
LOG.info("repo revision id: %s", current_repo_revision)
|
||||
|
||||
self.last_repo_revision = current_repo_revision
|
||||
|
||||
|
@ -477,83 +369,17 @@ class PatchAgent(PatchService):
|
|||
self.query_id = random.random()
|
||||
|
||||
self.changes = False
|
||||
self.installed_dnf = []
|
||||
self.installed = {}
|
||||
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 = dnf.sack._rpmdb_sack(self.dnfb).query().installed() # pylint: disable=protected-access
|
||||
avail = self.dnfb.sack.query().available().latest()
|
||||
|
||||
# Check for packages with multiple installed versions
|
||||
self.duplicated_pkgs = {}
|
||||
for pkg in pkgs_installed:
|
||||
pkglist = pkgs_installed.filter(name=pkg.name, arch=pkg.arch)
|
||||
if len(pkglist) > 1:
|
||||
if pkg.name not in self.duplicated_pkgs:
|
||||
self.duplicated_pkgs[pkg.name] = {}
|
||||
if pkg.arch not in self.duplicated_pkgs[pkg.name]:
|
||||
self.duplicated_pkgs[pkg.name][pkg.arch] = list(map(PatchAgent.pkgobj_to_version_str, pkglist))
|
||||
LOG.warn("Duplicate packages installed: %s %s",
|
||||
pkg.name, ", ".join(self.duplicated_pkgs[pkg.name][pkg.arch]))
|
||||
# todo(jcasteli): Can a patch contain commit SHAs from an easlier patch
|
||||
|
||||
# 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:
|
||||
highest = avail.filter(name=pkg.name, arch=pkg.arch)
|
||||
if highest:
|
||||
highest_pkg = highest[0]
|
||||
|
||||
if pkg.evr_eq(highest_pkg):
|
||||
continue
|
||||
|
||||
if pkg.evr_gt(highest_pkg):
|
||||
self.to_downgrade_dnf.append(highest_pkg)
|
||||
else:
|
||||
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.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)
|
||||
LOG.info("To install: %s", self.to_install)
|
||||
|
@ -564,35 +390,6 @@ 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
|
||||
|
@ -648,82 +445,45 @@ class PatchAgent(PatchService):
|
|||
changed = False
|
||||
rc = True
|
||||
|
||||
if len(self.duplicated_pkgs) > 0:
|
||||
LOG.error("Duplicate installed packages found. Manual recovery is required.")
|
||||
rc = False
|
||||
# todo(jcasteli): Are there things to install?
|
||||
# if so, set changed = True
|
||||
|
||||
if changed:
|
||||
# todo(jcasteli): See if the update is successful
|
||||
# set rc=False if it is not successful
|
||||
pass
|
||||
else:
|
||||
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)
|
||||
if verbose_to_stdout:
|
||||
print("Nothing to install.")
|
||||
LOG.info("Nothing to install")
|
||||
|
||||
for pkg in self.to_downgrade_dnf:
|
||||
self.dnfb.package_downgrade(pkg)
|
||||
if changed and rc:
|
||||
# Update the node_is_patched flag
|
||||
setflag(node_is_patched_file)
|
||||
|
||||
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:
|
||||
transaction_rc = self.resolve_dnf_transaction()
|
||||
except dnf.exceptions.DepsolveError:
|
||||
LOG.exception("Failures resolving dependencies in transaction")
|
||||
except dnf.exceptions.DownloadError:
|
||||
LOG.exception("Failures downloading in transaction")
|
||||
except dnf.exceptions.Error:
|
||||
LOG.exception("Failure resolving transaction")
|
||||
|
||||
if not transaction_rc:
|
||||
LOG.error("Failures occurred during transaction")
|
||||
rc = False
|
||||
if verbose_to_stdout:
|
||||
print("WARNING: Software update failed.")
|
||||
self.node_is_patched = True
|
||||
if verbose_to_stdout:
|
||||
print("This node has been patched.")
|
||||
|
||||
if os.path.exists(node_is_patched_rr_file):
|
||||
LOG.info("Reboot is required. Skipping patch-scripts")
|
||||
elif disallow_insvc_patch:
|
||||
LOG.info("Disallowing patch-scripts. Treating as reboot-required")
|
||||
setflag(node_is_patched_rr_file)
|
||||
else:
|
||||
if verbose_to_stdout:
|
||||
print("Nothing to install.")
|
||||
LOG.info("Nothing to install")
|
||||
LOG.info("Running in-service patch-scripts")
|
||||
|
||||
if changed and rc:
|
||||
# Update the node_is_patched flag
|
||||
setflag(node_is_patched_file)
|
||||
try:
|
||||
subprocess.check_output(run_insvc_patch_scripts_cmd, stderr=subprocess.STDOUT)
|
||||
|
||||
self.node_is_patched = True
|
||||
if verbose_to_stdout:
|
||||
print("This node has been patched.")
|
||||
|
||||
if os.path.exists(node_is_patched_rr_file):
|
||||
LOG.info("Reboot is required. Skipping patch-scripts")
|
||||
elif disallow_insvc_patch:
|
||||
LOG.info("Disallowing patch-scripts. Treating as reboot-required")
|
||||
setflag(node_is_patched_rr_file)
|
||||
else:
|
||||
LOG.info("Running in-service patch-scripts")
|
||||
|
||||
try:
|
||||
subprocess.check_output(run_insvc_patch_scripts_cmd, stderr=subprocess.STDOUT)
|
||||
|
||||
# Clear the node_is_patched flag, since we've handled it in-service
|
||||
clearflag(node_is_patched_file)
|
||||
self.node_is_patched = False
|
||||
except subprocess.CalledProcessError as e:
|
||||
LOG.exception("In-Service patch scripts failed")
|
||||
LOG.error("Command output: %s", e.output)
|
||||
# Fail the patching operation
|
||||
rc = False
|
||||
# Clear the node_is_patched flag, since we've handled it in-service
|
||||
clearflag(node_is_patched_file)
|
||||
self.node_is_patched = False
|
||||
except subprocess.CalledProcessError as e:
|
||||
LOG.exception("In-Service patch scripts failed")
|
||||
LOG.error("Command output: %s", e.output)
|
||||
# Fail the patching operation
|
||||
rc = False
|
||||
|
||||
# Clear the in-service patch dirs
|
||||
if os.path.exists(insvc_patch_scripts):
|
||||
|
@ -916,7 +676,7 @@ class PatchAgent(PatchService):
|
|||
def main():
|
||||
global pa
|
||||
|
||||
configure_logging(dnf_log=True)
|
||||
configure_logging()
|
||||
|
||||
cfg.read_config()
|
||||
|
||||
|
|
|
@ -1091,7 +1091,7 @@ def patch_install_local(debug, args): # pylint: disable=unused-argument
|
|||
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 dnf repos work.
|
||||
# to alias controller to localhost
|
||||
# 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.
|
||||
|
||||
|
|
|
@ -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, dnf_log=False):
|
||||
def configure_logging(logtofile=True, level=logging.INFO):
|
||||
if logtofile:
|
||||
my_exec = os.path.basename(sys.argv[0])
|
||||
|
||||
|
@ -85,10 +85,6 @@ def configure_logging(logtofile=True, level=logging.INFO, dnf_log=False):
|
|||
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:
|
||||
|
|
|
@ -3,24 +3,9 @@
|
|||
#
|
||||
# Copyright (c) 2019-2022 Wind River Systems, Inc.
|
||||
#
|
||||
|
||||
import mock
|
||||
import sys
|
||||
import testtools
|
||||
|
||||
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()
|
||||
|
||||
# Need to suppress E402 because the sys.modules need
|
||||
# to be mocked before importing patch_agent
|
||||
from cgcs_patch import patch_agent # noqa: E402
|
||||
from cgcs_patch import patch_agent
|
||||
|
||||
|
||||
class CgcsPatchAgentTestCase(testtools.TestCase):
|
||||
|
|
|
@ -319,7 +319,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=dnf,libdnf
|
||||
ignored-modules=
|
||||
|
||||
# List of classes names for which member attributes should not be checked
|
||||
# (useful for classes with attributes dynamically set).
|
||||
|
|
Loading…
Reference in New Issue