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:
parent
a4bf173714
commit
5642a575c5
@ -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
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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"
|
||||
|
@ -45,3 +45,8 @@ class PatchValidationFailure(PatchError):
|
||||
class PatchMismatchFailure(PatchError):
|
||||
"""Patch validation error."""
|
||||
pass
|
||||
|
||||
|
||||
class PatchInvalidRequest(PatchError):
|
||||
"""Invalid API request."""
|
||||
pass
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user