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 <gustavo.herzmann@windriver.com>
This commit is contained in:
Gustavo Herzmann 2023-07-24 14:17:19 -03:00
parent 2209aeb872
commit 2050cf7ceb
11 changed files with 221 additions and 24 deletions

View File

@ -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
**********************************

View File

@ -0,0 +1,3 @@
{
"subcloud": "subcloud1"
}

View File

@ -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"
}

View File

@ -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'))

View File

@ -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'
}
]
)

View File

@ -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'

View File

@ -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

View File

@ -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):

View File

@ -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,

View File

@ -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()

View File

@ -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'])