From 24588e857b658b8a05992e7f1da2cc781e40bb25 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Thu, 6 Oct 2022 15:14:42 -0300 Subject: [PATCH] Create subcloud-backup restore command Adds support for restoring backups for a subcloud or group of subcloud using the dcmanager API. Test Plan: 1. Verify command with multiple combinations of parameters. 2. Retest command after the changes are integrated with others related to subcloud-backup commands. Story: 2010116 Task: 46537 Signed-off-by: Andre Carneiro Change-Id: I50ac2b529be9da45f68f21f46e9355546328ac40 --- .../api/controllers/v1/subcloud_backup.py | 234 ++++++++++-------- .../dcmanager/api/controllers/v1/subclouds.py | 43 ++-- .../dcmanager/api/policies/subcloud_backup.py | 11 + distributedcloud/dcmanager/common/consts.py | 5 +- distributedcloud/dcmanager/common/utils.py | 22 +- 5 files changed, 184 insertions(+), 131 deletions(-) diff --git a/distributedcloud/dcmanager/api/controllers/v1/subcloud_backup.py b/distributedcloud/dcmanager/api/controllers/v1/subcloud_backup.py index af13d9fff..16b42d9af 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/subcloud_backup.py +++ b/distributedcloud/dcmanager/api/controllers/v1/subcloud_backup.py @@ -17,8 +17,6 @@ from pecan import expose from pecan import request as pecan_request from pecan import response -from dccommon import consts as dccommon_consts - from dcmanager.api.controllers import restcomm from dcmanager.api.policies import subcloud_backup as subcloud_backup_policy from dcmanager.api import policy @@ -49,30 +47,40 @@ class SubcloudBackupController(object): pass @staticmethod - def _get_backup_payload(request): - return SubcloudBackupController._get_payload(request, { - "subcloud": "text", - "group": "text", - "local_only": "text", - "registry_images": "text", - "backup_values": "yaml", - "sysadmin_password": "text" - }) + def _get_payload(request, verb): + expected_params = dict() + if verb == 'create': + expected_params = { + "subcloud": "text", + "group": "text", + "local_only": "text", + "registry_images": "text", + "backup_values": "yaml", + "sysadmin_password": "text" + } + elif verb == 'delete': + expected_params = { + "release": "text", + "subcloud": "text", + "group": "text", + "local_only": "text", + "sysadmin_password": "text" + } + elif verb == 'restore': + expected_params = { + "with_install": "text", + "local_only": "text", + "registry_images": "text", + "sysadmin_password": "text", + "restore_values": "text", + "subcloud": "text", + "group": "text" + } + else: + pecan.abort(400, _("Unexpected verb received")) - @staticmethod - def _get_backup_delete_payload(request): - return SubcloudBackupController._get_payload(request, { - "release": "text", - "subcloud": "text", - "group": "text", - "local_only": "text", - "sysadmin_password": "text" - }) - - @staticmethod - def _get_payload(request, expected_params): - return SubcloudBackupController._get_json_payload( - request, expected_params) + return SubcloudBackupController._get_json_payload(request, + expected_params) @staticmethod def _get_json_payload(request, expected_params): @@ -107,36 +115,47 @@ class SubcloudBackupController(object): pecan.abort(400, msg) @staticmethod - def _convert_param_to_bool(payload, param_name, default): - param = payload.get(param_name) - if param: - if param.lower() == 'true': - payload[param_name] = True - elif param.lower() == 'false': - payload[param_name] = False + def _convert_param_to_bool(payload, param_names, default=False): + for param_name in param_names: + param = payload.get(param_name) + if param: + if param.lower() == 'true': + payload[param_name] = True + elif param.lower() == 'false': + payload[param_name] = False + else: + pecan.abort(400, _('Invalid %s value, should be boolean' + % param_name)) else: - pecan.abort(400, _('Invalid %s value, should be boolean' - % param_name)) - else: - payload[param_name] = default + payload[param_name] = default @staticmethod - def _validate_subcloud(subcloud): - if not subcloud: - pecan.abort(404, _('Subcloud not found')) + def _validate_subclouds(subclouds, operation): + """Validate the subcloud according to the operation - if subcloud.availability_status != dccommon_consts.AVAILABILITY_ONLINE: - pecan.abort(400, _('Subcloud must be online for this operation')) + Create/Delete: The subcloud is managed, online and in complete state. + Restore: The subcloud is unmanaged, and not in the process of + installation, boostrap, deployment or rehoming. - if subcloud.management_state != dccommon_consts.MANAGEMENT_MANAGED: - pecan.abort(400, _('Operation not allowed while subcloud is unmanaged. ' - 'Please manage the subcloud and try again.')) + If none of the subclouds are valid, the operation will be aborted. - elif subcloud.deploy_status != consts.DEPLOY_STATE_DONE: - pecan.abort(400, _("The current subcloud deploy state is %s. " - "This operation is only allowed while subcloud " - "deploy state is 'complete'." - % subcloud.deploy_status)) + Args: + subclouds (list): List of subclouds to be validated + operation (string): Subcloud backup operation + """ + + if operation == 'create' or operation == 'delete': + valid_subclouds = [subcloud for subcloud in subclouds if + utils.is_valid_for_backup(subcloud)] + elif operation == 'restore': + valid_subclouds = [subcloud for subcloud in subclouds if + utils.is_valid_for_restore(subcloud)] + else: + pecan.abort(400, _('Operation %s is not valid' % operation)) + + if not valid_subclouds: + pecan.abort(400, _('Subcloud backup %s is not allowed because the ' + 'subcloud(s) are in invalid states.') % operation) @staticmethod def _get_subclouds_from_group(group, context): @@ -145,37 +164,7 @@ class SubcloudBackupController(object): return db_api.subcloud_get_for_group(context, group.id) - @staticmethod - def _validate_group_subclouds(group_subclouds): - if not group_subclouds: - pecan.abort(400, _('No subclouds present in group')) - - online_subclouds = [subcloud for subcloud in group_subclouds - if subcloud.availability_status == - dccommon_consts.AVAILABILITY_ONLINE] - - if not online_subclouds: - pecan.abort(400, _('No online subclouds present in group')) - - managed_subclouds = [subcloud for subcloud in group_subclouds - if subcloud.management_state == - dccommon_consts.MANAGEMENT_MANAGED] - - if not managed_subclouds: - pecan.abort(400, _('No online and managed subclouds present in group. ' - 'Please manage subclouds and try again.')) - - invalid_states = consts.INVALID_DEPLOY_STATES_FOR_BACKUP - valid_state_subclouds = [subcloud for subcloud in managed_subclouds - if subcloud.deploy_status not in invalid_states] - - if not valid_state_subclouds: - pecan.abort(400, _('This operation is not allowed while subcloud ' - 'install, bootstrap or deploy is in progress. ' - 'No online and managed subclouds in a valid ' - 'deploy state present for this group.')) - - def _read_entity_from_request_params(self, context, payload, validate_subclouds): + def _read_entity_from_request_params(self, context, payload): subcloud_ref = payload.get('subcloud') group_ref = payload.get('group') @@ -184,14 +173,14 @@ class SubcloudBackupController(object): pecan.abort(400, _("'subcloud' and 'group' parameters " "should not be given at the same time")) subcloud = utils.subcloud_get_by_ref(context, subcloud_ref) - if validate_subclouds: - self._validate_subcloud(subcloud) + if not subcloud: + pecan.abort(400, _('Subcloud not found')) return RequestEntity('subcloud', subcloud.id, [subcloud]) elif group_ref: group = utils.subcloud_group_get_by_ref(context, group_ref) group_subclouds = self._get_subclouds_from_group(group, context) - if validate_subclouds: - self._validate_group_subclouds(group_subclouds) + if not group_subclouds: + pecan.abort(400, _('No subclouds present in group')) return RequestEntity('group', group.id, group_subclouds) else: pecan.abort(400, _("'subcloud' or 'group' parameter is required")) @@ -207,8 +196,7 @@ class SubcloudBackupController(object): Subcloud.backup_status.name: consts.BACKUP_STATE_INITIAL } - db_api.subcloud_bulk_update_by_ids(context, subcloud_ids, - update_form) + db_api.subcloud_bulk_update_by_ids(context, subcloud_ids, update_form) @utils.synchronized(LOCK_NAME) @index.when(method='POST', template='json') @@ -216,20 +204,19 @@ class SubcloudBackupController(object): """Create a new subcloud backup.""" context = restcomm.extract_context_from_environ() - payload = self._get_backup_payload(pecan_request) + payload = self._get_payload(pecan_request, 'create') policy.authorize(subcloud_backup_policy.POLICY_ROOT % "create", {}, restcomm.extract_credentials_for_policy()) - request_entity = self._read_entity_from_request_params( - context, payload, validate_subclouds=True) + request_entity = self._read_entity_from_request_params(context, payload) + self._validate_subclouds(request_entity.subclouds, 'create') # Set subcloud/group ID as reference instead of name to ease processing payload[request_entity.type] = request_entity.id subclouds = request_entity.subclouds - self._convert_param_to_bool(payload, 'local_only', False) - self._convert_param_to_bool(payload, 'registry_images', False) + self._convert_param_to_bool(payload, ['local_only', 'registry_images']) if not payload.get('local_only') and payload.get('registry_images'): pecan.abort(400, _('Option registry_images can not be used without ' @@ -257,9 +244,8 @@ class SubcloudBackupController(object): :param release_version: Backup release version to be deleted """ - context = restcomm.extract_context_from_environ() - payload = self._get_backup_delete_payload(pecan_request) + payload = self._get_payload(pecan_request, verb) if verb == 'delete': policy.authorize(subcloud_backup_policy.POLICY_ROOT % "delete", {}, @@ -268,15 +254,16 @@ class SubcloudBackupController(object): if not release_version: pecan.abort(400, _('Release version required')) - self._convert_param_to_bool(payload, 'local_only', False) + self._convert_param_to_bool(payload, ['local_only']) self._validate_and_decode_sysadmin_password(payload, 'sysadmin_password') - local_delete = payload.get('local_only') + request_entity = self._read_entity_from_request_params(context, payload) # Validate subcloud state when deleting locally # Not needed for centralized storage, since connection is not required - request_entity = self._read_entity_from_request_params( - context, payload, validate_subclouds=local_delete) + local_only = payload.get('local_only') + if local_only: + self._validate_subclouds(request_entity.subclouds, verb) # Set subcloud/group ID as reference instead of name to ease processing payload[request_entity.type] = request_entity.id @@ -295,5 +282,58 @@ class SubcloudBackupController(object): except Exception: LOG.exception("Unable to delete subcloud backups") pecan.abort(500, _('Unable to delete subcloud backups')) + elif verb == 'restore': + policy.authorize(subcloud_backup_policy.POLICY_ROOT % "restore", {}, + restcomm.extract_credentials_for_policy()) + + if not payload: + pecan.abort(400, _('Body required')) + + self._validate_and_decode_sysadmin_password(payload, 'sysadmin_password') + + self._convert_param_to_bool(payload, ['local_only', 'with_install', + 'registry_images']) + + if not payload['local_only'] and payload['registry_images']: + pecan.abort(400, _('Option registry_images cannot be used ' + 'without local_only option.')) + + request_entity = self._read_entity_from_request_params(context, payload) + if len(request_entity.subclouds) == 0: + msg = "No subclouds exist under %s %s" % (request_entity.type, + request_entity.id) + pecan.abort(400, _(msg)) + + self._validate_subclouds(request_entity.subclouds, verb) + + payload[request_entity.type] = request_entity.id + + valid_subclouds = [subcloud for subcloud in + request_entity.subclouds if + subcloud.data_install] + + if not valid_subclouds: + pecan.abort(400, _('Cannot proceed with the restore operation ' + 'since the subcloud(s) do not contain ' + 'install data.')) + + if payload.get('with_install'): + # Confirm the active system controller load is still in dc-vault + matching_iso, err_msg = utils.get_matching_iso() + if err_msg: + LOG.exception(err_msg) + pecan.abort(400, _(err_msg)) + LOG.info("Restore operation will use image %s in subcloud " + "installation" % matching_iso) + + try: + message = self.dcmanager_rpc_client.restore_subcloud_backups( + context, payload) + return utils.subcloud_db_list_to_dict(request_entity.subclouds) + except RemoteError as e: + pecan.abort(422, e.value) + except Exception: + LOG.exception("Unable to restore subcloud") + pecan.abort(500, _('Unable to restore subcloud')) else: pecan.abort(400, _('Invalid request')) diff --git a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py index 754344617..f16468924 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py +++ b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py @@ -313,7 +313,8 @@ class SubcloudsController(object): ) return file_path - def _get_subcloud_db_install_values(self, subcloud): + @staticmethod + def _get_subcloud_db_install_values(subcloud): if not subcloud.data_install: msg = _("Failed to read data install from db") LOG.exception(msg) @@ -599,8 +600,10 @@ class SubcloudsController(object): if k == 'image': if software_version == tsc.SW_VERSION: # check for the image at load vault load location - matching_iso, matching_sig = \ - SubcloudsController.verify_active_load_in_vault() + matching_iso, err_msg = utils.get_matching_iso() + if err_msg: + LOG.exception(err_msg) + pecan.abort(400, _(err_msg)) LOG.info("image was not in install_values: will reference %s" % matching_iso) else: @@ -685,7 +688,6 @@ class SubcloudsController(object): @staticmethod def _validate_restore_values(payload): """Validate the restore values to ensure parameters for remote restore are present""" - restore_values = payload.get(RESTORE_VALUES) for p in MANDATORY_RESTORE_VALUES: if p not in restore_values: @@ -805,22 +807,6 @@ class SubcloudsController(object): data_install=data_install) return subcloud - @staticmethod - def verify_active_load_in_vault(): - try: - matching_iso, matching_sig = utils.get_vault_load_files(tsc.SW_VERSION) - if not matching_iso: - msg = _('Failed to get active load image. Provide ' - 'active load image via ' - '"system --os-region-name SystemController ' - 'load-import --active"') - LOG.exception(msg) - pecan.abort(400, msg) - return matching_iso, matching_sig - except Exception as e: - LOG.exception(str(e)) - pecan.abort(400, str(e)) - @index.when(method='GET', template='json') def get(self, subcloud_ref=None, detail=None): """Get details about subcloud. @@ -1313,8 +1299,10 @@ class SubcloudsController(object): # image not in install values, add the matching image into the # install values. if 'image' not in install_values: - matching_iso, matching_sig = \ - SubcloudsController.verify_active_load_in_vault() + matching_iso, err_msg = utils.get_matching_iso() + if err_msg: + LOG.exception(err_msg) + pecan.abort(400, _(err_msg)) LOG.info("image was not in install_values: will reference %s" % matching_iso) install_values['image'] = matching_iso @@ -1370,8 +1358,8 @@ class SubcloudsController(object): consts.DEPLOY_STATE_DEPLOYING]: pecan.abort(400, _('This operation is not allowed while subcloud install, ' 'bootstrap or deploy is in progress.')) - sysadmin_password = \ - payload.get('sysadmin_password') + + sysadmin_password = payload.get('sysadmin_password') if not sysadmin_password: pecan.abort(400, _('subcloud sysadmin_password required')) @@ -1405,8 +1393,11 @@ class SubcloudsController(object): 'install_values': install_values, }) - # Confirm the active system controller load is still in dc-vault - SubcloudsController.verify_active_load_in_vault() + # Get the active system controller load is still in dc-vault + matching_iso, err_msg = utils.get_matching_iso() + if err_msg: + LOG.exception(err_msg) + pecan.abort(400, _(err_msg)) else: # Not Redfish capable subcloud. The subcloud has been reinstalled # and required patches have been applied. diff --git a/distributedcloud/dcmanager/api/policies/subcloud_backup.py b/distributedcloud/dcmanager/api/policies/subcloud_backup.py index ef4b53729..307811bf4 100644 --- a/distributedcloud/dcmanager/api/policies/subcloud_backup.py +++ b/distributedcloud/dcmanager/api/policies/subcloud_backup.py @@ -32,6 +32,17 @@ subcloud_backup_rules = [ 'path': '/v1.0/subcloud-backup/delete/{release_version}' } ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'restore', + check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + description="Restore a subcloud backup.", + operations=[ + { + 'method': 'PATCH', + 'path': '/v1.0/subcloud-backup/restore' + } + ] ) ] diff --git a/distributedcloud/dcmanager/common/consts.py b/distributedcloud/dcmanager/common/consts.py index cf6a2bd20..951a736f1 100644 --- a/distributedcloud/dcmanager/common/consts.py +++ b/distributedcloud/dcmanager/common/consts.py @@ -186,10 +186,7 @@ ERROR_DESC_EMPTY = 'No errors present' # error_description max length ERROR_DESCRIPTION_LENGTH = 2048 -# States to discard while backing up subclouds -INVALID_DEPLOY_STATES_FOR_BACKUP = [DEPLOY_STATE_INSTALLING, - DEPLOY_STATE_BOOTSTRAPPING, - DEPLOY_STATE_DEPLOYING] +# States to discard while restoring subclouds INVALID_DEPLOY_STATES_FOR_RESTORE = [DEPLOY_STATE_INSTALLING, DEPLOY_STATE_BOOTSTRAPPING, DEPLOY_STATE_DEPLOYING, diff --git a/distributedcloud/dcmanager/common/utils.py b/distributedcloud/dcmanager/common/utils.py index 07fe60aff..3521e442c 100644 --- a/distributedcloud/dcmanager/common/utils.py +++ b/distributedcloud/dcmanager/common/utils.py @@ -32,15 +32,13 @@ from oslo_config import cfg from oslo_log import log as logging from dccommon import consts as dccommon_consts +from dccommon.drivers.openstack.sysinv_v1 import SysinvClient +from dccommon.drivers.openstack import vim from dccommon import exceptions as dccommon_exceptions from dcmanager.common import consts from dcmanager.common import exceptions from dcmanager.db import api as db_api -from dccommon.drivers.openstack import vim - -from dccommon.drivers.openstack.sysinv_v1 import SysinvClient - LOG = logging.getLogger(__name__) DC_MANAGER_USERNAME = "root" @@ -694,3 +692,19 @@ def is_valid_for_restore(subcloud): and subcloud.deploy_status not in consts.INVALID_DEPLOY_STATES_FOR_RESTORE ) + + +def get_matching_iso(): + try: + matching_iso, _ = get_vault_load_files(tsc.SW_VERSION) + if not matching_iso: + error_msg = ('Failed to get active load image. Provide ' + 'active load image via ' + '"system --os-region-name SystemController ' + 'load-import --active"') + LOG.exception(error_msg) + return None, error_msg + return matching_iso, None + except Exception as e: + LOG.exception("Could not load vault files.") + return None, str(e)