Merge "CLI command to deploy a subcloud"
This commit is contained in:
commit
a307733a52
@ -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
|
||||
*****************************
|
||||
|
@ -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')
|
||||
|
@ -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,
|
||||
|
@ -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"""
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -614,3 +614,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()
|
||||
|
Loading…
Reference in New Issue
Block a user