diff --git a/api-ref/source/api-ref-dcmanager-v1.rst b/api-ref/source/api-ref-dcmanager-v1.rst index 5842e1e9c..fbda0fbeb 100644 --- a/api-ref/source/api-ref-dcmanager-v1.rst +++ b/api-ref/source/api-ref-dcmanager-v1.rst @@ -1673,6 +1673,7 @@ Subcloud Deploy These APIs allow for the display and upload of the deployment manager common files which include deploy playbook, deploy overrides, deploy helm charts, and prestage images list. + ************************** Show Subcloud Deploy Files ************************** @@ -1910,4 +1911,76 @@ Response Example ---------------- .. literalinclude:: samples/phased-subcloud-deploy/phased-subcloud-deploy-post-response.json + :language: json + +********************************** +Configures a subcloud +********************************** + +.. rest_method:: PATCH /v1.0/phased-subcloud-deploy/{subcloud}/configure + +The attributes of a subcloud which are modifiable: + +- subcloud configuration (which is provided through deploy_config file) + +**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 + - deploy_config: deploy_config + - sysadmin_password: sysadmin_password + +Accepts Content-Type multipart/form-data + +Request Example +---------------- + +.. literalinclude:: samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-configure-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-patch-configure-response.json :language: json \ No newline at end of file diff --git a/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-configure-request.json b/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-configure-request.json new file mode 100644 index 000000000..d7b6d3776 --- /dev/null +++ b/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-configure-request.json @@ -0,0 +1,5 @@ +{ + "deploy_config": "content of deploy_config file", + "subcloud": "subcloud1", + "sysadmin_password": "XXXXXXX" +} \ No newline at end of file diff --git a/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-configure-response.json b/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-configure-response.json new file mode 100644 index 000000000..55777e614 --- /dev/null +++ b/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-configure-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": "online", + "data_install": null, + "data_upgrade": null, + "deploy-status": "complete", + "backup-status": "complete", + "backup-datetime": "2023-05-02 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": "21.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 f509653e8..ba1e936dd 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/phased_subcloud_deploy.py +++ b/distributedcloud/dcmanager/api/controllers/v1/phased_subcloud_deploy.py @@ -22,6 +22,7 @@ from dcmanager.common.context import RequestContext from dcmanager.common import exceptions from dcmanager.common.i18n import _ 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 from dcmanager.db.sqlalchemy import models @@ -46,6 +47,10 @@ SUBCLOUD_BOOTSTRAP_GET_FILE_CONTENTS = ( consts.BOOTSTRAP_VALUES, ) +SUBCLOUD_CONFIG_GET_FILE_CONTENTS = ( + consts.DEPLOY_CONFIG, +) + VALID_STATES_FOR_DEPLOY_BOOTSTRAP = [ consts.DEPLOY_STATE_INSTALLED, consts.DEPLOY_STATE_BOOTSTRAP_FAILED, @@ -56,6 +61,16 @@ VALID_STATES_FOR_DEPLOY_BOOTSTRAP = [ consts.DEPLOY_STATE_CREATED ] +# TODO(vgluzrom): remove deploy_failed once 'subcloud reconfig' +# has been deprecated +VALID_STATES_FOR_DEPLOY_CONFIG = ( + consts.DEPLOY_STATE_DONE, + consts.DEPLOY_STATE_PRE_CONFIG_FAILED, + consts.DEPLOY_STATE_CONFIG_FAILED, + consts.DEPLOY_STATE_DEPLOY_FAILED, + consts.DEPLOY_STATE_BOOTSTRAPPED +) + def get_create_payload(request: pecan.Request) -> dict: payload = dict() @@ -199,6 +214,34 @@ class PhasedSubcloudDeployController(object): pecan.abort(httpclient.INTERNAL_SERVER_ERROR, _('Unable to bootstrap subcloud')) + def _deploy_config(self, context: RequestContext, + request: pecan.Request, subcloud): + payload = psd_common.get_request_data( + request, subcloud, SUBCLOUD_CONFIG_GET_FILE_CONTENTS) + if not payload: + pecan.abort(400, _('Body required')) + + if not (subcloud.deploy_status in VALID_STATES_FOR_DEPLOY_CONFIG or + prestage.is_deploy_status_prestage(subcloud.deploy_status)): + allowed_states_str = ', '.join(VALID_STATES_FOR_DEPLOY_CONFIG) + pecan.abort(400, _('Subcloud deploy status must be either ' + '%s or prestage-...') % allowed_states_str) + + psd_common.populate_payload_with_pre_existing_data( + payload, subcloud, SUBCLOUD_CONFIG_GET_FILE_CONTENTS) + + psd_common.validate_sysadmin_password(payload) + + try: + subcloud = self.dcmanager_rpc_client.subcloud_deploy_config( + context, subcloud.id, payload) + return subcloud + except RemoteError as e: + pecan.abort(422, e.value) + except Exception: + LOG.exception("Unable to configure subcloud %s" % subcloud.name) + pecan.abort(500, _('Unable to configure subcloud')) + @pecan.expose(generic=True, template='json') def index(self): # Route the request to specific methods with parameters @@ -238,6 +281,8 @@ class PhasedSubcloudDeployController(object): if verb == 'bootstrap': subcloud = self._deploy_bootstrap(context, pecan.request, subcloud) + elif verb == 'configure': + subcloud = self._deploy_config(context, pecan.request, subcloud) else: pecan.abort(400, _('Invalid request')) diff --git a/distributedcloud/dcmanager/api/policies/phased_subcloud_deploy.py b/distributedcloud/dcmanager/api/policies/phased_subcloud_deploy.py index 1faea5a6e..39dfc3d99 100644 --- a/distributedcloud/dcmanager/api/policies/phased_subcloud_deploy.py +++ b/distributedcloud/dcmanager/api/policies/phased_subcloud_deploy.py @@ -30,6 +30,10 @@ phased_subcloud_deploy_rules = [ { 'method': 'PATCH', 'path': '/v1.0/phased-subcloud-deploy/{subcloud}/bootstrap' + }, + { + 'method': 'PATCH', + 'path': '/v1.0/phased-subcloud-deploy/{subcloud}/configure' } ] ) diff --git a/distributedcloud/dcmanager/common/consts.py b/distributedcloud/dcmanager/common/consts.py index a6d5b34b4..b0c64dd70 100644 --- a/distributedcloud/dcmanager/common/consts.py +++ b/distributedcloud/dcmanager/common/consts.py @@ -179,6 +179,10 @@ DEPLOY_STATE_BOOTSTRAPPING = 'bootstrapping' DEPLOY_STATE_BOOTSTRAP_FAILED = 'bootstrap-failed' DEPLOY_STATE_BOOTSTRAP_ABORTED = 'bootstrap-aborted' DEPLOY_STATE_BOOTSTRAPPED = 'bootstrap-complete' +DEPLOY_STATE_PRE_CONFIG = 'pre-config' +DEPLOY_STATE_PRE_CONFIG_FAILED = 'pre-config-failed' +DEPLOY_STATE_CONFIGURING = 'configuring' +DEPLOY_STATE_CONFIG_FAILED = 'config-failed' DEPLOY_STATE_DEPLOYING = 'deploying' DEPLOY_STATE_DEPLOY_FAILED = 'deploy-failed' DEPLOY_STATE_MIGRATING_DATA = 'migrating-data' diff --git a/distributedcloud/dcmanager/common/phased_subcloud_deploy.py b/distributedcloud/dcmanager/common/phased_subcloud_deploy.py index ae989365d..771d1d841 100644 --- a/distributedcloud/dcmanager/common/phased_subcloud_deploy.py +++ b/distributedcloud/dcmanager/common/phased_subcloud_deploy.py @@ -692,21 +692,15 @@ def upload_deploy_config_file(request, payload): def get_config_file_path(subcloud_name, config_file_type=None): + basepath = consts.ANSIBLE_OVERRIDES_PATH if config_file_type == consts.DEPLOY_CONFIG: - file_path = os.path.join( - consts.ANSIBLE_OVERRIDES_PATH, - subcloud_name + '_' + config_file_type + '.yml' - ) - elif config_file_type == INSTALL_VALUES: - file_path = os.path.join( - consts.ANSIBLE_OVERRIDES_PATH + '/' + subcloud_name, - config_file_type + '.yml' - ) + filename = f"{subcloud_name}_{config_file_type}.yml" + elif config_file_type == consts.INSTALL_VALUES: + basepath = os.path.join(basepath, subcloud_name) + filename = f'{config_file_type}.yml' else: - file_path = os.path.join( - consts.ANSIBLE_OVERRIDES_PATH, - subcloud_name + '.yml' - ) + filename = f"{subcloud_name}.yml" + file_path = os.path.join(basepath, filename) return file_path @@ -721,18 +715,24 @@ def upload_config_file(file_item, config_file, config_type): def get_common_deploy_files(payload, software_version): + missing_deploy_files = [] for f in consts.DEPLOY_COMMON_FILE_OPTIONS: - # Skip the prestage_images option as it is not relevant in this - # context + # Skip the prestage_images option as it is + # not relevant in this context if f == consts.DEPLOY_PRESTAGE: continue filename = None dir_path = os.path.join(dccommon_consts.DEPLOY_DIR, software_version) if os.path.isdir(dir_path): filename = utils.get_filename_by_prefix(dir_path, f + '_') - if filename is None: - pecan.abort(400, _("Missing required deploy file for %s") % f) - payload.update({f: os.path.join(dir_path, filename)}) + if not filename: + missing_deploy_files.append(f) + else: + payload.update({f: os.path.join(dir_path, filename)}) + if missing_deploy_files: + missing_deploy_files_str = ', '.join(missing_deploy_files) + msg = _("Missing required deploy files: %s" % missing_deploy_files_str) + pecan.abort(400, msg) def validate_subcloud_name_availability(context, subcloud_name): @@ -794,7 +794,7 @@ def get_request_data(request: pecan.Request, file_item = request.POST[f] file_item.file.seek(0, os.SEEK_SET) contents = file_item.file.read() - if subcloud.name and f == consts.DEPLOY_CONFIG: + if f == consts.DEPLOY_CONFIG: fn = get_config_file_path(subcloud.name, f) upload_config_file(contents, fn, f) payload.update({f: fn}) diff --git a/distributedcloud/dcmanager/manager/service.py b/distributedcloud/dcmanager/manager/service.py index feaabe1df..1d3281412 100644 --- a/distributedcloud/dcmanager/manager/service.py +++ b/distributedcloud/dcmanager/manager/service.py @@ -206,6 +206,14 @@ class DCManagerService(service.Service): subcloud_id, payload) + @request_context + def subcloud_deploy_config(self, context, subcloud_id, payload): + # Configures a subcloud + LOG.info("Handling subcloud_deploy_config request for: %s" % subcloud_id) + return self.subcloud_manager.subcloud_deploy_config(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 6ea412ceb..a2e211811 100644 --- a/distributedcloud/dcmanager/manager/subcloud_manager.py +++ b/distributedcloud/dcmanager/manager/subcloud_manager.py @@ -108,6 +108,8 @@ TRANSITORY_STATES = { consts.DEPLOY_STATE_PRE_INSTALL: consts.DEPLOY_STATE_PRE_INSTALL_FAILED, consts.DEPLOY_STATE_INSTALLING: consts.DEPLOY_STATE_INSTALL_FAILED, consts.DEPLOY_STATE_BOOTSTRAPPING: consts.DEPLOY_STATE_BOOTSTRAP_FAILED, + consts.DEPLOY_STATE_PRE_CONFIG: consts.DEPLOY_STATE_PRE_CONFIG_FAILED, + consts.DEPLOY_STATE_CONFIGURING: consts.DEPLOY_STATE_CONFIG_FAILED, consts.DEPLOY_STATE_DEPLOYING: consts.DEPLOY_STATE_DEPLOY_FAILED, consts.DEPLOY_STATE_MIGRATING_DATA: consts.DEPLOY_STATE_DATA_MIGRATION_FAILED, consts.DEPLOY_STATE_PRE_RESTORE: consts.DEPLOY_STATE_RESTORE_PREP_FAILED, @@ -260,6 +262,7 @@ class SubcloudManager(manager.Manager): software_version if software_version else SW_VERSION] return apply_command + # TODO(vgluzrom): rename compose_deploy_command to compose_config_command def compose_deploy_command(self, subcloud_name, ansible_subcloud_inventory_file, payload): deploy_command = [ "ansible-playbook", payload[consts.DEPLOY_PLAYBOOK], @@ -974,6 +977,41 @@ class SubcloudManager(manager.Manager): context, subcloud_id, deploy_status=consts.DEPLOY_STATE_PRE_BOOTSTRAP_FAILED) + def subcloud_deploy_config(self, context, subcloud_id, payload: dict) -> dict: + """Configure subcloud + + :param context: request context object + :param payload: subcloud configuration + """ + LOG.info("Configuring subcloud %s." % subcloud_id) + + subcloud = db_api.subcloud_update( + context, subcloud_id, + deploy_status=consts.DEPLOY_STATE_PRE_CONFIG) + try: + # Ansible inventory filename for the specified subcloud + ansible_subcloud_inventory_file = self._get_ansible_filename( + subcloud.name, INVENTORY_FILE_POSTFIX) + + self._prepare_for_deployment(payload, subcloud.name) + deploy_command = self.compose_deploy_command( + subcloud.name, + ansible_subcloud_inventory_file, + payload) + + del payload['sysadmin_password'] + apply_thread = threading.Thread( + target=self.run_deploy_commands, + args=(subcloud, payload, context), + kwargs={'deploy_command': deploy_command}) + apply_thread.start() + return db_api.subcloud_db_model_to_dict(subcloud) + except Exception: + LOG.exception("Failed to configure %s" % subcloud.name) + db_api.subcloud_update( + context, subcloud_id, + deploy_status=consts.DEPLOY_STATE_PRE_CONFIG_FAILED) + def _subcloud_operation_notice( self, operation, restore_subclouds, failed_subclouds, invalid_subclouds): @@ -1623,6 +1661,9 @@ class SubcloudManager(manager.Manager): 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 @@ -1701,6 +1742,32 @@ class SubcloudManager(manager.Manager): LOG.info("Successfully bootstrapped %s" % subcloud.name) + def _run_subcloud_config(self, subcloud, context, + deploy_command, log_file): + # Run the custom deploy playbook + LOG.info("Starting deploy of %s" % subcloud.name) + db_api.subcloud_update( + context, subcloud.id, + deploy_status=consts.DEPLOY_STATE_CONFIGURING, + error_description=consts.ERROR_DESC_EMPTY) + + try: + run_playbook(log_file, deploy_command) + except PlaybookExecutionFailed: + msg = utils.find_ansible_error_msg( + subcloud.name, log_file, consts.DEPLOY_STATE_CONFIGURING) + LOG.error(msg) + db_api.subcloud_update( + context, subcloud.id, + deploy_status=consts.DEPLOY_STATE_CONFIG_FAILED, + error_description=msg[0:consts.ERROR_DESCRIPTION_LENGTH]) + return + LOG.info("Successfully deployed %s" % subcloud.name) + db_api.subcloud_update( + context, subcloud.id, + deploy_status=consts.DEPLOY_STATE_DONE, + error_description=consts.ERROR_DESC_EMPTY) + def _create_addn_hosts_dc(self, context): """Generate the addn_hosts_dc file for hostname/ip translation""" diff --git a/distributedcloud/dcmanager/rpc/client.py b/distributedcloud/dcmanager/rpc/client.py index fc0b029f2..be0a0d38b 100644 --- a/distributedcloud/dcmanager/rpc/client.py +++ b/distributedcloud/dcmanager/rpc/client.py @@ -197,6 +197,11 @@ class ManagerClient(RPCClient): subcloud_id=subcloud_id, payload=payload)) + def subcloud_deploy_config(self, ctxt, subcloud_id, payload): + return self.call(ctxt, self.make_msg('subcloud_deploy_config', + subcloud_id=subcloud_id, + payload=payload)) + class DCManagerNotifications(RPCClient): """DC Manager Notification interface to broadcast subcloud state changed 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 5f3574ce8..6cfa71e5e 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 @@ -4,8 +4,10 @@ # SPDX-License-Identifier: Apache-2.0 # +import base64 import copy import json + import mock from os import path as os_path import six @@ -41,7 +43,7 @@ class FakeRPCClient(object): # Apply the TestSubcloudPost parameter validation tests to the subcloud deploy # add endpoint as it uses the same parameter validation functions class TestSubcloudDeployCreate(TestSubcloudPost): - API_PREFIX = '/v1.0/phased-subcloud-deploy' + API_PREFIX = FAKE_URL RESULT_KEY = 'phased-subcloud-deploy' def setUp(self): @@ -229,3 +231,77 @@ class TestSubcloudDeployBootstrap(testroot.DCManagerApiTest): upload_files=[("bootstrap_values", "bootstrap_fake_filename", fake_content)]) + + +class TestSubcloudDeployConfig(testroot.DCManagerApiTest): + def setUp(self): + super(TestSubcloudDeployConfig, 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(psd_common, 'populate_payload_with_pre_existing_data') + self.mock_populate_payload = 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_configure_subcloud(self): + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + fake_password = (base64.b64encode('testpass'.encode("utf-8"))).decode('ascii') + data = {'sysadmin_password': fake_password} + + self.mock_rpc_client().subcloud_deploy_config.return_value = True + self.mock_get_request_data.return_value = data + + response = self.app.patch_json(FAKE_URL + '/' + str(subcloud.id) + + '/configure', + headers=FAKE_HEADERS, + params=data) + self.mock_rpc_client().subcloud_deploy_config.assert_called_once_with( + mock.ANY, + subcloud.id, + mock.ANY) + self.assertEqual(response.status_int, 200) + + def test_configure_subcloud_no_body(self): + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + # Pass an empty request body + data = {} + self.mock_rpc_client().subcloud_deploy_config.return_value = True + self.mock_get_request_data.return_value = data + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/configure', + headers=FAKE_HEADERS, params=data) + + def test_configure_subcloud_bad_password(self): + subcloud = fake_subcloud.create_fake_subcloud(self.ctx) + # Pass a sysadmin_password which is not base64 encoded + data = {'sysadmin_password': 'not_base64'} + self.mock_rpc_client().subcloud_deploy_config.return_value = True + self.mock_get_request_data.return_value = data + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/configure', + headers=FAKE_HEADERS, params=data) + + def test_configure_invalid_deploy_status(self): + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, + deploy_status=consts.DEPLOY_STATE_BOOTSTRAP_FAILED) + fake_password = base64.b64encode('testpass'.encode("utf-8")).decode("utf-8") + data = {'sysadmin_password': fake_password} + self.mock_rpc_client().subcloud_deploy_config.return_value = True + self.mock_get_request_data.return_value = data + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/configure', + headers=FAKE_HEADERS, params=data) diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_deploy.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_deploy.py index af1d8f671..0d10ce6cc 100644 --- a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_deploy.py +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_deploy.py @@ -21,6 +21,7 @@ import webtest from dcmanager.api.controllers.v1 import subcloud_deploy from dcmanager.common import consts +from dcmanager.common import phased_subcloud_deploy as psd_common from dcmanager.common import utils as dutils from dcmanager.tests.unit.api import test_root_controller as testroot from dcmanager.tests import utils @@ -245,3 +246,16 @@ class TestSubcloudDeploy(testroot.DCManagerApiTest): response.json['subcloud_deploy'][consts.DEPLOY_CHART]) self.assertEqual(None, response.json['subcloud_deploy'][consts.DEPLOY_PRESTAGE]) + + def test_get_config_file_path(self): + bootstrap_file = psd_common.get_config_file_path("subcloud1") + install_values = psd_common.get_config_file_path("subcloud1", + consts.INSTALL_VALUES) + deploy_config = psd_common.get_config_file_path("subcloud1", + consts.DEPLOY_CONFIG) + self.assertEqual(bootstrap_file, + "/var/opt/dc/ansible/subcloud1.yml") + self.assertEqual(install_values, + "/var/opt/dc/ansible/subcloud1/install_values.yml") + self.assertEqual(deploy_config, + "/var/opt/dc/ansible/subcloud1_deploy_config.yml") diff --git a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py index 321241f16..6a740d90a 100644 --- a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py +++ b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py @@ -557,6 +557,29 @@ class TestSubcloudManager(base.DCManagerTestCase): self.assertEqual(consts.DEPLOY_STATE_PRE_BOOTSTRAP_FAILED, updated_subcloud.deploy_status) + @mock.patch.object(subcloud_manager.SubcloudManager, + '_prepare_for_deployment') + @mock.patch.object(threading.Thread, + 'start') + def test_configure_subcloud(self, mock_thread_start, + mock_prepare_for_deployment): + subcloud = self.create_subcloud_static( + self.ctx, + name='subcloud1', + deploy_status=consts.DEPLOY_STATE_PRE_CONFIG) + + fake_payload = {"sysadmin_password": "testpass", + "deploy_playbook": "test_playbook.yaml", + "deploy_overrides": "test_overrides.yaml", + "deploy_chart": "test_chart.yaml", + "deploy_config": "subcloud1.yaml"} + sm = subcloud_manager.SubcloudManager() + sm.subcloud_deploy_config(self.ctx, + subcloud.id, + payload=fake_payload) + mock_thread_start.assert_called_once() + mock_prepare_for_deployment.assert_called_once() + @mock.patch.object(subcloud_manager.SubcloudManager, 'compose_apply_command') @mock.patch.object(subcloud_manager.SubcloudManager, @@ -2157,12 +2180,14 @@ class TestSubcloudManager(base.DCManagerTestCase): self.assertTrue('Subcloud does not exist' in str(e)) + @mock.patch.object(os_path, 'isdir') @mock.patch.object(os_path, 'exists') @mock.patch.object(cutils, 'get_filename_by_prefix') @mock.patch.object(prestage, '_run_ansible') def test_prestage_remote_pass(self, mock_run_ansible, mock_get_filename_by_prefix, - mock_file_exists): + mock_file_exists, + mock_isdir): values = copy.copy(FAKE_PRESTAGE_PAYLOAD) subcloud = self.create_subcloud_static(self.ctx, @@ -2174,6 +2199,7 @@ class TestSubcloudManager(base.DCManagerTestCase): mock_run_ansible.return_value = None mock_get_filename_by_prefix.return_value = 'prestage_images_list.txt' mock_file_exists.return_value = True + mock_isdir.return_value = True # Verify that subcloud has the correct deploy status updated_subcloud = db_api.subcloud_get_by_name(self.ctx, subcloud.name)