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