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 <id/name> --deploy-config <file>

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 <jessica.castelino@windriver.com>
This commit is contained in:
Jessica Castelino 2020-05-25 13:51:18 -04:00
parent 94f6dc9fb4
commit 4fd65e9913
9 changed files with 471 additions and 120 deletions

View File

@ -325,19 +325,21 @@ internalServerError (500), serviceUnavailable (503)
"created-at": "2018-02-25 19:06:35.208505", "created-at": "2018-02-25 19:06:35.208505",
"updated-at": "2018-02-25 21:35:59.771779", "updated-at": "2018-02-25 21:35:59.771779",
"software-version": "18.01", "software-version": "18.01",
"deploy-status": "not-deployed",
"management-state": "unmanaged", "management-state": "unmanaged",
"availability-status": "offline", "availability-status": "offline",
"management-subnet": "192.168.204.0/24", "management-subnet": "192.168.204.0/24",
"systemcontroller-gateway-ip": "192.168.204.101", "systemcontroller-gateway-ip": "192.168.204.101",
"openstack-installed": false,
"location": "ottawa", "location": "ottawa",
"endpoint_sync_status": [ "endpoint_sync_status": [
{ {
"sync_status": "in-sync", "sync_status": "in-sync",
"endpoint_type": "compute" "endpoint_type": "identity"
}, },
{ {
"sync_status": "in-sync", "sync_status": "in-sync",
"endpoint_type": "network" "endpoint_type": "load"
}, },
{ {
"sync_status": "in-sync", "sync_status": "in-sync",
@ -346,10 +348,6 @@ internalServerError (500), serviceUnavailable (503)
{ {
"sync_status": "in-sync", "sync_status": "in-sync",
"endpoint_type": "platform" "endpoint_type": "platform"
},
{
"sync_status": "in-sync",
"endpoint_type": "volume"
} }
], ],
"management-gateway-ip": "192.168.204.1", "management-gateway-ip": "192.168.204.1",
@ -420,17 +418,19 @@ internalServerError (500), serviceUnavailable (503)
"software-version": "18.01", "software-version": "18.01",
"management-state": "unmanaged", "management-state": "unmanaged",
"availability-status": "offline", "availability-status": "offline",
"deploy-status": "not-deployed",
"management-subnet": "192.168.204.0/24", "management-subnet": "192.168.204.0/24",
"systemcontroller-gateway-ip": "192.168.204.101", "systemcontroller-gateway-ip": "192.168.204.101",
"openstack-installed": false,
"location": "ottawa", "location": "ottawa",
"endpoint_sync_status": [ "endpoint_sync_status": [
{ {
"sync_status": "in-sync", "sync_status": "in-sync",
"endpoint_type": "compute" "endpoint_type": "identity"
}, },
{ {
"sync_status": "in-sync", "sync_status": "in-sync",
"endpoint_type": "network" "endpoint_type": "load"
}, },
{ {
"sync_status": "in-sync", "sync_status": "in-sync",
@ -439,10 +439,6 @@ internalServerError (500), serviceUnavailable (503)
{ {
"sync_status": "in-sync", "sync_status": "in-sync",
"endpoint_type": "platform" "endpoint_type": "platform"
},
{
"sync_status": "in-sync",
"endpoint_type": "volume"
} }
], ],
"management-gateway-ip": "192.168.204.1", "management-gateway-ip": "192.168.204.1",
@ -527,7 +523,9 @@ serviceUnavailable (503)
"updated-at": "2018-02-25T23:01:17.490090", "updated-at": "2018-02-25T23:01:17.490090",
"software-version": "18.01", "software-version": "18.01",
"management-state": "unmanaged", "management-state": "unmanaged",
"openstack-installed": false,
"availability-status": "offline", "availability-status": "offline",
"deploy-status": "not-deployed",
"systemcontroller-gateway-ip": "192.168.204.101", "systemcontroller-gateway-ip": "192.168.204.101",
"location": "new location", "location": "new location",
"management-subnet": "192.168.204.0/24", "management-subnet": "192.168.204.0/24",
@ -538,6 +536,82 @@ serviceUnavailable (503)
"name": "subcloud6" "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 Deletes a specific subcloud
***************************** *****************************

View File

@ -18,6 +18,8 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# #
from requests_toolbelt.multipart import decoder
import base64 import base64
import keyring import keyring
from netaddr import AddrFormatError from netaddr import AddrFormatError
@ -52,7 +54,6 @@ from dcmanager.db import api as db_api
from dcmanager.rpc import client as rpc_client from dcmanager.rpc import client as rpc_client
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
# System mode # System mode
@ -69,6 +70,10 @@ SUBCLOUD_ADD_MANDATORY_FILE = [
BOOTSTRAP_VALUES, BOOTSTRAP_VALUES,
] ]
SUBCLOUD_RECONFIG_MANDATORY_FILE = [
consts.DEPLOY_CONFIG,
]
SUBCLOUD_ADD_GET_FILE_CONTENTS = [ SUBCLOUD_ADD_GET_FILE_CONTENTS = [
BOOTSTRAP_VALUES, BOOTSTRAP_VALUES,
INSTALL_VALUES, INSTALL_VALUES,
@ -118,13 +123,13 @@ class SubcloudsController(object):
file_item = request.POST[consts.DEPLOY_CONFIG] file_item = request.POST[consts.DEPLOY_CONFIG]
filename = getattr(file_item, 'filename', '') filename = getattr(file_item, 'filename', '')
if not filename: if not filename:
pecan.abort(400, _("No %s file uploaded" % pecan.abort(400, _("No %s file uploaded"
consts.DEPLOY_CONFIG)) % consts.DEPLOY_CONFIG))
file_item.file.seek(0, os.SEEK_SET) file_item.file.seek(0, os.SEEK_SET)
contents = file_item.file.read() contents = file_item.file.read()
# the deploy config needs to upload to the override location # the deploy config needs to upload to the override location
fn = os.path.join(consts.ANSIBLE_OVERRIDES_PATH, payload['name'] fn = os.path.join(consts.ANSIBLE_OVERRIDES_PATH, payload['name']
+ '_' + os.path.basename(filename)) + '_deploy_config.yml')
try: try:
with open(fn, "w") as f: with open(fn, "w") as f:
f.write(contents) f.write(contents)
@ -155,6 +160,32 @@ class SubcloudsController(object):
payload.update(request.POST) payload.update(request.POST)
return payload 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, def _validate_subcloud_config(self,
context, context,
name, name,
@ -713,10 +744,13 @@ class SubcloudsController(object):
@utils.synchronized(LOCK_NAME) @utils.synchronized(LOCK_NAME)
@index.when(method='PATCH', template='json') @index.when(method='PATCH', template='json')
def patch(self, subcloud_ref=None): def patch(self, subcloud_ref=None, reconfigure=None):
"""Update a subcloud. """Update a subcloud.
:param subcloud_ref: ID or name of subcloud to update :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() context = restcomm.extract_context_from_environ()
@ -725,10 +759,6 @@ class SubcloudsController(object):
if subcloud_ref is None: if subcloud_ref is None:
pecan.abort(400, _('Subcloud ID required')) pecan.abort(400, _('Subcloud ID required'))
payload = eval(request.body)
if not payload:
pecan.abort(400, _('Body required'))
if subcloud_ref.isdigit(): if subcloud_ref.isdigit():
# Look up subcloud as an ID # Look up subcloud as an ID
try: try:
@ -745,40 +775,78 @@ class SubcloudsController(object):
subcloud_id = subcloud.id subcloud_id = subcloud.id
management_state = payload.get('management-state') if reconfigure is None:
description = payload.get('description') payload = eval(request.body)
location = payload.get('location') if not payload:
group_id = payload.get('group_id') pecan.abort(400, _('Body required'))
if not (management_state or description or location or group_id): management_state = payload.get('management-state')
pecan.abort(400, _('nothing to update')) description = payload.get('description')
location = payload.get('location')
group_id = payload.get('group_id')
# Syntax checking if not (management_state or description or location or group_id):
if management_state and \ pecan.abort(400, _('nothing to update'))
management_state not in [consts.MANAGEMENT_UNMANAGED,
consts.MANAGEMENT_MANAGED]: # Syntax checking
pecan.abort(400, _('Invalid management-state')) 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: try:
db_api.subcloud_group_get(context, group_id) # Inform dcmanager-manager that subcloud has been updated.
except exceptions.SubcloudGroupNotFound: # It will do all the real work...
pecan.abort(400, _('Invalid group-id')) 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: if subcloud.deploy_status not in [consts.DEPLOY_STATE_DONE,
# Inform dcmanager-manager that subcloud has been updated. consts.DEPLOY_STATE_DEPLOY_PREP_FAILED,
# It will do all the real work... consts.DEPLOY_STATE_DEPLOY_FAILED]:
subcloud = self.rpc_client.update_subcloud( pecan.abort(400, _('Subcloud deploy status must be either '
context, subcloud_id, management_state=management_state, 'complete, deploy-prep-failed or deploy-failed'))
description=description, location=location, group_id=group_id) sysadmin_password = \
return subcloud payload.get('sysadmin_password')
except RemoteError as e: if not sysadmin_password:
pecan.abort(422, e.value) pecan.abort(400, _('subcloud sysadmin_password required'))
except Exception as e:
# additional exceptions. try:
LOG.exception(e) payload['sysadmin_password'] = base64.b64decode(
pecan.abort(500, _('Unable to update subcloud')) 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) @utils.synchronized(LOCK_NAME)
@index.when(method='delete', template='json') @index.when(method='delete', template='json')

View File

@ -135,6 +135,14 @@ class DCManagerService(service.Service):
return subcloud 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 @request_context
def update_subcloud_endpoint_status(self, context, subcloud_name=None, def update_subcloud_endpoint_status(self, context, subcloud_name=None,
endpoint_type=None, endpoint_type=None,

View File

@ -114,6 +114,13 @@ class SubcloudManager(manager.Manager):
self.dcorch_rpc_client = dcorch_rpc_client.EngineClient() self.dcorch_rpc_client = dcorch_rpc_client.EngineClient()
self.fm_api = fm_api.FaultAPIs() 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 @staticmethod
def _get_subcloud_cert_name(subcloud_name): def _get_subcloud_cert_name(subcloud_name):
cert_name = "%s-adminep-ca-certificate" % subcloud_name cert_name = "%s-adminep-ca-certificate" % subcloud_name
@ -200,9 +207,8 @@ class SubcloudManager(manager.Manager):
try: try:
# Ansible inventory filename for the specified subcloud # Ansible inventory filename for the specified subcloud
ansible_subcloud_inventory_file = os.path.join( ansible_subcloud_inventory_file = SubcloudManager.\
consts.ANSIBLE_OVERRIDES_PATH, _get_ansible_inventory_filename(subcloud.name)
subcloud.name + INVENTORY_FILE_POSTFIX)
# Create a new route to this subcloud on the management interface # Create a new route to this subcloud on the management interface
# on both controllers. # on both controllers.
@ -318,20 +324,16 @@ class SubcloudManager(manager.Manager):
payload['install_values']['ansible_ssh_pass'] = \ payload['install_values']['ansible_ssh_pass'] = \
payload['sysadmin_password'] payload['sysadmin_password']
deploy_command = None
if "deploy_playbook" in payload: if "deploy_playbook" in payload:
payload['deploy_values'] = dict() self._prepare_for_deployment(payload, subcloud.name)
payload['deploy_values']['ansible_become_pass'] = \ deploy_command = [
payload['sysadmin_password'] "ansible-playbook", payload[consts.DEPLOY_PLAYBOOK],
payload['deploy_values']['ansible_ssh_pass'] = \ "-e", "@%s" % consts.ANSIBLE_OVERRIDES_PATH + "/" +
payload['sysadmin_password'] subcloud.name + "_deploy_values.yml",
payload['deploy_values']['admin_password'] = \ "-i", ansible_subcloud_inventory_file,
str(keyring.get_password('CGCS', 'admin')) "--limit", subcloud.name
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]
del payload['sysadmin_password'] del payload['sysadmin_password']
@ -352,9 +354,6 @@ class SubcloudManager(manager.Manager):
# as it is used for debugging # as it is used for debugging
self._write_subcloud_ansible_config(context, payload) self._write_subcloud_ansible_config(context, payload)
if "deploy_playbook" in payload:
self._write_deploy_files(payload)
install_command = None install_command = None
if "install_values" in payload: if "install_values" in payload:
install_command = [ install_command = [
@ -377,20 +376,10 @@ class SubcloudManager(manager.Manager):
"-e", str("override_files_dir='%s' region_name=%s") % ( "-e", str("override_files_dir='%s' region_name=%s") % (
consts.ANSIBLE_OVERRIDES_PATH, subcloud.name)] 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( apply_thread = threading.Thread(
target=self.run_deploy, target=self.run_deploy,
args=(install_command, apply_command, deploy_command, subcloud, args=(subcloud, payload, context,
payload, context)) install_command, apply_command, deploy_command))
apply_thread.start() apply_thread.start()
return db_api.subcloud_db_model_to_dict(subcloud) return db_api.subcloud_db_model_to_dict(subcloud)
@ -403,9 +392,52 @@ class SubcloudManager(manager.Manager):
context, subcloud.id, context, subcloud.id,
deploy_status=consts.DEPLOY_STATE_DEPLOY_PREP_FAILED) 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 @staticmethod
def run_deploy(install_command, apply_command, deploy_command, subcloud, def run_deploy(subcloud, payload, context,
payload, context): install_command=None, apply_command=None,
deploy_command=None):
if install_command: if install_command:
db_api.subcloud_update( db_api.subcloud_update(
@ -440,39 +472,40 @@ class SubcloudManager(manager.Manager):
install.cleanup() install.cleanup()
LOG.info("Successfully installed subcloud %s" % subcloud.name) LOG.info("Successfully installed subcloud %s" % subcloud.name)
# Update the subcloud to bootstrapping if apply_command:
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:
try: try:
subprocess.check_call(apply_command, # Update the subcloud to bootstrapping
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( db_api.subcloud_update(
context, subcloud.id, context, subcloud.id,
deploy_status=consts.DEPLOY_STATE_BOOTSTRAP_FAILED) deploy_status=consts.DEPLOY_STATE_BOOTSTRAPPING)
return except Exception as e:
LOG.info("Successfully bootstrapped subcloud %s" % LOG.exception(e)
subcloud.name) 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: if deploy_command:
# Run the custom deploy playbook # Run the custom deploy playbook
@ -596,16 +629,32 @@ class SubcloudManager(manager.Manager):
'deploy_overrides', 'install_values']: 'deploy_overrides', 'install_values']:
f_out_overrides_file.write("%s: %s\n" % (k, json.dumps(v))) 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""" """Create the deploy value files for the subcloud"""
deploy_values_file = os.path.join( deploy_values_file = os.path.join(
consts.ANSIBLE_OVERRIDES_PATH, payload['name'] + consts.ANSIBLE_OVERRIDES_PATH, subcloud_name +
'_deploy_values.yml') '_deploy_values.yml')
with open(deploy_values_file, 'w') as f_out_deploy_values_file: with open(deploy_values_file, 'w') as f_out_deploy_values_file:
json.dump(payload['deploy_values'], 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): def _delete_subcloud_routes(self, context, subcloud):
"""Delete the routes to this subcloud""" """Delete the routes to this subcloud"""

View File

@ -80,6 +80,11 @@ class ManagerClient(object):
location=location, location=location,
group_id=group_id)) 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, def update_subcloud_endpoint_status(self, ctxt, subcloud_name=None,
endpoint_type=None, endpoint_type=None,
sync_status=consts. sync_status=consts.

View File

@ -25,6 +25,7 @@ import sqlalchemy
from oslo_config import cfg from oslo_config import cfg
from oslo_db import options from oslo_db import options
from dcmanager.common import consts
from dcmanager.db import api as api from dcmanager.db import api as api
from dcmanager.db.sqlalchemy import api as db_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.1", # external_oam_gateway_address
"10.10.10.12", # external_oam_floating_address "10.10.10.12", # external_oam_floating_address
"testpass", # sysadmin_password "testpass", # sysadmin_password
1 # group_id 1, # group_id
consts.DEPLOY_STATE_DONE # deploy_status
] ]

View File

@ -20,6 +20,8 @@
# of an applicable Wind River license agreement. # of an applicable Wind River license agreement.
# #
from oslo_utils import timeutils
import base64 import base64
import copy import copy
import mock import mock
@ -40,7 +42,8 @@ WRONG_URL = '/v1.0/wrong'
FAKE_HEADERS = {'X-Tenant-Id': FAKE_TENANT, 'X_ROLE': 'admin', FAKE_HEADERS = {'X-Tenant-Id': FAKE_TENANT, 'X_ROLE': 'admin',
'X-Identity-Status': 'Confirmed'} 'X-Identity-Status': 'Confirmed'}
FAKE_SUBCLOUD_DATA = {"name": "subcloud1", FAKE_SUBCLOUD_DATA = {"id": FAKE_ID,
"name": "subcloud1",
"description": "subcloud1 description", "description": "subcloud1 description",
"location": "subcloud1 location", "location": "subcloud1 location",
"system_mode": "duplex", "system_mode": "duplex",
@ -49,6 +52,7 @@ FAKE_SUBCLOUD_DATA = {"name": "subcloud1",
"management_end_address": "192.168.101.50", "management_end_address": "192.168.101.50",
"management_gateway_address": "192.168.101.1", "management_gateway_address": "192.168.101.1",
"systemcontroller_gateway_address": "192.168.204.101", "systemcontroller_gateway_address": "192.168.204.101",
"deploy_status": consts.DEPLOY_STATE_DONE,
"external_oam_subnet": "10.10.10.0/24", "external_oam_subnet": "10.10.10.0/24",
"external_oam_gateway_address": "10.10.10.1", "external_oam_gateway_address": "10.10.10.1",
"external_oam_floating_address": "10.10.10.12", "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): class FakeAddressPool(object):
def __init__(self, pool_network, pool_prefix, pool_start, pool_end): def __init__(self, pool_network, pool_prefix, pool_start, pool_end):
self.network = pool_network self.network = pool_network
@ -552,7 +583,8 @@ class TestSubclouds(testroot.DCManagerApiTest):
self.assertEqual(response.status_int, 200) self.assertEqual(response.status_int, 200)
@mock.patch.object(rpc_client, 'ManagerClient') @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 = {} data = {}
six.assertRaisesRegex(self, webtest.app.AppError, "400 *", six.assertRaisesRegex(self, webtest.app.AppError, "400 *",
self.app.patch_json, FAKE_URL + '/' + FAKE_ID, self.app.patch_json, FAKE_URL + '/' + FAKE_ID,
@ -565,3 +597,91 @@ class TestSubclouds(testroot.DCManagerApiTest):
six.assertRaisesRegex(self, webtest.app.AppError, "400 *", six.assertRaisesRegex(self, webtest.app.AppError, "400 *",
self.app.patch_json, FAKE_URL + '/' + FAKE_ID, self.app.patch_json, FAKE_URL + '/' + FAKE_ID,
headers=FAKE_HEADERS, params=data) 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)

View File

@ -107,7 +107,7 @@ class Subcloud(object):
self.availability_status = consts.AVAILABILITY_ONLINE self.availability_status = consts.AVAILABILITY_ONLINE
else: else:
self.availability_status = consts.AVAILABILITY_OFFLINE self.availability_status = consts.AVAILABILITY_OFFLINE
self.deploy_status = data['deploy_status']
self.management_subnet = data['management_subnet'] self.management_subnet = data['management_subnet']
self.management_gateway_ip = data['management_gateway_address'] self.management_gateway_ip = data['management_gateway_address']
self.management_start_ip = data['management_start_address'] self.management_start_ip = data['management_start_address']
@ -579,3 +579,27 @@ class TestSubcloudManager(base.DCManagerTestCase):
# Verify the subcloud openstack_installed was updated # Verify the subcloud openstack_installed was updated
updated_subcloud = db_api.subcloud_get_by_name(self.ctx, subcloud.name) updated_subcloud = db_api.subcloud_get_by_name(self.ctx, subcloud.name)
self.assertEqual(updated_subcloud.openstack_installed, False) 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()

View File

@ -118,4 +118,5 @@ def create_subcloud_dict(data_list):
'external_oam_gateway_address': data_list[20], 'external_oam_gateway_address': data_list[20],
'external_oam_floating_address': data_list[21], 'external_oam_floating_address': data_list[21],
'sysadmin_password': data_list[22], 'sysadmin_password': data_list[22],
'group_id': data_list[23]} 'group_id': data_list[23],
'deploy_status': data_list[24]}