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)