From 528f90afec403f63c6327a04f752eeef9be56f44 Mon Sep 17 00:00:00 2001 From: Victor Romano Date: Mon, 24 Jul 2023 09:37:53 -0300 Subject: [PATCH] Add subcloud redeploy option to dcmanager This commit adds the command "subcloud redeploy" to dcmanager. The redeploy operation is similar to "subcloud reinstall", performing a fresh install, bootstrapping and configuring the subcloud, but allowing the user to use either previously used install/bootstrap/config values stored on the system controller or new values from provided files. Since config is an optional phase, it will only be executed if respective parameters are provided in the current request or were provided in a previous deployment. Test Plan: Success cases: - PASS: Redeploy subcloud without passing any new files and verify the redeployment was successful and the final deploy state is "complete". - PASS: Redeploy subcloud passing new install/bootstrap/config files and verify the redeployment was successful and the final deploy state is "complete". - PASS: Redeploy a subcloud with a different management subnet and verify that the network reconfiguration was executed during the bootstrap phase and the redeployment completed successfully. - PASS: Redeploy a subcloud that wasn't configure by the "deploy config" command passing a config file and verify that the subcloud was redeploy and configured. - PASS: Redeploy a subcloud that wasn't configure by the "deploy config" command without passing a config file. and verify that the subcloud was redeployed and no configuration attempt was made. - PASS: Redeploy a subcloud passing a previous release (21.12) and verify the redeployment was successful and the final deploy state is "complete". - PASS: Abort each one of the three deployment phases. Verify the deployment was successfully aborted. - PASS: Resume the aborted deployment and verify the subcloud was successfully deployed. - PASS: Repeat previous tests but directly call the API (using CURL) instead of using the CLI. Failure cases: - PASS: Verify it's not possible to redeploy an online and/or managed subcloud. - PASS: Call the API directly, passing bmc-password and/or sysadmin-password as plain text as opposed to b64encoded and verify that the response contains the correct error code and message. Story: 2010756 Task: 48496 Change-Id: I6148096909adda2b95b6bb964bc7a749ac62c20c Signed-off-by: Victor Romano --- api-ref/source/api-ref-dcmanager-v1.rst | 72 ++++ .../subcloud-patch-redeploy-request.json | 8 + .../subcloud-patch-redeploy-response.json | 25 ++ .../dcmanager/api/controllers/v1/subclouds.py | 71 ++++ .../dcmanager/api/policies/subclouds.py | 4 + .../common/phased_subcloud_deploy.py | 8 +- distributedcloud/dcmanager/common/utils.py | 7 +- distributedcloud/dcmanager/manager/service.py | 10 +- .../dcmanager/manager/subcloud_manager.py | 97 +++-- distributedcloud/dcmanager/rpc/client.py | 5 + .../unit/api/v1/controllers/test_subclouds.py | 358 +++++++++++++++++- .../unit/manager/test_subcloud_manager.py | 59 ++- .../tests/unit/manager/test_utils.py | 13 +- 13 files changed, 688 insertions(+), 49 deletions(-) create mode 100644 api-ref/source/samples/subclouds/subcloud-patch-redeploy-request.json create mode 100644 api-ref/source/samples/subclouds/subcloud-patch-redeploy-response.json diff --git a/api-ref/source/api-ref-dcmanager-v1.rst b/api-ref/source/api-ref-dcmanager-v1.rst index d2cef35be..d1c0d039a 100644 --- a/api-ref/source/api-ref-dcmanager-v1.rst +++ b/api-ref/source/api-ref-dcmanager-v1.rst @@ -541,6 +541,78 @@ Response Example .. literalinclude:: samples/subclouds/subcloud-patch-reinstall-response.json :language: json +******************************** +Redeploy a specific subcloud +******************************** + +.. rest_method:: PATCH /v1.0/subclouds/{subcloud}/redeploy + +Redeploy and bootstrap a subcloud based on its previous install configurations. + +**Normal response codes** + +200 + +**Error response codes** + +badRequest (400), unauthorized (401), forbidden (403), badMethod (405), +HTTPUnprocessableEntity (422), internalServerError (500), +serviceUnavailable (503) + +**Request parameters** + +.. rest_parameters:: parameters.yaml + + - subcloud: subcloud_uri + - install_values: install_values + - bootstrap_values: bootstrap_values + - deploy_config: deploy_config + - release: release + - sysadmin_password: sysadmin_password + - bmc_password: bmc_password + +Request Example +---------------- + +.. literalinclude:: samples/subclouds/subcloud-patch-redeploy-request.json + :language: json + +**Response parameters** + +.. rest_parameters:: parameters.yaml + + - id: subcloud_id + - group_id: group_id + - name: subcloud_name + - description: subcloud_description + - location: subcloud_location + - software-version: software_version + - availability-status: availability_status + - error-description: error_description + - deploy-status: deploy_status + - backup-status: backup_status + - backup-datetime: backup_datetime + - openstack-installed: openstack_installed + - management-state: management_state + - systemcontroller-gateway-ip: systemcontroller_gateway_ip + - management-start-ip: management_start_ip + - management-end-ip: management_end_ip + - management-subnet: management_subnet + - management-gateway-ip: management_gateway_ip + - created-at: created_at + - updated-at: updated_at + - data_install: data_install + - data_upgrade: data_upgrade + - endpoint_sync_status: endpoint_sync_status + - sync_status: sync_status + - endpoint_type: sync_status_type + +Response Example +---------------- + +.. literalinclude:: samples/subclouds/subcloud-patch-redeploy-response.json + :language: json + ******************************** Prestage a specific subcloud ******************************** diff --git a/api-ref/source/samples/subclouds/subcloud-patch-redeploy-request.json b/api-ref/source/samples/subclouds/subcloud-patch-redeploy-request.json new file mode 100644 index 000000000..d2902f46b --- /dev/null +++ b/api-ref/source/samples/subclouds/subcloud-patch-redeploy-request.json @@ -0,0 +1,8 @@ +{ + "bmc_password": "YYYYYYY", + "bootstrap_values": "content of bootstrap_values file", + "deploy_config": "content of deploy_config file", + "install_values": "content of install_values file", + "release": "22.12", + "sysadmin_password": "XXXXXXX" +} \ No newline at end of file diff --git a/api-ref/source/samples/subclouds/subcloud-patch-redeploy-response.json b/api-ref/source/samples/subclouds/subcloud-patch-redeploy-response.json new file mode 100644 index 000000000..66d638225 --- /dev/null +++ b/api-ref/source/samples/subclouds/subcloud-patch-redeploy-response.json @@ -0,0 +1,25 @@ +{ + "id": 1, + "name": "subcloud1", + "created-at": "2021-11-08T18:41:19.530228", + "updated-at": "2021-11-15T14:15:59.944851", + "availability-status": "offline", + "data_install": { + "bootstrap_interface": "eno1" + }, + "data_upgrade": null, + "deploy-status": "pre-install", + "backup-status": "complete", + "backup-datetime": "2022-07-08 11:23:58.132134", + "description": "Ottawa Site", + "group_id": 1, + "location": "YOW", + "management-end-ip": "192.168.101.50", + "management-gateway-ip": "192.168.101.1", + "management-start-ip": "192.168.101.2", + "management-state": "unmanaged", + "management-subnet": "192.168.101.0/24", + "openstack-installed": false, + "software-version": "22.12", + "systemcontroller-gateway-ip": "192.168.204.101" +} diff --git a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py index c6dddb5fa..71e0d9cb9 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py +++ b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py @@ -23,6 +23,7 @@ from requests_toolbelt.multipart import decoder import base64 import json import keyring +import os from oslo_config import cfg from oslo_log import log as logging from oslo_messaging import RemoteError @@ -77,6 +78,12 @@ SUBCLOUD_ADD_GET_FILE_CONTENTS = [ INSTALL_VALUES, ] +SUBCLOUD_REDEPLOY_GET_FILE_CONTENTS = [ + INSTALL_VALUES, + BOOTSTRAP_VALUES, + consts.DEPLOY_CONFIG +] + BOOTSTRAP_VALUES_ADDRESSES = [ 'bootstrap-address', 'management_start_address', 'management_end_address', 'management_gateway_address', 'systemcontroller_gateway_address', @@ -761,6 +768,70 @@ class SubcloudsController(object): except Exception: LOG.exception("Unable to reinstall subcloud %s" % subcloud.name) pecan.abort(500, _('Unable to reinstall subcloud')) + elif verb == "redeploy": + config_file = psd_common.get_config_file_path(subcloud.name, + consts.DEPLOY_CONFIG) + has_bootstrap_values = consts.BOOTSTRAP_VALUES in request.POST + has_original_config_values = os.path.exists(config_file) + has_new_config_values = consts.DEPLOY_CONFIG in request.POST + has_config_values = has_original_config_values or has_new_config_values + payload = psd_common.get_request_data( + request, subcloud, SUBCLOUD_REDEPLOY_GET_FILE_CONTENTS) + + if (subcloud.availability_status == dccommon_consts.AVAILABILITY_ONLINE or + subcloud.management_state == dccommon_consts.MANAGEMENT_MANAGED): + msg = _('Cannot re-deploy an online and/or managed subcloud') + LOG.warning(msg) + pecan.abort(400, msg) + + # If a subcloud release is not passed, use the current + # system controller software_version + payload['software_version'] = payload.get('release', tsc.SW_VERSION) + + # Don't load previously stored bootstrap_values if they are present in + # the request, as this would override the already loaded values from it. + # As config_values are optional, only attempt to load previously stored + # values if this phase should be executed. + files_for_redeploy = SUBCLOUD_REDEPLOY_GET_FILE_CONTENTS.copy() + if has_bootstrap_values: + files_for_redeploy.remove(BOOTSTRAP_VALUES) + if not has_config_values: + files_for_redeploy.remove(consts.DEPLOY_CONFIG) + + psd_common.populate_payload_with_pre_existing_data( + payload, subcloud, files_for_redeploy) + + psd_common.validate_sysadmin_password(payload) + psd_common.pre_deploy_install(payload, validate_password=False) + psd_common.pre_deploy_bootstrap(context, payload, subcloud, + has_bootstrap_values, + validate_password=False) + payload['bootstrap-address'] = \ + payload['install_values']['bootstrap_address'] + + try: + # Align the software version of the subcloud with redeploy + # version. Update description, location and group id if offered, + # update the deploy status as pre-install. + subcloud = db_api.subcloud_update( + context, + subcloud_id, + description=payload.get('description'), + location=payload.get('location'), + software_version=payload['software_version'], + deploy_status=consts.DEPLOY_STATE_PRE_INSTALL, + first_identity_sync_complete=False, + data_install=json.dumps(payload['install_values'])) + + self.dcmanager_rpc_client.redeploy_subcloud( + context, subcloud_id, payload) + + return db_api.subcloud_db_model_to_dict(subcloud) + except RemoteError as e: + pecan.abort(422, e.value) + except Exception: + LOG.exception("Unable to redeploy subcloud %s" % subcloud.name) + pecan.abort(500, _('Unable to redeploy subcloud')) elif verb == "restore": pecan.abort(410, _('This API is deprecated. ' 'Please use /v1.0/subcloud-backup/restore')) diff --git a/distributedcloud/dcmanager/api/policies/subclouds.py b/distributedcloud/dcmanager/api/policies/subclouds.py index 261876c97..a5e02f901 100644 --- a/distributedcloud/dcmanager/api/policies/subclouds.py +++ b/distributedcloud/dcmanager/api/policies/subclouds.py @@ -73,6 +73,10 @@ subclouds_rules = [ 'method': 'PATCH', 'path': '/v1.0/subclouds/{subcloud}/reinstall' }, + { + 'method': 'PATCH', + 'path': '/v1.0/subclouds/{subcloud}/redeploy' + }, { 'method': 'PATCH', 'path': '/v1.0/subclouds/{subcloud}/restore' diff --git a/distributedcloud/dcmanager/common/phased_subcloud_deploy.py b/distributedcloud/dcmanager/common/phased_subcloud_deploy.py index 6e5a2ac8e..9c1f956be 100644 --- a/distributedcloud/dcmanager/common/phased_subcloud_deploy.py +++ b/distributedcloud/dcmanager/common/phased_subcloud_deploy.py @@ -491,9 +491,9 @@ def validate_install_values(payload, subcloud=None): if software_version and software_version != install_software_version: pecan.abort(400, _("The software_version value %s in the install values " - "yaml file does not match with the specified/current " - "software version of %s. Please correct or remove " - "this parameter from the yaml file and try again.") % + "yaml file does not match with the specified/current " + "software version of %s. Please correct or remove " + "this parameter from the yaml file and try again.") % (install_software_version, software_version)) else: # Only install_values payload will be passed to the subcloud @@ -959,7 +959,7 @@ def pre_deploy_install(payload: dict, validate_password=False): # If the software version of the subcloud is different from the # specified or active load, update the software version in install # value and delete the image path in install values, then the subcloud - # will be reinstalled using the image in dc_vault. + # will be installed using the image in dc_vault. if install_values.get( 'software_version') != payload['software_version']: install_values['software_version'] = payload['software_version'] diff --git a/distributedcloud/dcmanager/common/utils.py b/distributedcloud/dcmanager/common/utils.py index b118acfd9..d457e6cb4 100644 --- a/distributedcloud/dcmanager/common/utils.py +++ b/distributedcloud/dcmanager/common/utils.py @@ -133,7 +133,8 @@ def validate_network_str(network_str, minimum_size, existing_networks=None, "least %d addresses" % minimum_size) elif network.version == 6 and network.prefixlen < 64: raise exceptions.ValidateFail("IPv6 minimum prefix length is 64") - elif existing_networks and operation != 'reinstall': + elif existing_networks and (operation != 'reinstall' + and operation != 'redeploy'): if any(network.ip in subnet for subnet in existing_networks): raise exceptions.ValidateFail("Subnet overlaps with another " "configured subnet") @@ -943,12 +944,14 @@ def has_network_reconfig(payload, subcloud): start_address = get_management_start_address(payload) end_address = get_management_end_address(payload) gateway_address = get_management_gateway_address(payload) + sys_controller_gw_ip = payload.get("systemcontroller_gateway_address") has_network_reconfig = any([ management_subnet != subcloud.management_subnet, start_address != subcloud.management_start_ip, end_address != subcloud.management_end_ip, - gateway_address != subcloud.management_gateway_ip + gateway_address != subcloud.management_gateway_ip, + sys_controller_gw_ip != subcloud.systemcontroller_gateway_ip ]) return has_network_reconfig diff --git a/distributedcloud/dcmanager/manager/service.py b/distributedcloud/dcmanager/manager/service.py index fc82bb5da..7b5d42d1c 100644 --- a/distributedcloud/dcmanager/manager/service.py +++ b/distributedcloud/dcmanager/manager/service.py @@ -144,11 +144,19 @@ class DCManagerService(service.Service): @request_context def reinstall_subcloud(self, context, subcloud_id, payload): # Reinstall a subcloud - LOG.info("Handling reinstall_subcloud request for: %s" % subcloud_id) + LOG.info("Handling reinstall_subcloud request for: %s" % payload.get('name')) return self.subcloud_manager.reinstall_subcloud(context, subcloud_id, payload) + @request_context + def redeploy_subcloud(self, context, subcloud_id, payload): + # Redeploy a subcloud + LOG.info("Handling redeploy_subcloud request for: %s" % subcloud_id) + return self.subcloud_manager.redeploy_subcloud(context, + subcloud_id, + payload) + @request_context def backup_subclouds(self, context, payload): # Backup a subcloud or group of subclouds diff --git a/distributedcloud/dcmanager/manager/subcloud_manager.py b/distributedcloud/dcmanager/manager/subcloud_manager.py index d6b42a43c..0b9d1aa75 100644 --- a/distributedcloud/dcmanager/manager/subcloud_manager.py +++ b/distributedcloud/dcmanager/manager/subcloud_manager.py @@ -508,6 +508,35 @@ class SubcloudManager(manager.Manager): context, subcloud_id, deploy_status=consts.DEPLOY_STATE_PRE_INSTALL_FAILED) + def redeploy_subcloud(self, context, subcloud_id, payload): + """Redeploy subcloud + + :param context: request context object + :param subcloud_id: subcloud id from db + :param payload: subcloud redeploy + """ + + # Retrieve the subcloud details from the database + subcloud = db_api.subcloud_get(context, subcloud_id) + + LOG.info("Redeploying subcloud %s." % subcloud.name) + + # Define which deploy phases to run + phases_to_run = [consts.DEPLOY_PHASE_INSTALL, + consts.DEPLOY_PHASE_BOOTSTRAP] + if consts.DEPLOY_CONFIG in payload: + phases_to_run.append(consts.DEPLOY_PHASE_CONFIG) + + succeeded = self.run_deploy_phases(context, subcloud_id, payload, + phases_to_run) + + if succeeded: + db_api.subcloud_update( + context, subcloud.id, + deploy_status=consts.DEPLOY_STATE_DONE, + error_description=consts.ERROR_DESC_EMPTY) + LOG.info(f"Finished redeploying subcloud {subcloud['name']}.") + def create_subcloud_backups(self, context, payload): """Backup subcloud or group of subclouds @@ -637,29 +666,18 @@ class SubcloudManager(manager.Manager): :param ansible_subcloud_inventory_file: the ansible inventory file path :return: ansible command needed to run the bootstrap playbook """ - management_subnet = utils.get_management_subnet(payload) - sys_controller_gw_ip = payload.get( - "systemcontroller_gateway_address") - - if (management_subnet != subcloud.management_subnet) or ( - sys_controller_gw_ip != subcloud.systemcontroller_gateway_ip): - m_ks_client = OpenStackDriver( - region_name=dccommon_consts.DEFAULT_REGION_NAME, - region_clients=None).keystone_client - # Create a new route - self._create_subcloud_route(payload, m_ks_client, - sys_controller_gw_ip) - # Delete previous route - self._delete_subcloud_routes(m_ks_client, subcloud) - # Update endpoints - self._update_services_endpoint(context, payload, subcloud.name, - m_ks_client) + network_reconfig = utils.has_network_reconfig(payload, subcloud) + if network_reconfig: + self._configure_system_controller_network(context, payload, subcloud, + update_db=False) + # Regenerate the addn_hosts_dc file + self._create_addn_hosts_dc(context) # Update subcloud subcloud = db_api.subcloud_update( context, subcloud.id, - description=payload.get("description", None), + description=payload.get("description"), management_subnet=utils.get_management_subnet(payload), management_gateway_ip=utils.get_management_gateway_address( payload), @@ -667,8 +685,8 @@ class SubcloudManager(manager.Manager): payload), management_end_ip=utils.get_management_end_address(payload), systemcontroller_gateway_ip=payload.get( - "systemcontroller_gateway_address", None), - location=payload.get("location", None), + "systemcontroller_gateway_address"), + location=payload.get("location"), deploy_status=consts.DEPLOY_STATE_PRE_BOOTSTRAP) # Populate payload with passwords @@ -959,8 +977,7 @@ class SubcloudManager(manager.Manager): deploy_status=deploy_state) # The RPC call must return the subcloud as a dictionary, otherwise it - # should return the DB object for dcmanager internal use (subcloud add, - # resume and redeploy) + # should return the DB object for dcmanager internal use (subcloud add) if return_as_dict: subcloud = db_api.subcloud_db_model_to_dict(subcloud) @@ -2308,35 +2325,47 @@ class SubcloudManager(manager.Manager): # Regenerate the addn_hosts_dc file self._create_addn_hosts_dc(context) - def _configure_system_controller_network(self, context, payload, subcloud): + def _configure_system_controller_network(self, context, payload, subcloud, + update_db=True): + """Configure system controller network + + :param context: request context object + :param payload: subcloud bootstrap configuration + :param subcloud: subcloud model object + :param update_db: whether it should update the db on success/failure + """ subcloud_name = subcloud.name subcloud_id = subcloud.id + sys_controller_gw_ip = payload.get("systemcontroller_gateway_address", + subcloud.systemcontroller_gateway_ip) try: m_ks_client = OpenStackDriver( region_name=dccommon_consts.DEFAULT_REGION_NAME, region_clients=None).keystone_client self._create_subcloud_route(payload, m_ks_client, - subcloud.systemcontroller_gateway_ip) + sys_controller_gw_ip) except Exception: LOG.exception( "Failed to create route to subcloud %s." % subcloud_name) - db_api.subcloud_update( - context, subcloud_id, - deploy_status=consts.DEPLOY_STATE_RECONFIGURING_NETWORK_FAILED, - error_description=consts.ERROR_DESC_EMPTY - ) + if update_db: + db_api.subcloud_update( + context, subcloud_id, + deploy_status=consts.DEPLOY_STATE_RECONFIGURING_NETWORK_FAILED, + error_description=consts.ERROR_DESC_EMPTY + ) return try: self._update_services_endpoint( context, payload, subcloud_name, m_ks_client) except Exception: LOG.exception("Failed to update subcloud %s endpoints" % subcloud_name) - db_api.subcloud_update( - context, subcloud_id, - deploy_status=consts.DEPLOY_STATE_RECONFIGURING_NETWORK_FAILED, - error_description=consts.ERROR_DESC_EMPTY - ) + if update_db: + db_api.subcloud_update( + context, subcloud_id, + deploy_status=consts.DEPLOY_STATE_RECONFIGURING_NETWORK_FAILED, + error_description=consts.ERROR_DESC_EMPTY + ) return # Delete old routes diff --git a/distributedcloud/dcmanager/rpc/client.py b/distributedcloud/dcmanager/rpc/client.py index 53141a64c..ec894d2c2 100644 --- a/distributedcloud/dcmanager/rpc/client.py +++ b/distributedcloud/dcmanager/rpc/client.py @@ -160,6 +160,11 @@ class ManagerClient(RPCClient): subcloud_id=subcloud_id, payload=payload)) + def redeploy_subcloud(self, ctxt, subcloud_id, payload): + return self.cast(ctxt, self.make_msg('redeploy_subcloud', + subcloud_id=subcloud_id, + payload=payload)) + def backup_subclouds(self, ctxt, payload): return self.cast(ctxt, self.make_msg('backup_subclouds', payload=payload)) diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py index f01920b51..f56805e17 100644 --- a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py @@ -15,15 +15,17 @@ # under the License. # -from oslo_utils import timeutils - import base64 import copy import json +import os + import keyring import mock +from oslo_utils import timeutils import six from six.moves import http_client +from tsconfig.tsconfig import SW_VERSION import webtest from dccommon import consts as dccommon_consts @@ -34,15 +36,12 @@ from dcmanager.common import prestage from dcmanager.common import utils as cutils from dcmanager.db.sqlalchemy import api as db_api from dcmanager.rpc import client as rpc_client - from dcmanager.tests.unit.api import test_root_controller as testroot from dcmanager.tests.unit.api.v1.controllers.mixins import APIMixin from dcmanager.tests.unit.api.v1.controllers.mixins import PostMixin from dcmanager.tests.unit.common import fake_subcloud from dcmanager.tests import utils -from tsconfig.tsconfig import SW_VERSION - SAMPLE_SUBCLOUD_NAME = 'SubcloudX' SAMPLE_SUBCLOUD_DESCRIPTION = 'A Subcloud of mystery' @@ -1791,6 +1790,355 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest): headers=FAKE_HEADERS, params=reinstall_data) self.assertEqual(response.status_int, 200) + @mock.patch.object(psd_common, 'upload_config_file') + @mock.patch.object(psd_common.PatchingClient, 'query') + @mock.patch.object(os.path, 'isdir') + @mock.patch.object(os, 'listdir') + @mock.patch.object(cutils, 'get_vault_load_files') + @mock.patch.object(psd_common, 'validate_k8s_version') + @mock.patch.object(psd_common, 'validate_subcloud_config') + @mock.patch.object(psd_common, 'validate_bootstrap_values') + def test_redeploy_subcloud( + self, mock_validate_bootstrap_values, mock_validate_subcloud_config, + mock_validate_k8s_version, mock_get_vault_load_files, + mock_os_listdir, mock_os_isdir, mock_query, mock_upload_config_file): + + fake_bmc_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + fake_sysadmin_password = base64.b64encode( + 'sysadmin_password'.encode("utf-8")).decode('utf-8') + + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + install_data.pop('software_version') + bootstrap_data = copy.copy(fake_subcloud.FAKE_BOOTSTRAP_FILE_DATA) + config_data = {'deploy_config': 'deploy config values'} + redeploy_data = {**install_data, **bootstrap_data, **config_data, + 'sysadmin_password': fake_sysadmin_password, + 'bmc_password': fake_bmc_password} + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, name=bootstrap_data["name"]) + + mock_query.return_value = {} + mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + mock_os_isdir.return_value = True + mock_upload_config_file.return_value = True + mock_os_listdir.return_value = ['deploy_chart_fake.tgz', + 'deploy_overrides_fake.yaml', + 'deploy_playbook_fake.yaml'] + + upload_files = [("install_values", "install_fake_filename", + json.dumps(install_data).encode("utf-8")), + ("bootstrap_values", "bootstrap_fake_filename", + json.dumps(bootstrap_data).encode("utf-8")), + ("deploy_config", "config_fake_filename", + json.dumps(config_data).encode("utf-8"))] + + response = self.app.patch( + FAKE_URL + '/' + str(subcloud.id) + '/redeploy', + headers=FAKE_HEADERS, params=redeploy_data, + upload_files=upload_files) + + mock_validate_bootstrap_values.assert_called_once() + mock_validate_subcloud_config.assert_called_once() + mock_validate_k8s_version.assert_called_once() + self.mock_rpc_client().redeploy_subcloud.assert_called_once_with( + mock.ANY, + subcloud.id, + mock.ANY) + self.assertEqual(response.status_int, 200) + self.assertEqual(SW_VERSION, response.json['software-version']) + + @mock.patch.object(cutils, 'load_yaml_file') + @mock.patch.object(psd_common.PatchingClient, 'query') + @mock.patch.object(os.path, 'exists') + @mock.patch.object(os.path, 'isdir') + @mock.patch.object(os, 'listdir') + @mock.patch.object(cutils, 'get_vault_load_files') + @mock.patch.object(psd_common, 'validate_k8s_version') + def test_redeploy_subcloud_no_request_data( + self, mock_validate_k8s_version, mock_get_vault_load_files, + mock_os_listdir, mock_os_isdir, mock_path_exists, mock_query, + mock_load_yaml): + + fake_bmc_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + fake_sysadmin_password = base64.b64encode( + 'sysadmin_password'.encode("utf-8")).decode('utf-8') + + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + install_data.pop('software_version') + install_data['bmc_password'] = fake_bmc_password + redeploy_data = {'sysadmin_password': fake_sysadmin_password} + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, name=fake_subcloud.FAKE_BOOTSTRAP_FILE_DATA["name"], + data_install=json.dumps(install_data)) + + config_file = psd_common.get_config_file_path(subcloud.name, + consts.DEPLOY_CONFIG) + mock_query.return_value = {} + mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + mock_os_isdir.return_value = True + mock_os_listdir.return_value = ['deploy_chart_fake.tgz', + 'deploy_overrides_fake.yaml', + 'deploy_playbook_fake.yaml'] + mock_path_exists.side_effect = lambda x: True if x == config_file else False + mock_load_yaml.return_value = {"software_version": SW_VERSION} + + response = self.app.patch( + FAKE_URL + '/' + str(subcloud.id) + '/redeploy', + headers=FAKE_HEADERS, params=redeploy_data) + + mock_validate_k8s_version.assert_called_once() + self.mock_rpc_client().redeploy_subcloud.assert_called_once_with( + mock.ANY, + subcloud.id, + mock.ANY) + self.assertEqual(response.status_int, 200) + self.assertEqual(SW_VERSION, response.json['software-version']) + + @mock.patch.object(psd_common, 'upload_config_file') + @mock.patch.object(psd_common.PatchingClient, 'query') + @mock.patch.object(os.path, 'isdir') + @mock.patch.object(os, 'listdir') + @mock.patch.object(cutils, 'get_vault_load_files') + @mock.patch.object(psd_common, 'validate_k8s_version') + @mock.patch.object(psd_common, 'validate_subcloud_config') + @mock.patch.object(psd_common, 'validate_bootstrap_values') + def test_redeploy_subcloud_with_release_version( + self, mock_validate_bootstrap_values, mock_validate_subcloud_config, + mock_validate_k8s_version, mock_get_vault_load_files, + mock_os_listdir, mock_os_isdir, mock_query, mock_upload_config_file): + + fake_bmc_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + fake_sysadmin_password = base64.b64encode( + 'sysadmin_password'.encode("utf-8")).decode('utf-8') + + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + install_data.pop('software_version') + bootstrap_data = copy.copy(fake_subcloud.FAKE_BOOTSTRAP_FILE_DATA) + config_data = {'deploy_config': 'deploy config values'} + redeploy_data = {**install_data, **bootstrap_data, **config_data, + 'sysadmin_password': fake_sysadmin_password, + 'bmc_password': fake_bmc_password, + 'release': fake_subcloud.FAKE_SOFTWARE_VERSION} + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, name=bootstrap_data["name"], + software_version=SW_VERSION) + + mock_query.return_value = {} + mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + mock_os_isdir.return_value = True + mock_upload_config_file.return_value = True + mock_os_listdir.return_value = ['deploy_chart_fake.tgz', + 'deploy_overrides_fake.yaml', + 'deploy_playbook_fake.yaml'] + + upload_files = [("install_values", "install_fake_filename", + json.dumps(install_data).encode("utf-8")), + ("bootstrap_values", "bootstrap_fake_filename", + json.dumps(bootstrap_data).encode("utf-8")), + ("deploy_config", "config_fake_filename", + json.dumps(config_data).encode("utf-8"))] + + response = self.app.patch( + FAKE_URL + '/' + str(subcloud.id) + '/redeploy', + headers=FAKE_HEADERS, params=redeploy_data, + upload_files=upload_files) + + mock_validate_bootstrap_values.assert_called_once() + mock_validate_subcloud_config.assert_called_once() + mock_validate_k8s_version.assert_called_once() + self.mock_rpc_client().redeploy_subcloud.assert_called_once_with( + mock.ANY, + subcloud.id, + mock.ANY) + self.assertEqual(response.status_int, 200) + self.assertEqual(fake_subcloud.FAKE_SOFTWARE_VERSION, + response.json['software-version']) + + @mock.patch.object(cutils, 'load_yaml_file') + @mock.patch.object(psd_common.PatchingClient, 'query') + @mock.patch.object(os.path, 'exists') + @mock.patch.object(os.path, 'isdir') + @mock.patch.object(os, 'listdir') + @mock.patch.object(cutils, 'get_vault_load_files') + def test_redeploy_subcloud_no_request_body( + self, mock_get_vault_load_files, mock_os_listdir, + mock_os_isdir, mock_path_exists, mock_query, mock_load_yaml): + + fake_bmc_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + install_data.pop('software_version') + install_data['bmc_password'] = fake_bmc_password + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, name=fake_subcloud.FAKE_BOOTSTRAP_FILE_DATA["name"], + data_install=json.dumps(install_data)) + + config_file = psd_common.get_config_file_path(subcloud.name, + consts.DEPLOY_CONFIG) + mock_query.return_value = {} + mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + mock_os_isdir.return_value = True + mock_os_listdir.return_value = ['deploy_chart_fake.tgz', + 'deploy_overrides_fake.yaml', + 'deploy_playbook_fake.yaml'] + mock_path_exists.side_effect = lambda x: True if x == config_file else False + mock_load_yaml.return_value = {"software_version": SW_VERSION} + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/redeploy', + headers=FAKE_HEADERS, params={}) + + def test_redeploy_online_subcloud(self): + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, name=fake_subcloud.FAKE_BOOTSTRAP_FILE_DATA["name"]) + db_api.subcloud_update(self.ctx, subcloud.id, + availability_status=dccommon_consts.AVAILABILITY_ONLINE) + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/redeploy', + headers=FAKE_HEADERS, params={}) + self.mock_rpc_client().redeploy_subcloud.assert_not_called() + + def test_redeploy_managed_subcloud(self): + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, name=fake_subcloud.FAKE_BOOTSTRAP_FILE_DATA["name"]) + db_api.subcloud_update(self.ctx, subcloud.id, + management_state=dccommon_consts.MANAGEMENT_MANAGED) + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/redeploy', + headers=FAKE_HEADERS, params={}) + self.mock_rpc_client().redeploy_subcloud.assert_not_called() + + @mock.patch.object(cutils, 'load_yaml_file') + @mock.patch.object(psd_common.PatchingClient, 'query') + @mock.patch.object(os.path, 'exists') + @mock.patch.object(os.path, 'isdir') + @mock.patch.object(os, 'listdir') + @mock.patch.object(cutils, 'get_vault_load_files') + @mock.patch.object(psd_common, 'validate_k8s_version') + def test_redeploy_subcloud_missing_required_value( + self, mock_validate_k8s_version, mock_get_vault_load_files, + mock_os_listdir, mock_os_isdir, mock_path_exists, mock_query, + mock_load_yaml): + + fake_bmc_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + fake_sysadmin_password = base64.b64encode( + 'sysadmin_password'.encode("utf-8")).decode('utf-8') + + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + install_data.pop('software_version') + install_data['bmc_password'] = fake_bmc_password + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, name=fake_subcloud.FAKE_BOOTSTRAP_FILE_DATA["name"], + data_install=json.dumps(install_data)) + + config_file = psd_common.get_config_file_path(subcloud.name, + consts.DEPLOY_CONFIG) + mock_query.return_value = {} + mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + mock_os_isdir.return_value = True + mock_os_listdir.return_value = ['deploy_chart_fake.tgz', + 'deploy_overrides_fake.yaml', + 'deploy_playbook_fake.yaml'] + mock_path_exists.side_effect = lambda x: True if x == config_file else False + mock_load_yaml.return_value = {"software_version": SW_VERSION} + + for k in ['name', 'system_mode', 'external_oam_subnet', + 'external_oam_gateway_address', 'external_oam_floating_address', + 'sysadmin_password']: + bootstrap_values = copy.copy(fake_subcloud.FAKE_BOOTSTRAP_FILE_DATA) + redeploy_data = {**bootstrap_values, + 'sysadmin_password': fake_sysadmin_password} + del redeploy_data[k] + upload_files = [("bootstrap_values", "bootstrap_fake_filename", + json.dumps(redeploy_data).encode("utf-8"))] + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/redeploy', + headers=FAKE_HEADERS, params=redeploy_data, + upload_files=upload_files) + + @mock.patch.object(psd_common, 'upload_config_file') + @mock.patch.object(psd_common.PatchingClient, 'query') + @mock.patch.object(os.path, 'isdir') + @mock.patch.object(os, 'listdir') + @mock.patch.object(cutils, 'get_vault_load_files') + @mock.patch.object(psd_common, 'validate_k8s_version') + @mock.patch.object(psd_common, 'validate_subcloud_config') + @mock.patch.object(psd_common, 'validate_bootstrap_values') + def test_redeploy_subcloud_missing_stored_values( + self, mock_validate_bootstrap_values, mock_validate_subcloud_config, + mock_validate_k8s_version, mock_get_vault_load_files, + mock_os_listdir, mock_os_isdir, mock_query, mock_upload_config_values): + + fake_bmc_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + fake_sysadmin_password = base64.b64encode( + 'sysadmin_password'.encode("utf-8")).decode('utf-8') + + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + install_data.pop('software_version') + bootstrap_data = copy.copy(fake_subcloud.FAKE_BOOTSTRAP_FILE_DATA) + config_data = {'deploy_config': 'deploy config values'} + + for k in ['management_subnet', 'management_start_address', + 'management_end_address', 'management_gateway_address', + 'systemcontroller_gateway_address']: + del bootstrap_data[k] + + redeploy_data = {**install_data, **bootstrap_data, **config_data, + 'sysadmin_password': fake_sysadmin_password, + 'bmc_password': fake_bmc_password} + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, name=bootstrap_data["name"]) + + mock_query.return_value = {} + mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + mock_os_isdir.return_value = True + mock_upload_config_values.return_value = True + mock_os_listdir.return_value = ['deploy_chart_fake.tgz', + 'deploy_overrides_fake.yaml', + 'deploy_playbook_fake.yaml'] + + upload_files = [("install_values", "install_fake_filename", + json.dumps(install_data).encode("utf-8")), + ("bootstrap_values", "bootstrap_fake_filename", + json.dumps(bootstrap_data).encode("utf-8")), + ("deploy_config", "config_fake_filename", + json.dumps(config_data).encode("utf-8"))] + + response = self.app.patch( + FAKE_URL + '/' + str(subcloud.id) + '/redeploy', + headers=FAKE_HEADERS, params=redeploy_data, + upload_files=upload_files) + + mock_validate_bootstrap_values.assert_called_once() + mock_validate_subcloud_config.assert_called_once() + mock_validate_k8s_version.assert_called_once() + self.mock_rpc_client().redeploy_subcloud.assert_called_once_with( + mock.ANY, + subcloud.id, + mock.ANY) + self.assertEqual(response.status_int, 200) + self.assertEqual(SW_VERSION, response.json['software-version']) + @mock.patch.object(prestage, '_get_system_controller_upgrades') @mock.patch.object(prestage, '_get_prestage_subcloud_info') @mock.patch.object(subclouds.SubcloudsController, '_get_prestage_payload') diff --git a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py index e60249e20..ba7ac46e0 100644 --- a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py +++ b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py @@ -417,8 +417,8 @@ class TestSubcloudManager(base.DCManagerTestCase): 'software_version': "18.03", "management_subnet": "192.168.101.0/24", "management_gateway_ip": "192.168.101.1", - "management_start_ip": "192.168.101.3", - "management_end_ip": "192.168.101.4", + "management_start_ip": "192.168.101.2", + "management_end_ip": "192.168.101.50", "systemcontroller_gateway_ip": "192.168.204.101", 'deploy_status': "not-deployed", 'error_description': "No errors present", @@ -1826,6 +1826,61 @@ class TestSubcloudManager(base.DCManagerTestCase): FAKE_PREVIOUS_SW_VERSION) mock_thread_start.assert_called_once() + @mock.patch.object(subcloud_manager.SubcloudManager, + '_run_subcloud_install') + @mock.patch.object(subcloud_manager.SubcloudManager, + '_prepare_for_deployment') + @mock.patch.object(cutils, 'create_subcloud_inventory') + @mock.patch.object(subcloud_manager, 'keyring') + @mock.patch.object(cutils, 'get_playbook_for_software_version') + @mock.patch.object(cutils, 'update_values_on_yaml_file') + @mock.patch.object(RunAnsible, 'exec_playbook') + def test_subcloud_redeploy(self, mock_exec_playbook, mock_update_yml, + mock_get_playbook_for_software_version, + mock_keyring, create_subcloud_inventory, + mock_prepare_for_deployment, + mock_run_subcloud_install): + mock_get_playbook_for_software_version.return_value = "22.12" + mock_keyring.get_password.return_value = "testpass" + mock_exec_playbook.return_value = False + mock_run_subcloud_install.return_value = True + + subcloud = self.create_subcloud_static( + self.ctx, + name='subcloud1', + deploy_status=consts.DEPLOY_STATE_CREATED) + + fake_install_values = \ + copy.copy(fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES) + fake_install_values['software_version'] = SW_VERSION + fake_payload_install = {'bmc_password': 'bmc_pass', + 'install_values': fake_install_values, + 'software_version': SW_VERSION, + 'sysadmin_password': 'sys_pass'} + + fake_payload_bootstrap = {**fake_subcloud.FAKE_BOOTSTRAP_VALUE, + **fake_subcloud.FAKE_BOOTSTRAP_FILE_DATA} + fake_payload_bootstrap["sysadmin_password"] = "testpass" + + fake_payload_config = {"sysadmin_password": "testpass", + "deploy_playbook": "test_playbook.yaml", + "deploy_overrides": "test_overrides.yaml", + "deploy_chart": "test_chart.yaml", + "deploy_config": "subcloud1.yaml"} + + fake_payload = {**fake_payload_install, + **fake_payload_bootstrap, + **fake_payload_config} + + sm = subcloud_manager.SubcloudManager() + sm.redeploy_subcloud(self.ctx, subcloud.id, fake_payload) + + # Verify subcloud was updated with correct values + updated_subcloud = db_api.subcloud_get_by_name(self.ctx, + subcloud.name) + self.assertEqual(consts.DEPLOY_STATE_DONE, + updated_subcloud.deploy_status) + def test_handle_subcloud_operations_in_progress(self): subcloud1 = self.create_subcloud_static( self.ctx, diff --git a/distributedcloud/dcmanager/tests/unit/manager/test_utils.py b/distributedcloud/dcmanager/tests/unit/manager/test_utils.py index 46d7907c1..a3edc5dcf 100644 --- a/distributedcloud/dcmanager/tests/unit/manager/test_utils.py +++ b/distributedcloud/dcmanager/tests/unit/manager/test_utils.py @@ -30,7 +30,8 @@ class TestUtils(base.DCManagerTestCase): payload = {"management_subnet": "192.168.101.0/24", "management_gateway_address": "192.168.101.1", "management_start_address": "192.168.101.2", - "management_end_address": "192.168.101.50"} + "management_end_address": "192.168.101.50", + "systemcontroller_gateway_address": "192.168.204.101"} result = utils.has_network_reconfig(payload, subcloud) self.assertFalse(result) @@ -51,3 +52,13 @@ class TestUtils(base.DCManagerTestCase): "management_end_address": "192.168.101.50"} result = utils.has_network_reconfig(payload, subcloud) self.assertTrue(result) + + def test_has_network_reconfig_different_sc_gateway(self): + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + payload = {"management_subnet": "192.168.101.0/24", + "management_gateway_address": "192.168.101.1", + "management_start_address": "192.168.101.2", + "management_end_address": "192.168.101.50", + "systemcontroller_gateway_address": "192.168.204.102"} + result = utils.has_network_reconfig(payload, subcloud) + self.assertTrue(result)