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:
parent
4d5364cae0
commit
24588e857b
@ -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'))
|
||||
|
@ -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.
|
||||
|
@ -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'
|
||||
}
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user