From 9824f80d957332599f134557bfbc8df448496ba0 Mon Sep 17 00:00:00 2001 From: Victor Romano Date: Fri, 24 Mar 2023 01:31:34 -0300 Subject: [PATCH] Add release parameter to subcloud-backup restore Add optional --release parameter to subcloud-backup restore so that the user can specify which loaded image to use during the installation process triggered by --with-install. This parameter can only be used if --with-install is also specified. Test plan: PASS: Verify that the subcloud is restored with installation of current active release when specified. PASS: Verify that the subcloud is restored with installation of previous inactive release when specified. PASS: Verify that the subcloud is restored with installation of current active release if the "--release" parameter is omitted. Story: 2010611 Task: 47709 Signed-off-by: Victor Romano Change-Id: I619a1bf50c221fe6fc8cdfa87724d18393aef9cb --- api-ref/source/api-ref-dcmanager-v1.rst | 1 + .../subcloud-restore-backup-request.json | 3 +- .../api/controllers/v1/subcloud_backup.py | 21 +- distributedcloud/dcmanager/common/utils.py | 4 + .../dcmanager/manager/subcloud_manager.py | 15 +- .../v1/controllers/test_subcloud_backup.py | 100 ++++++++- .../unit/manager/test_subcloud_manager.py | 192 ++++++++++++++++++ 7 files changed, 321 insertions(+), 15 deletions(-) diff --git a/api-ref/source/api-ref-dcmanager-v1.rst b/api-ref/source/api-ref-dcmanager-v1.rst index 191a97176..cdca8937f 100644 --- a/api-ref/source/api-ref-dcmanager-v1.rst +++ b/api-ref/source/api-ref-dcmanager-v1.rst @@ -1031,6 +1031,7 @@ serviceUnavailable (503) .. rest_parameters:: parameters.yaml - with_install: with_install + - release: release - local_only: backup_local_only - registry_images: backup_registry_images - sysadmin_password: sysadmin_password diff --git a/api-ref/source/samples/subcloud-backup/subcloud-restore-backup-request.json b/api-ref/source/samples/subcloud-backup/subcloud-restore-backup-request.json index 9b689d4b7..d468b88e7 100644 --- a/api-ref/source/samples/subcloud-backup/subcloud-restore-backup-request.json +++ b/api-ref/source/samples/subcloud-backup/subcloud-restore-backup-request.json @@ -2,7 +2,8 @@ "subcloud": 1, "local_only": "false", "registry_images": "false", - "with_install": "false", + "with_install": "true", + "release": "21.12", "sysadmin_password": "XXXXXXX", "restore_values": { "backup_filename": "filename" diff --git a/distributedcloud/dcmanager/api/controllers/v1/subcloud_backup.py b/distributedcloud/dcmanager/api/controllers/v1/subcloud_backup.py index a070c8936..5c0b1f07a 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/subcloud_backup.py +++ b/distributedcloud/dcmanager/api/controllers/v1/subcloud_backup.py @@ -1,22 +1,22 @@ # -# Copyright (c) 2022 Wind River Systems, Inc. +# Copyright (c) 2022-2023 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # -import json from collections import namedtuple - +import json import os + from oslo_config import cfg from oslo_log import log as logging from oslo_messaging import RemoteError import pecan -import yaml - from pecan import expose from pecan import request as pecan_request from pecan import response +import tsconfig.tsconfig as tsc +import yaml from dcmanager.api.controllers import restcomm from dcmanager.api.policies import subcloud_backup as subcloud_backup_policy @@ -28,6 +28,7 @@ from dcmanager.common import utils from dcmanager.db import api as db_api from dcmanager.rpc import client as rpc_client + CONF = cfg.CONF LOG = logging.getLogger(__name__) @@ -71,6 +72,7 @@ class SubcloudBackupController(object): elif verb == 'restore': expected_params = { "with_install": "text", + "release": "text", "local_only": "text", "registry_images": "text", "sysadmin_password": "text", @@ -339,6 +341,10 @@ class SubcloudBackupController(object): pecan.abort(400, _('Option registry_images cannot be used ' 'without local_only option.')) + if not payload['with_install'] and payload.get('release'): + pecan.abort(400, _('Option release cannot be used ' + 'without with_install 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, @@ -359,8 +365,9 @@ class SubcloudBackupController(object): '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() + # Confirm the requested or active load is still in dc-vault + payload['software_version'] = payload.get('release', tsc.SW_VERSION) + matching_iso, err_msg = utils.get_matching_iso(payload['software_version']) if err_msg: LOG.exception(err_msg) pecan.abort(400, _(err_msg)) diff --git a/distributedcloud/dcmanager/common/utils.py b/distributedcloud/dcmanager/common/utils.py index 94b7e1f46..18e281c0c 100644 --- a/distributedcloud/dcmanager/common/utils.py +++ b/distributedcloud/dcmanager/common/utils.py @@ -742,10 +742,14 @@ def _is_valid_for_backup_delete(subcloud): def _is_valid_for_backup_restore(subcloud): + msg = None if subcloud.management_state != dccommon_consts.MANAGEMENT_UNMANAGED \ or subcloud.deploy_status in consts.INVALID_DEPLOY_STATES_FOR_RESTORE: msg = ('Subcloud %s must be unmanaged and in a valid deploy state ' 'for the subcloud-backup restore operation.' % subcloud.name) + elif not subcloud.data_install: + msg = ('Data installation on %s is missing.' % subcloud.name) + if msg: raise exceptions.ValidateFail(msg) return True diff --git a/distributedcloud/dcmanager/manager/subcloud_manager.py b/distributedcloud/dcmanager/manager/subcloud_manager.py index 2138a866a..ce7cc9e1d 100644 --- a/distributedcloud/dcmanager/manager/subcloud_manager.py +++ b/distributedcloud/dcmanager/manager/subcloud_manager.py @@ -745,8 +745,9 @@ class SubcloudManager(manager.Manager): def _subcloud_operation_notice( self, operation, restore_subclouds, failed_subclouds, invalid_subclouds): - all_failed = not set(restore_subclouds) - set(failed_subclouds) - if restore_subclouds and all_failed: + all_failed = ((not set(restore_subclouds) - set(failed_subclouds)) + and not invalid_subclouds) + if all_failed: LOG.error("Backup %s failed for all applied subclouds" % operation) raise exceptions.SubcloudBackupOperationFailed(operation=operation) @@ -972,12 +973,13 @@ class SubcloudManager(manager.Manager): return subcloud, False if payload.get('with_install'): + software_version = payload.get('software_version') install_command = self.compose_install_command( - subcloud.name, subcloud_inventory_file, subcloud.software_version) + subcloud.name, subcloud_inventory_file, software_version) # Update data_install with missing data - matching_iso, _ = utils.get_vault_load_files(subcloud.software_version) + matching_iso, _ = utils.get_vault_load_files(software_version) + data_install['software_version'] = software_version data_install['image'] = matching_iso - data_install['software_version'] = subcloud.software_version data_install['ansible_ssh_pass'] = payload['sysadmin_password'] data_install['ansible_become_pass'] = payload['sysadmin_password'] install_success = self._run_subcloud_install( @@ -1371,7 +1373,8 @@ class SubcloudManager(manager.Manager): db_api.subcloud_update( context, subcloud.id, deploy_status=consts.DEPLOY_STATE_INSTALLING, - error_description=consts.ERROR_DESC_EMPTY) + error_description=consts.ERROR_DESC_EMPTY, + software_version=str(payload['software_version'])) try: install.install(consts.DC_ANSIBLE_LOG_DIR, install_command) except Exception as e: diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_backup.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_backup.py index a1fe9b01a..58ac2c025 100644 --- a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_backup.py +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_backup.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 Wind River Systems, Inc. +# Copyright (c) 2022-2023 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -1216,3 +1216,101 @@ class TestSubcloudRestore(testroot.DCManagerApiTest): six.assertRaisesRegex(self, webtest.app.AppError, "400 *", self.app.patch_json, FAKE_URL_RESTORE, headers=FAKE_HEADERS, params=data) + + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch('os.path.isdir') + @mock.patch('os.listdir') + def test_backup_restore_subcloud_with_install_no_release(self, + mock_listdir, + mock_isdir, + mock_rpc_client): + + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + data_install = str(fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES).replace('\'', '"') + db_api.subcloud_update(self.ctx, + subcloud.id, + availability_status=dccommon_consts.AVAILABILITY_ONLINE, + management_state=dccommon_consts.MANAGEMENT_UNMANAGED, + data_install=data_install) + + fake_password = (base64.b64encode('testpass'.encode("utf-8"))).decode('ascii') + data = {'sysadmin_password': fake_password, + 'subcloud': '1', + 'with_install': 'True' + } + + mock_isdir.return_value = True + mock_listdir.return_value = ['test.iso', 'test.sig'] + mock_rpc_client().restore_subcloud_backups.return_value = True + response = self.app.patch_json(FAKE_URL_RESTORE, + headers=FAKE_HEADERS, + params=data) + + self.assertEqual(response.status_int, 200) + + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch('os.path.isdir') + @mock.patch('os.listdir') + def test_backup_restore_subcloud_with_install_with_release(self, + mock_listdir, + mock_isdir, + mock_rpc_client): + + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + data_install = str(fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES).replace('\'', '"') + db_api.subcloud_update(self.ctx, + subcloud.id, + availability_status=dccommon_consts.AVAILABILITY_ONLINE, + management_state=dccommon_consts.MANAGEMENT_UNMANAGED, + data_install=data_install) + + fake_password = (base64.b64encode('testpass'.encode("utf-8"))).decode('ascii') + data = {'sysadmin_password': fake_password, + 'subcloud': '1', + 'with_install': 'True', + 'release': '22.12' + } + + mock_isdir.return_value = True + mock_listdir.return_value = ['test.iso', 'test.sig'] + mock_rpc_client().restore_subcloud_backups.return_value = True + + response = self.app.patch_json(FAKE_URL_RESTORE, + headers=FAKE_HEADERS, + params=data) + + self.assertEqual(response.status_int, 200) + + @mock.patch.object(rpc_client, 'ManagerClient') + def test_backup_restore_subcloud_no_install_with_release(self, mock_rpc_client): + + fake_password = (base64.b64encode('testpass'.encode("utf-8"))).decode('ascii') + data = {'sysadmin_password': fake_password, + 'subcloud': '1', + 'release': '22.12' + } + + mock_rpc_client().restore_subcloud_backups.return_value = True + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL_RESTORE, + headers=FAKE_HEADERS, params=data) + + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch('dcmanager.common.utils.get_matching_iso') + def test_backup_restore_subcloud_invalid_release(self, + mock_rpc_client, + mock_matching_iso): + + fake_password = (base64.b64encode('testpass'.encode("utf-8"))).decode('ascii') + data = {'sysadmin_password': fake_password, + 'subcloud': '1', + 'release': '00.00' + } + + mock_rpc_client().restore_subcloud_backups.return_value = True + mock_matching_iso.return_value = [None, True] + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL_RESTORE, + headers=FAKE_HEADERS, params=data) diff --git a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py index 5a17953ad..dfcc0e4f8 100644 --- a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py +++ b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py @@ -318,6 +318,17 @@ FAKE_BACKUP_CREATE_LOAD_1 = { "registry_images": False, } +FAKE_BACKUP_RESTORE_LOAD = { + "sysadmin_password": "testpasswd", + "subcloud": 1 +} + +FAKE_BACKUP_RESTORE_LOAD_WITH_INSTALL = { + "sysadmin_password": "testpasswd", + "subcloud": 1, + "install_values": fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES +} + class Subcloud(object): def __init__(self, data, is_online): @@ -2187,3 +2198,184 @@ class TestSubcloudManager(base.DCManagerTestCase): mock_keystone_client, mock_sysinv_client) expiry2 = cached_regionone_data['expiry'] self.assertEqual(expiry1, expiry2) + + @mock.patch.object(subcloud_manager.SubcloudManager, + '_run_subcloud_backup_restore_playbook') + @mock.patch.object(subcloud_manager.SubcloudManager, + '_create_overrides_for_backup_or_restore') + @mock.patch.object(subcloud_manager.SubcloudManager, + '_create_subcloud_inventory_file') + def test_backup_restore_unmanaged_online(self, + mock_create_inventory_file, + mock_create_overrides, + mock_run_playbook + ): + mock_create_inventory_file.return_value = 'inventory_file.yml' + mock_create_overrides.return_value = 'overrides_file.yml' + + values = copy.copy(FAKE_BACKUP_RESTORE_LOAD) + subcloud = self.create_subcloud_static( + self.ctx, + name='subcloud1', + deploy_status=consts.DEPLOY_STATE_DONE) + + data_install = str(fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES).replace('\'', '"') + + db_api.subcloud_update(self.ctx, + subcloud.id, + availability_status=dccommon_consts.AVAILABILITY_ONLINE, + management_state=dccommon_consts.MANAGEMENT_UNMANAGED, + data_install=data_install) + + sm = subcloud_manager.SubcloudManager() + sm.restore_subcloud_backups(self.ctx, payload=values) + + mock_create_inventory_file.assert_called_once() + mock_create_overrides.assert_called_once() + mock_run_playbook.assert_called_once() + + # Verify that subcloud has the correct deploy status + updated_subcloud = db_api.subcloud_get_by_name(self.ctx, subcloud.name) + self.assertEqual(consts.DEPLOY_STATE_PRE_RESTORE, + updated_subcloud.deploy_status) + + @mock.patch.object(subcloud_manager.SubcloudManager, + '_run_subcloud_backup_restore_playbook') + @mock.patch.object(subcloud_manager.SubcloudManager, + '_create_overrides_for_backup_or_restore') + @mock.patch.object(subcloud_manager.SubcloudManager, + '_create_subcloud_inventory_file') + def test_backup_restore_managed_online(self, + mock_create_inventory_file, + mock_create_overrides, + mock_run_playbook + ): + + values = copy.copy(FAKE_BACKUP_RESTORE_LOAD) + subcloud = self.create_subcloud_static( + self.ctx, + name='subcloud1', + deploy_status=consts.DEPLOY_STATE_NONE) + + data_install = str(fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES).replace('\'', '"') + + db_api.subcloud_update(self.ctx, + subcloud.id, + availability_status=dccommon_consts.AVAILABILITY_ONLINE, + management_state=dccommon_consts.MANAGEMENT_MANAGED, + data_install=data_install) + + sm = subcloud_manager.SubcloudManager() + return_log = sm.restore_subcloud_backups(self.ctx, payload=values) + + expected_log = 'skipped for local backup restore operation' + + self.assertIn(expected_log, return_log) + + @mock.patch.object(subcloud_manager.SubcloudManager, + '_run_subcloud_backup_restore_playbook') + @mock.patch.object(subcloud_manager.SubcloudManager, + '_create_overrides_for_backup_or_restore') + @mock.patch.object(subcloud_manager.SubcloudManager, + '_create_subcloud_inventory_file') + def test_backup_restore_unmanaged_offline(self, + mock_create_inventory_file, + mock_create_overrides, + mock_run_playbook + ): + + values = copy.copy(FAKE_BACKUP_RESTORE_LOAD) + subcloud = self.create_subcloud_static( + self.ctx, + name='subcloud1', + deploy_status=consts.DEPLOY_STATE_NONE) + + data_install = str(fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES).replace('\'', '"') + + db_api.subcloud_update(self.ctx, + subcloud.id, + availability_status=dccommon_consts.AVAILABILITY_OFFLINE, + management_state=dccommon_consts.MANAGEMENT_UNMANAGED, + data_install=data_install) + + sm = subcloud_manager.SubcloudManager() + sm.restore_subcloud_backups(self.ctx, payload=values) + + mock_create_inventory_file.assert_called_once() + mock_create_overrides.assert_called_once() + mock_run_playbook.assert_called_once() + + # Verify that subcloud has the correct deploy status + updated_subcloud = db_api.subcloud_get_by_name(self.ctx, subcloud.name) + self.assertEqual(consts.DEPLOY_STATE_PRE_RESTORE, + updated_subcloud.deploy_status) + + def test_backup_restore_managed_offline(self): + + values = copy.copy(FAKE_BACKUP_RESTORE_LOAD) + subcloud = self.create_subcloud_static( + self.ctx, + name='subcloud1', + deploy_status=consts.DEPLOY_STATE_NONE) + + db_api.subcloud_update(self.ctx, + subcloud.id, + availability_status=dccommon_consts.AVAILABILITY_OFFLINE, + management_state=dccommon_consts.MANAGEMENT_MANAGED) + + sm = subcloud_manager.SubcloudManager() + return_log = sm.restore_subcloud_backups(self.ctx, payload=values) + + expected_log = 'skipped for local backup restore operation' + + self.assertIn(expected_log, return_log) + + @mock.patch.object(subcloud_manager.SubcloudManager, + '_run_subcloud_backup_restore_playbook') + @mock.patch.object(subcloud_manager.SubcloudManager, '_run_subcloud_install') + @mock.patch.object(subcloud_manager.SubcloudManager, + '_create_overrides_for_backup_or_restore') + @mock.patch.object(subcloud_manager.SubcloudManager, + '_create_subcloud_inventory_file') + @mock.patch('os.path.isdir') + @mock.patch('os.listdir') + def test_backup_restore_with_install(self, + mock_listdir, + mock_isdir, + mock_create_inventory_file, + mock_create_overrides, + mock_subcloud_install, + mock_run_restore_playbook + ): + mock_isdir.return_value = True + mock_listdir.return_value = ['test.iso', 'test.sig'] + mock_create_inventory_file.return_value = 'inventory_file.yml' + mock_create_overrides.return_value = 'overrides_file.yml' + mock_subcloud_install.return_value = True + mock_run_restore_playbook.return_value = True + + data_install = str(fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES).replace('\'', '"') + + values = copy.copy(FAKE_BACKUP_RESTORE_LOAD_WITH_INSTALL) + values['with_install'] = True + subcloud = self.create_subcloud_static( + self.ctx, + name='subcloud1', + data_install=data_install, + deploy_status=consts.DEPLOY_STATE_DONE) + + db_api.subcloud_update(self.ctx, + subcloud.id, + availability_status=dccommon_consts.AVAILABILITY_ONLINE, + management_state=dccommon_consts.MANAGEMENT_UNMANAGED) + + sm = subcloud_manager.SubcloudManager() + sm.restore_subcloud_backups(self.ctx, payload=values) + + mock_create_inventory_file.assert_called_once() + mock_create_overrides.assert_called_once() + + # Verify that subcloud has the correct deploy status + updated_subcloud = db_api.subcloud_get_by_name(self.ctx, subcloud.name) + self.assertEqual(consts.DEPLOY_STATE_PRE_RESTORE, + updated_subcloud.deploy_status)