From 2050cf7cebc426ecbcd8b7bc22800861243a13bf Mon Sep 17 00:00:00 2001 From: Gustavo Herzmann Date: Mon, 24 Jul 2023 14:17:19 -0300 Subject: [PATCH] Add 'subcloud deploy complete' command to dcmanager This commit adds the subcloud deploy complete command to dcmanager. It's used to mark the subcloud deployment as 'complete'. This is useful when the user manually configures the subcloud and wants to finalize the deployment without running 'dcmanager subcloud deploy config'. To run the 'deploy complete' operation deploy status of the subcloud must be 'bootstrap-complete'. This commit also fixes an issue with the value returned from the subcloud_deploy_create function. It was returning the database model object to the RCP call when it should be returning a dictionary. Test Plan: 1. PASS - Bootstrap a subcloud, manually configuring it and then run the deploy complete command (by CLI and directly through the API). Verify that the deploy status updates from 'bootstrap-complete' to 'complete'; 2. PASS - Verify that the command is rejected when the deploy status is not 'bootstrap-complete'. Story: 2010756 Task: 48453 Change-Id: Ie2eca930e4b13a50cc12e8b9eef79bcb5e7c671f Signed-off-by: Gustavo Herzmann --- api-ref/source/api-ref-dcmanager-v1.rst | 67 +++++++++++++++++++ ...ubcloud-deploy-patch-complete-request.json | 3 + ...bcloud-deploy-patch-continue-response.json | 23 +++++++ .../controllers/v1/phased_subcloud_deploy.py | 30 ++++++++- .../api/policies/phased_subcloud_deploy.py | 4 ++ distributedcloud/dcmanager/common/consts.py | 1 + distributedcloud/dcmanager/manager/service.py | 6 ++ .../dcmanager/manager/subcloud_manager.py | 51 +++++++++----- distributedcloud/dcmanager/rpc/client.py | 4 ++ .../test_phased_subcloud_deploy.py | 44 +++++++++++- .../unit/manager/test_subcloud_manager.py | 12 ++-- 11 files changed, 221 insertions(+), 24 deletions(-) create mode 100644 api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-complete-request.json create mode 100644 api-ref/source/samples/phased-subcloud-deploy/phased-subcloud-deploy-patch-continue-response.json 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'])