From 5642a575c58453fce323761ad0f25be4eb538876 Mon Sep 17 00:00:00 2001 From: Don Penney Date: Wed, 3 Apr 2019 13:26:07 -0400 Subject: [PATCH] Add support for application patch dependencies This update introduces support for application dependencies on specified software patches. This allows sysinv to query patch states and to report application dependencies, to block removal of required patches. APIs introduced: is_applied: Provide a list of patch IDs. Returns True if ALL patches are in Applied (fully applied/installed) state, False if any are not. report_app_dependencies: Provide an app name-version string and list of patches. * Apps are dropped by reporting app name with no patch list / empty list. * Triggers sync with patch-controller neighbour (ie. standby controller) query_app_dependencies: Query for dict of app => patch list, for all reported apps. On patch removal, the patch-controller checks to see if the patch being removed is required by any applications. Reject the removal, if so. * Provide backdoor --skipappcheck option to skip this dependency check. Change-Id: I6b0dcf3c88312677a95c7eb860cb3b30779554d0 Story: 2005248 Task: 30318 Signed-off-by: Don Penney --- cgcs-patch/bin/sw-patch.completion | 25 ++- .../cgcs_patch/api/controllers/root.py | 21 ++- cgcs-patch/cgcs-patch/cgcs_patch/constants.py | 4 +- .../cgcs-patch/cgcs_patch/exceptions.py | 5 + .../cgcs-patch/cgcs_patch/patch_client.py | 144 +++++++++++++++++- .../cgcs-patch/cgcs_patch/patch_controller.py | 108 ++++++++++++- .../cgcs-patch/cgcs_patch/patch_functions.py | 4 +- 7 files changed, 299 insertions(+), 12 deletions(-) diff --git a/cgcs-patch/bin/sw-patch.completion b/cgcs-patch/bin/sw-patch.completion index 145e7638..59c55297 100644 --- a/cgcs-patch/bin/sw-patch.completion +++ b/cgcs-patch/bin/sw-patch.completion @@ -31,6 +31,9 @@ function _swpatch() upload-dir what-requires drop-host + is-applied + report-app-dependencies + query-app-dependencies " if [ -f /etc/platform/.initial_config_complete ]; then # Post-config, so the host-install commands are accessible @@ -48,12 +51,18 @@ function _swpatch() # Complete the arguments to the subcommands. # case "$subcommand" in - apply|remove|delete|show|what-requires) + apply|delete|show|what-requires|is-applied) # Query the list of known patches local patches=$(sw-patch completion patches 2>/dev/null) COMPREPLY=( $(compgen -W "${patches}" -- ${cur}) ) return 0 ;; + remove) + # Query the list of known patches + local patches=$(sw-patch completion patches 2>/dev/null) + COMPREPLY=( $(compgen -W "--skipappcheck ${patches}" -- ${cur}) ) + return 0 + ;; host-install|host-install-async|drop-host) if [ "${prev}" = "${subcommand}" -o "${prev}" = "--force" ]; then # Query the list of known hosts @@ -109,6 +118,20 @@ function _swpatch() fi return 0 ;; + report-app-dependencies) + if [ "${prev}" = "${subcommand}" ]; then + COMPREPLY=( $(compgen -W "--app" -- ${cur}) ) + elif [ "${prev}" = "--app" ]; then + COMPREPLY= + else + local patches=$(sw-patch completion patches 2>/dev/null) + COMPREPLY=( $(compgen -W "${patches}" -- ${cur}) ) + fi + return 0 + ;; + query-app-dependencies) + return 0 + ;; *) ;; esac 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 dd7de11a..f76c5394 100644 --- a/cgcs-patch/cgcs-patch/cgcs_patch/api/controllers/root.py +++ b/cgcs-patch/cgcs-patch/cgcs_patch/api/controllers/root.py @@ -1,5 +1,5 @@ """ -Copyright (c) 2014-2017 Wind River Systems, Inc. +Copyright (c) 2014-2019 Wind River Systems, Inc. SPDX-License-Identifier: Apache-2.0 @@ -258,6 +258,25 @@ class PatchAPIController(object): return result + @expose('json') + def is_applied(self, *args): + return pc.is_applied(list(args)) + + @expose('json') + def report_app_dependencies(self, *args, **kwargs): + try: + result = pc.report_app_dependencies(list(args), **kwargs) + except PatchError as e: + return dict(status=500, error=e.message) + + pc.patch_sync() + + return result + + @expose('json') + def query_app_dependencies(self): + return pc.query_app_dependencies() + class RootController(object): diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/constants.py b/cgcs-patch/cgcs-patch/cgcs_patch/constants.py index 8060d9a1..3a986a0c 100644 --- a/cgcs-patch/cgcs-patch/cgcs_patch/constants.py +++ b/cgcs-patch/cgcs-patch/cgcs_patch/constants.py @@ -1,5 +1,5 @@ """ -Copyright (c) 2015-2017 Wind River Systems, Inc. +Copyright (c) 2015-2019 Wind River Systems, Inc. SPDX-License-Identifier: Apache-2.0 @@ -20,6 +20,8 @@ PATCH_AGENT_STATE_INSTALLING = "installing" PATCH_AGENT_STATE_INSTALL_FAILED = "install-failed" PATCH_AGENT_STATE_INSTALL_REJECTED = "install-rejected" +PATCH_STORAGE_DIR = "/opt/patching" + ADDRESS_VERSION_IPV4 = 4 ADDRESS_VERSION_IPV6 = 6 CONTROLLER_FLOATING_HOSTNAME = "controller" diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/exceptions.py b/cgcs-patch/cgcs-patch/cgcs_patch/exceptions.py index 1cd22e79..f03e7189 100644 --- a/cgcs-patch/cgcs-patch/cgcs_patch/exceptions.py +++ b/cgcs-patch/cgcs-patch/cgcs_patch/exceptions.py @@ -45,3 +45,8 @@ class PatchValidationFailure(PatchError): class PatchMismatchFailure(PatchError): """Patch validation error.""" pass + + +class PatchInvalidRequest(PatchError): + """Invalid API request.""" + pass diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py index 82599481..32fd1223 100644 --- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py +++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_client.py @@ -60,6 +60,13 @@ help_install_local = "Trigger patch install/remove on the local host. " + \ help_drop_host = "Drop specified host from table." help_query_dependencies = "List dependencies for specified patch. Use " + \ constants.CLI_OPT_RECURSIVE + " for recursive query." +help_is_applied = "Query Applied state for list of patches. " + \ + "Returns True if all are Applied, False otherwise." +help_report_app_dependencies = "Report application patch dependencies, " + \ + "specifying application name with --app option, plus a list of patches. " + \ + "Reported dependencies can be dropped by specifying app with no patch list." +help_query_app_dependencies = "Display set of reported application patch " + \ + "dependencies." help_commit = "Commit patches to free disk space. WARNING: This action " + \ "is irreversible!" help_region_name = "Send the request to a specified region" @@ -132,6 +139,15 @@ def print_help(): print(textwrap.fill(" {0:<15} ".format("query-dependencies:") + help_query_dependencies, width=TERM_WIDTH, subsequent_indent=' ' * 20)) print("") + print(textwrap.fill(" {0:<15} ".format("is-applied:") + help_is_applied, + width=TERM_WIDTH, subsequent_indent=' ' * 20)) + print("") + print(textwrap.fill(" {0:<15} ".format("report-app-dependencies:") + help_report_app_dependencies, + width=TERM_WIDTH, subsequent_indent=' ' * 20)) + print("") + print(textwrap.fill(" {0:<15} ".format("query-app-dependencies:") + help_query_app_dependencies, + width=TERM_WIDTH, subsequent_indent=' ' * 20)) + print("") print(textwrap.fill(" {0:<15} ".format("commit:") + help_commit, width=TERM_WIDTH, subsequent_indent=' ' * 20)) print("") @@ -433,14 +449,14 @@ def patch_apply_req(debug, args): def patch_remove_req(debug, args): - extra_opts = "" - if len(args) == 0: print_help() # Ignore interrupts during this function signal.signal(signal.SIGINT, signal.SIG_IGN) + extra_opts = [] + # The removeunremovable option is hidden and should not be added to help # text or customer documentation. It is for emergency use only - under # supervision of the design team. @@ -450,12 +466,25 @@ def patch_remove_req(debug, args): # Get rid of the --removeunremovable args.pop(idx) - # Format the extra opts - extra_opts = "?removeunremovable=yes" + # Append the extra opts + extra_opts.append('removeunremovable=yes') + + if "--skipappcheck" in args: + idx = args.index("--skipappcheck") + + # Get rid of the --skipappcheck + args.pop(idx) + + # Append the extra opts + extra_opts.append("skipappcheck=yes") + + if len(extra_opts) == 0: + extra_opts_str = '' + else: + extra_opts_str = '?%s' % '&'.join(extra_opts) patches = "/".join(args) - - url = "http://%s/patch/remove/%s%s" % (api_addr, patches, extra_opts) + url = "http://%s/patch/remove/%s%s" % (api_addr, patches, extra_opts_str) headers = {} append_auth_token_if_required(headers) @@ -1097,6 +1126,103 @@ def patch_del_release(debug, args): return check_rc(req) +def patch_is_applied_req(args): + if len(args) == 0: + print_help() + + patches = "/".join(args) + url = "http://%s/patch/is_applied/%s" % (api_addr, patches) + + headers = {} + append_auth_token_if_required(headers) + req = requests.post(url, headers=headers) + + rc = 1 + + if req.status_code == 200: + result = json.loads(req.text) + print(result) + if result is True: + rc = 0 + elif req.status_code == 500: + print("An internal error has occurred. Please check /var/log/patching.log for details") + + return rc + + +def patch_report_app_dependencies_req(debug, args): + if len(args) < 2: + print_help() + + extra_opts = [] + + if "--app" in args: + idx = args.index("--app") + + # Get rid of the --app and get the app name + args.pop(idx) + app = args.pop(idx) + + # Append the extra opts + extra_opts.append("app=%s" % app) + else: + print("Application name must be specified with --app argument.") + return 1 + + extra_opts_str = '?%s' % '&'.join(extra_opts) + + patches = "/".join(args) + url = "http://%s/patch/report_app_dependencies/%s%s" % (api_addr, patches, extra_opts_str) + + headers = {} + append_auth_token_if_required(headers) + req = requests.post(url, headers=headers) + + if req.status_code == 200: + return 0 + else: + return 1 + + +def patch_query_app_dependencies_req(): + url = "http://%s/patch/query_app_dependencies" % api_addr + + headers = {} + append_auth_token_if_required(headers) + req = requests.post(url, headers=headers) + + if req.status_code == 200: + data = json.loads(req.text) + if len(data) == 0: + print("There are no application dependencies.") + else: + hdr_app = "Application" + hdr_list = "Required Patches" + width_app = len(hdr_app) + width_list = len(hdr_list) + + for app, patch_list in data.items(): + width_app = max(width_app, len(app)) + width_list = max(width_list, len(', '.join(patch_list))) + + print("{0:<{width_app}} {1:<{width_list}}".format( + hdr_app, hdr_list, + width_app=width_app, width_list=width_list)) + + print("{0} {1}".format( + '=' * width_app, '=' * width_list)) + + for app, patch_list in sorted(data.items()): + print("{0:<{width_app}} {1:<{width_list}}".format( + app, ', '.join(patch_list), + width_app=width_app, width_list=width_list)) + + return 0 + else: + print("An internal error has occurred. Please check /var/log/patching.log for details") + return 1 + + def completion_opts(args): if len(args) != 1: return 1 @@ -1306,6 +1432,12 @@ def main(): rc = patch_init_release(debug, sys.argv[2:]) elif action == "del-release": rc = patch_del_release(debug, sys.argv[2:]) + elif action == "is-applied": + rc = patch_is_applied_req(sys.argv[2:]) + elif action == "report-app-dependencies": + rc = patch_report_app_dependencies_req(debug, sys.argv[2:]) + elif action == "query-app-dependencies": + rc = patch_query_app_dependencies_req() elif action == "completion": rc = completion_opts(sys.argv[2:]) else: diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py index 4cf48d46..ab3346d3 100644 --- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py +++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_controller.py @@ -5,6 +5,7 @@ SPDX-License-Identifier: Apache-2.0 """ import shutil +import tempfile import threading import time import socket @@ -36,6 +37,7 @@ from cgcs_patch.exceptions import MetadataFail from cgcs_patch.exceptions import RpmFail from cgcs_patch.exceptions import PatchError from cgcs_patch.exceptions import PatchFail +from cgcs_patch.exceptions import PatchInvalidRequest from cgcs_patch.exceptions import PatchValidationFailure from cgcs_patch.exceptions import PatchMismatchFailure from cgcs_patch.patch_functions import LOG @@ -58,7 +60,9 @@ CONF = oslo_cfg.CONF pidfile_path = "/var/run/patch_controller.pid" pc = None -state_file = "/opt/patching/.controller.state" +state_file = "%s/.controller.state" % constants.PATCH_STORAGE_DIR +app_dependency_basename = "app_dependencies.json" +app_dependency_filename = "%s/%s" % (constants.PATCH_STORAGE_DIR, app_dependency_basename) insvc_patch_restart_controller = "/run/patching/.restart.patch-controller" @@ -581,6 +585,15 @@ class PatchController(PatchService): self.allow_insvc_patching = True + if os.path.exists(app_dependency_filename): + try: + 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) + else: + self.app_dependencies = {} + if os.path.isfile(state_file): self.read_state_file() else: @@ -669,6 +682,16 @@ class PatchController(PatchService): self.patch_data.load_all() self.check_patch_states() self.hosts_lock.release() + + if os.path.exists(app_dependency_filename): + try: + 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) + else: + self.app_dependencies = {} + self.patch_data_lock.release() return True @@ -1221,6 +1244,24 @@ class PatchController(PatchService): return dict(info=msg_info, warning=msg_warning, error=msg_error) + if kwargs.get("skipappcheck") != "yes": + # Check application dependencies before removing + required_patches = {} + for patch_id in patch_list: + for appname, iter_patch_list in self.app_dependencies.items(): + if patch_id in iter_patch_list: + if patch_id not in required_patches: + required_patches[patch_id] = [] + required_patches[patch_id].append(appname) + + if len(required_patches) > 0: + for req_patch, app_list in required_patches.items(): + msg = "%s is required by application(s): %s" % (req_patch, ", ".join(sorted(app_list))) + msg_error += msg + "\n" + LOG.info(msg) + + return dict(info=msg_info, warning=msg_warning, error=msg_error) + for patch_id in patch_list: msg = "Removing patch: %s" % patch_id LOG.info(msg) @@ -2103,6 +2144,71 @@ class PatchController(PatchService): return dict(info=msg_info, warning=msg_warning, error=msg_error) + def is_applied(self, patch_ids): + all_applied = True + + self.patch_data_lock.acquire() + + for patch_id in patch_ids: + if patch_id not in self.patch_data.metadata: + all_applied = False + break + + if self.patch_data.metadata[patch_id]["patchstate"] != constants.APPLIED: + all_applied = False + break + + self.patch_data_lock.release() + + return all_applied + + def report_app_dependencies(self, patch_ids, **kwargs): + """ + Handle report of application dependencies + """ + if "app" not in kwargs: + raise PatchInvalidRequest + + appname = kwargs.get("app") + + LOG.info("Handling app dependencies report: app=%s, patch_ids=%s" % + (appname, ','.join(patch_ids))) + + self.patch_data_lock.acquire() + + if len(patch_ids) == 0: + if appname in self.app_dependencies: + del self.app_dependencies[appname] + else: + self.app_dependencies[appname] = patch_ids + + try: + tmpfile, tmpfname = tempfile.mkstemp( + prefix=app_dependency_basename, + dir=constants.PATCH_STORAGE_DIR) + + os.write(tmpfile, json.dumps(self.app_dependencies)) + os.close(tmpfile) + + os.rename(tmpfname, app_dependency_filename) + except Exception: + LOG.exception("Failed in report_app_dependencies") + raise PatchFail("Internal failure") + finally: + self.patch_data_lock.release() + + def query_app_dependencies(self): + """ + Query application dependencies + """ + self.patch_data_lock.acquire() + + data = self.app_dependencies + + self.patch_data_lock.release() + + return dict(data) + # The wsgiref.simple_server module has an error handler that catches # and prints any exceptions that occur during the API handling to stderr. diff --git a/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py b/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py index ef8f0362..79037c3c 100644 --- a/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py +++ b/cgcs-patch/cgcs-patch/cgcs_patch/patch_functions.py @@ -1,5 +1,5 @@ """ -Copyright (c) 2014-2018 Wind River Systems, Inc. +Copyright (c) 2014-2019 Wind River Systems, Inc. SPDX-License-Identifier: Apache-2.0 @@ -38,7 +38,7 @@ except Exception: SW_VERSION = "unknown" # Constants -patch_dir = "/opt/patching" +patch_dir = constants.PATCH_STORAGE_DIR avail_dir = "%s/metadata/available" % patch_dir applied_dir = "%s/metadata/applied" % patch_dir committed_dir = "%s/metadata/committed" % patch_dir