diff --git a/api-ref/source/api-ref-dcmanager-v1.rst b/api-ref/source/api-ref-dcmanager-v1.rst index fbda0fbeb..995f778c3 100644 --- a/api-ref/source/api-ref-dcmanager-v1.rst +++ b/api-ref/source/api-ref-dcmanager-v1.rst @@ -1913,6 +1913,76 @@ Response Example .. literalinclude:: samples/phased-subcloud-deploy/phased-subcloud-deploy-post-response.json :language: json +********************************** +Installs a subcloud +********************************** + +.. rest_method:: POST /v1.0/phased-subcloud-deploy/{subcloud}/install + +**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 + - release: release + - sysadmin_password: sysadmin_password + - bmc_password: bmc_password + +Accepts Content-Type multipart/form-data + +Request Example +---------------- + +.. literalinclude:: samples/phased-subcloud-deploy/phased-subcloud-deploy-post-install-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/phased-subcloud-deploy/phased-subcloud-deploy-post-install-response.json + :language: json + + ********************************** Configures a subcloud ********************************** diff --git a/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-post-install-request.json b/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-post-install-request.json new file mode 100644 index 000000000..835f2d487 --- /dev/null +++ b/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-post-install-request.json @@ -0,0 +1,7 @@ +{ + "bmc_password": "YYYYYYY", + "install_values": "content of install_values file", + "release": "22.12", + "subcloud": "subcloud1", + "sysadmin_password": "XXXXXXX" +} \ No newline at end of file diff --git a/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-post-install-response.json b/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-post-install-response.json new file mode 100644 index 000000000..39e056326 --- /dev/null +++ b/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-post-install-response.json @@ -0,0 +1,23 @@ +{ + "id": 1, + "name": "subcloud1", + "created-at": "2023-01-02T03:04:05.678987", + "updated-at": "2023-04-08T15:16:23.424851", + "availability-status": "offline", + "data_install": null, + "data_upgrade": null, + "deploy-status": "pre-install", + "backup-status": null, + "backup-datetime": null, + "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" +} \ No newline at end of file diff --git a/distributedcloud/dcmanager/api/controllers/v1/phased_subcloud_deploy.py b/distributedcloud/dcmanager/api/controllers/v1/phased_subcloud_deploy.py index ba1e936dd..b84ca06e9 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/phased_subcloud_deploy.py +++ b/distributedcloud/dcmanager/api/controllers/v1/phased_subcloud_deploy.py @@ -5,6 +5,7 @@ # import http.client as httpclient +import json import os from oslo_log import log as logging @@ -13,6 +14,7 @@ import pecan import tsconfig.tsconfig as tsc import yaml +from dccommon import consts as dccommon_consts from dcmanager.api.controllers import restcomm from dcmanager.api.policies import phased_subcloud_deploy as \ phased_subcloud_deploy_policy @@ -43,6 +45,10 @@ SUBCLOUD_CREATE_GET_FILE_CONTENTS = ( consts.INSTALL_VALUES, ) +SUBCLOUD_INSTALL_GET_FILE_CONTENTS = ( + consts.INSTALL_VALUES, +) + SUBCLOUD_BOOTSTRAP_GET_FILE_CONTENTS = ( consts.BOOTSTRAP_VALUES, ) @@ -51,6 +57,13 @@ SUBCLOUD_CONFIG_GET_FILE_CONTENTS = ( consts.DEPLOY_CONFIG, ) +VALID_STATES_FOR_DEPLOY_INSTALL = ( + consts.DEPLOY_STATE_CREATED, + consts.DEPLOY_STATE_PRE_INSTALL_FAILED, + consts.DEPLOY_STATE_INSTALL_FAILED, + consts.DEPLOY_STATE_INSTALLED +) + VALID_STATES_FOR_DEPLOY_BOOTSTRAP = [ consts.DEPLOY_STATE_INSTALLED, consts.DEPLOY_STATE_BOOTSTRAP_FAILED, @@ -147,6 +160,48 @@ class PhasedSubcloudDeployController(object): pecan.abort(httpclient.INTERNAL_SERVER_ERROR, _('Unable to create subcloud')) + def _deploy_install(self, context: RequestContext, + request: pecan.Request, subcloud): + payload = psd_common.get_request_data( + request, subcloud, SUBCLOUD_INSTALL_GET_FILE_CONTENTS) + if not payload: + pecan.abort(400, _('Body required')) + + if subcloud.deploy_status not in VALID_STATES_FOR_DEPLOY_INSTALL: + allowed_states_str = ', '.join(VALID_STATES_FOR_DEPLOY_INSTALL) + pecan.abort(400, _('Subcloud deploy status must be either: %s') + % allowed_states_str) + + payload['software_version'] = payload.get('release', tsc.SW_VERSION) + psd_common.populate_payload_with_pre_existing_data( + payload, subcloud, SUBCLOUD_INSTALL_GET_FILE_CONTENTS) + + psd_common.validate_sysadmin_password(payload) + psd_common.pre_deploy_install(payload, subcloud) + + try: + # Align the software version of the subcloud with install + # version. Update the deploy status as pre-install. + subcloud = db_api.subcloud_update( + context, + subcloud.id, + description=payload.get('description', subcloud.description), + location=payload.get('location', subcloud.location), + software_version=payload['software_version'], + management_state=dccommon_consts.MANAGEMENT_UNMANAGED, + deploy_status=consts.DEPLOY_STATE_PRE_INSTALL, + data_install=json.dumps(payload['install_values'])) + + self.dcmanager_rpc_client.subcloud_deploy_install( + 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 install subcloud %s" % subcloud.name) + pecan.abort(500, _('Unable to install subcloud')) + def _deploy_bootstrap(self, context: RequestContext, request: pecan.Request, subcloud: models.Subcloud): @@ -279,7 +334,9 @@ class PhasedSubcloudDeployController(object): except (exceptions.SubcloudNotFound, exceptions.SubcloudNameNotFound): pecan.abort(404, _('Subcloud not found')) - if verb == 'bootstrap': + if verb == 'install': + subcloud = self._deploy_install(context, pecan.request, subcloud) + elif verb == 'bootstrap': subcloud = self._deploy_bootstrap(context, pecan.request, subcloud) elif verb == 'configure': subcloud = self._deploy_config(context, pecan.request, subcloud) diff --git a/distributedcloud/dcmanager/api/policies/phased_subcloud_deploy.py b/distributedcloud/dcmanager/api/policies/phased_subcloud_deploy.py index 39dfc3d99..fb848bb78 100644 --- a/distributedcloud/dcmanager/api/policies/phased_subcloud_deploy.py +++ b/distributedcloud/dcmanager/api/policies/phased_subcloud_deploy.py @@ -27,6 +27,10 @@ phased_subcloud_deploy_rules = [ check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, description="Modify the subcloud deployment.", operations=[ + { + 'method': 'PATCH', + 'path': '/v1.0/phased-subcloud-deploy/{subcloud}/install' + }, { 'method': 'PATCH', 'path': '/v1.0/phased-subcloud-deploy/{subcloud}/bootstrap' diff --git a/distributedcloud/dcmanager/common/phased_subcloud_deploy.py b/distributedcloud/dcmanager/common/phased_subcloud_deploy.py index 771d1d841..d3974c302 100644 --- a/distributedcloud/dcmanager/common/phased_subcloud_deploy.py +++ b/distributedcloud/dcmanager/common/phased_subcloud_deploy.py @@ -603,7 +603,8 @@ def validate_k8s_version(payload): yaml file must be of the same value as fresh_install_k8s_version of the specified release. """ - if payload['software_version'] == tsc.SW_VERSION: + software_version = payload['software_version'] + if software_version == tsc.SW_VERSION: return kubernetes_version = payload.get(KUBERNETES_VERSION) @@ -611,14 +612,14 @@ def validate_k8s_version(payload): try: bootstrap_var_file = utils.get_playbook_for_software_version( ANSIBLE_BOOTSTRAP_VALIDATE_CONFIG_VARS, - payload['software_version']) + software_version) fresh_install_k8s_version = utils.get_value_from_yaml_file( bootstrap_var_file, FRESH_INSTALL_K8S_VERSION) if not fresh_install_k8s_version: pecan.abort(400, _("%s not found in %s") % (FRESH_INSTALL_K8S_VERSION, - bootstrap_var_file)) + bootstrap_var_file)) if kubernetes_version != fresh_install_k8s_version: pecan.abort(400, _("The kubernetes_version value (%s) " "specified in the subcloud bootstrap " @@ -626,12 +627,12 @@ def validate_k8s_version(payload): "fresh_install_k8s_version value (%s) " "of the specified release %s") % (kubernetes_version, - fresh_install_k8s_version, - payload['software_version'])) + fresh_install_k8s_version, + software_version)) except exceptions.PlaybookNotFound: pecan.abort(400, _("The bootstrap playbook validate-config vars " "not found for %s software version") - % payload['software_version']) + % software_version) def validate_sysadmin_password(payload: dict): @@ -809,12 +810,42 @@ def get_request_data(request: pecan.Request, return payload +def get_subcloud_db_install_values(subcloud): + if not subcloud.data_install: + msg = _("Failed to read data install from db") + LOG.exception(msg) + pecan.abort(400, msg) + + install_values = json.loads(subcloud.data_install) + + # mandatory install parameters + mandatory_install_parameters = [ + 'bootstrap_interface', + 'bootstrap_address', + 'bootstrap_address_prefix', + 'bmc_username', + 'bmc_address', + 'bmc_password', + ] + for p in mandatory_install_parameters: + if p not in install_values: + msg = _("Failed to get %s from data_install" % p) + LOG.exception(msg) + pecan.abort(400, msg) + + return install_values + + def populate_payload_with_pre_existing_data(payload: dict, subcloud: models.Subcloud, mandatory_values: typing.Sequence): for value in mandatory_values: if value == consts.INSTALL_VALUES: - pass + if not payload.get(consts.INSTALL_VALUES): + install_values = get_subcloud_db_install_values(subcloud) + payload.update({value: install_values}) + else: + validate_install_values(payload) elif value == consts.BOOTSTRAP_VALUES: filename = get_config_file_path(subcloud.name) LOG.info("Loading existing bootstrap values from: %s" % filename) @@ -834,3 +865,33 @@ def populate_payload_with_pre_existing_data(payload: dict, pecan.abort(400, msg) payload.update({value: fn}) get_common_deploy_files(payload, subcloud.software_version) + + +def pre_deploy_install(payload: dict, + subcloud: models.Subcloud): + + install_values = payload['install_values'] + + # 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. + if install_values.get( + 'software_version') != payload['software_version']: + install_values['software_version'] = payload['software_version'] + install_values.pop('image', None) + + # Confirm the specified or active load is still in dc-vault if + # image not in install values, add the matching image into the + # install values. + matching_iso, err_msg = utils.get_matching_iso(payload['software_version']) + if err_msg: + LOG.exception(err_msg) + pecan.abort(400, _(err_msg)) + LOG.info("Image in install_values is set to %s" % matching_iso) + install_values['image'] = matching_iso + + # Update the install values in payload + if not payload.get('bmc_password'): + payload.update({'bmc_password': install_values.get('bmc_password')}) + payload.update({'install_values': install_values}) diff --git a/distributedcloud/dcmanager/manager/service.py b/distributedcloud/dcmanager/manager/service.py index 1d3281412..2041ea714 100644 --- a/distributedcloud/dcmanager/manager/service.py +++ b/distributedcloud/dcmanager/manager/service.py @@ -214,6 +214,14 @@ class DCManagerService(service.Service): subcloud_id, payload) + @request_context + def subcloud_deploy_install(self, context, subcloud_id, payload): + # Install a subcloud + LOG.info("Handling subcloud_deploy_install request for: %s" % subcloud_id) + return self.subcloud_manager.subcloud_deploy_install(context, + subcloud_id, + payload) + def _stop_rpc_server(self): # Stop RPC connection to prevent new requests LOG.debug(_("Attempting to stop RPC service...")) diff --git a/distributedcloud/dcmanager/manager/subcloud_manager.py b/distributedcloud/dcmanager/manager/subcloud_manager.py index a2e211811..5c337e0b0 100644 --- a/distributedcloud/dcmanager/manager/subcloud_manager.py +++ b/distributedcloud/dcmanager/manager/subcloud_manager.py @@ -53,6 +53,7 @@ from dcmanager.common import exceptions from dcmanager.common.exceptions import DCManagerException from dcmanager.common.i18n import _ from dcmanager.common import manager +from dcmanager.common import phased_subcloud_deploy as psd_common from dcmanager.common import prestage from dcmanager.common import utils from dcmanager.db import api as db_api @@ -758,6 +759,33 @@ class SubcloudManager(manager.Manager): return self._subcloud_operation_notice('restore', restore_subclouds, failed_subclouds, invalid_subclouds) + def _deploy_install_prep(self, subcloud, payload: dict, + ansible_subcloud_inventory_file): + payload['install_values']['ansible_ssh_pass'] = \ + payload['sysadmin_password'] + payload['install_values']['ansible_become_pass'] = \ + payload['sysadmin_password'] + + # If all update_values already exists on override file or are + # the same as the existing ones, the update won't happen + # and the file will remain untouched + bootstrap_file = psd_common.get_config_file_path(subcloud.name, + consts.BOOTSTRAP_VALUES) + update_values = {'software_version': payload['software_version'], + 'bmc_password': payload['bmc_password'], + 'ansible_ssh_pass': payload['sysadmin_password'], + 'ansible_become_pass': payload['sysadmin_password'] + } + utils.update_values_on_yaml_file(bootstrap_file, + update_values) + + install_command = self.compose_install_command( + subcloud.name, + ansible_subcloud_inventory_file, + payload['software_version']) + + return install_command + def subcloud_deploy_create(self, context, subcloud_id, payload): """Create subcloud and notify orchestrators. @@ -897,6 +925,40 @@ class SubcloudManager(manager.Manager): deploy_status=consts.DEPLOY_STATE_CREATE_FAILED) return db_api.subcloud_db_model_to_dict(subcloud) + def subcloud_deploy_install(self, context, subcloud_id, payload: dict) -> dict: + """Install subcloud + + :param context: request context object + :param subcloud_id: subcloud id from db + :param payload: subcloud Install + """ + + # Retrieve the subcloud details from the database + subcloud = db_api.subcloud_get(context, subcloud_id) + + LOG.info("Installing subcloud %s." % subcloud_id) + + try: + ansible_subcloud_inventory_file = self._get_ansible_filename( + subcloud.name, INVENTORY_FILE_POSTFIX) + + install_command = self._deploy_install_prep( + subcloud, payload, ansible_subcloud_inventory_file) + + apply_thread = threading.Thread( + target=self.run_deploy_commands, + args=(subcloud, payload, context), + kwargs={'install_command': install_command}) + apply_thread.start() + return db_api.subcloud_db_model_to_dict(subcloud) + except Exception: + LOG.exception("Failed to install subcloud %s" % subcloud.name) + # If we failed to install the subcloud, + # update the deployment status + db_api.subcloud_update( + context, subcloud_id, + deploy_status=consts.DEPLOY_STATE_PRE_INSTALL_FAILED) + def subcloud_deploy_bootstrap(self, context, subcloud_id, payload): """Bootstrap subcloud @@ -1658,12 +1720,26 @@ class SubcloudManager(manager.Manager): os.path.join(consts.DC_ANSIBLE_LOG_DIR, subcloud.name) + "_playbook_output.log" ) + + if install_command: + install_success = self._run_subcloud_install( + context, subcloud, install_command, + log_file, payload['install_values']) + if not install_success: + return + db_api.subcloud_update( + context, subcloud.id, + deploy_status=consts.DEPLOY_STATE_INSTALLED, + error_description=consts.ERROR_DESC_EMPTY) + if apply_command: self._run_subcloud_bootstrap(context, subcloud, apply_command, log_file) + if deploy_command: self._run_subcloud_config(subcloud, context, deploy_command, log_file) + except Exception as ex: LOG.exception("run_deploy failed") raise ex diff --git a/distributedcloud/dcmanager/rpc/client.py b/distributedcloud/dcmanager/rpc/client.py index be0a0d38b..ff420b5cb 100644 --- a/distributedcloud/dcmanager/rpc/client.py +++ b/distributedcloud/dcmanager/rpc/client.py @@ -192,6 +192,11 @@ class ManagerClient(RPCClient): subcloud_id=subcloud_id, payload=payload)) + def subcloud_deploy_install(self, ctxt, subcloud_id, payload): + return self.cast(ctxt, self.make_msg('subcloud_deploy_install', + subcloud_id=subcloud_id, + payload=payload)) + def subcloud_deploy_bootstrap(self, ctxt, subcloud_id, payload): return self.cast(ctxt, self.make_msg('subcloud_deploy_bootstrap', subcloud_id=subcloud_id, diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_phased_subcloud_deploy.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_phased_subcloud_deploy.py index 6cfa71e5e..2825ba0eb 100644 --- a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_phased_subcloud_deploy.py +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_phased_subcloud_deploy.py @@ -11,6 +11,7 @@ import json import mock from os import path as os_path import six +from tsconfig.tsconfig import SW_VERSION import webtest from dcmanager.common import consts @@ -28,10 +29,12 @@ from dcmanager.tests import utils FAKE_URL = '/v1.0/phased-subcloud-deploy' +FAKE_SOFTWARE_VERSION = '21.12' FAKE_TENANT = utils.UUID1 FAKE_HEADERS = {'X-Tenant-Id': FAKE_TENANT, 'X_ROLE': 'admin,member,reader', 'X-Identity-Status': 'Confirmed', 'X-Project-Name': 'admin'} +FAKE_SUBCLOUD_INSTALL_VALUES = fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES class FakeRPCClient(object): @@ -305,3 +308,158 @@ class TestSubcloudDeployConfig(testroot.DCManagerApiTest): self.app.patch_json, FAKE_URL + '/' + str(subcloud.id) + '/configure', headers=FAKE_HEADERS, params=data) + + +class TestSubcloudDeployInstall(testroot.DCManagerApiTest): + def setUp(self): + super(TestSubcloudDeployInstall, self).setUp() + self.ctx = utils.dummy_context() + + p = mock.patch.object(rpc_client, 'ManagerClient') + self.mock_rpc_client = p.start() + self.addCleanup(p.stop) + + p = mock.patch.object(dutils, 'get_vault_load_files') + self.mock_get_vault_load_files = p.start() + self.addCleanup(p.stop) + + p = mock.patch.object(psd_common, 'get_subcloud_db_install_values') + self.mock_get_subcloud_db_install_values = p.start() + self.addCleanup(p.stop) + + p = mock.patch.object(psd_common, 'validate_k8s_version') + self.mock_validate_k8s_version = p.start() + self.addCleanup(p.stop) + + p = mock.patch.object(psd_common, 'get_request_data') + self.mock_get_request_data = p.start() + self.addCleanup(p.stop) + + def test_install_subcloud(self): + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, + deploy_status=consts.DEPLOY_STATE_CREATED) + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + install_data.pop('software_version') + + fake_sysadmin_password = base64.b64encode( + 'testpass'.encode("utf-8")).decode('utf-8') + fake_bmc_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + bmc_password = {'bmc_password': fake_bmc_password} + install_data.update(bmc_password) + install_payload = {'install_values': install_data, + 'sysadmin_password': fake_sysadmin_password, + 'bmc_password': fake_bmc_password} + self.mock_get_request_data.return_value = install_payload + self.mock_get_subcloud_db_install_values.return_value = install_data + + self.mock_rpc_client().subcloud_deploy_install.return_value = True + self.mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + + response = self.app.patch_json( + FAKE_URL + '/' + str(subcloud.id) + '/install', + headers=FAKE_HEADERS, params=install_payload) + + self.assertEqual(response.status_int, 200) + self.assertEqual(consts.DEPLOY_STATE_PRE_INSTALL, + response.json['deploy-status']) + self.assertEqual(SW_VERSION, response.json['software-version']) + + def test_install_subcloud_with_release_parameter(self): + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, + deploy_status=consts.DEPLOY_STATE_CREATED) + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + install_data.pop('software_version') + + fake_sysadmin_password = base64.b64encode( + 'testpass'.encode("utf-8")).decode('utf-8') + fake_bmc_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + bmc_password = {'bmc_password': fake_bmc_password} + install_data.update(bmc_password) + install_payload = {'install_values': install_data, + 'sysadmin_password': fake_sysadmin_password, + 'bmc_password': fake_bmc_password, + 'release': FAKE_SOFTWARE_VERSION} + self.mock_get_request_data.return_value = install_payload + self.mock_get_subcloud_db_install_values.return_value = install_data + + self.mock_rpc_client().subcloud_deploy_install.return_value = True + self.mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + + response = self.app.patch_json( + FAKE_URL + '/' + str(subcloud.id) + '/install', + headers=FAKE_HEADERS, params=install_payload) + + self.assertEqual(response.status_int, 200) + self.assertEqual(consts.DEPLOY_STATE_PRE_INSTALL, + response.json['deploy-status']) + self.assertEqual(FAKE_SOFTWARE_VERSION, + json.loads(response.json['data_install'])['software_version']) + + def test_install_subcloud_no_body(self): + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, + deploy_status=consts.DEPLOY_STATE_CREATED) + + self.mock_get_request_data.return_value = {} + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/install', + headers=FAKE_HEADERS, params={}) + + def test_install_subcloud_no_install_values_on_request_or_db(self): + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, + deploy_status=consts.DEPLOY_STATE_CREATED, + data_install='') + + fake_sysadmin_password = base64.b64encode( + 'testpass'.encode("utf-8")).decode('utf-8') + fake_bmc_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + install_payload = {'sysadmin_password': fake_sysadmin_password, + 'bmc_password': fake_bmc_password} + self.mock_get_request_data.return_value = install_payload + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/install', + headers=FAKE_HEADERS, params=install_payload) + + def test_install_subcloud_no_install_values_on_request(self): + + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, + deploy_status=consts.DEPLOY_STATE_CREATED) + install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES) + install_data.pop('software_version') + + fake_sysadmin_password = base64.b64encode( + 'testpass'.encode("utf-8")).decode('utf-8') + fake_bmc_password = base64.b64encode( + 'bmc_password'.encode("utf-8")).decode('utf-8') + bmc_password = {'bmc_password': fake_bmc_password} + install_data.update(bmc_password) + install_payload = {'sysadmin_password': fake_sysadmin_password} + self.mock_get_request_data.return_value = install_payload + self.mock_get_subcloud_db_install_values.return_value = install_data + + self.mock_rpc_client().subcloud_deploy_install.return_value = True + self.mock_get_vault_load_files.return_value = ('iso_file_path', 'sig_file_path') + + response = self.app.patch_json( + FAKE_URL + '/' + str(subcloud.id) + '/install', + headers=FAKE_HEADERS, params=install_payload) + + self.assertEqual(response.status_int, 200) + self.assertEqual(consts.DEPLOY_STATE_PRE_INSTALL, + response.json['deploy-status']) + self.assertEqual(SW_VERSION, response.json['software-version']) diff --git a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py index 6a740d90a..b6fda3b13 100644 --- a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py +++ b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py @@ -425,6 +425,36 @@ class TestSubcloudManager(base.DCManagerTestCase): self.assertEqual('localhost', sm.host) self.assertEqual(self.ctx, sm.context) + @mock.patch.object( + subcloud_manager.SubcloudManager, 'compose_install_command') + @mock.patch.object(threading.Thread, 'start') + def test_deploy_install_subcloud(self, + mock_thread_start, + mock_compose_install_command): + + subcloud_name = 'subcloud1' + subcloud = self.create_subcloud_static( + self.ctx, + name=subcloud_name, + deploy_status=consts.DEPLOY_STATE_PRE_INSTALL) + + 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, + 'software_version': FAKE_PREVIOUS_SW_VERSION, + 'sysadmin_password': 'sys_pass'} + + sm = subcloud_manager.SubcloudManager() + + sm.subcloud_deploy_install(self.ctx, subcloud.id, payload=fake_payload) + mock_compose_install_command.assert_called_once_with( + subcloud_name, + sm._get_ansible_filename(subcloud_name, consts.INVENTORY_FILE_POSTFIX), + FAKE_PREVIOUS_SW_VERSION) + mock_thread_start.assert_called_once() + @mock.patch.object(subcloud_manager.SubcloudManager, '_create_intermediate_ca_cert') @mock.patch.object(cutils, 'delete_subcloud_inventory')