Support subcloud deploy upload the common files
Add new REST APIs to upload and display the deploy manager common files on the System Controller. The deploy manager common files which include playbook, overrides and helm charts are uploaded to /opt/platform/deploy/<version>: /opt/platform/deploy/<version>/deploy_playbook_<original name> /opt/platform/deploy/<version>/deploy_overrides_<original name> /opt/platform/deploy/<version>/deploy_chart_<original name> Modify the subcloud post request to accept the bootstrap-values, install-values and deploy-config as file contents. The deploy config file is only used by the deploy manager and it is uploaded to /opt/dc/ansible. The information that used to create the overrides for the playbook are extracted and sent to the dcmanager, which include bootstrap values, install values and the full path of deploy file names, if the deploy-config is presented in the request. Testcases: REST APIs: 1. curl -X POST -H "X-Auth-Token: $TOKEN" $APIURL/subcloud-deploy \ -F deploy_playbook=@<full path of the playbook name> \ -F deploy_overrides=@<full path of the override file name> \ -F deploy_chart=@full path of the helm chart name> 2. curl -X GET -H "X-Auth-Token: $TOKEN" $APIURL/subcloud-deploy 3. curl -X POST -H "X-Auth-Token: $TOKEN" \ $APIURL/subclouds \ -F bootstrap_values=@<full path of the bootstrap override file> \ -F sysadmin_password=<encoded password> \ -F bootstrap-address=<bootstrap IP> 4. curl -X POST -H "X-Auth-Token: $TOKEN" \ $APIURL/subclouds \ -F bootstrap_values=@<full path of the bootstrap override file> \ -F install_values=@<full path of the install value file> \ -F deploy_config=@<full path of the deploy config file> \ -F sysadmin_password=<encoded password> \ -F bmc_password=<encoded password> \ -F bootstrap-address=<bootstrap IP> \ CLI: 1. dcmanager subcloud-deploy upload \ --deploy-playbook <full path of the playbook name> \ --deploy-chart <full path of the override file name> \ --deploy-overrides <full path of the override file name> 2. dcmanager subcloud-deploy show 3. dcmanager subcloud add --bootstrap-address <IP> \ --bootstrap-values <full path of the bootstrap override> \ --deploy-config <full path of the deploy config file> \ 4. dcmanager subcloud add --bootstrap-address <IP> \ --bootstrap-values <full path of the bootstrap override> \ --install-values <full path of the install value file> \ 5.dcmanager subcloud add --bootstrap-address <IP> \ --bootstrap-values <full path of the bootstrap override> \ --install-values <full path of the install value file> \ --deploy-config <full path of the deploy config file> \ Host swact and deploy of a subcloud Closes-Bug: 1864508 Change-Id: I3ce0da6efb8c2d78a213647789fc6bdb3b348b2d Signed-off-by: Tao Liu <tao.liu@windriver.com>
This commit is contained in:
parent
1190428cd5
commit
58a7186bea
|
@ -1,4 +1,4 @@
|
|||
====================================================
|
||||
====================================================
|
||||
Dcmanager API v1
|
||||
====================================================
|
||||
|
||||
|
@ -190,6 +190,9 @@ Creates a subcloud
|
|||
|
||||
.. rest_method:: POST /v1.0/subclouds
|
||||
|
||||
Accepts Content-Type multipart/form-data.
|
||||
|
||||
|
||||
**Normal response codes**
|
||||
|
||||
200
|
||||
|
@ -206,13 +209,12 @@ serviceUnavailable (503)
|
|||
:header: "Parameter", "Style", "Type", "Description"
|
||||
:widths: 20, 20, 20, 60
|
||||
|
||||
"name", "plain", "xsd:string", "The name for the subcloud. Must be a unique name."
|
||||
"description (Optional)", "plain", "xsd:string", "The description of the subcloud."
|
||||
"location (Optional)", "plain", "xsd:string", "The location of the subcloud."
|
||||
"management-subnet", "plain", "xsd:string", "Management subnet for subcloud in CIDR format. Must be unique."
|
||||
"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."
|
||||
"bootstrap-address", "plain", "xsd:string", "An OAM IP address of the subcloud controller-0."
|
||||
"sysadmin_password", "plain", "xsd:string", "The sysadmin password of the subcloud. Must be base64 encoded."
|
||||
"bmc_password (optional)", "plain", "xsd:string", "The BMC password of the subcloud. Must be base64 encoded."
|
||||
"bootstrap_values", "plain", "xsd:string", "The content of a file containing the bootstrap overrides such as subcloud name, management and OAM subnet."
|
||||
"install_values (Optional)", "plain", "xsd:string", "The content of a file containing install variables such as subcloud bootstrap interface and BMC information."
|
||||
"deploy_config (Optional)", "plain", "xsd:string", "The content of a file containing the resource definitions describing the desired subcloud configuration."
|
||||
"group_id", "plain", "xsd:int", "Id of the subcloud group. Defaults to 1"
|
||||
|
||||
**Response parameters**
|
||||
|
@ -1493,5 +1495,97 @@ Delete per subcloud patch options
|
|||
|
||||
This operation does not accept a request body.
|
||||
|
||||
----------------
|
||||
Subcloud Deploy
|
||||
----------------
|
||||
|
||||
These APIs allow for the display and upload of the deployment manager common
|
||||
files which include deploy playbook, deploy overrides, and deploy helm charts.
|
||||
|
||||
|
||||
**************************
|
||||
Show Subcloud Deploy Files
|
||||
**************************
|
||||
|
||||
.. rest_method:: GET /v1.0/subcloud-deploy
|
||||
|
||||
|
||||
**Normal response codes**
|
||||
|
||||
200
|
||||
|
||||
**Error response codes**
|
||||
|
||||
badRequest (400), unauthorized (401), forbidden
|
||||
(403), badMethod (405), HTTPUnprocessableEntity (422),
|
||||
internalServerError (500), serviceUnavailable (503)
|
||||
|
||||
**Response parameters**
|
||||
|
||||
.. csv-table::
|
||||
:header: "Parameter", "Style", "Type", "Description"
|
||||
:widths: 20, 20, 20, 60
|
||||
|
||||
"subcloud_deploy", "plain", "xsd:dict", "The dictionary of subcloud deploy files."
|
||||
"deploy_chart", "plain", "xsd:string", "The file name of the deployment manager helm charts."
|
||||
"deploy_playbook", "plain", "xsd:string", "The file name of the deployment manager playbook."
|
||||
"deploy_overrides", "plain", "xsd:string", "The file name of the deployment manager overrides."
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"subcloud_deploy":
|
||||
{
|
||||
"deploy_chart": "deployment-manager.tgz",
|
||||
"deploy_playbook": "deployment-manager-playbook.yaml",
|
||||
"deploy_overrides": "deployment-manager-overrides-subcloud.yaml"
|
||||
}
|
||||
}
|
||||
|
||||
This operation does not accept a request body.
|
||||
|
||||
****************************
|
||||
Upload Subcloud Deploy Files
|
||||
****************************
|
||||
|
||||
.. rest_method:: POST /v1.0/subcloud-deploy
|
||||
|
||||
Accepts Content-Type multipart/form-data.
|
||||
|
||||
**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
|
||||
|
||||
"deploy_chart", "plain", "xsd:string", "The content of a file containing the deployment manager helm charts."
|
||||
"deploy_playbook", "plain", "xsd:string", "The content of a file containing the deployment manager playbook."
|
||||
"deploy_overrides", "plain", "xsd:string", "The content of a file containing the deployment manager overrides."
|
||||
|
||||
**Response parameters**
|
||||
|
||||
.. csv-table::
|
||||
:header: "Parameter", "Style", "Type", "Description"
|
||||
:widths: 20, 20, 20, 60
|
||||
|
||||
"deploy_chart", "plain", "xsd:string", "The file name of the deployment manager helm charts."
|
||||
"deploy_playbook", "plain", "xsd:string", "The file name of the deployment manager playbook."
|
||||
"deploy_overrides", "plain", "xsd:string", "The file name of the deployment manager overrides."
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"deploy_chart": "deployment-manager.tgz",
|
||||
"deploy_playbook": "deployment-manager-playbook.yaml",
|
||||
"deploy_overrides": "deployment-manager-overrides-subcloud.yaml"
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
|
||||
from dcmanager.api.controllers.v1 import alarm_manager
|
||||
from dcmanager.api.controllers.v1 import subcloud_deploy
|
||||
from dcmanager.api.controllers.v1 import subcloud_group
|
||||
from dcmanager.api.controllers.v1 import subclouds
|
||||
from dcmanager.api.controllers.v1 import sw_update_options
|
||||
|
@ -42,6 +43,8 @@ class Controller(object):
|
|||
sub_controllers = dict()
|
||||
if minor_version == '0':
|
||||
sub_controllers["subclouds"] = subclouds.SubcloudsController
|
||||
sub_controllers["subcloud-deploy"] = subcloud_deploy.\
|
||||
SubcloudDeployController
|
||||
sub_controllers["alarms"] = alarm_manager.SubcloudAlarmController
|
||||
sub_controllers["sw-update-strategy"] = \
|
||||
sw_update_strategy.SwUpdateStrategyController
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright (c) 2020 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
import http.client as httpclient
|
||||
import pecan
|
||||
from pecan import expose
|
||||
from pecan import request
|
||||
|
||||
from dcmanager.common import consts
|
||||
from dcmanager.common.i18n import _
|
||||
from dcmanager.common import utils
|
||||
|
||||
import tsconfig.tsconfig as tsc
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
LOCK_NAME = 'SubcloudDeployController'
|
||||
|
||||
|
||||
class SubcloudDeployController(object):
|
||||
|
||||
def __init__(self):
|
||||
super(SubcloudDeployController, self).__init__()
|
||||
|
||||
@staticmethod
|
||||
def _upload_files(dir_path, file_option, file_item, binary):
|
||||
prefix = file_option + '_'
|
||||
# create the version directory if it does not exist
|
||||
if not os.path.isdir(dir_path):
|
||||
os.mkdir(dir_path, 0o755)
|
||||
else:
|
||||
# check if the file exists, if so remove it
|
||||
filename = utils.get_filename_by_prefix(dir_path, prefix)
|
||||
if filename is not None:
|
||||
os.remove(dir_path + '/' + filename)
|
||||
|
||||
# upload the new file
|
||||
file_item.file.seek(0, os.SEEK_SET)
|
||||
contents = file_item.file.read()
|
||||
fn = os.path.join(dir_path, prefix + os.path.basename(
|
||||
file_item.filename))
|
||||
if binary:
|
||||
dst = open(fn, 'wb')
|
||||
dst.write(contents)
|
||||
else:
|
||||
dst = os.open(fn, os.O_WRONLY | os.O_CREAT)
|
||||
os.write(dst, contents)
|
||||
|
||||
@expose(generic=True, template='json')
|
||||
def index(self):
|
||||
# Route the request to specific methods with parameters
|
||||
pass
|
||||
|
||||
@utils.synchronized(LOCK_NAME)
|
||||
@index.when(method='POST', template='json')
|
||||
def post(self):
|
||||
deploy_dicts = dict()
|
||||
for f in consts.DEPLOY_COMMON_FILE_OPTIONS:
|
||||
if f not in request.POST:
|
||||
pecan.abort(httpclient.BAD_REQUEST,
|
||||
_("Missing required file for %s") % f)
|
||||
|
||||
file_item = request.POST[f]
|
||||
filename = getattr(file_item, 'filename', '')
|
||||
if not filename:
|
||||
pecan.abort(httpclient.BAD_REQUEST,
|
||||
_("No %s file uploaded" % f))
|
||||
|
||||
dir_path = tsc.DEPLOY_PATH
|
||||
binary = False
|
||||
if f == consts.DEPLOY_CHART:
|
||||
binary = True
|
||||
try:
|
||||
self._upload_files(dir_path, f, file_item, binary)
|
||||
except Exception as e:
|
||||
pecan.abort(httpclient.INTERNAL_SERVER_ERROR,
|
||||
_("Failed to upload %s file: %s" % (f, e)))
|
||||
deploy_dicts.update({f: filename})
|
||||
|
||||
return deploy_dicts
|
||||
|
||||
@index.when(method='GET', template='json')
|
||||
def get(self):
|
||||
"""Get the subcloud deploy files that has been uploaded and stored"""
|
||||
|
||||
deploy_dicts = dict()
|
||||
for f in consts.DEPLOY_COMMON_FILE_OPTIONS:
|
||||
dir_path = tsc.DEPLOY_PATH
|
||||
filename = None
|
||||
if os.path.isdir(dir_path):
|
||||
prefix = f + '_'
|
||||
filename = utils.get_filename_by_prefix(dir_path, prefix)
|
||||
if filename is not None:
|
||||
filename = filename.replace(prefix, '', 1)
|
||||
deploy_dicts.update({f: filename})
|
||||
return dict(subcloud_deploy=deploy_dicts)
|
|
@ -18,11 +18,13 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import base64
|
||||
import keyring
|
||||
from netaddr import AddrFormatError
|
||||
from netaddr import IPAddress
|
||||
from netaddr import IPNetwork
|
||||
from netaddr import IPRange
|
||||
import os
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_messaging import RemoteError
|
||||
|
@ -38,6 +40,8 @@ from dccommon import exceptions as dccommon_exceptions
|
|||
|
||||
from keystoneauth1 import exceptions as keystone_exceptions
|
||||
|
||||
import tsconfig.tsconfig as tsc
|
||||
|
||||
from dcmanager.api.controllers import restcomm
|
||||
from dcmanager.common import consts
|
||||
from dcmanager.common import exceptions
|
||||
|
@ -48,6 +52,7 @@ 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
|
||||
|
@ -57,6 +62,18 @@ SYSTEM_MODE_DUPLEX_DIRECT = "duplex-direct"
|
|||
|
||||
LOCK_NAME = 'SubcloudsController'
|
||||
|
||||
BOOTSTRAP_VALUES = 'bootstrap_values'
|
||||
INSTALL_VALUES = 'install_values'
|
||||
|
||||
SUBCLOUD_ADD_MANDATORY_FILE = [
|
||||
BOOTSTRAP_VALUES,
|
||||
]
|
||||
|
||||
SUBCLOUD_ADD_GET_FILE_CONTENTS = [
|
||||
BOOTSTRAP_VALUES,
|
||||
INSTALL_VALUES,
|
||||
]
|
||||
|
||||
|
||||
class SubcloudsController(object):
|
||||
VERSION_ALIASES = {
|
||||
|
@ -85,6 +102,57 @@ class SubcloudsController(object):
|
|||
LOG.exception(e)
|
||||
pecan.abort(400, _("Invalid group_id"))
|
||||
|
||||
@staticmethod
|
||||
def _get_common_deploy_files(payload):
|
||||
for f in consts.DEPLOY_COMMON_FILE_OPTIONS:
|
||||
dir_path = tsc.DEPLOY_PATH
|
||||
filename = utils.get_filename_by_prefix(dir_path, f + '_')
|
||||
if filename is None:
|
||||
pecan.abort(400, _("Missing required deploy file for %s") % f)
|
||||
payload.update({f: os.path.join(dir_path, filename)})
|
||||
|
||||
def _upload_deploy_config_file(self, request, payload):
|
||||
if consts.DEPLOY_CONFIG in request.POST:
|
||||
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))
|
||||
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))
|
||||
try:
|
||||
dst = os.open(fn, os.O_WRONLY | os.O_CREAT)
|
||||
os.write(dst, contents)
|
||||
except Exception:
|
||||
msg = _("Failed to upload %s file" % consts.DEPLOY_CONFIG)
|
||||
LOG.exception(msg)
|
||||
pecan.abort(400, msg)
|
||||
payload.update({consts.DEPLOY_CONFIG: fn})
|
||||
self._get_common_deploy_files(payload)
|
||||
|
||||
@staticmethod
|
||||
def _get_request_data(request):
|
||||
payload = dict()
|
||||
for f in SUBCLOUD_ADD_MANDATORY_FILE:
|
||||
if f not in request.POST:
|
||||
pecan.abort(400, _("Missing required file for %s") % f)
|
||||
|
||||
for f in SUBCLOUD_ADD_GET_FILE_CONTENTS:
|
||||
if f in request.POST:
|
||||
file_item = request.POST[f]
|
||||
file_item.file.seek(0, os.SEEK_SET)
|
||||
data = yaml.safe_load(file_item.file.read().decode('utf8'))
|
||||
if f == BOOTSTRAP_VALUES:
|
||||
payload.update(data)
|
||||
else:
|
||||
payload.update({f: data})
|
||||
del request.POST[f]
|
||||
payload.update(request.POST)
|
||||
return payload
|
||||
|
||||
def _validate_subcloud_config(self,
|
||||
context,
|
||||
name,
|
||||
|
@ -242,6 +310,13 @@ class SubcloudsController(object):
|
|||
bmc_password = payload.get('bmc_password')
|
||||
if not bmc_password:
|
||||
pecan.abort(400, _('subcloud bmc_password required'))
|
||||
try:
|
||||
bmc_password = base64.b64decode(bmc_password).decode('utf-8')
|
||||
except Exception:
|
||||
msg = _('Failed to decode subcloud bmc_password, verify'
|
||||
' the password is base64 encoded')
|
||||
LOG.exception(msg)
|
||||
pecan.abort(400, msg)
|
||||
payload['install_values'].update({'bmc_password': bmc_password})
|
||||
|
||||
for k in install_consts.MANDATORY_INSTALL_VALUES:
|
||||
|
@ -500,13 +575,16 @@ class SubcloudsController(object):
|
|||
context = restcomm.extract_context_from_environ()
|
||||
|
||||
if subcloud_ref is None:
|
||||
payload = yaml.safe_load(request.body)
|
||||
|
||||
payload = self._get_request_data(request)
|
||||
|
||||
if not payload:
|
||||
pecan.abort(400, _('Body required'))
|
||||
|
||||
name = payload.get('name')
|
||||
if not name:
|
||||
pecan.abort(400, _('name required'))
|
||||
|
||||
system_mode = payload.get('system_mode')
|
||||
if not system_mode:
|
||||
pecan.abort(400, _('system_mode required'))
|
||||
|
@ -551,6 +629,14 @@ class SubcloudsController(object):
|
|||
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)
|
||||
|
||||
# If a subcloud group is not passed, use the default
|
||||
group_id = payload.get('group_id',
|
||||
|
@ -571,6 +657,11 @@ class SubcloudsController(object):
|
|||
if 'install_values' in payload:
|
||||
self._validate_install_values(payload)
|
||||
|
||||
# Upload the deploy config files if it is included in the request
|
||||
# It has a dependency on the subcloud name, and it is called after
|
||||
# the name has been validated
|
||||
self._upload_deploy_config_file(request, payload)
|
||||
|
||||
try:
|
||||
# Ask dcmanager-manager to add the subcloud.
|
||||
# It will do all the real work...
|
||||
|
|
|
@ -111,3 +111,16 @@ ALARMS_DISABLED = "disabled"
|
|||
ALARM_OK_STATUS = "OK"
|
||||
ALARM_DEGRADED_STATUS = "degraded"
|
||||
ALARM_CRITICAL_STATUS = "critical"
|
||||
|
||||
# subcloud deploy file options
|
||||
ANSIBLE_OVERRIDES_PATH = '/opt/dc/ansible'
|
||||
DEPLOY_PLAYBOOK = "deploy_playbook"
|
||||
DEPLOY_OVERRIDES = "deploy_overrides"
|
||||
DEPLOY_CHART = "deploy_chart"
|
||||
DEPLOY_CONFIG = 'deploy_config'
|
||||
|
||||
DEPLOY_COMMON_FILE_OPTIONS = [
|
||||
DEPLOY_PLAYBOOK,
|
||||
DEPLOY_OVERRIDES,
|
||||
DEPLOY_CHART
|
||||
]
|
||||
|
|
|
@ -200,3 +200,10 @@ def synchronized(name, external=True, fair=False):
|
|||
return lockutils.synchronized(name, lock_file_prefix=prefix,
|
||||
external=external, lock_path=lock_path,
|
||||
semaphores=None, delay=0.01, fair=fair)
|
||||
|
||||
|
||||
def get_filename_by_prefix(dir_path, prefix):
|
||||
for filename in os.listdir(dir_path):
|
||||
if filename.startswith(prefix):
|
||||
return filename
|
||||
return None
|
||||
|
|
|
@ -60,7 +60,6 @@ LOG = logging.getLogger(__name__)
|
|||
ADDN_HOSTS_DC = 'dnsmasq.addn_hosts_dc'
|
||||
|
||||
# Subcloud configuration paths
|
||||
ANSIBLE_OVERRIDES_PATH = '/opt/dc/ansible'
|
||||
INVENTORY_FILE_POSTFIX = '_inventory.yml'
|
||||
ANSIBLE_SUBCLOUD_PLAYBOOK = \
|
||||
'/usr/share/ansible/stx-ansible/playbooks/bootstrap.yml'
|
||||
|
@ -160,7 +159,7 @@ class SubcloudManager(manager.Manager):
|
|||
try:
|
||||
# Ansible inventory filename for the specified subcloud
|
||||
ansible_subcloud_inventory_file = os.path.join(
|
||||
ANSIBLE_OVERRIDES_PATH,
|
||||
consts.ANSIBLE_OVERRIDES_PATH,
|
||||
subcloud.name + INVENTORY_FILE_POSTFIX)
|
||||
|
||||
# Create a new route to this subcloud on the management interface
|
||||
|
@ -277,12 +276,19 @@ class SubcloudManager(manager.Manager):
|
|||
payload['sysadmin_password']
|
||||
|
||||
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]
|
||||
|
||||
del payload['sysadmin_password']
|
||||
|
||||
|
@ -309,7 +315,7 @@ class SubcloudManager(manager.Manager):
|
|||
"ansible-playbook", ANSIBLE_SUBCLOUD_INSTALL_PLAYBOOK,
|
||||
"-i", ansible_subcloud_inventory_file,
|
||||
"--limit", subcloud.name,
|
||||
"-e", "@%s" % ANSIBLE_OVERRIDES_PATH + "/" +
|
||||
"-e", "@%s" % consts.ANSIBLE_OVERRIDES_PATH + "/" +
|
||||
payload['name'] + '/' + "install_values.yml"
|
||||
]
|
||||
|
||||
|
@ -323,17 +329,15 @@ class SubcloudManager(manager.Manager):
|
|||
# which overrides to load
|
||||
apply_command += [
|
||||
"-e", str("override_files_dir='%s' region_name=%s") % (
|
||||
ANSIBLE_OVERRIDES_PATH, subcloud.name)]
|
||||
consts.ANSIBLE_OVERRIDES_PATH, subcloud.name)]
|
||||
|
||||
deploy_command = None
|
||||
if "deploy_playbook" in payload:
|
||||
deploy_command = [
|
||||
"ansible-playbook", ANSIBLE_OVERRIDES_PATH + '/' +
|
||||
payload['name'] + "_deploy.yml",
|
||||
"-e", "@%s" % ANSIBLE_OVERRIDES_PATH + "/" +
|
||||
"ansible-playbook", payload[consts.DEPLOY_PLAYBOOK],
|
||||
"-e", "@%s" % consts.ANSIBLE_OVERRIDES_PATH + "/" +
|
||||
payload['name'] + "_deploy_values.yml",
|
||||
"-i",
|
||||
ansible_subcloud_inventory_file,
|
||||
"-i", ansible_subcloud_inventory_file,
|
||||
"--limit", subcloud.name
|
||||
]
|
||||
|
||||
|
@ -365,7 +369,8 @@ class SubcloudManager(manager.Manager):
|
|||
deploy_status=consts.DEPLOY_STATE_PRE_INSTALL)
|
||||
try:
|
||||
install = SubcloudInstall(context, subcloud.name)
|
||||
install.prep(ANSIBLE_OVERRIDES_PATH, payload['install_values'])
|
||||
install.prep(consts.ANSIBLE_OVERRIDES_PATH,
|
||||
payload['install_values'])
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
db_api.subcloud_update(
|
||||
|
@ -513,7 +518,7 @@ class SubcloudManager(manager.Manager):
|
|||
def _write_subcloud_ansible_config(self, context, payload):
|
||||
"""Create the override file for usage with the specified subcloud"""
|
||||
|
||||
overrides_file = os.path.join(ANSIBLE_OVERRIDES_PATH,
|
||||
overrides_file = os.path.join(consts.ANSIBLE_OVERRIDES_PATH,
|
||||
payload['name'] + '.yml')
|
||||
|
||||
m_ks_client = KeystoneClient()
|
||||
|
@ -543,19 +548,17 @@ class SubcloudManager(manager.Manager):
|
|||
|
||||
for k, v in payload.items():
|
||||
if k not in ['deploy_playbook', 'deploy_values',
|
||||
'install_values']:
|
||||
'deploy_config', 'deploy_chart',
|
||||
'deploy_overrides', 'install_values']:
|
||||
f_out_overrides_file.write("%s: %s\n" % (k, json.dumps(v)))
|
||||
|
||||
def _write_deploy_files(self, payload):
|
||||
"""Create the deploy playbook and value files for the subcloud"""
|
||||
"""Create the deploy value files for the subcloud"""
|
||||
|
||||
deploy_playbook_file = os.path.join(
|
||||
ANSIBLE_OVERRIDES_PATH, payload['name'] + '_deploy.yml')
|
||||
deploy_values_file = os.path.join(
|
||||
ANSIBLE_OVERRIDES_PATH, payload['name'] + '_deploy_values.yml')
|
||||
consts.ANSIBLE_OVERRIDES_PATH, payload['name'] +
|
||||
'_deploy_values.yml')
|
||||
|
||||
with open(deploy_playbook_file, 'w') as f_out_deploy_playbook_file:
|
||||
json.dump(payload['deploy_playbook'], f_out_deploy_playbook_file)
|
||||
with open(deploy_values_file, 'w') as f_out_deploy_values_file:
|
||||
json.dump(payload['deploy_values'], f_out_deploy_values_file)
|
||||
|
||||
|
@ -650,7 +653,7 @@ class SubcloudManager(manager.Manager):
|
|||
|
||||
# Ansible inventory filename for the specified subcloud
|
||||
ansible_subcloud_inventory_file = os.path.join(
|
||||
ANSIBLE_OVERRIDES_PATH,
|
||||
consts.ANSIBLE_OVERRIDES_PATH,
|
||||
subcloud.name + INVENTORY_FILE_POSTFIX)
|
||||
|
||||
self._remove_subcloud_details(context,
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright (c) 2020 Wind River Systems, Inc.
|
||||
#
|
||||
# The right to copy, distribute, modify, or otherwise make use
|
||||
# of this software may be licensed only pursuant to the terms
|
||||
# of an applicable Wind River license agreement.
|
||||
#
|
||||
|
||||
import mock
|
||||
from six.moves import http_client
|
||||
|
||||
from dcmanager.api.controllers.v1 import subcloud_deploy
|
||||
from dcmanager.common import consts
|
||||
from dcmanager.tests.unit.api import test_root_controller as testroot
|
||||
from dcmanager.tests import utils
|
||||
|
||||
FAKE_TENANT = utils.UUID1
|
||||
FAKE_ID = '1'
|
||||
FAKE_URL = '/v1.0/subcloud-deploy'
|
||||
FAKE_HEADERS = {'X-Tenant-Id': FAKE_TENANT, 'X_ROLE': 'admin',
|
||||
'X-Identity-Status': 'Confirmed'}
|
||||
|
||||
|
||||
class TestSubcloudDeploy(testroot.DCManagerApiTest):
|
||||
def setUp(self):
|
||||
super(TestSubcloudDeploy, self).setUp()
|
||||
self.ctx = utils.dummy_context()
|
||||
|
||||
@mock.patch.object(subcloud_deploy.SubcloudDeployController,
|
||||
'_upload_files')
|
||||
def test_post_subcloud_deploy(self, mock_upload_files):
|
||||
fields = list()
|
||||
for opt in consts.DEPLOY_COMMON_FILE_OPTIONS:
|
||||
fake_name = opt + "_fake"
|
||||
fields.append((opt, fake_name, "fake content"))
|
||||
mock_upload_files.return_value = True
|
||||
response = self.app.post(FAKE_URL,
|
||||
headers=FAKE_HEADERS,
|
||||
upload_files=fields)
|
||||
self.assertEqual(response.status_code, http_client.OK)
|
||||
|
||||
@mock.patch.object(subcloud_deploy.SubcloudDeployController,
|
||||
'_upload_files')
|
||||
def test_post_subcloud_deploy_missing_file(self, mock_upload_files):
|
||||
opts = [consts.DEPLOY_PLAYBOOK, consts.DEPLOY_OVERRIDES]
|
||||
fields = list()
|
||||
for opt in opts:
|
||||
fake_name = opt + "_fake"
|
||||
fields.append((opt, fake_name, "fake content"))
|
||||
mock_upload_files.return_value = True
|
||||
response = self.app.post(FAKE_URL,
|
||||
headers=FAKE_HEADERS,
|
||||
upload_files=fields,
|
||||
expect_errors=True)
|
||||
self.assertEqual(response.status_code, http_client.BAD_REQUEST)
|
||||
|
||||
@mock.patch.object(subcloud_deploy.SubcloudDeployController,
|
||||
'_upload_files')
|
||||
def test_post_subcloud_deploy_missing_file_name(self, mock_upload_files):
|
||||
fields = list()
|
||||
for opt in consts.DEPLOY_COMMON_FILE_OPTIONS:
|
||||
fields.append((opt, "", "fake content"))
|
||||
mock_upload_files.return_value = True
|
||||
response = self.app.post(FAKE_URL,
|
||||
headers=FAKE_HEADERS,
|
||||
upload_files=fields,
|
||||
expect_errors=True)
|
||||
self.assertEqual(response.status_code, http_client.BAD_REQUEST)
|
|
@ -13,13 +13,14 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright (c) 2017 Wind River Systems, Inc.
|
||||
# Copyright (c) 2017-2020 Wind River Systems, Inc.
|
||||
#
|
||||
# The right to copy, distribute, modify, or otherwise make use
|
||||
# of this software may be licensed only pursuant to the terms
|
||||
# of an applicable Wind River license agreement.
|
||||
#
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import mock
|
||||
import six
|
||||
|
@ -51,8 +52,7 @@ FAKE_SUBCLOUD_DATA = {"name": "subcloud1",
|
|||
"external_oam_subnet": "10.10.10.0/24",
|
||||
"external_oam_gateway_address": "10.10.10.1",
|
||||
"external_oam_floating_address": "10.10.10.12",
|
||||
"availability-status": "disabled",
|
||||
"sysadmin_password": "testpass"}
|
||||
"availability-status": "disabled"}
|
||||
|
||||
FAKE_SUBCLOUD_INSTALL_VALUES = {
|
||||
"image": "http://192.168.101.2:8080/iso/bootimage.iso",
|
||||
|
@ -72,6 +72,11 @@ FAKE_SUBCLOUD_INSTALL_VALUES = {
|
|||
"boot_device": "/dev/disk/by-path/pci-0000:5c:00.0-scsi-0:1:0:0"
|
||||
}
|
||||
|
||||
FAKE_BOOTSTRAP_VALUE = {
|
||||
'bootstrap-address': '10.10.10.12',
|
||||
'sysadmin_password': base64.b64encode('testpass'.encode("utf-8"))
|
||||
}
|
||||
|
||||
|
||||
class FakeAddressPool(object):
|
||||
def __init__(self, pool_network, pool_prefix, pool_start, pool_end):
|
||||
|
@ -103,45 +108,75 @@ class TestSubclouds(testroot.DCManagerApiTest):
|
|||
super(TestSubclouds, self).setUp()
|
||||
self.ctx = utils.dummy_context()
|
||||
|
||||
@mock.patch.object(subclouds.SubcloudsController,
|
||||
'_upload_deploy_config_file')
|
||||
@mock.patch.object(subclouds.SubcloudsController,
|
||||
'_get_request_data')
|
||||
@mock.patch.object(subclouds.SubcloudsController,
|
||||
'_get_management_address_pool')
|
||||
@mock.patch.object(rpc_client, 'ManagerClient')
|
||||
@mock.patch.object(subclouds, 'db_api')
|
||||
def test_post_subcloud(self, mock_db_api, mock_rpc_client,
|
||||
mock_get_management_address_pool):
|
||||
data = FAKE_SUBCLOUD_DATA
|
||||
mock_get_management_address_pool,
|
||||
mock_get_request_data,
|
||||
mock_upload_deploy_config_file):
|
||||
management_address_pool = FakeAddressPool('192.168.204.0', 24,
|
||||
'192.168.204.2',
|
||||
'192.168.204.100')
|
||||
mock_get_management_address_pool.return_value = management_address_pool
|
||||
mock_rpc_client().add_subcloud.return_value = True
|
||||
response = self.app.post_json(FAKE_URL,
|
||||
fields = list()
|
||||
for f in subclouds.SUBCLOUD_ADD_MANDATORY_FILE:
|
||||
fake_name = f + "_fake"
|
||||
fields.append((f, fake_name, "fake content"))
|
||||
data = copy.copy(FAKE_SUBCLOUD_DATA)
|
||||
data.update(FAKE_BOOTSTRAP_VALUE)
|
||||
mock_get_request_data.return_value = data
|
||||
mock_upload_deploy_config_file.return_value = True
|
||||
response = self.app.post(FAKE_URL,
|
||||
headers=FAKE_HEADERS,
|
||||
params=data)
|
||||
params=FAKE_BOOTSTRAP_VALUE,
|
||||
upload_files=fields)
|
||||
mock_rpc_client().add_subcloud.assert_called_once_with(
|
||||
mock.ANY,
|
||||
data)
|
||||
self.assertEqual(response.status_int, 200)
|
||||
|
||||
@mock.patch.object(subclouds.SubcloudsController,
|
||||
'_upload_deploy_config_file')
|
||||
@mock.patch.object(subclouds.SubcloudsController,
|
||||
'_get_request_data')
|
||||
@mock.patch.object(subclouds.SubcloudsController,
|
||||
'_get_management_address_pool')
|
||||
@mock.patch.object(rpc_client, 'ManagerClient')
|
||||
@mock.patch.object(subclouds, 'db_api')
|
||||
def test_post_subcloud_with_install_values(
|
||||
self, mock_db_api, mock_rpc_client,
|
||||
mock_get_management_address_pool):
|
||||
mock_get_management_address_pool,
|
||||
mock_get_request_data,
|
||||
mock_upload_deploy_config_file):
|
||||
data = copy.copy(FAKE_SUBCLOUD_DATA)
|
||||
install_data = copy.copy(FAKE_SUBCLOUD_INSTALL_VALUES)
|
||||
data['bmc_password'] = 'bmc_password'
|
||||
data.update({'install_values': install_data})
|
||||
management_address_pool = FakeAddressPool('192.168.204.0', 24,
|
||||
'192.168.204.2',
|
||||
'192.168.204.100')
|
||||
mock_get_management_address_pool.return_value = management_address_pool
|
||||
mock_rpc_client().add_subcloud.return_value = True
|
||||
response = self.app.post_json(FAKE_URL,
|
||||
fields = list()
|
||||
for f in subclouds.SUBCLOUD_ADD_GET_FILE_CONTENTS:
|
||||
fake_name = f + "_fake"
|
||||
fields.append((f, fake_name, "fake content"))
|
||||
params = copy.copy(FAKE_BOOTSTRAP_VALUE)
|
||||
params.update({'bmc_password':
|
||||
base64.b64encode('bmc_password'.encode("utf-8"))})
|
||||
data.update(params)
|
||||
mock_get_request_data.return_value = data
|
||||
mock_upload_deploy_config_file.return_value = True
|
||||
response = self.app.post(FAKE_URL,
|
||||
headers=FAKE_HEADERS,
|
||||
params=data)
|
||||
params=params,
|
||||
upload_files=fields)
|
||||
self.assertEqual(response.status_int, 200)
|
||||
|
||||
@mock.patch.object(subclouds.SubcloudsController,
|
||||
|
|
Loading…
Reference in New Issue