diff --git a/api-ref/source/api-ref-dcmanager-v1.rst b/api-ref/source/api-ref-dcmanager-v1.rst index b959fc588..d2cef35be 100644 --- a/api-ref/source/api-ref-dcmanager-v1.rst +++ b/api-ref/source/api-ref-dcmanager-v1.rst @@ -2056,6 +2056,73 @@ Response Example :language: json +********************************** +Completes the subcloud deployment +********************************** + +.. rest_method:: PATCH /v1.0/phased-subcloud-deploy/{subcloud}/complete + +**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 + +Accepts Content-Type multipart/form-data + +Request Example +---------------- + +.. literalinclude:: samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-complete-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-continue-response.json + :language: json + + ********************************** Abort subcloud deployment ********************************** diff --git a/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-complete-request.json b/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-complete-request.json new file mode 100644 index 000000000..e4f22488d --- /dev/null +++ b/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-complete-request.json @@ -0,0 +1,3 @@ +{ + "subcloud": "subcloud1" +} \ No newline at end of file diff --git a/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-continue-response.json b/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-continue-response.json new file mode 100644 index 000000000..7a53a1913 --- /dev/null +++ b/api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-continue-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 11f13689e..f1685a232 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/phased_subcloud_deploy.py +++ b/distributedcloud/dcmanager/api/controllers/v1/phased_subcloud_deploy.py @@ -33,6 +33,7 @@ LOCK_NAME = 'PhasedSubcloudDeployController' INSTALL = consts.DEPLOY_PHASE_INSTALL BOOTSTRAP = consts.DEPLOY_PHASE_BOOTSTRAP CONFIG = consts.DEPLOY_PHASE_CONFIG +COMPLETE = consts.DEPLOY_PHASE_COMPLETE ABORT = consts.DEPLOY_PHASE_ABORT RESUME = consts.DEPLOY_PHASE_RESUME @@ -177,10 +178,9 @@ class PhasedSubcloudDeployController(object): # Ask dcmanager-manager to create the subcloud. # It will do all the real work... - subcloud = self.dcmanager_rpc_client.subcloud_deploy_create( + subcloud_dict = self.dcmanager_rpc_client.subcloud_deploy_create( context, subcloud.id, payload) - subcloud_dict = db_api.subcloud_db_model_to_dict(subcloud) return subcloud_dict except RemoteError as e: @@ -299,6 +299,30 @@ class PhasedSubcloudDeployController(object): LOG.exception("Unable to configure subcloud %s" % subcloud.name) pecan.abort(500, _('Unable to configure subcloud')) + def _deploy_complete(self, context: RequestContext, subcloud): + + # The deployment should be able to be completed when the deploy state + # is consts.DEPLOY_STATE_BOOTSTRAPPED because the user could have + # configured the subcloud manually + if subcloud.deploy_status != consts.DEPLOY_STATE_BOOTSTRAPPED: + pecan.abort(400, _('Subcloud deploy can only be completed when' + ' its deploy status is: %s') + % consts.DEPLOY_STATE_BOOTSTRAPPED) + + try: + # Ask dcmanager-manager to complete the subcloud deployment + subcloud = self.dcmanager_rpc_client.subcloud_deploy_complete( + context, subcloud.id) + return subcloud + + except RemoteError as e: + pecan.abort(httpclient.UNPROCESSABLE_ENTITY, e.value) + except Exception: + LOG.exception("Unable to complete subcloud %s deployment" % + subcloud.name) + pecan.abort(httpclient.INTERNAL_SERVER_ERROR, + _('Unable to complete subcloud deployment')) + def _deploy_abort(self, context, subcloud): if subcloud.deploy_status not in VALID_STATES_FOR_DEPLOY_ABORT: @@ -451,6 +475,8 @@ class PhasedSubcloudDeployController(object): subcloud = self._deploy_bootstrap(context, pecan.request, subcloud) elif verb == CONFIG: subcloud = self._deploy_config(context, pecan.request, subcloud) + elif verb == COMPLETE: + subcloud = self._deploy_complete(context, 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 5281e1aa2..3c6c0a142 100644 --- a/distributedcloud/dcmanager/api/policies/phased_subcloud_deploy.py +++ b/distributedcloud/dcmanager/api/policies/phased_subcloud_deploy.py @@ -46,6 +46,10 @@ phased_subcloud_deploy_rules = [ { 'method': 'PATCH', 'path': '/v1.0/phased-subcloud-deploy/{subcloud}/configure' + }, + { + 'method': 'PATCH', + 'path': '/v1.0/phased-subcloud-deploy/{subcloud}/complete' } ] ) diff --git a/distributedcloud/dcmanager/common/consts.py b/distributedcloud/dcmanager/common/consts.py index 3a5274ae6..6d9211ad7 100644 --- a/distributedcloud/dcmanager/common/consts.py +++ b/distributedcloud/dcmanager/common/consts.py @@ -35,6 +35,7 @@ DEPLOY_PHASE_CREATE = 'create' DEPLOY_PHASE_INSTALL = 'install' DEPLOY_PHASE_BOOTSTRAP = 'bootstrap' DEPLOY_PHASE_CONFIG = 'configure' +DEPLOY_PHASE_COMPLETE = 'complete' DEPLOY_PHASE_ABORT = 'abort' DEPLOY_PHASE_RESUME = 'resume' diff --git a/distributedcloud/dcmanager/manager/service.py b/distributedcloud/dcmanager/manager/service.py index 4d85bff6a..fc82bb5da 100644 --- a/distributedcloud/dcmanager/manager/service.py +++ b/distributedcloud/dcmanager/manager/service.py @@ -225,6 +225,12 @@ class DCManagerService(service.Service): subcloud_id, payload) + @request_context + def subcloud_deploy_complete(self, context, subcloud_id): + # Complete the subcloud deployment + LOG.info("Handling subcloud_deploy_complete request for: %s" % subcloud_id) + return self.subcloud_manager.subcloud_deploy_complete(context, subcloud_id) + @request_context def subcloud_deploy_abort(self, context, subcloud_id, deploy_status): # Abort the subcloud deployment diff --git a/distributedcloud/dcmanager/manager/subcloud_manager.py b/distributedcloud/dcmanager/manager/subcloud_manager.py index dc78001b6..573a384a6 100644 --- a/distributedcloud/dcmanager/manager/subcloud_manager.py +++ b/distributedcloud/dcmanager/manager/subcloud_manager.py @@ -358,7 +358,8 @@ class SubcloudManager(manager.Manager): # Create the subcloud subcloud = self.subcloud_deploy_create(context, subcloud_id, - payload, rehoming) + payload, rehoming, + return_as_dict=False) # Return if create failed if rehoming: @@ -641,8 +642,7 @@ class SubcloudManager(manager.Manager): "systemcontroller_gateway_address") if (management_subnet != subcloud.management_subnet) or ( - sys_controller_gw_ip != subcloud.systemcontroller_gateway_ip - ): + sys_controller_gw_ip != subcloud.systemcontroller_gateway_ip): m_ks_client = OpenStackDriver( region_name=dccommon_consts.DEFAULT_REGION_NAME, region_clients=None).keystone_client @@ -808,14 +808,16 @@ class SubcloudManager(manager.Manager): self.run_deploy_phases(context, subcloud_id, payload, deploy_states_to_run) - def subcloud_deploy_create(self, context, subcloud_id, payload, rehoming=False): + def subcloud_deploy_create(self, context, subcloud_id, payload, + rehoming=False, return_as_dict=True): """Create subcloud and notify orchestrators. :param context: request context object :param subcloud_id: subcloud_id from db :param payload: subcloud configuration :param rehoming: flag indicating if this is part of a rehoming operation - :return: resulting subcloud DB object + :param return_as_dict: converts the subcloud DB object to a dict before returning + :return: resulting subcloud DB object or dictionary """ LOG.info("Creating subcloud %s." % payload['name']) @@ -943,12 +945,6 @@ class SubcloudManager(manager.Manager): if not rehoming: deploy_state = consts.DEPLOY_STATE_CREATED - subcloud = db_api.subcloud_update( - context, subcloud_id, - deploy_status=deploy_state) - - return subcloud - except Exception: LOG.exception("Failed to create subcloud %s" % payload['name']) # If we failed to create the subcloud, update the deployment status @@ -958,10 +954,17 @@ class SubcloudManager(manager.Manager): else: deploy_state = consts.DEPLOY_STATE_CREATE_FAILED - subcloud = db_api.subcloud_update( - context, subcloud.id, - deploy_status=deploy_state) - return subcloud + subcloud = db_api.subcloud_update( + context, subcloud.id, + deploy_status=deploy_state) + + # The RPC call must return the subcloud as a dictionary, otherwise it + # should return the DB object for dcmanager internal use (subcloud add, + # resume and redeploy) + if return_as_dict: + subcloud = db_api.subcloud_db_model_to_dict(subcloud) + + return subcloud def subcloud_deploy_install(self, context, subcloud_id, payload: dict) -> bool: """Install subcloud @@ -1086,6 +1089,24 @@ class SubcloudManager(manager.Manager): deploy_status=consts.DEPLOY_STATE_PRE_CONFIG_FAILED) return False + def subcloud_deploy_complete(self, context, subcloud_id): + """Completes the subcloud deployment. + + :param context: request context object + :param subcloud_id: subcloud_id from db + :return: resulting subcloud dictionary + """ + LOG.info("Completing subcloud %s deployment." % subcloud_id) + + # Just update the deploy status + subcloud = db_api.subcloud_update(context, subcloud_id, + deploy_status=consts.DEPLOY_STATE_DONE) + + LOG.info("Subcloud %s deploy status set to: %s" + % (subcloud_id, consts.DEPLOY_STATE_DONE)) + + return db_api.subcloud_db_model_to_dict(subcloud) + def _subcloud_operation_notice( self, operation, restore_subclouds, failed_subclouds, invalid_subclouds): diff --git a/distributedcloud/dcmanager/rpc/client.py b/distributedcloud/dcmanager/rpc/client.py index 3555d3ffc..53141a64c 100644 --- a/distributedcloud/dcmanager/rpc/client.py +++ b/distributedcloud/dcmanager/rpc/client.py @@ -208,6 +208,10 @@ class ManagerClient(RPCClient): subcloud_id=subcloud_id, payload=payload)) + def subcloud_deploy_complete(self, ctxt, subcloud_id): + return self.call(ctxt, self.make_msg('subcloud_deploy_complete', + subcloud_id=subcloud_id)) + def subcloud_deploy_abort(self, ctxt, subcloud_id, deploy_status): return self.cast(ctxt, self.make_msg('subcloud_deploy_abort', 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 c2f61a6a9..b91ac88b3 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 @@ -15,6 +15,7 @@ import six from tsconfig.tsconfig import SW_VERSION import webtest +from dccommon import consts as dccommon_consts from dcmanager.api.controllers.v1 import phased_subcloud_deploy as psd_api from dcmanager.common import consts from dcmanager.common import phased_subcloud_deploy as psd_common @@ -42,7 +43,7 @@ FAKE_SUBCLOUD_INSTALL_VALUES = fake_subcloud.FAKE_SUBCLOUD_INSTALL_VALUES class FakeRPCClient(object): def subcloud_deploy_create(self, context, subcloud_id, _): subcloud = db_api.subcloud_get(context, subcloud_id) - return subcloud + return db_api.subcloud_db_model_to_dict(subcloud) # Apply the TestSubcloudPost parameter validation tests to the subcloud deploy @@ -470,6 +471,47 @@ class TestSubcloudDeployInstall(testroot.DCManagerApiTest): self.assertEqual(SW_VERSION, response.json['software-version']) +class TestSubcloudDeployComplete(testroot.DCManagerApiTest): + def setUp(self): + super().setUp() + self.ctx = utils.dummy_context() + + p = mock.patch.object(rpc_client, 'ManagerClient') + self.mock_rpc_client = p.start() + self.addCleanup(p.stop) + + def test_complete_subcloud_deployment(self): + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, deploy_status=consts.DEPLOY_STATE_BOOTSTRAPPED) + + subcloud = db_api.subcloud_update( + self.ctx, subcloud.id, + availability_status=dccommon_consts.AVAILABILITY_ONLINE) + + self.mock_rpc_client().subcloud_deploy_complete.return_value = True + + response = self.app.patch_json(FAKE_URL + '/' + str(subcloud.id) + + '/complete', + headers=FAKE_HEADERS) + self.mock_rpc_client().subcloud_deploy_complete.assert_called_once_with( + mock.ANY, + subcloud.id) + self.assertEqual(response.status_int, 200) + + def test_complete_subcloud_deployment_not_bootstrapped(self): + subcloud = fake_subcloud.create_fake_subcloud( + self.ctx, deploy_status=consts.DEPLOY_STATE_INSTALLED) + + subcloud = db_api.subcloud_update( + self.ctx, subcloud.id, + availability_status=dccommon_consts.AVAILABILITY_ONLINE) + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + str(subcloud.id) + '/complete', + headers=FAKE_HEADERS) + + class TestSubcloudDeployAbort(testroot.DCManagerApiTest): def setUp(self): super(TestSubcloudDeployAbort, self).setUp() diff --git a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py index 7264ce284..e71e90436 100644 --- a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py +++ b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py @@ -504,8 +504,8 @@ class TestSubcloudManager(base.DCManagerTestCase): mock_get_cached_regionone_data.return_value = FAKE_CACHED_REGIONONE_DATA sm = subcloud_manager.SubcloudManager() - subcloud = sm.subcloud_deploy_create(self.ctx, subcloud.id, - payload=values) + subcloud_dict = sm.subcloud_deploy_create(self.ctx, subcloud.id, + payload=values) mock_get_cached_regionone_data.assert_called_once() mock_sysinv_client().create_route.assert_called() self.fake_dcorch_api.add_subcloud.assert_called_once() @@ -517,7 +517,7 @@ class TestSubcloudManager(base.DCManagerTestCase): # Verify subcloud was updated with correct values self.assertEqual(consts.DEPLOY_STATE_CREATED, - subcloud.deploy_status) + subcloud_dict['deploy-status']) # Verify subcloud was updated with correct values updated_subcloud = db_api.subcloud_get_by_name(self.ctx, values['name']) @@ -536,12 +536,12 @@ class TestSubcloudManager(base.DCManagerTestCase): mock_keystone_client.side_effect = FakeException('boom') sm = subcloud_manager.SubcloudManager() - subcloud = sm.subcloud_deploy_create(self.ctx, subcloud.id, - payload=values) + subcloud_dict = sm.subcloud_deploy_create(self.ctx, subcloud.id, + payload=values) # Verify subcloud was updated with correct values self.assertEqual(consts.DEPLOY_STATE_CREATE_FAILED, - subcloud.deploy_status) + subcloud_dict['deploy-status']) # Verify subcloud was updated with correct values updated_subcloud = db_api.subcloud_get_by_name(self.ctx, values['name'])