From 77faee83d2e0bd0894218c774c9a7a97c3f1df0a Mon Sep 17 00:00:00 2001 From: Victor Romano Date: Tue, 23 May 2023 16:33:53 -0300 Subject: [PATCH] Add subcloud deploy install option to dcmanager This commit adds the command "subcloud deploy install" to dcmanager. It runs the subcloud install step only. The install values file is optional if it has already been provided in the previous phase using subcloud deploy create. Test Plan: Success cases: - PASS: Install passing install_values and verify that the subcloud was successfully installed. - PASS: Install without passing install_values and verify that the subcloud was successfully installed using install data previously saved in db. - PASS: Install passing current release and verify that the subcloud was successfully installed. - PASS: Install passing previous release and verify that the subcloud was successfully installed. - PASS: Repeat previous tests but directly call the API (using CURL) instead of using the CLI. Failure cases: - PASS: Verify that it's not possible to run the install if deploy state is not 'create-complete', 'pre-install-failed', 'install-failed' or 'install-complete'. - PASS: Call the API directly, passing bmc-password as plain text as opposed to b64encoded and verify that the response contains the correct error code and message. Story: 2010756 Task: 48056 Signed-off-by: Victor Romano Change-Id: I3a9f4e8c2f39964b2b0b784181bc78494f3078a2 --- api-ref/source/api-ref-dcmanager-v1.rst | 70 ++++++++ ...-subcloud-deploy-post-install-request.json | 7 + ...subcloud-deploy-post-install-response.json | 23 +++ .../controllers/v1/phased_subcloud_deploy.py | 59 ++++++- .../api/policies/phased_subcloud_deploy.py | 4 + .../common/phased_subcloud_deploy.py | 75 ++++++++- distributedcloud/dcmanager/manager/service.py | 8 + .../dcmanager/manager/subcloud_manager.py | 76 +++++++++ distributedcloud/dcmanager/rpc/client.py | 5 + .../test_phased_subcloud_deploy.py | 158 ++++++++++++++++++ .../unit/manager/test_subcloud_manager.py | 30 ++++ 11 files changed, 507 insertions(+), 8 deletions(-) create mode 100644 api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-post-install-request.json create mode 100644 api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-post-install-response.json 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')