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 <don.penney@windriver.com>
This commit is contained in:
Don Penney 2019-04-03 13:26:07 -04:00
parent a4bf173714
commit 5642a575c5
7 changed files with 299 additions and 12 deletions

View File

@ -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

View File

@ -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):

View File

@ -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"

View File

@ -45,3 +45,8 @@ class PatchValidationFailure(PatchError):
class PatchMismatchFailure(PatchError):
"""Patch validation error."""
pass
class PatchInvalidRequest(PatchError):
"""Invalid API request."""
pass

View File

@ -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:

View File

@ -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.

View File

@ -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