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",
|
"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
|
||||||
*****************************
|
*****************************
|
||||||
|
@ -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')
|
||||||
|
@ -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,
|
||||||
|
@ -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"""
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -614,3 +614,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()
|
||||||
|
Loading…
Reference in New Issue
Block a user