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 <Andre.DexheimerCarneiro@windriver.com>
Change-Id: I50ac2b529be9da45f68f21f46e9355546328ac40
This commit is contained in:
Andre Carneiro 2022-10-06 15:14:42 -03:00
parent 4d5364cae0
commit 24588e857b
5 changed files with 184 additions and 131 deletions

View File

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

View File

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

View File

@ -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'
}
]
)
]

View File

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

View File

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