""" Copyright (c) 2014-2019 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 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 LOG import cgcs_patch.config as cfg from cgcs_patch.base import PatchService import cgcs_patch.utils as utils import cgcs_patch.messages as messages import cgcs_patch.constants as constants from tsconfig.tsconfig import http_port from tsconfig.tsconfig import install_uuid from tsconfig.tsconfig import subfunctions from tsconfig.tsconfig import SW_VERSION pidfile_path = "/var/run/patch_agent.pid" node_is_patched_file = "/var/run/node_is_patched" node_is_patched_rr_file = "/var/run/node_is_patched_rr" patch_installing_file = "/var/run/patch_installing" patch_failed_file = "/var/run/patch_install_failed" node_is_locked_file = "/var/run/.node_locked" insvc_patch_scripts = "/run/patching/patch-scripts" insvc_patch_flags = "/run/patching/patch-flags" insvc_patch_restart_agent = "/run/patching/.restart.patch-agent" run_insvc_patch_scripts_cmd = "/usr/sbin/run-patch-scripts" 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: with open(fname, "w") as f: f.write("%d\n" % os.getpid()) except Exception: LOG.exception("Failed to update %s flag", fname) def clearflag(fname): if os.path.exists(fname): try: os.remove(fname) except Exception: LOG.exception("Failed to clear %s flag", fname) def check_install_uuid(): controller_install_uuid_url = "http://controller:%s/feed/rel-%s/install_uuid" % (http_port_real, SW_VERSION) try: req = requests.get(controller_install_uuid_url) if req.status_code != 200: # If we're on controller-1, controller-0 may not have the install_uuid # matching this release, if we're in an upgrade. If the file doesn't exist, # bypass this check if socket.gethostname() == "controller-1": return True LOG.error("Failed to get install_uuid from controller") return False except requests.ConnectionError: LOG.error("Failed to connect to controller") return False 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) return False return True class PatchMessageHelloAgent(messages.PatchMessage): def __init__(self): messages.PatchMessage.__init__(self, messages.PATCHMSG_HELLO_AGENT) self.patch_op_counter = 0 def decode(self, data): messages.PatchMessage.decode(self, data) if 'patch_op_counter' in data: self.patch_op_counter = data['patch_op_counter'] def encode(self): messages.PatchMessage.encode(self) def handle(self, sock, addr): # Send response # # If a user tries to do a host-install on an unlocked node, # without bypassing the lock check (either via in-service # patch or --force option), the agent will set its state # to Install-Rejected in order to report back the rejection. # However, since this should just be a transient state, # we don't want the client reporting the Install-Rejected # state indefinitely, so reset it to Idle after a minute or so. # if pa.state == constants.PATCH_AGENT_STATE_INSTALL_REJECTED: if os.path.exists(node_is_locked_file): # Node has been locked since rejected attempt. Reset the state pa.state = constants.PATCH_AGENT_STATE_IDLE elif (time.time() - pa.rejection_timestamp) > 60: # Rejected state for more than a minute. Reset it. pa.state = constants.PATCH_AGENT_STATE_IDLE if self.patch_op_counter > 0: pa.handle_patch_op_counter(self.patch_op_counter) resp = PatchMessageHelloAgentAck() resp.send(sock) def send(self, sock): # pylint: disable=unused-argument LOG.error("Should not get here") class PatchMessageHelloAgentAck(messages.PatchMessage): def __init__(self): messages.PatchMessage.__init__(self, messages.PATCHMSG_HELLO_AGENT_ACK) def encode(self): global pa messages.PatchMessage.encode(self) self.message['query_id'] = pa.query_id self.message['out_of_date'] = pa.changes self.message['hostname'] = socket.gethostname() self.message['requires_reboot'] = pa.node_is_patched self.message['patch_failed'] = pa.patch_failed self.message['sw_version'] = SW_VERSION self.message['state'] = pa.state def handle(self, sock, addr): LOG.error("Should not get here") def send(self, sock): global pa self.encode() message = json.dumps(self.message) sock.sendto(str.encode(message), (pa.controller_address, cfg.controller_port)) class PatchMessageQueryDetailed(messages.PatchMessage): def __init__(self): messages.PatchMessage.__init__(self, messages.PATCHMSG_QUERY_DETAILED) def decode(self, data): messages.PatchMessage.decode(self, data) def encode(self): # Nothing to add to the HELLO_AGENT, so just call the super class messages.PatchMessage.encode(self) def handle(self, sock, addr): # Send response LOG.info("Handling detailed query") resp = PatchMessageQueryDetailedResp() resp.send(sock) def send(self, sock): # pylint: disable=unused-argument LOG.error("Should not get here") class PatchMessageQueryDetailedResp(messages.PatchMessage): def __init__(self): messages.PatchMessage.__init__(self, messages.PATCHMSG_QUERY_DETAILED_RESP) def encode(self): global pa messages.PatchMessage.encode(self) self.message['installed'] = pa.installed self.message['to_remove'] = pa.to_remove self.message['missing_pkgs'] = pa.missing_pkgs self.message['duplicated_pkgs'] = pa.duplicated_pkgs self.message['nodetype'] = cfg.nodetype self.message['sw_version'] = SW_VERSION self.message['subfunctions'] = subfunctions self.message['state'] = pa.state def handle(self, sock, addr): LOG.error("Should not get here") def send(self, sock): self.encode() message = json.dumps(self.message) sock.sendall(str.encode(message)) class PatchMessageAgentInstallReq(messages.PatchMessage): def __init__(self): messages.PatchMessage.__init__(self, messages.PATCHMSG_AGENT_INSTALL_REQ) self.force = False def decode(self, data): messages.PatchMessage.decode(self, data) if 'force' in data: self.force = data['force'] def encode(self): # Nothing to add to the HELLO_AGENT, so just call the super class messages.PatchMessage.encode(self) def handle(self, sock, addr): LOG.info("Handling host install request, force=%s", self.force) global pa resp = PatchMessageAgentInstallResp() if not os.path.exists(node_is_locked_file): if self.force: LOG.info("Installing on unlocked node, with force option") else: LOG.info("Rejecting install request on unlocked node") pa.state = constants.PATCH_AGENT_STATE_INSTALL_REJECTED pa.rejection_timestamp = time.time() resp.status = False resp.reject_reason = 'Node must be locked.' resp.send(sock, addr) return resp.status = pa.handle_install() resp.send(sock, addr) def send(self, sock): # pylint: disable=unused-argument LOG.error("Should not get here") class PatchMessageAgentInstallResp(messages.PatchMessage): def __init__(self): messages.PatchMessage.__init__(self, messages.PATCHMSG_AGENT_INSTALL_RESP) self.status = False self.reject_reason = None def encode(self): global pa messages.PatchMessage.encode(self) self.message['status'] = self.status if self.reject_reason is not None: self.message['reject_reason'] = self.reject_reason def handle(self, sock, addr): LOG.error("Should not get here") def send(self, sock, addr): address = (addr[0], cfg.controller_port) self.encode() message = json.dumps(self.message) sock.sendto(str.encode(message), address) # Send a hello ack to follow it resp = PatchMessageHelloAgentAck() 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) self.sock_out = None self.sock_in = None self.controller_address = None 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) self.node_is_patched_timestamp = 0 self.query_id = 0 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 if os.path.exists(patch_installing_file): # We restarted while installing. Change to failed setflag(patch_failed_file) os.remove(patch_installing_file) if os.path.exists(patch_failed_file): self.state = constants.PATCH_AGENT_STATE_INSTALL_FAILED self.patch_failed = os.path.exists(patch_failed_file) def update_config(self): cfg.read_config() if self.port != cfg.agent_port: self.port = cfg.agent_port # Loopback interface does not support multicast messaging, therefore # revert to using unicast messaging when configured against the # loopback device if cfg.get_mgmt_iface() == constants.LOOPBACK_INTERFACE_NAME: self.mcast_addr = None self.controller_address = cfg.get_mgmt_ip() else: self.mcast_addr = cfg.agent_mcast_group self.controller_address = cfg.controller_mcast_group def setup_tcp_socket(self): address_family = utils.get_management_family() self.listener = socket.socket(address_family, socket.SOCK_STREAM) self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.listener.bind(('', self.port)) self.listener.listen(2) # Allow two connections, for two controllers @staticmethod def pkgobj_to_version_str(pkg): # Transform pkgobj version to format used by patch-controller if pkg.epoch != 0: output = "%s:%s-%s@%s" % (pkg.epoch, pkg.version, pkg.release, pkg.arch) else: output = "%s-%s@%s" % (pkg.version, pkg.release, pkg.arch) return output @staticmethod def pkgobjs_to_list(pkgobjs): # Transform pkgobj list to format used by patch-controller output = {} for pkg in pkgobjs: output[pkg.name] = PatchAgent.pkgobj_to_version_str(pkg) 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 query(self, check_revision=False): """ Check current patch state """ if not check_install_uuid(): 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) self.last_repo_revision = current_repo_revision # Generate a unique query id 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])) # 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) LOG.info("To remove: %s", self.to_remove) LOG.info("Missing: %s", self.missing_pkgs) if len(self.duplicated_pkgs) > 0: LOG.info("Duplicated: %s", self.duplicated_pkgs) 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 # the patch during init. At that time, we don't want to deal with # in-service patch scripts, so instead we'll treat any patch as # a reboot-required when this parameter is set. Rather than running # any scripts, the RR flag will be set, which will result in the node # being rebooted immediately upon completion of the installation. # LOG.info("Handling install") # Check the INSTALL_UUID first. If it doesn't match the active # controller, we don't want to install patches. if not check_install_uuid(): LOG.error("Failed install_uuid check. Skipping install") self.patch_failed = True setflag(patch_failed_file) self.state = constants.PATCH_AGENT_STATE_INSTALL_FAILED # Send a hello to provide a state update if self.sock_out is not None: hello_ack = PatchMessageHelloAgentAck() hello_ack.send(self.sock_out) return False self.state = constants.PATCH_AGENT_STATE_INSTALLING setflag(patch_installing_file) try: # Create insvc patch directories if os.path.exists(insvc_patch_scripts): shutil.rmtree(insvc_patch_scripts, ignore_errors=True) if os.path.exists(insvc_patch_flags): shutil.rmtree(insvc_patch_flags, ignore_errors=True) os.mkdir(insvc_patch_scripts, 0o700) os.mkdir(insvc_patch_flags, 0o700) except Exception: LOG.exception("Failed to create in-service patch directories") # Send a hello to provide a state update if self.sock_out is not None: hello_ack = PatchMessageHelloAgentAck() hello_ack.send(self.sock_out) # Build up the install set if verbose_to_stdout: print("Checking for software updates...") self.query() changed = False rc = True if len(self.duplicated_pkgs) > 0: LOG.error("Duplicate installed packages found. Manual recovery is required.") rc = False 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) 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: 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.") else: if verbose_to_stdout: print("Nothing to install.") LOG.info("Nothing to install") if changed and rc: # Update the node_is_patched flag setflag(node_is_patched_file) 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 in-service patch dirs if os.path.exists(insvc_patch_scripts): shutil.rmtree(insvc_patch_scripts, ignore_errors=True) if os.path.exists(insvc_patch_flags): shutil.rmtree(insvc_patch_flags, ignore_errors=True) if rc: self.patch_failed = False clearflag(patch_failed_file) self.state = constants.PATCH_AGENT_STATE_IDLE else: # Update the patch_failed flag self.patch_failed = True setflag(patch_failed_file) self.state = constants.PATCH_AGENT_STATE_INSTALL_FAILED clearflag(patch_installing_file) self.query() # Send a hello to provide a state update if self.sock_out is not None: hello_ack = PatchMessageHelloAgentAck() hello_ack.send(self.sock_out) return rc def handle_patch_op_counter(self, counter): changed = False if os.path.exists(node_is_patched_file): # The node has been patched. Run a query if: # - node_is_patched didn't exist previously # - node_is_patched timestamp changed timestamp = os.path.getmtime(node_is_patched_file) if not self.node_is_patched: self.node_is_patched = True self.node_is_patched_timestamp = timestamp changed = True elif self.node_is_patched_timestamp != timestamp: self.node_is_patched_timestamp = timestamp changed = True elif self.node_is_patched: self.node_is_patched = False self.node_is_patched_timestamp = 0 changed = True if self.patch_op_counter < counter: self.patch_op_counter = counter changed = True if changed: rc = self.query(check_revision=True) if not rc: # Query failed. Reset the op counter self.patch_op_counter = 0 def run(self): self.setup_socket() while self.sock_out is None: # Check every thirty seconds? # Once we've got a conf file, tied into packstack, # we'll get restarted when the file is updated, # and this should be unnecessary. time.sleep(30) self.setup_socket() self.setup_tcp_socket() # Ok, now we've got our socket. # Let's let the controllers know we're here hello_ack = PatchMessageHelloAgentAck() hello_ack.send(self.sock_out) first_hello = True connections = [] timeout = time.time() + 30.0 remaining = 30 while True: inputs = [self.sock_in, self.listener] + connections outputs = [] rlist, wlist, xlist = select.select(inputs, outputs, inputs, remaining) remaining = int(timeout - time.time()) if remaining <= 0 or remaining > 30: timeout = time.time() + 30.0 remaining = 30 if (len(rlist) == 0 and len(wlist) == 0 and len(xlist) == 0): # Timeout hit self.audit_socket() continue for s in rlist: if s == self.listener: conn, addr = s.accept() connections.append(conn) continue data = '' addr = None msg = None if s == self.sock_in: # Receive from UDP data, addr = s.recvfrom(1024) else: # Receive from TCP while True: try: packet = s.recv(1024) except socket.error: LOG.exception("Socket error on recv") data = '' break if packet: data += packet.decode() if data == '': break try: json.loads(data) break except ValueError: # Message is incomplete continue else: # End of TCP message received break if data == '': # Connection dropped connections.remove(s) s.close() continue msgdata = json.loads(data) # For now, discard any messages that are not msgversion==1 if 'msgversion' in msgdata and msgdata['msgversion'] != 1: continue if 'msgtype' in msgdata: if msgdata['msgtype'] == messages.PATCHMSG_HELLO_AGENT: if first_hello: self.query() first_hello = False msg = PatchMessageHelloAgent() elif msgdata['msgtype'] == messages.PATCHMSG_QUERY_DETAILED: msg = PatchMessageQueryDetailed() elif msgdata['msgtype'] == messages.PATCHMSG_AGENT_INSTALL_REQ: msg = PatchMessageAgentInstallReq() if msg is None: msg = messages.PatchMessage() msg.decode(msgdata) if s == self.sock_in: msg.handle(self.sock_out, addr) else: msg.handle(s, addr) for s in xlist: if s in connections: connections.remove(s) s.close() # Check for in-service patch restart flag if os.path.exists(insvc_patch_restart_agent): # Make sure it's safe to restart, ie. no reqs queued rlist, wlist, xlist = select.select(inputs, outputs, inputs, 0) if (len(rlist) == 0 and len(wlist) == 0 and len(xlist) == 0): # Restart LOG.info("In-service patch restart flag detected. Exiting.") os.remove(insvc_patch_restart_agent) exit(0) def main(): global pa configure_logging(dnf_log=True) cfg.read_config() pa = PatchAgent() pa.query() if len(sys.argv) <= 1: pa.run() elif sys.argv[1] == "--install": if not check_install_uuid(): # 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) http_port_real = 80 pa.handle_install(verbose_to_stdout=True, disallow_insvc_patch=True) elif sys.argv[1] == "--status": rc = 0 if pa.changes: rc = 1 exit(rc)