commit 99616a7f72125c6fd5c86e5dd90af3d87ef3902c Author: Dan Voiculeasa Date: Mon Sep 21 13:49:18 2020 +0300 Introduce CLI commands for system restore control Introduce 3 new sysinv commands: restore-show, restore-start, restore-complete. When doing a restore, the system will be put automatically into a restore state by the restore playbook. After all the nodes are up and unlocked the user must do a `system restore-complete` to get out of the system restore state. Note: In the case of multi-nodes setups helm overrides may have been detected so apps will be auto-applied after exiting the restore state. The auto apply is started by a periodic audit thread. Depends-On: I44fc4aaa528e372a84115714f271b4f5e063f86e Partial-Bug: 1887648 Change-Id: I7b7fab99d457056032dbbd612363cd5036736cda Signed-off-by: Dan Voiculeasa diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py index af8bc52..f3cd9b0 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py @@ -71,6 +71,7 @@ from cgtsclient.v1 import port from cgtsclient.v1 import ptp from cgtsclient.v1 import registry_image from cgtsclient.v1 import remotelogging +from cgtsclient.v1 import restore from cgtsclient.v1 import route from cgtsclient.v1 import sdn_controller from cgtsclient.v1 import service_parameter @@ -171,3 +172,4 @@ class Client(http.HTTPClient): self.device_image = device_image.DeviceImageManager(self) self.device_image_state = device_image_state.DeviceImageStateManager(self) self.device_label = device_label.DeviceLabelManager(self) + self.restore = restore.RestoreManager(self) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/restore.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/restore.py new file mode 100644 index 0000000..d078c77 --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/restore.py @@ -0,0 +1,25 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# -*- encoding: utf-8 -*- +# + +from cgtsclient.common import base + + +class RestoreManager(base.Manager): + path = '/v1/restore' + + def get(self): + return self._json_get(self.path, {}) + + def start(self): + _, body = self.api.json_request('PATCH', self.path, body={'action': 'start'}) + return body + + def complete(self): + _, body = self.api.json_request('PATCH', self.path, body={'action': 'complete'}) + return body diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/restore_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/restore_shell.py new file mode 100755 index 0000000..f47da3d --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/restore_shell.py @@ -0,0 +1,26 @@ +# +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# -*- encoding: utf-8 -*- +# + + +def do_restore_start(cc, args): + """Start software restore.""" + + print(cc.restore.start()) + + +def do_restore_show(cc, args): + """Show software restore.""" + + print(cc.restore.get()) + + +def do_restore_complete(cc, args): + """Complete software restore.""" + + print(cc.restore.complete()) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py index 14d7aa7..e53e270 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/shell.py @@ -58,6 +58,7 @@ from cgtsclient.v1 import port_shell from cgtsclient.v1 import ptp_shell from cgtsclient.v1 import registry_image_shell from cgtsclient.v1 import remotelogging_shell +from cgtsclient.v1 import restore_shell from cgtsclient.v1 import route_shell from cgtsclient.v1 import sdn_controller_shell from cgtsclient.v1 import service_parameter_shell @@ -129,6 +130,7 @@ COMMAND_MODULES = [ device_image_shell, device_image_state_shell, device_label_shell, + restore_shell, ] diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py index ab20ce6..dda9bd1 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py @@ -67,6 +67,7 @@ from sysinv.api.controllers.v1 import ptp from sysinv.api.controllers.v1 import pv from sysinv.api.controllers.v1 import registry_image from sysinv.api.controllers.v1 import remotelogging +from sysinv.api.controllers.v1 import restore from sysinv.api.controllers.v1 import route from sysinv.api.controllers.v1 import sdn_controller from sysinv.api.controllers.v1 import certificate @@ -273,6 +274,9 @@ class V1(base.APIBase): device_labels = [link.Link] "Links to the device labels resource" + restore = [link.Link] + "Links to the restore resource" + @classmethod def convert(self): v1 = V1() @@ -841,6 +845,14 @@ class V1(base.APIBase): pecan.request.host_url, 'device_labels', '', bookmark=True)] + + v1.restore = [link.Link.make_link('self', pecan.request.host_url, + 'restore', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'restore', '', + bookmark=True) + ] return v1 @@ -915,6 +927,7 @@ class Controller(rest.RestController): device_images = device_image.DeviceImageController() device_image_state = device_image_state.DeviceImageStateController() device_labels = device_label.DeviceLabelController() + restore = restore.RestoreController() @wsme_pecan.wsexpose(V1) def get(self): diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/restore.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/restore.py new file mode 100755 index 0000000..f2b5fd8 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/restore.py @@ -0,0 +1,73 @@ +# Copyright (c) 2020 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import pecan +from pecan import rest +import wsme +from wsme import types as wtypes +import wsmeext.pecan as wsme_pecan + +from oslo_log import log +from sysinv._i18n import _ +from sysinv.api.controllers.v1 import base +from sysinv.common import utils as cutils + +LOG = log.getLogger(__name__) +LOCK_NAME = 'RestoreController' + + +class Restore(base.APIBase): + """API representation of a restore. + + This class enforces type checking and value constraints, and converts + between the internal object model and the API representation of + a restore. + """ + + action = wtypes.text + "Action to take" + + def __repr__(self): + return "" % self.action + + +class RestoreController(rest.RestController): + """REST controller for Restore.""" + + @wsme_pecan.wsexpose(wtypes.text) + def get_all(self): + """Query the restore state""" + + try: + output = pecan.request.rpcapi.get_restore_state( + pecan.request.context) + except Exception as e: + LOG.exception(e) + raise wsme.exc.ClientSideError(_( + "Unable to perform restore query.")) + return output + + @cutils.synchronized(LOCK_NAME) + @wsme_pecan.wsexpose(wtypes.text, body=Restore) + def patch(self, body): + """Modify the restore state""" + + try: + if body.action == "start": + output = pecan.request.rpcapi.start_restore( + pecan.request.context) + elif body.action == "complete": + output = pecan.request.rpcapi.complete_restore( + pecan.request.context) + else: + raise wsme.exc.ClientSideError(_( + "Unknown restore action {}".format(body.action))) + + except Exception as e: + LOG.exception(e) + raise wsme.exc.ClientSideError(_( + "Unable to perform restore modify state.")) + + return output diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py index 302fb3b..0a922cd 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py @@ -12189,6 +12189,66 @@ class ConductorManager(service.PeriodicService): # and trigger an update of it. self.host_device_image_update_next(context, host_uuid) + def start_restore(self, context): + """Start the restore + + :param context: request context. + """ + + LOG.info("Preparing for restore procedure. Creating flag file.") + + cutils.touch(constants.SYSINV_RESTORE_FLAG) + + return "Restore procedure started" + + def complete_restore(self, context): + """Complete the restore + + :param context: request context. + """ + + try: + controllers = self.dbapi.ihost_get_by_personality( + constants.CONTROLLER) + invalid_controllers = [ + controller for controller in controllers if + controller.administrative != constants.ADMIN_UNLOCKED or + controller.operational != constants.OPERATIONAL_ENABLED or + (controller.availability != constants.AVAILABILITY_AVAILABLE and + controller.availability != constants.AVAILABILITY_DEGRADED)] + + if invalid_controllers: + message = "Cannot complete the restore procedure. " \ + "One of the controllers is not unlocked enabled available/degraded" + LOG.info(message) + return message + except Exception as e: + message = "Cannot complete the restore procedure. " \ + "Cannot query controllers state." + LOG.info(message) + LOG.error(e) + return message + + LOG.info("Complete the restore procedure. Remove flag file.") + + cutils.delete_if_exists(constants.SYSINV_RESTORE_FLAG) + + return "Restore procedure completed" + + def get_restore_state(self, context): + """Get the restore state + + :param context: request context. + """ + + if self._verify_restore_in_progress(): + output = "Restore procedure is in progress" + else: + output = "Restore procedure is not in progress" + + LOG.info(output) + return output + def device_image_state_sort_key(dev_img_state): if dev_img_state.bitstream_type == dconstants.BITSTREAM_TYPE_ROOT_KEY: diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py b/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py index e1e071f..5c1df90 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py @@ -2065,3 +2065,24 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy): status=status, progress=progress, err=err)) + + def start_restore(self, context): + """Synchronously, have the conductor start the restore + + :param context: request context. + """ + return self.call(context, self.make_msg('start_restore')) + + def complete_restore(self, context): + """Synchronously, have the conductor complete the restore + + :param context: request context. + """ + return self.call(context, self.make_msg('complete_restore')) + + def get_restore_state(self, context): + """Get the restore state + + :param context: request context. + """ + return self.call(context, self.make_msg('get_restore_state'))