From df6fa08f779e803869522fcae0fedfa9d4464698 Mon Sep 17 00:00:00 2001 From: Yuxing Jiang Date: Mon, 21 Jun 2021 19:33:03 -0400 Subject: [PATCH] Add mandatory/optional parameters in subcloud reinstall API The subcloud reinstall requires bootstrap values and sysadmin password which are not stored in the central cloud database for bootstrapping a subcloud after reinstall. This commit adds these mandatory values to the subcloud reinstall API, parses these values along with the existing install values to reinstall a subcloud. In addtion, the deploy config values are also accepted to deploy the re-installed post bootstrap. Tests: Unhappy path: 1. Reinstall an online subcloud, reinstall rejected. 2. Reinstall a subcloud without mandatory bootstrap value "system_mode", reinstall rejected. 3. Reinstall a subcloud with "management_start_address" differs from the value stored in database, reinstall rejected. 4. Reinstall a subcloud without image in data_install, and the software image is not uploaded in dc-vault, reinstall rejected. Happy path: 1. Power off a managed subcloud, reinstall this subcloud with correct bootstrap values and deploy config, the subcloud goes "installing", "bootstrapping" and turns online and unmanaged after deployment. After managing this subcloud, it turns in-sync status. 2. Power off a subcloud, reinstall this subcloud with only bootstrap values offered. After the deploy status changes to "complete", issue a dcmanager subcloud reconfigure with its deploy config values. The subcloud will turn online after deployment. 3. Swact the active system controller, power off a subcloud, reinstall this subcloud on the previous standby system controller. the subcloud is reinstalled successfully and goes online after deployment. 4. Upgrade the system controllers and subcloud controllers in a DC system, power off a subcloud after the upgrade, reinstall the subcloud on the upgraded system controller, the reinstall is successful, and the subcloud goes online after deployment. 5. Power off a subcloud, manually manipulate the software version(including the value in data_install), add an image path in data_install, reinstall this subcloud. The reinstall is successful. Check the data in database, the software version is corrected and the image path is changed to the image in dc-vault. Partial-Bug: 1932034 Signed-off-by: Yuxing Jiang Change-Id: I6cdfaa3d476b1c2cdd3970fdfad4a5273d1b1222 --- api-ref/source/api-ref-dcmanager-v1.rst | 3 + .../dcmanager/api/controllers/v1/subclouds.py | 159 ++++++++++++++++- .../dcmanager/manager/subcloud_manager.py | 129 +++++++------- .../unit/api/v1/controllers/test_subclouds.py | 168 +++++++++++++++++- .../tests/unit/common/fake_subcloud.py | 24 ++- .../unit/manager/test_subcloud_manager.py | 123 +++++-------- 6 files changed, 451 insertions(+), 155 deletions(-) diff --git a/api-ref/source/api-ref-dcmanager-v1.rst b/api-ref/source/api-ref-dcmanager-v1.rst index fc300bdaa..6504a8322 100644 --- a/api-ref/source/api-ref-dcmanager-v1.rst +++ b/api-ref/source/api-ref-dcmanager-v1.rst @@ -642,6 +642,9 @@ serviceUnavailable (503) :widths: 20, 20, 20, 60 "subcloud", "URI", "xsd:string", "The subcloud reference, name or id." + "sysadmin_password", "plain", "xsd:string", "The sysadmin password of the subcloud. Must be base64 encoded." + "bootstrap_values", "plain", "xsd:string", "The content of a file containing the bootstrap overrides such as subcloud name, management and OAM subnet." + "deploy_config (Optional)", "plain", "xsd:string", "The content of a file containing the resource definitions describing the desired subcloud configuration." **Response parameters** diff --git a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py index f4c2e651d..ebb049d86 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py +++ b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py @@ -428,6 +428,19 @@ class SubcloudsController(object): "%(end)s") % {'start': mgmt_address_start, 'end': mgmt_address_end}) + self._validate_oam_network_config(external_oam_subnet_str, + external_oam_gateway_address_str, + external_oam_floating_address_str, + subcloud_subnets) + self._validate_group_id(context, group_id) + + def _validate_oam_network_config(self, + external_oam_subnet_str, + external_oam_gateway_address_str, + external_oam_floating_address_str, + existing_networks): + """validate whether oam network configuration is valid""" + # Parse/validate the oam subnet MIN_OAM_SUBNET_SIZE = 3 oam_subnet = None @@ -435,7 +448,7 @@ class SubcloudsController(object): oam_subnet = utils.validate_network_str( external_oam_subnet_str, minimum_size=MIN_OAM_SUBNET_SIZE, - existing_networks=subcloud_subnets) + existing_networks=existing_networks) except exceptions.ValidateFail as e: LOG.exception(e) pecan.abort(400, _("external_oam_subnet invalid: %s") % e) @@ -454,7 +467,6 @@ class SubcloudsController(object): except exceptions.ValidateFail as e: LOG.exception(e) pecan.abort(400, _("oam_floating_address invalid: %s") % e) - self._validate_group_id(context, group_id) def _format_ip_address(self, payload): """Format IP addresses in 'bootstrap_values' and 'install_values'. @@ -1080,23 +1092,154 @@ class SubcloudsController(object): LOG.exception("Unable to reconfigure subcloud %s" % subcloud.name) pecan.abort(500, _('Unable to reconfigure subcloud')) elif verb == "reinstall": + payload = self._get_request_data(request) install_values = self._get_subcloud_db_install_values(subcloud) - payload = db_api.subcloud_db_model_to_dict(subcloud) - for k in ['data_install', 'data_upgrade', 'created-at', 'updated-at']: - if k in payload: - del payload[k] + if subcloud.availability_status == consts.AVAILABILITY_ONLINE: + msg = _('Cannot re-install an online subcloud') + LOG.exception(msg) + pecan.abort(400, msg) + + # Validate the bootstrap values with the data in central cloud. + # Stop the process if the boostrap value is not equal to the data + # in DB. Re-use the data from DB if it is not passed. + name = payload.get('name', subcloud.name) + if name != subcloud.name: + pecan.abort(400, _('name is incorrect for the subcloud')) + else: + payload['name'] = name + + system_mode = payload.get('system_mode') + if not system_mode: + pecan.abort(400, _('system_mode required')) + + management_subnet = payload.get('management_subnet', + subcloud.management_subnet) + if management_subnet != subcloud.management_subnet: + pecan.abort(400, _('management_subnet is incorrect for subcloud')) + else: + payload['management_subnet'] = management_subnet + + management_start_ip = payload.get('management_start_address', + subcloud.management_start_ip) + if management_start_ip != subcloud.management_start_ip: + pecan.abort(400, _('management_start_address is incorrect for ' + 'the subcloud')) + else: + payload['management_start_address'] = management_start_ip + + management_end_ip = payload.get('management_end_address', + subcloud.management_end_ip) + if management_end_ip != subcloud.management_end_ip: + pecan.abort(400, _('management_end_address is incorrect for ' + 'the subcloud')) + else: + payload['management_end_address'] = management_end_ip + + management_gateway_ip = payload.get('management_gateway_address', + subcloud.management_gateway_ip) + if management_gateway_ip != subcloud.management_gateway_ip: + pecan.abort(400, _('management_gateway_address is incorrect for ' + 'the subcloud')) + else: + payload['management_gateway_address'] = management_gateway_ip + + systemcontroller_gateway_ip = \ + payload.get('systemcontroller_gateway_address', + subcloud.systemcontroller_gateway_ip) + if systemcontroller_gateway_ip != subcloud.systemcontroller_gateway_ip: + pecan.abort(400, _('systemcontroller_gateway_address is incorrect ' + 'for the subcloud')) + else: + payload['systemcontroller_gateway_address'] = \ + systemcontroller_gateway_ip + + external_oam_subnet = payload.get('external_oam_subnet') + if not external_oam_subnet: + pecan.abort(400, _('external_oam_subnet required')) + + external_oam_gateway_ip = payload.get('external_oam_gateway_address') + if not external_oam_gateway_ip: + pecan.abort(400, _('external_oam_gateway_address required')) + + external_oam_floating_ip = \ + payload.get('external_oam_floating_address') + if not external_oam_floating_ip: + pecan.abort(400, _('external_oam_floating_address required')) + + sysadmin_password = payload.get('sysadmin_password') + if not sysadmin_password: + pecan.abort(400, _('subcloud sysadmin_password required')) + + try: + payload['sysadmin_password'] = base64.b64decode( + sysadmin_password).decode('utf-8') + except Exception: + msg = _('Failed to decode subcloud sysadmin_password, ' + 'verify the password is base64 encoded') + LOG.exception(msg) + pecan.abort(400, msg) + + # Search existing subcloud subnets in db + subcloud_subnets = [] + subclouds = db_api.subcloud_get_all(context) + for k in subclouds: + subcloud_subnets.append(IPNetwork(k.management_subnet)) + + self._validate_oam_network_config(external_oam_subnet, + external_oam_gateway_ip, + external_oam_floating_ip, + subcloud_subnets) + + # If the software version of the subcloud is different from the + # central cloud, update the software version in install valuse and + # delete the image path in install values, then the subcloud will + # be reinstalled using the image in dc_vault. + if install_values.get('software_version') != tsc.SW_VERSION: + install_values['software_version'] = tsc.SW_VERSION + install_values.pop('image', None) + + # Confirm the active system controller load is still in dc-vault if + # image not in install values, add the matching image into the + # install values. + if 'image' not in install_values: + matching_iso, matching_sig = \ + SubcloudsController.verify_active_load_in_vault() + LOG.info("image was not in install_values: will reference %s" % + matching_iso) + install_values['image'] = matching_iso + + # Update the install values in payload payload.update({ 'bmc_password': install_values.get('bmc_password'), 'install_values': install_values, }) + # Update data install(software version, image path) + data_install = None + if 'install_values' in payload: + data_install = json.dumps(payload['install_values']) + + # Upload the deploy config files if it is included in the request + self._upload_deploy_config_file(request, payload) + try: + # Align the software version of the subcloud with the central + # cloud. Update description, location and group id if offered, + # update the deploy status as pre-install. + db_api.subcloud_update( + context, + subcloud_id, + description=payload.get('description', subcloud.description), + location=payload.get('location', subcloud.location), + software_version=tsc.SW_VERSION, + management_state=consts.MANAGEMENT_UNMANAGED, + deploy_status=consts.DEPLOY_STATE_PRE_INSTALL, + data_install=data_install) + self.rpc_client.reinstall_subcloud( context, subcloud_id, payload) - # Return deploy_status as pre-install - subcloud.deploy_status = consts.DEPLOY_STATE_PRE_INSTALL return db_api.subcloud_db_model_to_dict(subcloud) except RemoteError as e: pecan.abort(422, e.value) diff --git a/distributedcloud/dcmanager/manager/subcloud_manager.py b/distributedcloud/dcmanager/manager/subcloud_manager.py index 563042917..338c086c9 100644 --- a/distributedcloud/dcmanager/manager/subcloud_manager.py +++ b/distributedcloud/dcmanager/manager/subcloud_manager.py @@ -392,38 +392,7 @@ class SubcloudManager(manager.Manager): # Query system controller keystone admin user/project IDs, # services project id, sysinv and dcmanager user id and store in # payload so they get copied to the override file - admin_user_id = None - sysinv_user_id = None - dcmanager_user_id = None - admin_project_id = None - services_project_id = None - - user_list = m_ks_client.get_enabled_users(id_only=False) - for user in user_list: - if user.name == dccommon_consts.ADMIN_USER_NAME: - admin_user_id = user.id - elif user.name == dccommon_consts.SYSINV_USER_NAME: - sysinv_user_id = user.id - elif user.name == dccommon_consts.DCMANAGER_USER_NAME: - dcmanager_user_id = user.id - - project_list = m_ks_client.get_enabled_projects(id_only=False) - for project in project_list: - if project.name == dccommon_consts.ADMIN_PROJECT_NAME: - admin_project_id = project.id - elif project.name == dccommon_consts.SERVICES_USER_NAME: - services_project_id = project.id - - payload['system_controller_keystone_admin_user_id'] = \ - admin_user_id - payload['system_controller_keystone_admin_project_id'] = \ - admin_project_id - payload['system_controller_keystone_services_project_id'] = \ - services_project_id - payload['system_controller_keystone_sysinv_user_id'] = \ - sysinv_user_id - payload['system_controller_keystone_dcmanager_user_id'] = \ - dcmanager_user_id + self._get_keystone_ids(m_ks_client, payload) # Add the admin and service user passwords to the payload so they # get copied to the override file @@ -550,6 +519,45 @@ class SubcloudManager(manager.Manager): context, subcloud_id, deploy_status=consts.DEPLOY_STATE_DEPLOY_PREP_FAILED) + def _get_keystone_ids(self, keystone_client, payload): + """Get keystone user_id and project_id + + :param keystone_client: keystone client + :param payload: subcloud configuration + """ + admin_user_id = None + sysinv_user_id = None + dcmanager_user_id = None + admin_project_id = None + services_project_id = None + + user_list = keystone_client.get_enabled_users(id_only=False) + for user in user_list: + if user.name == dccommon_consts.ADMIN_USER_NAME: + admin_user_id = user.id + elif user.name == dccommon_consts.SYSINV_USER_NAME: + sysinv_user_id = user.id + elif user.name == dccommon_consts.DCMANAGER_USER_NAME: + dcmanager_user_id = user.id + + project_list = keystone_client.get_enabled_projects(id_only=False) + for project in project_list: + if project.name == dccommon_consts.ADMIN_PROJECT_NAME: + admin_project_id = project.id + elif project.name == dccommon_consts.SERVICES_USER_NAME: + services_project_id = project.id + + payload['system_controller_keystone_admin_user_id'] = \ + admin_user_id + payload['system_controller_keystone_admin_project_id'] = \ + admin_project_id + payload['system_controller_keystone_services_project_id'] = \ + services_project_id + payload['system_controller_keystone_sysinv_user_id'] = \ + sysinv_user_id + payload['system_controller_keystone_dcmanager_user_id'] = \ + dcmanager_user_id + def reinstall_subcloud(self, context, subcloud_id, payload): """Reinstall subcloud @@ -561,51 +569,50 @@ class SubcloudManager(manager.Manager): # Retrieve the subcloud details from the database subcloud = db_api.subcloud_get(context, subcloud_id) - # Semantic checking - if subcloud.availability_status == \ - consts.AVAILABILITY_ONLINE: - raise exceptions.SubcloudNotOffline() - - software_version = str(payload['install_values'].get('software_version')) - LOG.info("The type of sw version is %s" % type(SW_VERSION)) - - if software_version != SW_VERSION: - raise exceptions.BadRequest( - resource='subcloud', - msg='Software version should match the system controller') - - if 'image' not in payload['install_values']: - matching_iso, matching_sig = utils.get_vault_load_files( - SW_VERSION) - payload['install_values'].update({'image': matching_iso}) - LOG.info("Reinstalling subcloud %s." % subcloud_id) - subcloud = db_api.subcloud_update( - context, subcloud_id, - software_version=SW_VERSION, - deploy_status=consts.DEPLOY_STATE_PRE_INSTALL) - try: ansible_subcloud_inventory_file = self._get_ansible_filename( subcloud.name, INVENTORY_FILE_POSTFIX) + m_ks_client = OpenStackDriver( + region_name=consts.DEFAULT_REGION_NAME, + region_clients=None).keystone_client + self._get_keystone_ids(m_ks_client, payload) + payload['admin_password'] = str( keyring.get_password('CGCS', 'admin')) - payload['ansible_become_pass'] = payload['admin_password'] - payload['ansible_ssh_pass'] = payload['admin_password'] + payload['ansible_become_pass'] = payload['sysadmin_password'] + payload['ansible_ssh_pass'] = payload['sysadmin_password'] payload['install_values']['ansible_ssh_pass'] = \ - payload['admin_password'] + payload['sysadmin_password'] payload['install_values']['ansible_become_pass'] = \ - payload['admin_password'] + payload['sysadmin_password'] payload['bootstrap-address'] = \ payload['install_values']['bootstrap_address'] + deploy_command = None + if "deploy_playbook" in payload: + self._prepare_for_deployment(payload, subcloud.name) + deploy_command = self.compose_deploy_command( + subcloud.name, + ansible_subcloud_inventory_file, + payload) + del payload['sysadmin_password'] + + payload['users'] = dict() + for user in USERS_TO_REPLICATE: + payload['users'][user] = \ + str(keyring.get_password( + user, dccommon_consts.SERVICES_USER_NAME)) + utils.create_subcloud_inventory(payload, ansible_subcloud_inventory_file) self._create_intermediate_ca_cert(payload) + self._write_subcloud_ansible_config(context, payload) + install_command = self.compose_install_command( subcloud.name, ansible_subcloud_inventory_file) @@ -615,7 +622,7 @@ class SubcloudManager(manager.Manager): apply_thread = threading.Thread( target=self.run_deploy, args=(subcloud, payload, context, - install_command, apply_command)) + install_command, apply_command, deploy_command)) apply_thread.start() return db_api.subcloud_db_model_to_dict(subcloud) except Exception: @@ -624,7 +631,7 @@ class SubcloudManager(manager.Manager): # deployment status db_api.subcloud_update( context, subcloud_id, - deploy_status=consts.DEPLOY_STATE_DEPLOY_PREP_FAILED) + deploy_status=consts.DEPLOY_STATE_PRE_INSTALL_FAILED) def _create_check_target_override_file(self, payload, subcloud_name): check_target_override_file = os.path.join( 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 7d1be1a54..7ece85d9c 100644 --- a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py @@ -53,6 +53,7 @@ FAKE_HEADERS = fake_subcloud.FAKE_HEADERS FAKE_SUBCLOUD_DATA = fake_subcloud.FAKE_SUBCLOUD_DATA FAKE_BOOTSTRAP_VALUE = fake_subcloud.FAKE_BOOTSTRAP_VALUE FAKE_SUBCLOUD_INSTALL_VALUES = fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES +FAKE_SUBCLOUD_BOOTSTRAP_PAYLOAD = fake_subcloud.FAKE_SUBCLOUD_BOOTSTRAP_PAYLOAD class Subcloud(object): @@ -1266,31 +1267,186 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest): @mock.patch.object(cutils, 'get_vault_load_files') @mock.patch.object(rpc_client, 'ManagerClient') @mock.patch.object(subclouds.SubcloudsController, '_get_subcloud_db_install_values') - @mock.patch.object(subclouds.SubcloudsController, '_validate_install_values') + @mock.patch.object(subclouds.SubcloudsController, '_validate_oam_network_config') + @mock.patch.object(subclouds.SubcloudsController, '_get_request_data') def test_reinstall_subcloud( - self, moc_validate_install_values, mock_get_subcloud_db_install_values, - mock_rpc_client, mock_get_vault_load_files): + self, mock_get_request_data, mock_validate_oam_network_config, + mock_get_subcloud_db_install_values, mock_rpc_client, + mock_get_vault_load_files): subcloud = fake_subcloud.create_fake_subcloud(self.ctx) - install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + reinstall_data = copy.copy(FAKE_SUBCLOUD_BOOTSTRAP_PAYLOAD) + mock_get_request_data.return_value = reinstall_data + encoded_password = base64.b64encode( 'bmc_password'.encode("utf-8")).decode('utf-8') bmc_password = {'bmc_password': encoded_password} install_data.update(bmc_password) - mock_get_subcloud_db_install_values.return_value = install_data + mock_rpc_client().reinstall_subcloud.return_value = True mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + response = self.app.patch_json( FAKE_URL + '/' + str(subcloud.id) + '/reinstall', - headers=FAKE_HEADERS) + headers=FAKE_HEADERS, params=reinstall_data) + + mock_validate_oam_network_config.assert_called_once() mock_rpc_client().reinstall_subcloud.assert_called_once_with( mock.ANY, subcloud.id, mock.ANY) self.assertEqual(response.status_int, 200) + @mock.patch.object(cutils, 'get_vault_load_files') + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch.object(subclouds.SubcloudsController, '_get_subcloud_db_install_values') + @mock.patch.object(subclouds.SubcloudsController, '_validate_oam_network_config') + @mock.patch.object(subclouds.SubcloudsController, '_get_request_data') + def test_reinstall_subcloud_no_body( + self, mock_get_request_data, mock_validate_oam_network_config, + mock_get_subcloud_db_install_values, mock_rpc_client, + mock_get_vault_load_files): + + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + mock_get_request_data.return_value = {} + encoded_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + bmc_password = {'bmc_password': encoded_password} + install_data.update(bmc_password) + + mock_validate_oam_network_config.assert_not_called() + mock_get_subcloud_db_install_values.return_value = install_data + mock_rpc_client().reinstall_subcloud.return_value = True + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/reinstall', + headers=FAKE_HEADERS, params={}) + + @mock.patch.object(cutils, 'get_vault_load_files') + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch.object(subclouds.SubcloudsController, '_get_subcloud_db_install_values') + @mock.patch.object(subclouds.SubcloudsController, '_validate_oam_network_config') + @mock.patch.object(subclouds.SubcloudsController, '_get_request_data') + def test_reinstall_online_subcloud( + self, mock_get_request_data, mock_validate_oam_network_config, + mock_get_subcloud_db_install_values, mock_rpc_client, + mock_get_vault_load_files): + + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + db_api.subcloud_update(self.ctx, + subcloud.id, + availability_status=consts.AVAILABILITY_ONLINE) + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + reinstall_data = copy.copy(FAKE_SUBCLOUD_BOOTSTRAP_PAYLOAD) + mock_get_request_data.return_value = reinstall_data + encoded_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + bmc_password = {'bmc_password': encoded_password} + install_data.update(bmc_password) + + mock_validate_oam_network_config.assert_not_called() + mock_get_subcloud_db_install_values.return_value = install_data + mock_rpc_client().reinstall_subcloud.return_value = True + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/reinstall', + headers=FAKE_HEADERS, params={}) + + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch.object(subclouds.SubcloudsController, '_get_subcloud_db_install_values') + @mock.patch.object(subclouds.SubcloudsController, '_get_request_data') + def test_reinstall_subcloud_missing_required_value( + self, mock_get_request_data, mock_get_subcloud_db_install_values, + mock_rpc_client): + + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + + encoded_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + bmc_password = {'bmc_password': encoded_password} + install_data.update(bmc_password) + mock_get_subcloud_db_install_values.return_value = install_data + mock_rpc_client().reinstall_subcloud.return_value = True + + for k in ['name', 'system_mode', 'external_oam_subnet', + 'external_oam_gateway_address', 'external_oam_floating_address', + 'sysadmin_password']: + reinstall_data = copy.copy(FAKE_SUBCLOUD_BOOTSTRAP_PAYLOAD) + del reinstall_data[k] + mock_get_request_data.return_value = reinstall_data + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/reinstall', + headers=FAKE_HEADERS, params=reinstall_data) + + @mock.patch.object(cutils, 'get_vault_load_files') + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch.object(subclouds.SubcloudsController, '_get_subcloud_db_install_values') + @mock.patch.object(subclouds.SubcloudsController, '_validate_oam_network_config') + @mock.patch.object(subclouds.SubcloudsController, '_get_request_data') + def test_reinstall_subcloud_missing_stored_value( + self, mock_get_request_data, mock_validate_oam_network_config, + mock_get_subcloud_db_install_values, mock_rpc_client, + mock_get_vault_load_files): + + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + + encoded_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + bmc_password = {'bmc_password': encoded_password} + install_data.update(bmc_password) + mock_get_subcloud_db_install_values.return_value = install_data + + mock_rpc_client().reinstall_subcloud.return_value = True + mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + + for k in ['management_subnet', 'management_start_address', 'management_end_address', + 'management_gateway_address', 'systemcontroller_gateway_address']: + reinstall_data = copy.copy(FAKE_SUBCLOUD_BOOTSTRAP_PAYLOAD) + del reinstall_data[k] + mock_get_request_data.return_value = reinstall_data + response = self.app.patch_json( + FAKE_URL + '/' + str(subcloud.id) + '/reinstall', + headers=FAKE_HEADERS, params=reinstall_data) + self.assertEqual(response.status_int, 200) + + @mock.patch.object(cutils, 'get_vault_load_files') + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch.object(subclouds.SubcloudsController, '_get_subcloud_db_install_values') + @mock.patch.object(subclouds.SubcloudsController, '_validate_subcloud_config') + @mock.patch.object(subclouds.SubcloudsController, '_get_request_data') + def test_reinstall_subcloud_stored_value_not_match( + self, mock_get_request_data, mock_validate_subcloud_config, + mock_get_subcloud_db_install_values, mock_rpc_client, + mock_get_vault_load_files): + + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + + encoded_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + bmc_password = {'bmc_password': encoded_password} + install_data.update(bmc_password) + mock_get_subcloud_db_install_values.return_value = install_data + + mock_rpc_client().reinstall_subcloud.return_value = True + mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + + for k in ['management_subnet', 'management_start_address', 'management_end_address', + 'management_gateway_address', 'systemcontroller_gateway_address']: + reinstall_data = copy.copy(FAKE_SUBCLOUD_BOOTSTRAP_PAYLOAD) + reinstall_data[k] = 'wrong_value' + mock_get_request_data.return_value = reinstall_data + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/reinstall', + headers=FAKE_HEADERS, params=reinstall_data) + @mock.patch.object(rpc_client, 'ManagerClient') @mock.patch.object(subclouds.SubcloudsController, '_get_restore_payload') def test_restore_subcloud_no_body(self, mock_get_restore_payload, diff --git a/distributedcloud/dcmanager/tests/unit/common/fake_subcloud.py b/distributedcloud/dcmanager/tests/unit/common/fake_subcloud.py index 34821d8f5..af8e98abf 100644 --- a/distributedcloud/dcmanager/tests/unit/common/fake_subcloud.py +++ b/distributedcloud/dcmanager/tests/unit/common/fake_subcloud.py @@ -40,9 +40,27 @@ FAKE_BOOTSTRAP_VALUE = { 'sysadmin_password': base64.b64encode('testpass'.encode("utf-8")) } +FAKE_SUBCLOUD_BOOTSTRAP_PAYLOAD = { + 'bootstrap-address': '10.10.10.12', + "system_mode": "simplex", + "name": "subcloud1", + "description": "subcloud1 description", + "location": "subcloud1 location", + "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.101", + "external_oam_subnet": "10.10.10.0/24", + "external_oam_gateway_address": "10.10.10.1", + "external_oam_floating_address": "10.10.10.12", + 'sysadmin_password': + (base64.b64encode('testpass'.encode("utf-8"))).decode('ascii'), +} + FAKE_SUBCLOUD_INSTALL_VALUES = { "image": "http://192.168.101.2:8080/iso/bootimage.iso", - "software_version": "12.34", + "software_version": "18.03", "bootstrap_interface": "eno1", "bootstrap_address": "128.224.151.183", "bootstrap_address_prefix": 23, @@ -68,8 +86,8 @@ def create_fake_subcloud(ctxt, **kwargs): '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': consts.DEPLOY_STATE_DONE, 'openstack_installed': False, diff --git a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py index 9bec14d8f..43d8d0ae6 100644 --- a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py +++ b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py @@ -17,6 +17,7 @@ # of an applicable Wind River license agreement. # +import copy import mock from oslo_concurrency import lockutils @@ -1169,6 +1170,8 @@ class TestSubcloudManager(base.DCManagerTestCase): ] ) + @mock.patch.object( + subcloud_manager.SubcloudManager, '_write_subcloud_ansible_config') @mock.patch.object( subcloud_manager.SubcloudManager, '_create_intermediate_ca_cert') @mock.patch.object( @@ -1176,104 +1179,70 @@ class TestSubcloudManager(base.DCManagerTestCase): @mock.patch.object( subcloud_manager.SubcloudManager, 'compose_apply_command') @mock.patch.object(cutils, 'create_subcloud_inventory') + @mock.patch.object(subcloud_manager.SubcloudManager, '_get_keystone_ids') + @mock.patch.object(subcloud_manager, 'OpenStackDriver') @mock.patch.object(threading.Thread, 'start') @mock.patch.object(subcloud_manager, 'keyring') - def test_reinstall_subcloud_with_image( + def test_reinstall_subcloud( self, mock_keyring, mock_thread_start, - mock_create_subcloud_inventory, + mock_keystone_client, mock_get_keystone_ids, mock_create_subcloud_inventory, mock_compose_apply_command, mock_compose_install_command, - mock_create_intermediate_ca_cert): + mock_create_intermediate_ca_cert, mock_write_subcloud_ansible_config): subcloud = self.create_subcloud_static( self.ctx, name='subcloud1', - deploy_status=consts.DEPLOY_STATE_PRE_DEPLOY) + deploy_status=consts.DEPLOY_STATE_PRE_INSTALL) - fake_install_values = fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES + fake_install_values = \ + copy.copy(fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES) fake_install_values['software_version'] = SW_VERSION - fake_payload = { - "bmc_password": "bmc_pass", - "install_values": fake_install_values} + fake_payload = copy.copy(fake_subcloud.FAKE_SUBCLOUD_BOOTSTRAP_PAYLOAD) + fake_payload.update({ + 'bmc_password': 'bmc_pass', + 'software_version': SW_VERSION, + 'install_values': fake_install_values}) sm = subcloud_manager.SubcloudManager() mock_keyring.get_password.return_value = "testpassword" sm.reinstall_subcloud(self.ctx, subcloud.id, payload=fake_payload) - mock_keyring.get_password.assert_called_once() + mock_keystone_client.assert_called_once() + mock_get_keystone_ids.assert_called_once() mock_create_subcloud_inventory.assert_called_once() + mock_create_intermediate_ca_cert.assert_called_once() + mock_write_subcloud_ansible_config.assert_called_once() mock_compose_install_command.assert_called_once() mock_compose_apply_command.assert_called_once() mock_thread_start.assert_called_once() - @mock.patch.object( - subcloud_manager.SubcloudManager, '_create_intermediate_ca_cert') - @mock.patch.object(cutils, "get_vault_load_files") - @mock.patch.object( - subcloud_manager.SubcloudManager, 'compose_install_command') - @mock.patch.object( - subcloud_manager.SubcloudManager, 'compose_apply_command') - @mock.patch.object(cutils, 'create_subcloud_inventory') - @mock.patch.object(threading.Thread, 'start') - @mock.patch.object(subcloud_manager, 'keyring') - def test_reinstall_subcloud_without_image( - self, mock_keyring, mock_thread_start, mock_create_subcloud_inventory, - mock_compose_apply_command, mock_compose_install_command, - mock_get_vault_load_files, mock_create_intermediate_ca_cert): - - subcloud = self.create_subcloud_static( - self.ctx, - name='subcloud1', - deploy_status=consts.DEPLOY_STATE_PRE_DEPLOY) - - fake_install_values = fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES - fake_install_values['software_version'] = SW_VERSION - del fake_install_values['image'] - fake_payload = { - "bmc_password": "bmc_pass", - "install_values": fake_install_values} - + def test_get_keystone_get_keystone_ids(self): + keystone_client = FakeKeystoneClient() + payload = dict() sm = subcloud_manager.SubcloudManager() - mock_keyring.get_password.return_value = "testpassword" - mock_get_vault_load_files.return_value = ("iso file path", "sig file path") - - sm.reinstall_subcloud(self.ctx, subcloud.id, payload=fake_payload) - mock_keyring.get_password.assert_called_once() - mock_create_subcloud_inventory.assert_called_once() - mock_compose_install_command.assert_called_once() - mock_compose_apply_command.assert_called_once() - mock_thread_start.assert_called_once() - - def test_reinstall_online_subcloud(self): - subcloud = self.create_subcloud_static( - self.ctx, - name='subcloud1', - deploy_status=consts.DEPLOY_STATE_PRE_DEPLOY) - db_api.subcloud_update(self.ctx, - subcloud.id, - availability_status=consts.AVAILABILITY_ONLINE) - - data = utils.create_subcloud_dict(base.SUBCLOUD_SAMPLE_DATA_0) - sm = subcloud_manager.SubcloudManager() - self.assertRaises(exceptions.SubcloudNotOffline, - sm.reinstall_subcloud, self.ctx, - subcloud.id, data) - - def test_reinstall_subcloud_software_not_match(self): - subcloud = self.create_subcloud_static( - self.ctx, - name='subcloud1', - deploy_status=consts.DEPLOY_STATE_PRE_DEPLOY) - db_api.subcloud_update(self.ctx, - subcloud.id, - availability_status=consts.AVAILABILITY_OFFLINE) - - fake_install_values = fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES - data = utils.create_subcloud_dict(base.SUBCLOUD_SAMPLE_DATA_0) - data.update({'install_values': fake_install_values}) - sm = subcloud_manager.SubcloudManager() - self.assertRaises(exceptions.BadRequest, - sm.reinstall_subcloud, self.ctx, - subcloud.id, data) + sm._get_keystone_ids(keystone_client, payload) + for fake_user in FAKE_USERS: + if fake_user.name == dccommon_consts.ADMIN_USER_NAME: + self.assertEqual( + payload['system_controller_keystone_admin_user_id'], + fake_user.id) + elif fake_user.name == dccommon_consts.SYSINV_USER_NAME: + self.assertEqual( + payload['system_controller_keystone_sysinv_user_id'], + fake_user.id) + elif fake_user.name == dccommon_consts.DCMANAGER_USER_NAME: + self.assertEqual( + payload['system_controller_keystone_dcmanager_user_id'], + fake_user.id) + for fake_project in FAKE_PROJECTS: + if fake_project.name == dccommon_consts.ADMIN_PROJECT_NAME: + self.assertEqual( + payload['system_controller_keystone_admin_project_id'], + fake_project.id) + elif fake_project.name == dccommon_consts.SERVICES_USER_NAME: + self.assertEqual( + payload['system_controller_keystone_services_project_id'], + fake_project.id) def test_handle_subcloud_operations_in_progress(self): subcloud1 = self.create_subcloud_static(