From 4fd65e9913d28bd097b16ad3bfb80c04ca6b5419 Mon Sep 17 00:00:00 2001 From: Jessica Castelino Date: Mon, 25 May 2020 13:51:18 -0400 Subject: [PATCH] CLI command to deploy a subcloud If deployment failed, the user has no option than to delete and re-add it. If the user was re-adding the subcloud without re-installing, it would further result in a bootstrap failure. Thus, to simplify things, a new CLI command is provided to allow re-deployment. Furthermore, if the user still chooses to delete the subcloud and re-add it without a re-install, a better error message is provided asking them to re-install the host. CLI: dcmanager subcloud reconfig --deploy-config Test Cases: 1) Successfully add a subcloud with or without deployment option 2) Fail to re-add a subcloud without re-installation after a failed deployment 3) Re-deploy with new CLI command after successful and unsuccessful deployment 4) Re-deploy with new CLI command before and after the subcloud is unlocked 5) Test new CLI command by passing wrong parameters Change-Id: I9fe7e3791e3887160668281048c3c12a7f40c2af Partial-Bug: 1864756 Signed-off-by: Jessica Castelino --- api-ref/source/api-ref-dcmanager-v1.rst | 98 ++++++++-- .../dcmanager/api/controllers/v1/subclouds.py | 144 ++++++++++---- distributedcloud/dcmanager/manager/service.py | 8 + .../dcmanager/manager/subcloud_manager.py | 179 +++++++++++------- distributedcloud/dcmanager/rpc/client.py | 5 + distributedcloud/dcmanager/tests/base.py | 4 +- .../unit/api/v1/controllers/test_subclouds.py | 124 +++++++++++- .../unit/manager/test_subcloud_manager.py | 26 ++- distributedcloud/dcmanager/tests/utils.py | 3 +- 9 files changed, 471 insertions(+), 120 deletions(-) diff --git a/api-ref/source/api-ref-dcmanager-v1.rst b/api-ref/source/api-ref-dcmanager-v1.rst index 1a79942da..0ef1a59b4 100644 --- a/api-ref/source/api-ref-dcmanager-v1.rst +++ b/api-ref/source/api-ref-dcmanager-v1.rst @@ -325,19 +325,21 @@ internalServerError (500), serviceUnavailable (503) "created-at": "2018-02-25 19:06:35.208505", "updated-at": "2018-02-25 21:35:59.771779", "software-version": "18.01", + "deploy-status": "not-deployed", "management-state": "unmanaged", "availability-status": "offline", "management-subnet": "192.168.204.0/24", "systemcontroller-gateway-ip": "192.168.204.101", + "openstack-installed": false, "location": "ottawa", "endpoint_sync_status": [ { "sync_status": "in-sync", - "endpoint_type": "compute" + "endpoint_type": "identity" }, { "sync_status": "in-sync", - "endpoint_type": "network" + "endpoint_type": "load" }, { "sync_status": "in-sync", @@ -346,10 +348,6 @@ internalServerError (500), serviceUnavailable (503) { "sync_status": "in-sync", "endpoint_type": "platform" - }, - { - "sync_status": "in-sync", - "endpoint_type": "volume" } ], "management-gateway-ip": "192.168.204.1", @@ -420,17 +418,19 @@ internalServerError (500), serviceUnavailable (503) "software-version": "18.01", "management-state": "unmanaged", "availability-status": "offline", + "deploy-status": "not-deployed", "management-subnet": "192.168.204.0/24", "systemcontroller-gateway-ip": "192.168.204.101", + "openstack-installed": false, "location": "ottawa", "endpoint_sync_status": [ { "sync_status": "in-sync", - "endpoint_type": "compute" + "endpoint_type": "identity" }, { "sync_status": "in-sync", - "endpoint_type": "network" + "endpoint_type": "load" }, { "sync_status": "in-sync", @@ -439,10 +439,6 @@ internalServerError (500), serviceUnavailable (503) { "sync_status": "in-sync", "endpoint_type": "platform" - }, - { - "sync_status": "in-sync", - "endpoint_type": "volume" } ], "management-gateway-ip": "192.168.204.1", @@ -527,7 +523,9 @@ serviceUnavailable (503) "updated-at": "2018-02-25T23:01:17.490090", "software-version": "18.01", "management-state": "unmanaged", + "openstack-installed": false, "availability-status": "offline", + "deploy-status": "not-deployed", "systemcontroller-gateway-ip": "192.168.204.101", "location": "new location", "management-subnet": "192.168.204.0/24", @@ -538,6 +536,82 @@ serviceUnavailable (503) "name": "subcloud6" } +********************************** +Reconfigures a specific subcloud +********************************** + +.. rest_method:: PATCH /v1.0/subclouds/<200b>{subcloud}<200b>/reconfigure + +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** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "subcloud", "URI", "xsd:string", "The subcloud reference, name or id." + "deploy_config", "plain", "xsd:string", "The content of a file containing the resource definitions describing the desired subcloud configuration." + "sysadmin_password", "plain", "xsd:string", "The sysadmin password of the subcloud. Must be base64 encoded." + +**Response parameters** + +.. csv-table:: + :header: "Parameter", "Style", "Type", "Description" + :widths: 20, 20, 20, 60 + + "id", "plain", "xsd:int", "The unique identifier for this object." + "created_at", "plain", "xsd:dateTime", "The time when the object was created." + "updated_at", "plain", "xsd:dateTime", "The time when the object was last updated." + "name", "plain", "xsd:string", "The name provisioned for the subcloud." + "description", "plain", "xsd:string", "The description of the subcloud." + "location", "plain", "xsd:string", "The location of the subcloud." + "software-version", "plain", "xsd:string", "The software version of the subcloud." + "deploy_status", "plain", "xsd:string", "The deployment status of the subcloud." + "management (Optional)", "plain", "xsd:string", "Management state of the subcloud." + "availability", "plain", "xsd:string", "Availability status of the subcloud." + "management-subnet", "plain", "xsd:string", "Management subnet for subcloud in CIDR format." + "management-start-ip", "plain", "xsd:string", "Start of management IP address range for subcloud." + "management-end-ip", "plain", "xsd:string", "End of management IP address range for subcloud." + "systemcontroller-gateway-ip", "plain", "xsd:string", "Systemcontroller gateway IP Address." + "group_id", "plain", "xsd:int", "Id of the subcloud group." + +Accepts Content-Type multipart/form-data + +:: + + { + "description": "subcloud description", + "management-start-ip": "192.168.204.50", + "created-at": "2018-02-25T19:06:35.208505", + "updated-at": "2018-02-25T23:01:17.490090", + "software-version": "20.06", + "management-state": "unmanaged", + "availability-status": "offline", + "openstack-installed": false, + "deploy-status": "pre-deploy", + "systemcontroller-gateway-ip": "192.168.204.101", + "location": "location", + "management-subnet": "192.168.204.0/24", + "management-gateway-ip": "192.168.204.1", + "management-end-ip": "192.168.204.100", + "group_id": 2, + "id": 1, + "name": "subcloud6" + } + ***************************** Deletes a specific subcloud ***************************** diff --git a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py index 9f5ce8b3d..8abf09d54 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py +++ b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py @@ -18,6 +18,8 @@ # SPDX-License-Identifier: Apache-2.0 # +from requests_toolbelt.multipart import decoder + import base64 import keyring from netaddr import AddrFormatError @@ -52,7 +54,6 @@ from dcmanager.db import api as db_api from dcmanager.rpc import client as rpc_client - CONF = cfg.CONF LOG = logging.getLogger(__name__) # System mode @@ -69,6 +70,10 @@ SUBCLOUD_ADD_MANDATORY_FILE = [ BOOTSTRAP_VALUES, ] +SUBCLOUD_RECONFIG_MANDATORY_FILE = [ + consts.DEPLOY_CONFIG, +] + SUBCLOUD_ADD_GET_FILE_CONTENTS = [ BOOTSTRAP_VALUES, INSTALL_VALUES, @@ -118,13 +123,13 @@ class SubcloudsController(object): file_item = request.POST[consts.DEPLOY_CONFIG] filename = getattr(file_item, 'filename', '') if not filename: - pecan.abort(400, _("No %s file uploaded" % - consts.DEPLOY_CONFIG)) + pecan.abort(400, _("No %s file uploaded" + % consts.DEPLOY_CONFIG)) file_item.file.seek(0, os.SEEK_SET) contents = file_item.file.read() # the deploy config needs to upload to the override location fn = os.path.join(consts.ANSIBLE_OVERRIDES_PATH, payload['name'] - + '_' + os.path.basename(filename)) + + '_deploy_config.yml') try: with open(fn, "w") as f: f.write(contents) @@ -155,6 +160,32 @@ class SubcloudsController(object): payload.update(request.POST) return payload + @staticmethod + def _get_reconfig_payload(request, subcloud_name): + payload = dict() + multipart_data = decoder.MultipartDecoder(request.body, + pecan.request.headers.get('Content-Type')) + + for filename in SUBCLOUD_RECONFIG_MANDATORY_FILE: + for part in multipart_data.parts: + header = part.headers.get('Content-Disposition') + if filename in header: + file_item = part.content + fn = os.path.join(consts.ANSIBLE_OVERRIDES_PATH, subcloud_name + + '_deploy_config.yml') + try: + with open(fn, "w") as f: + f.write(file_item) + except Exception: + msg = _("Failed to upload %s file" % consts.DEPLOY_CONFIG) + LOG.exception(msg) + pecan.abort(400, msg) + payload.update({consts.DEPLOY_CONFIG: fn}) + elif "sysadmin_password" in header: + payload.update({'sysadmin_password': part.content}) + SubcloudsController._get_common_deploy_files(payload) + return payload + def _validate_subcloud_config(self, context, name, @@ -713,10 +744,13 @@ class SubcloudsController(object): @utils.synchronized(LOCK_NAME) @index.when(method='PATCH', template='json') - def patch(self, subcloud_ref=None): + def patch(self, subcloud_ref=None, reconfigure=None): """Update a subcloud. :param subcloud_ref: ID or name of subcloud to update + + :param reconfigure: Specifies if this is a subcloud reconfigure + or subcloud update operation """ context = restcomm.extract_context_from_environ() @@ -725,10 +759,6 @@ class SubcloudsController(object): if subcloud_ref is None: pecan.abort(400, _('Subcloud ID required')) - payload = eval(request.body) - if not payload: - pecan.abort(400, _('Body required')) - if subcloud_ref.isdigit(): # Look up subcloud as an ID try: @@ -745,40 +775,78 @@ class SubcloudsController(object): subcloud_id = subcloud.id - management_state = payload.get('management-state') - description = payload.get('description') - location = payload.get('location') - group_id = payload.get('group_id') + if reconfigure is None: + payload = eval(request.body) + if not payload: + pecan.abort(400, _('Body required')) - if not (management_state or description or location or group_id): - pecan.abort(400, _('nothing to update')) + management_state = payload.get('management-state') + description = payload.get('description') + location = payload.get('location') + group_id = payload.get('group_id') - # Syntax checking - if management_state and \ - management_state not in [consts.MANAGEMENT_UNMANAGED, - consts.MANAGEMENT_MANAGED]: - pecan.abort(400, _('Invalid management-state')) + if not (management_state or description or location or group_id): + pecan.abort(400, _('nothing to update')) + + # Syntax checking + if management_state and \ + management_state not in [consts.MANAGEMENT_UNMANAGED, + consts.MANAGEMENT_MANAGED]: + pecan.abort(400, _('Invalid management-state')) + + # Verify the group_id is valid + if group_id: + try: + db_api.subcloud_group_get(context, group_id) + except exceptions.SubcloudGroupNotFound: + pecan.abort(400, _('Invalid group-id')) - # Verify the group_id is valid - if group_id: try: - db_api.subcloud_group_get(context, group_id) - except exceptions.SubcloudGroupNotFound: - pecan.abort(400, _('Invalid group-id')) + # Inform dcmanager-manager that subcloud has been updated. + # It will do all the real work... + subcloud = self.rpc_client.update_subcloud( + context, subcloud_id, management_state=management_state, + description=description, location=location, group_id=group_id) + return subcloud + except RemoteError as e: + pecan.abort(422, e.value) + except Exception as e: + # additional exceptions. + LOG.exception(e) + pecan.abort(500, _('Unable to update subcloud')) + else: + payload = self._get_reconfig_payload(request, subcloud.name) + if not payload: + pecan.abort(400, _('Body required')) - try: - # Inform dcmanager-manager that subcloud has been updated. - # It will do all the real work... - subcloud = self.rpc_client.update_subcloud( - context, subcloud_id, management_state=management_state, - description=description, location=location, group_id=group_id) - return subcloud - except RemoteError as e: - pecan.abort(422, e.value) - except Exception as e: - # additional exceptions. - LOG.exception(e) - pecan.abort(500, _('Unable to update subcloud')) + if subcloud.deploy_status not in [consts.DEPLOY_STATE_DONE, + consts.DEPLOY_STATE_DEPLOY_PREP_FAILED, + consts.DEPLOY_STATE_DEPLOY_FAILED]: + pecan.abort(400, _('Subcloud deploy status must be either ' + 'complete, deploy-prep-failed or deploy-failed')) + sysadmin_password = \ + payload.get('sysadmin_password') + if not sysadmin_password: + pecan.abort(400, _('subcloud sysadmin_password required')) + + try: + payload['sysadmin_password'] = base64.b64decode( + sysadmin_password).decode('utf-8') + except Exception: + msg = _('Failed to decode subcloud sysadmin_password, ' + 'verify the password is base64 encoded') + LOG.exception(msg) + pecan.abort(400, msg) + + try: + subcloud = self.rpc_client.reconfigure_subcloud(context, subcloud_id, + payload) + return subcloud + except RemoteError as e: + pecan.abort(422, e.value) + except Exception: + LOG.exception("Unable to reconfigure subcloud %s" % subcloud.name) + pecan.abort(500, _('Unable to reconfigure subcloud')) @utils.synchronized(LOCK_NAME) @index.when(method='delete', template='json') diff --git a/distributedcloud/dcmanager/manager/service.py b/distributedcloud/dcmanager/manager/service.py index de25fbf4b..10e0453e2 100644 --- a/distributedcloud/dcmanager/manager/service.py +++ b/distributedcloud/dcmanager/manager/service.py @@ -135,6 +135,14 @@ class DCManagerService(service.Service): return subcloud + @request_context + def reconfigure_subcloud(self, context, subcloud_id, payload): + # Reconfigures a subcloud + LOG.info("Handling reconfigure_subcloud request for: %s" % subcloud_id) + return self.subcloud_manager.reconfigure_subcloud(context, + subcloud_id, + payload) + @request_context def update_subcloud_endpoint_status(self, context, subcloud_name=None, endpoint_type=None, diff --git a/distributedcloud/dcmanager/manager/subcloud_manager.py b/distributedcloud/dcmanager/manager/subcloud_manager.py index 7b91f50b5..751d53c40 100644 --- a/distributedcloud/dcmanager/manager/subcloud_manager.py +++ b/distributedcloud/dcmanager/manager/subcloud_manager.py @@ -114,6 +114,13 @@ class SubcloudManager(manager.Manager): self.dcorch_rpc_client = dcorch_rpc_client.EngineClient() self.fm_api = fm_api.FaultAPIs() + @staticmethod + def _get_ansible_inventory_filename(subcloud_name): + ansible_inventory_filename = os.path.join( + consts.ANSIBLE_OVERRIDES_PATH, + subcloud_name + INVENTORY_FILE_POSTFIX) + return ansible_inventory_filename + @staticmethod def _get_subcloud_cert_name(subcloud_name): cert_name = "%s-adminep-ca-certificate" % subcloud_name @@ -200,9 +207,8 @@ class SubcloudManager(manager.Manager): try: # Ansible inventory filename for the specified subcloud - ansible_subcloud_inventory_file = os.path.join( - consts.ANSIBLE_OVERRIDES_PATH, - subcloud.name + INVENTORY_FILE_POSTFIX) + ansible_subcloud_inventory_file = SubcloudManager.\ + _get_ansible_inventory_filename(subcloud.name) # Create a new route to this subcloud on the management interface # on both controllers. @@ -318,20 +324,16 @@ class SubcloudManager(manager.Manager): payload['install_values']['ansible_ssh_pass'] = \ payload['sysadmin_password'] + deploy_command = None if "deploy_playbook" in payload: - payload['deploy_values'] = dict() - payload['deploy_values']['ansible_become_pass'] = \ - payload['sysadmin_password'] - payload['deploy_values']['ansible_ssh_pass'] = \ - payload['sysadmin_password'] - payload['deploy_values']['admin_password'] = \ - str(keyring.get_password('CGCS', 'admin')) - payload['deploy_values']['deployment_config'] = \ - payload[consts.DEPLOY_CONFIG] - payload['deploy_values']['deployment_manager_chart'] = \ - payload[consts.DEPLOY_CHART] - payload['deploy_values']['deployment_manager_overrides'] = \ - payload[consts.DEPLOY_OVERRIDES] + self._prepare_for_deployment(payload, subcloud.name) + deploy_command = [ + "ansible-playbook", payload[consts.DEPLOY_PLAYBOOK], + "-e", "@%s" % consts.ANSIBLE_OVERRIDES_PATH + "/" + + subcloud.name + "_deploy_values.yml", + "-i", ansible_subcloud_inventory_file, + "--limit", subcloud.name + ] del payload['sysadmin_password'] @@ -352,9 +354,6 @@ class SubcloudManager(manager.Manager): # as it is used for debugging self._write_subcloud_ansible_config(context, payload) - if "deploy_playbook" in payload: - self._write_deploy_files(payload) - install_command = None if "install_values" in payload: install_command = [ @@ -377,20 +376,10 @@ class SubcloudManager(manager.Manager): "-e", str("override_files_dir='%s' region_name=%s") % ( consts.ANSIBLE_OVERRIDES_PATH, subcloud.name)] - deploy_command = None - if "deploy_playbook" in payload: - deploy_command = [ - "ansible-playbook", payload[consts.DEPLOY_PLAYBOOK], - "-e", "@%s" % consts.ANSIBLE_OVERRIDES_PATH + "/" + - payload['name'] + "_deploy_values.yml", - "-i", ansible_subcloud_inventory_file, - "--limit", subcloud.name - ] - apply_thread = threading.Thread( target=self.run_deploy, - args=(install_command, apply_command, deploy_command, subcloud, - payload, context)) + args=(subcloud, payload, context, + install_command, apply_command, deploy_command)) apply_thread.start() return db_api.subcloud_db_model_to_dict(subcloud) @@ -403,9 +392,52 @@ class SubcloudManager(manager.Manager): context, subcloud.id, deploy_status=consts.DEPLOY_STATE_DEPLOY_PREP_FAILED) + def reconfigure_subcloud(self, context, subcloud_id, payload): + """Reconfigure subcloud + + :param context: request context object + :param payload: subcloud configuration + """ + LOG.info("Reconfiguring subcloud %s." % subcloud_id) + + subcloud = db_api.subcloud_update( + context, subcloud_id, + deploy_status=consts.DEPLOY_STATE_PRE_DEPLOY) + try: + # Ansible inventory filename for the specified subcloud + ansible_subcloud_inventory_file = SubcloudManager.\ + _get_ansible_inventory_filename(subcloud.name) + + deploy_command = None + if "deploy_playbook" in payload: + self._prepare_for_deployment(payload, subcloud.name) + deploy_command = [ + "ansible-playbook", payload[consts.DEPLOY_PLAYBOOK], + "-e", "@%s" % consts.ANSIBLE_OVERRIDES_PATH + "/" + + subcloud.name + "_deploy_values.yml", + "-i", ansible_subcloud_inventory_file, + "--limit", subcloud.name + ] + + del payload['sysadmin_password'] + + apply_thread = threading.Thread( + target=self.run_deploy, + args=(subcloud, payload, context, None, None, deploy_command)) + apply_thread.start() + return db_api.subcloud_db_model_to_dict(subcloud) + except Exception: + LOG.exception("Failed to create subcloud %s" % subcloud.name) + # If we failed to create the subcloud, update the + # deployment status + db_api.subcloud_update( + context, subcloud_id, + deploy_status=consts.DEPLOY_STATE_DEPLOY_PREP_FAILED) + @staticmethod - def run_deploy(install_command, apply_command, deploy_command, subcloud, - payload, context): + def run_deploy(subcloud, payload, context, + install_command=None, apply_command=None, + deploy_command=None): if install_command: db_api.subcloud_update( @@ -440,39 +472,40 @@ class SubcloudManager(manager.Manager): install.cleanup() LOG.info("Successfully installed subcloud %s" % subcloud.name) - # Update the subcloud to bootstrapping - try: - db_api.subcloud_update( - context, subcloud.id, - deploy_status=consts.DEPLOY_STATE_BOOTSTRAPPING) - except Exception as e: - LOG.exception(e) - raise e - - # Run the ansible boostrap-subcloud playbook - log_file = \ - DC_LOG_DIR + subcloud.name + '_bootstrap_' + \ - str(datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')) \ - + '.log' - with open(log_file, "w") as f_out_log: + if apply_command: try: - subprocess.check_call(apply_command, - stdout=f_out_log, - stderr=f_out_log) - except subprocess.CalledProcessError as ex: - msg = "Failed to run the subcloud bootstrap playbook" \ - " for subcloud %s, check individual log at " \ - "%s for detailed output." % ( - subcloud.name, - log_file) - ex.cmd = 'ansible-playbook' - LOG.error(msg) + # Update the subcloud to bootstrapping db_api.subcloud_update( context, subcloud.id, - deploy_status=consts.DEPLOY_STATE_BOOTSTRAP_FAILED) - return - LOG.info("Successfully bootstrapped subcloud %s" % - subcloud.name) + deploy_status=consts.DEPLOY_STATE_BOOTSTRAPPING) + except Exception as e: + LOG.exception(e) + raise e + + # Run the ansible boostrap-subcloud playbook + log_file = \ + DC_LOG_DIR + subcloud.name + '_bootstrap_' + \ + str(datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')) \ + + '.log' + with open(log_file, "w") as f_out_log: + try: + subprocess.check_call(apply_command, + stdout=f_out_log, + stderr=f_out_log) + except subprocess.CalledProcessError as ex: + msg = "Failed to run the subcloud bootstrap playbook" \ + " for subcloud %s, check individual log at " \ + "%s for detailed output." % ( + subcloud.name, + log_file) + ex.cmd = 'ansible-playbook' + LOG.error(msg) + db_api.subcloud_update( + context, subcloud.id, + deploy_status=consts.DEPLOY_STATE_BOOTSTRAP_FAILED) + return + LOG.info("Successfully bootstrapped subcloud %s" % + subcloud.name) if deploy_command: # Run the custom deploy playbook @@ -596,16 +629,32 @@ class SubcloudManager(manager.Manager): 'deploy_overrides', 'install_values']: f_out_overrides_file.write("%s: %s\n" % (k, json.dumps(v))) - def _write_deploy_files(self, payload): + def _write_deploy_files(self, payload, subcloud_name): """Create the deploy value files for the subcloud""" deploy_values_file = os.path.join( - consts.ANSIBLE_OVERRIDES_PATH, payload['name'] + + consts.ANSIBLE_OVERRIDES_PATH, subcloud_name + '_deploy_values.yml') with open(deploy_values_file, 'w') as f_out_deploy_values_file: json.dump(payload['deploy_values'], f_out_deploy_values_file) + def _prepare_for_deployment(self, payload, subcloud_name): + payload['deploy_values'] = dict() + payload['deploy_values']['ansible_become_pass'] = \ + payload['sysadmin_password'] + payload['deploy_values']['ansible_ssh_pass'] = \ + payload['sysadmin_password'] + payload['deploy_values']['admin_password'] = \ + str(keyring.get_password('CGCS', 'admin')) + payload['deploy_values']['deployment_config'] = \ + payload[consts.DEPLOY_CONFIG] + payload['deploy_values']['deployment_manager_chart'] = \ + payload[consts.DEPLOY_CHART] + payload['deploy_values']['deployment_manager_overrides'] = \ + payload[consts.DEPLOY_OVERRIDES] + self._write_deploy_files(payload, subcloud_name) + def _delete_subcloud_routes(self, context, subcloud): """Delete the routes to this subcloud""" diff --git a/distributedcloud/dcmanager/rpc/client.py b/distributedcloud/dcmanager/rpc/client.py index 45eeb21e7..571732fd0 100644 --- a/distributedcloud/dcmanager/rpc/client.py +++ b/distributedcloud/dcmanager/rpc/client.py @@ -80,6 +80,11 @@ class ManagerClient(object): location=location, group_id=group_id)) + def reconfigure_subcloud(self, ctxt, subcloud_id, payload): + return self.call(ctxt, self.make_msg('reconfigure_subcloud', + subcloud_id=subcloud_id, + payload=payload)) + def update_subcloud_endpoint_status(self, ctxt, subcloud_name=None, endpoint_type=None, sync_status=consts. diff --git a/distributedcloud/dcmanager/tests/base.py b/distributedcloud/dcmanager/tests/base.py index 7455db811..1260d7165 100644 --- a/distributedcloud/dcmanager/tests/base.py +++ b/distributedcloud/dcmanager/tests/base.py @@ -25,6 +25,7 @@ import sqlalchemy from oslo_config import cfg from oslo_db import options +from dcmanager.common import consts from dcmanager.db import api as api from dcmanager.db.sqlalchemy import api as db_api @@ -63,7 +64,8 @@ SUBCLOUD_SAMPLE_DATA_0 = [ "10.10.10.1", # external_oam_gateway_address "10.10.10.12", # external_oam_floating_address "testpass", # sysadmin_password - 1 # group_id + 1, # group_id + consts.DEPLOY_STATE_DONE # deploy_status ] diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py index b521f3586..7e58a0edd 100644 --- a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py @@ -20,6 +20,8 @@ # of an applicable Wind River license agreement. # +from oslo_utils import timeutils + import base64 import copy import mock @@ -40,7 +42,8 @@ WRONG_URL = '/v1.0/wrong' FAKE_HEADERS = {'X-Tenant-Id': FAKE_TENANT, 'X_ROLE': 'admin', 'X-Identity-Status': 'Confirmed'} -FAKE_SUBCLOUD_DATA = {"name": "subcloud1", +FAKE_SUBCLOUD_DATA = {"id": FAKE_ID, + "name": "subcloud1", "description": "subcloud1 description", "location": "subcloud1 location", "system_mode": "duplex", @@ -49,6 +52,7 @@ FAKE_SUBCLOUD_DATA = {"name": "subcloud1", "management_end_address": "192.168.101.50", "management_gateway_address": "192.168.101.1", "systemcontroller_gateway_address": "192.168.204.101", + "deploy_status": consts.DEPLOY_STATE_DONE, "external_oam_subnet": "10.10.10.0/24", "external_oam_gateway_address": "10.10.10.1", "external_oam_floating_address": "10.10.10.12", @@ -78,6 +82,33 @@ FAKE_BOOTSTRAP_VALUE = { } +class Subcloud(object): + def __init__(self, data, is_online): + self.id = data['id'] + self.name = data['name'] + self.description = data['description'] + self.location = data['location'] + self.management_state = consts.MANAGEMENT_UNMANAGED + if is_online: + self.availability_status = consts.AVAILABILITY_ONLINE + else: + self.availability_status = consts.AVAILABILITY_OFFLINE + self.deploy_status = data['deploy_status'] + self.management_subnet = data['management_subnet'] + self.management_gateway_ip = data['management_gateway_address'] + self.management_start_ip = data['management_start_address'] + self.management_end_ip = data['management_end_address'] + self.external_oam_subnet = data['external_oam_subnet'] + self.external_oam_gateway_address = \ + data['external_oam_gateway_address'] + self.external_oam_floating_address = \ + data['external_oam_floating_address'] + self.systemcontroller_gateway_ip = \ + data['systemcontroller_gateway_address'] + self.created_at = timeutils.utcnow() + self.updated_at = timeutils.utcnow() + + class FakeAddressPool(object): def __init__(self, pool_network, pool_prefix, pool_start, pool_end): self.network = pool_network @@ -552,7 +583,8 @@ class TestSubclouds(testroot.DCManagerApiTest): self.assertEqual(response.status_int, 200) @mock.patch.object(rpc_client, 'ManagerClient') - def test_patch_subcloud_no_body(self, mock_rpc_client): + @mock.patch.object(subclouds, 'db_api') + def test_patch_subcloud_no_body(self, mock_db_api, mock_rpc_client): data = {} six.assertRaisesRegex(self, webtest.app.AppError, "400 *", self.app.patch_json, FAKE_URL + '/' + FAKE_ID, @@ -565,3 +597,91 @@ class TestSubclouds(testroot.DCManagerApiTest): six.assertRaisesRegex(self, webtest.app.AppError, "400 *", self.app.patch_json, FAKE_URL + '/' + FAKE_ID, headers=FAKE_HEADERS, params=data) + + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch.object(subclouds, 'db_api') + @mock.patch.object(subclouds.SubcloudsController, '_get_reconfig_payload') + def test_reconfigure_subcloud(self, mock_get_reconfig_payload, + mock_db_api, mock_rpc_client): + fake_password = (base64.b64encode('testpass'.encode("utf-8"))).decode('ascii') + data = {'sysadmin_password': fake_password} + + mock_rpc_client().reconfigure_subcloud.return_value = True + mock_get_reconfig_payload.return_value = data + + # Return a fake subcloud database object + fake_subcloud = Subcloud(FAKE_SUBCLOUD_DATA, False) + mock_db_api.subcloud_get.return_value = fake_subcloud + + response = self.app.patch_json(FAKE_URL + '/' + FAKE_ID + + '/reconfigure', + headers=FAKE_HEADERS, + params=data) + mock_rpc_client().reconfigure_subcloud.assert_called_once_with( + mock.ANY, + FAKE_ID, + mock.ANY) + self.assertEqual(response.status_int, 200) + + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch.object(subclouds, 'db_api') + @mock.patch.object(subclouds.SubcloudsController, '_get_reconfig_payload') + def test_reconfigure_subcloud_no_body(self, mock_get_reconfig_payload, + mock_db_api, mock_rpc_client): + # Pass an empty request body + data = {} + mock_get_reconfig_payload.return_value = data + mock_rpc_client().reconfigure_subcloud.return_value = True + + # Return a fake subcloud database object + fake_subcloud = Subcloud(FAKE_SUBCLOUD_DATA, False) + mock_db_api.subcloud_get.return_value = fake_subcloud + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + FAKE_ID + '/reconfigure', + headers=FAKE_HEADERS, params=data) + + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch.object(subclouds, 'db_api') + @mock.patch.object(subclouds.SubcloudsController, '_get_reconfig_payload') + def test_reconfigure_subcloud_bad_password(self, mock_get_reconfig_payload, + mock_db_api, mock_rpc_client): + # Pass a sysadmin_password which is not base64 encoded + data = {'sysadmin_password': 'not_base64'} + mock_get_reconfig_payload.return_value = data + mock_rpc_client().reconfigure_subcloud.return_value = True + + # Return a fake subcloud database object + fake_subcloud = Subcloud(FAKE_SUBCLOUD_DATA, False) + mock_db_api.subcloud_get.return_value = fake_subcloud + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + FAKE_ID + '/reconfigure', + headers=FAKE_HEADERS, params=data) + + @mock.patch.object(rpc_client, 'ManagerClient') + @mock.patch.object(subclouds, 'db_api') + @mock.patch.object(subclouds.SubcloudsController, '_get_reconfig_payload') + def test_reconfigure_invalid_deploy_status(self, + mock_get_reconfig_payload, + mock_db_api, + mock_rpc_client): + fake_password = base64.b64encode('testpass'.encode("utf-8")).decode("utf-8") + data = {'sysadmin_password': fake_password} + # Update the deploy status to bootstrap-failed + FAKE_SUBCLOUD_DATA_NEW = copy.copy(FAKE_SUBCLOUD_DATA) + FAKE_SUBCLOUD_DATA_NEW["deploy_status"] = \ + consts.DEPLOY_STATE_BOOTSTRAP_FAILED + mock_get_reconfig_payload.return_value = data + mock_rpc_client().reconfigure_subcloud.return_value = True + + # Return a fake subcloud database object + fake_subcloud = Subcloud(FAKE_SUBCLOUD_DATA_NEW, False) + mock_db_api.subcloud_get.return_value = fake_subcloud + + six.assertRaisesRegex(self, webtest.app.AppError, "400 *", + self.app.patch_json, FAKE_URL + '/' + + FAKE_ID + '/reconfigure', + headers=FAKE_HEADERS, params=data) diff --git a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py index 7a40e5fa6..d16871d68 100644 --- a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py +++ b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py @@ -107,7 +107,7 @@ class Subcloud(object): self.availability_status = consts.AVAILABILITY_ONLINE else: self.availability_status = consts.AVAILABILITY_OFFLINE - + self.deploy_status = data['deploy_status'] self.management_subnet = data['management_subnet'] self.management_gateway_ip = data['management_gateway_address'] self.management_start_ip = data['management_start_address'] @@ -579,3 +579,27 @@ class TestSubcloudManager(base.DCManagerTestCase): # Verify the subcloud openstack_installed was updated updated_subcloud = db_api.subcloud_get_by_name(self.ctx, subcloud.name) self.assertEqual(updated_subcloud.openstack_installed, False) + + @mock.patch.object(subcloud_manager, 'db_api') + @mock.patch.object(subcloud_manager.SubcloudManager, + '_prepare_for_deployment') + @mock.patch.object(threading.Thread, + 'start') + def test_reconfig_subcloud(self, mock_thread_start, + mock_prepare_for_deployment, + mock_db_api): + values = utils.create_subcloud_dict(base.SUBCLOUD_SAMPLE_DATA_0) + values['deploy_status'] = consts.DEPLOY_STATE_PRE_DEPLOY + fake_subcloud_result = Subcloud(values, False) + mock_db_api.subcloud_update.return_value = fake_subcloud_result + 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.reconfigure_subcloud(self.ctx, + values['id'], + payload=fake_payload) + mock_thread_start.assert_called_once() + mock_prepare_for_deployment.assert_called_once() diff --git a/distributedcloud/dcmanager/tests/utils.py b/distributedcloud/dcmanager/tests/utils.py index b1d127fc0..7096f9c85 100644 --- a/distributedcloud/dcmanager/tests/utils.py +++ b/distributedcloud/dcmanager/tests/utils.py @@ -118,4 +118,5 @@ def create_subcloud_dict(data_list): 'external_oam_gateway_address': data_list[20], 'external_oam_floating_address': data_list[21], 'sysadmin_password': data_list[22], - 'group_id': data_list[23]} + 'group_id': data_list[23], + 'deploy_status': data_list[24]}