From 8c2bd5fa14a27e488082e50c2957468b600fd1cd Mon Sep 17 00:00:00 2001 From: Tyler Smith Date: Thu, 11 Jul 2019 10:56:45 -0400 Subject: [PATCH] Zero Touch Provisioning changes for subcloud configuration - Adding logic to automatically deploy subclouds via ansible when they are added, as well as a 'deploy' field to subcloud entity to report status - Converting subcloud fields to take underscored parameters instead of dashed to match ansible variable style - Adding checks to OAM network parameters - Removing generate subcloud config logic Depends-On: https://review.opendev.org/#/c/670321/ Depends-On: https://review.opendev.org/#/c/670325/ Change-Id: Ib7fe2f4a42fffb7bd5082e6e851cb9136edf5a00 Story: 2004766 Task: 35756 Signed-off-by: Tyler Smith --- dcmanager/api/controllers/v1/subclouds.py | 365 +++++------------- dcmanager/common/consts.py | 8 +- dcmanager/db/api.py | 13 +- dcmanager/db/sqlalchemy/api.py | 10 +- .../versions/003_add_deploy_status_column.py | 36 ++ dcmanager/db/sqlalchemy/models.py | 1 + dcmanager/manager/subcloud_manager.py | 166 +++++++- .../ipv6_R5_install/dcmanager/subclouds.json | 6 +- .../unit/api/v1/controllers/test_subclouds.py | 51 ++- .../tests/unit/db/test_subcloud_db_api.py | 13 +- .../unit/manager/test_subcloud_manager.py | 45 ++- dcmanager/tests/utils.py | 16 +- 12 files changed, 391 insertions(+), 339 deletions(-) create mode 100644 dcmanager/db/sqlalchemy/migrate_repo/versions/003_add_deploy_status_column.py diff --git a/dcmanager/api/controllers/v1/subclouds.py b/dcmanager/api/controllers/v1/subclouds.py index 28d4ab1cf..c8d019bce 100644 --- a/dcmanager/api/controllers/v1/subclouds.py +++ b/dcmanager/api/controllers/v1/subclouds.py @@ -30,7 +30,6 @@ import pecan from pecan import expose from pecan import request -from controllerconfig.common import crypt from controllerconfig.common.exceptions import ValidateFail from controllerconfig.utils import validate_address_str from controllerconfig.utils import validate_network_str @@ -45,8 +44,6 @@ from dcmanager.db import api as db_api from dcmanager.drivers.openstack.sysinv_v1 import SysinvClient from dcmanager.rpc import client as rpc_client -from Crypto.Hash import MD5 -import json CONF = cfg.CONF LOG = logging.getLogger(__name__) @@ -82,6 +79,9 @@ class SubcloudsController(object): management_start_ip_str, management_end_ip_str, management_gateway_ip_str, + external_oam_subnet_str, + external_oam_gateway_address_str, + external_oam_floating_address_str, systemcontroller_gateway_ip_str): """Check whether subcloud config is valid.""" @@ -113,7 +113,7 @@ class SubcloudsController(object): existing_networks=subcloud_subnets) except ValidateFail as e: LOG.exception(e) - pecan.abort(400, _("management-subnet invalid: %s") % e) + pecan.abort(400, _("management_subnet invalid: %s") % e) # Parse/validate the start/end addresses management_start_ip = None @@ -122,7 +122,7 @@ class SubcloudsController(object): management_start_ip_str, management_subnet) except ValidateFail as e: LOG.exception(e) - pecan.abort(400, _("management-start-ip invalid: %s") % e) + pecan.abort(400, _("management_start_address invalid: %s") % e) management_end_ip = None try: @@ -130,12 +130,13 @@ class SubcloudsController(object): management_end_ip_str, management_subnet) except ValidateFail as e: LOG.exception(e) - pecan.abort(400, _("management-end-ip invalid: %s") % e) + pecan.abort(400, _("management_end_address invalid: %s") % e) if not management_start_ip < management_end_ip: pecan.abort( 400, - _("management-start-ip not less than management-end-ip")) + _("management_start_address not less than " + "management_end_address")) if not len(IPRange(management_start_ip, management_end_ip)) >= \ MIN_MANAGEMENT_ADDRESSES: @@ -150,7 +151,7 @@ class SubcloudsController(object): management_gateway_ip_str, management_subnet) except ValidateFail as e: LOG.exception(e) - pecan.abort(400, _("management-gateway-ip invalid: %s") % e) + pecan.abort(400, _("management_gateway_address invalid: %s") % e) # Ensure subcloud management gateway is not within the actual subcloud # management subnet address pool for consistency with the @@ -161,7 +162,7 @@ class SubcloudsController(object): subcloud_mgmt_gw_ip = IPAddress(management_gateway_ip_str) if ((subcloud_mgmt_gw_ip >= subcloud_mgmt_address_start) and (subcloud_mgmt_gw_ip <= subcloud_mgmt_address_end)): - pecan.abort(400, _("management-gateway-ip invalid, " + pecan.abort(400, _("management_gateway_address invalid, " "is within management pool: %(start)s - " "%(end)s") % {'start': subcloud_mgmt_address_start, @@ -179,7 +180,8 @@ class SubcloudsController(object): systemcontroller_gateway_ip_str, systemcontroller_subnet) except ValidateFail as e: LOG.exception(e) - pecan.abort(400, _("systemcontroller-gateway-ip invalid: %s") % e) + pecan.abort(400, + _("systemcontroller_gateway_address invalid: %s") % e) # Ensure systemcontroller gateway is not within the actual # management subnet address pool to prevent address collision. mgmt_address_start = IPAddress(management_address_pool.ranges[0][0]) @@ -187,188 +189,37 @@ class SubcloudsController(object): systemcontroller_gw_ip = IPAddress(systemcontroller_gateway_ip_str) if ((systemcontroller_gw_ip >= mgmt_address_start) and (systemcontroller_gw_ip <= mgmt_address_end)): - pecan.abort(400, _("systemcontroller-gateway-ip invalid, " + pecan.abort(400, _("systemcontroller_gateway_address invalid, " "is within management pool: %(start)s - " "%(end)s") % {'start': mgmt_address_start, 'end': mgmt_address_end}) - def _create_subcloud_config_file(self, context, subcloud, payload): - """Creates the subcloud config file for a subcloud.""" - DEFAULT_STR = '' + # Parse/validate the oam subnet + MIN_OAM_SUBNET_SIZE = 3 + oam_subnet = None + try: + oam_subnet = validate_network_str( + external_oam_subnet_str, + minimum_size=MIN_OAM_SUBNET_SIZE, + existing_networks=subcloud_subnets) + except ValidateFail as e: + LOG.exception(e) + pecan.abort(400, _("external_oam_subnet invalid: %s") % e) - pxe_cidr = payload.get( - 'pxe-subnet', DEFAULT_STR) - management_vlan = payload.get( - 'management-vlan', DEFAULT_STR) - management_interface_mtu = payload.get( - 'management-interface-mtu', DEFAULT_STR) - management_interface_ports = payload.get( - 'management-interface-port', DEFAULT_STR) - cluster_vlan = payload.get( - 'cluster-vlan', DEFAULT_STR) - cluster_interface_mtu = payload.get( - 'cluster-interface-mtu', DEFAULT_STR) - cluster_interface_ports = payload.get( - 'cluster-interface-port', management_interface_ports) - cluster_cidr = payload.get( - 'cluster-subnet', IPNetwork("192.168.206.0/24")) - oam_cidr = payload.get( - 'oam-subnet', DEFAULT_STR) - oam_gateway = payload.get( - 'oam-gateway-ip', DEFAULT_STR) - oam_ip_floating_address = payload.get( - 'oam-floating-ip', DEFAULT_STR) - oam_ip_unit_0_address = payload.get( - 'oam-unit-0-ip', DEFAULT_STR) - oam_ip_unit_1_address = payload.get( - 'oam-unit-1-ip', DEFAULT_STR) - oam_interface_mtu = payload.get( - 'oam-interface-mtu', DEFAULT_STR) - oam_interface_ports = payload.get( - 'oam-interface-port', DEFAULT_STR) - system_mode = payload.get( - 'system-mode', DEFAULT_STR) + # Parse/validate the addresses + try: + validate_address_str( + external_oam_gateway_address_str, oam_subnet) + except ValidateFail as e: + LOG.exception(e) + pecan.abort(400, _("oam_gateway_address invalid: %s") % e) - management_address_pool = self._get_management_address_pool(context) - systemcontroller_subnet = "%s/%d" % ( - management_address_pool.network, - management_address_pool.prefix) - sc_mgmt_floating_ip = management_address_pool.floating_address - - subcloud_config = "" - if system_mode in [SYSTEM_MODE_SIMPLEX, SYSTEM_MODE_DUPLEX, - SYSTEM_MODE_DUPLEX_DIRECT]: - subcloud_config += ( - "[SYSTEM]\n" - "SYSTEM_MODE={}\n".format(system_mode)) - - if system_mode == SYSTEM_MODE_SIMPLEX: - subcloud_oamip_config = ( - "IP_ADDRESS = {oam_ip_floating_address}\n" - ).format( - oam_ip_floating_address=oam_ip_floating_address, - ) - else: - subcloud_oamip_config = ( - "IP_FLOATING_ADDRESS = {oam_ip_floating_address}\n" - "IP_UNIT_0_ADDRESS = {oam_ip_unit_0_address}\n" - "IP_UNIT_1_ADDRESS = {oam_ip_unit_1_address}\n" - ).format( - oam_ip_floating_address=oam_ip_floating_address, - oam_ip_unit_0_address=oam_ip_unit_0_address, - oam_ip_unit_1_address=oam_ip_unit_1_address, - ) - - subcloud_config += ( - "[CLUSTER_NETWORK]\n" - "CIDR = {cluster_cidr}\n" - "DYNAMIC_ALLOCATION = Y\n" - ).format( - cluster_cidr=cluster_cidr - ) - - if cluster_vlan != DEFAULT_STR: - subcloud_config += ( - "VLAN={cluster_vlan}\n" - ).format( - cluster_vlan=cluster_vlan - ) - - if management_interface_ports == cluster_interface_ports: - subcloud_config += ( - "LOGICAL_INTERFACE = LOGICAL_INTERFACE_1\n") - else: - subcloud_config += ( - "LOGICAL_INTERFACE = LOGICAL_INTERFACE_3\n" - "[LOGICAL_INTERFACE_3]\n" - "LAG_INTERFACE = N\n" - "INTERFACE_MTU = {cluster_interface_mtu}\n" - "INTERFACE_PORTS = {cluster_interface_ports}\n" - ).format( - cluster_interface_mtu=cluster_interface_mtu, - cluster_interface_ports=cluster_interface_ports, - ) - - MIN_MANAGEMENT_SUBNET_SIZE = 8 - tmp_management_subnet = validate_network_str( - subcloud.management_subnet, - minimum_size=MIN_MANAGEMENT_SUBNET_SIZE) - - is_ipv6_mgmt = (tmp_management_subnet.version == 6) - - # If ipv6 then we need pxe subnet and management_vlan. - # If user specified pxe boot subnet, then management vlan is required - # and vice versa - if is_ipv6_mgmt or (pxe_cidr != DEFAULT_STR) or \ - (management_vlan != DEFAULT_STR): - subcloud_config += ( - "[REGION2_PXEBOOT_NETWORK]\n" - "PXEBOOT_CIDR = {pxe_cidr}\n" - "[MGMT_NETWORK]\n" - "VLAN = {management_vlan}\n" - ).format( - pxe_cidr=pxe_cidr, - management_vlan=management_vlan, - ) - else: - subcloud_config += "[MGMT_NETWORK]\n" - - subcloud_config += ( - "CIDR = {management_cidr}\n" - "GATEWAY = {management_gateway}\n" - "IP_START_ADDRESS = {management_ip_start_address}\n" - "IP_END_ADDRESS = {management_ip_end_address}\n" - "DYNAMIC_ALLOCATION = Y\n" - "LOGICAL_INTERFACE = LOGICAL_INTERFACE_1\n" - "[LOGICAL_INTERFACE_1]\n" - "LAG_INTERFACE = N\n" - "INTERFACE_MTU = {management_interface_mtu}\n" - "INTERFACE_PORTS = {management_interface_ports}\n" - "[OAM_NETWORK]\n" - "CIDR = {oam_cidr}\n" - "GATEWAY = {oam_gateway}\n" + - subcloud_oamip_config + - "LOGICAL_INTERFACE = LOGICAL_INTERFACE_2\n" - "[LOGICAL_INTERFACE_2]\n" - "LAG_INTERFACE = N\n" - "INTERFACE_MTU = {oam_interface_mtu}\n" - "INTERFACE_PORTS = {oam_interface_ports}\n" - "[SHARED_SERVICES]\n" - "SYSTEM_CONTROLLER_SUBNET = {systemcontroller_subnet}\n" - "SYSTEM_CONTROLLER_FLOATING_ADDRESS = {sc_mgmt_floating_ip}\n" - "REGION_NAME = SystemController\n" - "ADMIN_PROJECT_NAME = admin\n" - "ADMIN_USER_NAME = admin\n" - "ADMIN_PASSWORD = {admin_password}\n" - "KEYSTONE_ADMINURL = {keystone_adminurl}\n" - "KEYSTONE_SERVICE_NAME = keystone\n" - "KEYSTONE_SERVICE_TYPE = identity\n" - "GLANCE_SERVICE_NAME = glance\n" - "GLANCE_SERVICE_TYPE = image\n" - "GLANCE_CACHED = True\n" - "[REGION_2_SERVICES]\n" - "REGION_NAME = {region_2_name}\n" - "[VERSION]\n" - "RELEASE = {release}\n" - ).format( - management_cidr=subcloud.management_subnet, - management_gateway=subcloud.management_gateway_ip, - management_ip_start_address=subcloud.management_start_ip, - management_ip_end_address=subcloud.management_end_ip, - management_interface_mtu=management_interface_mtu, - management_interface_ports=management_interface_ports, - oam_cidr=oam_cidr, - oam_gateway=oam_gateway, - oam_interface_mtu=oam_interface_mtu, - oam_interface_ports=oam_interface_ports, - systemcontroller_subnet=systemcontroller_subnet, - sc_mgmt_floating_ip=sc_mgmt_floating_ip, - admin_password=cfg.CONF.cache.admin_password, - keystone_adminurl=cfg.CONF.cache.auth_uri, - region_2_name=subcloud.name, - release=subcloud.software_version, - ) - return subcloud_config + try: + validate_address_str( + external_oam_floating_address_str, oam_subnet) + except ValidateFail as e: + LOG.exception(e) + pecan.abort(400, _("oam_floating_address invalid: %s") % e) def _get_subcloud_users(self): """Get the subcloud users and passwords from keyring""" @@ -410,7 +261,7 @@ class SubcloudsController(object): return sysinv_client.get_management_address_pool() @index.when(method='GET', template='json') - def get(self, subcloud_ref=None, qualifier=None): + def get(self, subcloud_ref=None): """Get details about subcloud. :param subcloud_ref: ID or name of subcloud @@ -497,81 +348,86 @@ class SubcloudsController(object): subcloud_id = subcloud.id - if qualifier: - # Configuration for this subcloud requested. - # Encrypt before sending. - if qualifier == 'config': - result = dict() - user_list = self._get_subcloud_users() + # Data for this subcloud requested + # Build up and append a dictionary of the endpoints + # sync status to the result. + for subcloud, subcloud_status in db_api. \ + subcloud_get_with_status(context, subcloud_id): + subcloud_dict = db_api.subcloud_db_model_to_dict( + subcloud) + # may be empty subcloud_status entry, account for this + if subcloud_status: + subcloud_status_list.append( + db_api.subcloud_endpoint_status_db_model_to_dict( + subcloud_status)) + endpoint_sync_dict = {consts.ENDPOINT_SYNC_STATUS: + subcloud_status_list} + subcloud_dict.update(endpoint_sync_dict) - # Use a hash of the subcloud name + management subnet - # as the encryption key - hashstring = subcloud.name + subcloud.management_subnet - h = MD5.new() - h.update(hashstring) - encryption_key = h.hexdigest() - user_list_string = json.dumps(user_list) - user_list_encrypted = crypt.urlsafe_encrypt( - encryption_key, - user_list_string) - result['users'] = user_list_encrypted - return result - else: - pecan.abort(400, _('Invalid request')) - else: - # Data for this subcloud requested - # Build up and append a dictionary of the endpoints - # sync status to the result. - for subcloud, subcloud_status in db_api. \ - subcloud_get_with_status(context, subcloud_id): - subcloud_dict = db_api.subcloud_db_model_to_dict( - subcloud) - # may be empty subcloud_status entry, account for this - if subcloud_status: - subcloud_status_list.append( - db_api.subcloud_endpoint_status_db_model_to_dict( - subcloud_status)) - endpoint_sync_dict = {consts.ENDPOINT_SYNC_STATUS: - subcloud_status_list} - subcloud_dict.update(endpoint_sync_dict) - - return subcloud_dict + return subcloud_dict @index.when(method='POST', template='json') - def post(self, subcloud_ref=None, qualifier=None): - """Create a new subcloud. + def post(self, subcloud_ref=None): + """Create and deploy a new subcloud. :param subcloud_ref: ID of or name subcloud (only used when generating config) - :param qualifier: if 'config', returns the config INI file for the - subcloud """ context = restcomm.extract_context_from_environ() if subcloud_ref is None: payload = eval(request.body) + if not payload: pecan.abort(400, _('Body required')) name = payload.get('name') if not name: pecan.abort(400, _('name required')) - management_subnet = payload.get('management-subnet') + system_mode = payload.get('system_mode') + if not system_mode: + pecan.abort(400, _('system_mode required')) + + management_subnet = payload.get('management_subnet') if not management_subnet: - pecan.abort(400, _('management-subnet required')) - management_start_ip = payload.get('management-start-ip') + pecan.abort(400, _('management_subnet required')) + + management_start_ip = payload.get('management_start_address') if not management_start_ip: - pecan.abort(400, _('management-start-ip required')) - management_end_ip = payload.get('management-end-ip') + pecan.abort(400, _('management_start_address required')) + + management_end_ip = payload.get('management_end_address') if not management_end_ip: - pecan.abort(400, _('management-end-ip required')) - management_gateway_ip = payload.get('management-gateway-ip') + pecan.abort(400, _('management_end_address required')) + + management_gateway_ip = payload.get('management_gateway_address') if not management_gateway_ip: - pecan.abort(400, _('management-gateway-ip required')) + pecan.abort(400, _('management_gateway_address required')) + systemcontroller_gateway_ip = \ - payload.get('systemcontroller-gateway-ip') + payload.get('systemcontroller_gateway_address') if not systemcontroller_gateway_ip: - pecan.abort(400, _('systemcontroller-gateway-ip required')) + pecan.abort(400, + _('systemcontroller_gateway_address required')) + + external_oam_subnet = payload.get('external_oam_subnet') + if not external_oam_subnet: + pecan.abort(400, _('external_oam_subnet required')) + + external_oam_gateway_ip = \ + payload.get('external_oam_gateway_address') + if not external_oam_gateway_ip: + pecan.abort(400, _('external_oam_gateway_address required')) + + external_oam_floating_ip = \ + payload.get('external_oam_floating_address') + if not external_oam_floating_ip: + pecan.abort(400, _('external_oam_floating_address required')) + + subcloud_password = \ + payload.get('subcloud_password') + if not subcloud_password: + pecan.abort(400, _('subcloud_password required')) self._validate_subcloud_config(context, name, @@ -579,6 +435,9 @@ class SubcloudsController(object): management_start_ip, management_end_ip, management_gateway_ip, + external_oam_subnet, + external_oam_gateway_ip, + external_oam_floating_ip, systemcontroller_gateway_ip) try: @@ -590,34 +449,6 @@ class SubcloudsController(object): except Exception as e: LOG.exception(e) pecan.abort(500, _('Unable to create subcloud')) - elif qualifier: - if qualifier == 'config': - subcloud = None - - if subcloud_ref.isdigit(): - # Look up subcloud as an ID - try: - subcloud = db_api.subcloud_get(context, subcloud_ref) - except exceptions.SubcloudNotFound: - pecan.abort(404, _('Subcloud not found')) - else: - # Look up subcloud by name - try: - subcloud = db_api.subcloud_get_by_name(context, - subcloud_ref) - except exceptions.SubcloudNameNotFound: - pecan.abort(404, _('Subcloud not found')) - - payload = dict() - if request.body: - payload = eval(request.body) - config_file = self._create_subcloud_config_file( - context, subcloud, payload) - result = dict() - result['config'] = config_file - return result - else: - pecan.abort(400, _('Invalid request')) else: pecan.abort(400, _('Invalid request')) diff --git a/dcmanager/common/consts.py b/dcmanager/common/consts.py index 7e63ef979..a9ee2e4a4 100644 --- a/dcmanager/common/consts.py +++ b/dcmanager/common/consts.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. # -# Copyright (c) 2017 Wind River Systems, Inc. +# Copyright (c) 2017-2019 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 @@ -88,3 +88,9 @@ STRATEGY_STATE_ABORTED = "aborted" STRATEGY_STATE_FAILED = "failed" SW_UPDATE_DEFAULT_TITLE = "all clouds default" + +# Subcloud deploy status states +DEPLOY_STATE_NONE = 'not-deployed' +DEPLOY_STATE_DEPLOYING = 'deploying' +DEPLOY_STATE_DONE = 'complete' +DEPLOY_STATE_FAILED = 'failed' diff --git a/dcmanager/db/api.py b/dcmanager/db/api.py index a50bedde0..9c887b4a5 100644 --- a/dcmanager/db/api.py +++ b/dcmanager/db/api.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. # -# Copyright (c) 2017 Wind River Systems, Inc. +# Copyright (c) 2017-2019 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 @@ -58,6 +58,7 @@ def subcloud_db_model_to_dict(subcloud): "software-version": subcloud.software_version, "management-state": subcloud.management_state, "availability-status": subcloud.availability_status, + "deploy-status": subcloud.deploy_status, "management-subnet": subcloud.management_subnet, "management-start-ip": subcloud.management_start_ip, "management-end-ip": subcloud.management_end_ip, @@ -72,13 +73,13 @@ def subcloud_db_model_to_dict(subcloud): def subcloud_create(context, name, description, location, software_version, management_subnet, management_gateway_ip, management_start_ip, management_end_ip, - systemcontroller_gateway_ip): + systemcontroller_gateway_ip, deploy_status): """Create a subcloud.""" return IMPL.subcloud_create(context, name, description, location, software_version, management_subnet, management_gateway_ip, management_start_ip, management_end_ip, - systemcontroller_gateway_ip) + systemcontroller_gateway_ip, deploy_status) def subcloud_get(context, subcloud_id): @@ -108,11 +109,13 @@ def subcloud_get_all_with_status(context): def subcloud_update(context, subcloud_id, management_state=None, availability_status=None, software_version=None, - description=None, location=None, audit_fail_count=None): + description=None, location=None, audit_fail_count=None, + deploy_status=None): """Update a subcloud or raise if it does not exist.""" return IMPL.subcloud_update(context, subcloud_id, management_state, availability_status, software_version, - description, location, audit_fail_count) + description, location, audit_fail_count, + deploy_status) def subcloud_destroy(context, subcloud_id): diff --git a/dcmanager/db/sqlalchemy/api.py b/dcmanager/db/sqlalchemy/api.py index 245e3b985..36c54d9b6 100644 --- a/dcmanager/db/sqlalchemy/api.py +++ b/dcmanager/db/sqlalchemy/api.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. # -# Copyright (c) 2017 Wind River Systems, Inc. +# Copyright (c) 2017-2019 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 @@ -205,7 +205,7 @@ def subcloud_get_all_with_status(context): def subcloud_create(context, name, description, location, software_version, management_subnet, management_gateway_ip, management_start_ip, management_end_ip, - systemcontroller_gateway_ip): + systemcontroller_gateway_ip, deploy_status): with write_session() as session: subcloud_ref = models.Subcloud() subcloud_ref.name = name @@ -219,6 +219,7 @@ def subcloud_create(context, name, description, location, software_version, subcloud_ref.management_start_ip = management_start_ip subcloud_ref.management_end_ip = management_end_ip subcloud_ref.systemcontroller_gateway_ip = systemcontroller_gateway_ip + subcloud_ref.deploy_status = deploy_status subcloud_ref.audit_fail_count = 0 session.add(subcloud_ref) return subcloud_ref @@ -227,7 +228,8 @@ def subcloud_create(context, name, description, location, software_version, @require_admin_context def subcloud_update(context, subcloud_id, management_state=None, availability_status=None, software_version=None, - description=None, location=None, audit_fail_count=None): + description=None, location=None, audit_fail_count=None, + deploy_status=None): with write_session() as session: subcloud_ref = subcloud_get(context, subcloud_id) if management_state is not None: @@ -242,6 +244,8 @@ def subcloud_update(context, subcloud_id, management_state=None, subcloud_ref.location = location if audit_fail_count is not None: subcloud_ref.audit_fail_count = audit_fail_count + if deploy_status is not None: + subcloud_ref.deploy_status = deploy_status subcloud_ref.save(session) return subcloud_ref diff --git a/dcmanager/db/sqlalchemy/migrate_repo/versions/003_add_deploy_status_column.py b/dcmanager/db/sqlalchemy/migrate_repo/versions/003_add_deploy_status_column.py new file mode 100644 index 000000000..98b8acd9b --- /dev/null +++ b/dcmanager/db/sqlalchemy/migrate_repo/versions/003_add_deploy_status_column.py @@ -0,0 +1,36 @@ +# 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) 2019 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. +# + +from sqlalchemy import Column, MetaData, String, Table + + +def upgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + subclouds = Table('subclouds', meta, autoload=True) + + # Add the 'deploy_status' column to the subclouds table. + subclouds.create_column(Column('deploy_status', String(255))) + + return True + + +def downgrade(migrate_engine): + raise NotImplementedError('Database downgrade is unsupported.') diff --git a/dcmanager/db/sqlalchemy/models.py b/dcmanager/db/sqlalchemy/models.py index 2418a7447..5b3538bd2 100644 --- a/dcmanager/db/sqlalchemy/models.py +++ b/dcmanager/db/sqlalchemy/models.py @@ -87,6 +87,7 @@ class Subcloud(BASE, DCManagerBase): software_version = Column(String(255)) management_state = Column(String(255)) availability_status = Column(String(255)) + deploy_status = Column(String(255)) management_subnet = Column(String(255)) management_gateway_ip = Column(String(255)) management_start_ip = Column(String(255), unique=True) diff --git a/dcmanager/manager/subcloud_manager.py b/dcmanager/manager/subcloud_manager.py index 7b5217655..e4d36ca39 100644 --- a/dcmanager/manager/subcloud_manager.py +++ b/dcmanager/manager/subcloud_manager.py @@ -13,16 +13,20 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Copyright (c) 2017-2018 Wind River Systems, Inc. +# Copyright (c) 2017-2019 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 datetime import filecmp +import keyring import netaddr import os +import subprocess +import threading from oslo_log import log as logging from oslo_messaging import RemoteError @@ -51,6 +55,24 @@ LOG = logging.getLogger(__name__) # to read. This file is referenced in dnsmasq.conf ADDN_HOSTS_DC = 'dnsmasq.addn_hosts_dc' +# Subcloud configuration paths +ANSIBLE_OVERRIDES_PATH = '/opt/dc/ansible' +ANSIBLE_SUBCLOUD_INVENTORY_FILE = 'subclouds.yml' +ANSIBLE_SUBCLOUD_PLAYBOOK = \ + '/usr/share/ansible/stx-ansible/playbooks/bootstrap/bootstrap.yml' + +DC_LOG_DIR = '/var/log/dcmanager/' + +USERS_TO_REPLICATE = [ + 'sysinv', + 'patching', + 'vim', + 'mtce', + 'fm', + 'barbican'] + +SERVICES_USER = 'services' + class SubcloudManager(manager.Manager): """Manages tasks related to subclouds.""" @@ -92,11 +114,12 @@ class SubcloudManager(manager.Manager): payload.get('description'), payload.get('location'), software_version, - payload['management-subnet'], - payload['management-gateway-ip'], - payload['management-start-ip'], - payload['management-end-ip'], - payload['systemcontroller-gateway-ip']) + payload['management_subnet'], + payload['management_gateway_address'], + payload['management_start_address'], + payload['management_end_address'], + payload['systemcontroller_gateway_address'], + consts.DEPLOY_STATE_NONE) except Exception as e: LOG.exception(e) raise e @@ -111,7 +134,7 @@ class SubcloudManager(manager.Manager): # Create a new route to this subcloud on the management interface # on both controllers. m_ks_client = KeystoneClient() - subcloud_subnet = netaddr.IPNetwork(payload['management-subnet']) + subcloud_subnet = netaddr.IPNetwork(payload['management_subnet']) session = m_ks_client.endpoint_cache.get_session_from_token( context.auth_token, context.project) sysinv_client = SysinvClient(consts.DEFAULT_REGION_NAME, session) @@ -124,7 +147,7 @@ class SubcloudManager(manager.Manager): management_interface.uuid, str(subcloud_subnet.ip), subcloud_subnet.prefixlen, - payload['systemcontroller-gateway-ip'], + payload['systemcontroller_gateway_address'], 1) # Create identity endpoints to this subcloud on the @@ -145,7 +168,7 @@ class SubcloudManager(manager.Manager): resource='subcloud', msg='No Identity service found on SystemController') - identity_endpoint_ip = payload['management-start-ip'] + identity_endpoint_ip = payload['management_start_address'] if netaddr.IPAddress(identity_endpoint_ip).version == 6: identity_endpoint_url = \ @@ -168,6 +191,51 @@ class SubcloudManager(manager.Manager): # Regenerate the addn_hosts_dc file self._create_addn_hosts_dc(context) + # Add the admin and service user passwords to the payload so they + # get copied to the override file + payload['ansible_become_pass'] = payload['subcloud_password'] + payload['ansible_ssh_pass'] = payload['subcloud_password'] + payload['admin_password'] = keyring.get_password('CGCS', 'admin') + del payload['subcloud_password'] + + payload['users'] = dict() + for user in USERS_TO_REPLICATE: + payload['users'][user] = \ + keyring.get_password(user, SERVICES_USER) + + # Update the ansible inventory with the new subcloud + self._update_subcloud_inventory(payload) + + # Write this subclouds overrides to file + self._write_subcloud_ansible_config(context, payload) + + # Update the subcloud to deploying + try: + db_api.subcloud_update( + context, subcloud.id, + deploy_status=consts.DEPLOY_STATE_DEPLOYING) + except Exception as e: + LOG.exception(e) + raise e + + apply_command = [ + "ansible-playbook", ANSIBLE_SUBCLOUD_PLAYBOOK, "-i", + ANSIBLE_OVERRIDES_PATH + '/' + + ANSIBLE_SUBCLOUD_INVENTORY_FILE, + "--limit", subcloud.name + ] + + # Add the overrides dir and region_name so the playbook knows + # which overrides to load + apply_command += [ + "-e", str("override_files_dir='%s' region_name=%s") % ( + ANSIBLE_OVERRIDES_PATH, subcloud.name)] + + apply_thread = threading.Thread( + target=self.run_bootstrap, + args=(apply_command, subcloud, context)) + apply_thread.start() + return db_api.subcloud_db_model_to_dict(subcloud) except Exception as e: @@ -178,6 +246,34 @@ class SubcloudManager(manager.Manager): db_api.subcloud_destroy(context, subcloud.id) raise e + @staticmethod + def run_bootstrap(apply_command, subcloud, context): + # Run the ansible boostrap-subcloud playbook + with open(DC_LOG_DIR + subcloud.name + '_' + + str(datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')) + + '.log', "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, + DC_LOG_DIR + subcloud.name) + ex.cmd = 'ansible-playbook' + LOG.error(msg) + db_api.subcloud_update( + context, subcloud.id, + deploy_status=consts.DEPLOY_STATE_FAILED) + return + LOG.info("Successfully deployed subcloud %s" % + subcloud.name) + db_api.subcloud_update( + context, subcloud.id, + deploy_status=consts.DEPLOY_STATE_DONE) + def _create_addn_hosts_dc(self, context): """Generate the addn_hosts_dc file for hostname/ip translation""" @@ -201,6 +297,58 @@ class SubcloudManager(manager.Manager): # restart dnsmasq so it can re-read our addn_hosts file. os.system("pkill -HUP dnsmasq") + def _update_subcloud_inventory(self, new_subcloud): + """Update the inventory file for usage with the specified subcloud""" + + inventory_file = os.path.join(ANSIBLE_OVERRIDES_PATH, + ANSIBLE_SUBCLOUD_INVENTORY_FILE) + exists = False + if os.path.isfile(inventory_file): + exists = True + + with open(inventory_file, 'a') as f_out_inventory: + if not exists: + f_out_inventory.write( + '---\n' + 'all:\n' + ' vars:\n' + ' ansible_ssh_user: sysadmin\n' + ' hosts:\n', + ) + + f_out_inventory.write( + ' ' + new_subcloud['name'] + ':\n' + ' ansible_host: ' + + new_subcloud['bootstrap-address'] + '\n' + ) + + 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, + payload['name'] + '.yml') + + m_ks_client = KeystoneClient() + session = m_ks_client.endpoint_cache.get_session_from_token( + context.auth_token, context.project) + sysinv_client = SysinvClient(consts.DEFAULT_REGION_NAME, session) + + pool = sysinv_client.get_management_address_pool() + floating_ip = pool.floating_address + subnet = "%s/%d" % (pool.network, pool.prefix) + + with open(overrides_file, 'w') as f_out_overrides_file: + f_out_overrides_file.write( + '---' + '\nregion_config: yes' + '\ndistributed_cloud_role: subcloud' + '\nsystem_controller_subnet: ' + subnet + + '\nsystem_controller_floating_address: ' + floating_ip + '\n' + ) + + for k, v in payload.items(): + f_out_overrides_file.write("%s: %s\n" % (k, v)) + def _delete_subcloud_routes(self, context, subcloud): """Delete the routes to this subcloud""" diff --git a/dcmanager/tests/data/ipv6_R5_install/dcmanager/subclouds.json b/dcmanager/tests/data/ipv6_R5_install/dcmanager/subclouds.json index efed1acff..bd4a55bff 100644 --- a/dcmanager/tests/data/ipv6_R5_install/dcmanager/subclouds.json +++ b/dcmanager/tests/data/ipv6_R5_install/dcmanager/subclouds.json @@ -1,5 +1,5 @@ { - "subclouds_0": [6, "subcloud-4", "wcp85 subcloud", "Ottawa-PheonixLab-Aisle_3-Rack_C", "18.03", "managed", "online", "fd01:3::0/64", "fd01:3::1", "fd01:3::2", "fd01:3::f", "fd01:1::1", 0, "NULL", "NULL", "2018-05-15 14:45:12.508708", "2018-05-24 10:48:18.090931", "NULL", 0], - "subclouds_1": [1, "subcloud-1", "wcp80 subcloud", "Ottawa-PheonixLab-Aisle_3-Rack_B", "18.03", "managed", "online", "fd01:2::0/64", "fd01:2::1", "fd01:2::2", "fd01:2::f", "fd01:1::1", 0, "NULL", "NULL", "2018-04-11 17:01:48.54467", "2018-05-24 00:17:34.74161", "NULL", 0], - "subclouds_2": [7, "subcloud-5", "wcp87 subcloud", "Ottawa-PheonixLab-Aisle_4-Rack_B", "18.03", "managed", "online", "fd01:4::0/64", "fd01:4::1", "fd01:4::2", "fd01:4::f", "fd01:1::1", 0, "NULL", "NULL", "2018-05-15 14:45:48.95625", "2018-05-24 10:48:17.907767", "NULL", 0] + "subclouds_0": [6, "subcloud-4", "wcp85 subcloud", "Ottawa-PheonixLab-Aisle_3-Rack_C", "18.03", "managed", "online", "fd01:3::0/64", "fd01:3::1", "fd01:3::2", "fd01:3::f", "fd01:1::1", 0, "NULL", "NULL", "2018-05-15 14:45:12.508708", "2018-05-24 10:48:18.090931", "NULL", 0, "10.10.10.0/24", "10.10.10.1", "10.10.10.12", "testpass"], + "subclouds_1": [1, "subcloud-1", "wcp80 subcloud", "Ottawa-PheonixLab-Aisle_3-Rack_B", "18.03", "managed", "online", "fd01:2::0/64", "fd01:2::1", "fd01:2::2", "fd01:2::f", "fd01:1::1", 0, "NULL", "NULL", "2018-04-11 17:01:48.54467", "2018-05-24 00:17:34.74161", "NULL", 0, "10.10.10.0/24", "10.10.10.1", "10.10.10.12", "testpass"], + "subclouds_2": [7, "subcloud-5", "wcp87 subcloud", "Ottawa-PheonixLab-Aisle_4-Rack_B", "18.03", "managed", "online", "fd01:4::0/64", "fd01:4::1", "fd01:4::2", "fd01:4::f", "fd01:1::1", 0, "NULL", "NULL", "2018-05-15 14:45:48.95625", "2018-05-24 10:48:17.907767", "NULL", 0, "10.10.10.0/24", "10.10.10.1", "10.10.10.12", "testpass"] } diff --git a/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py b/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py index 321c1ecc3..3deb0d44d 100644 --- a/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py +++ b/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py @@ -41,12 +41,17 @@ FAKE_HEADERS = {'X-Tenant-Id': FAKE_TENANT, 'X_ROLE': 'admin', FAKE_SUBCLOUD_DATA = {"name": "subcloud1", "description": "subcloud1 description", "location": "subcloud1 location", - "management-subnet": "192.168.101.0/24", - "management-start-ip": "192.168.101.2", - "management-end-ip": "192.168.101.50", - "management-gateway-ip": "192.168.101.1", - "systemcontroller-gateway-ip": "192.168.204.101", - "availability-status": "disabled"} + "system_mode": "duplex", + "management_subnet": "192.168.101.0/24", + "management_start_address": "192.168.101.2", + "management_end_address": "192.168.101.50", + "management_gateway_address": "192.168.101.1", + "systemcontroller_gateway_address": "192.168.204.101", + "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", + "subcloud_password": "testpass"} class FakeAddressPool(object): @@ -92,7 +97,7 @@ class TestSubclouds(testroot.DCManagerApiTest): def test_post_subcloud_bad_gateway(self, mock_db_api, mock_rpc_client, mock_get_management_address_pool): data = copy.copy(FAKE_SUBCLOUD_DATA) - data["systemcontroller-gateway-ip"] = "192.168.205.101" + data["systemcontroller_gateway_address"] = "192.168.205.101" management_address_pool = FakeAddressPool('192.168.204.0', 24, '192.168.204.2', '192.168.204.100') @@ -120,7 +125,7 @@ class TestSubclouds(testroot.DCManagerApiTest): @mock.patch.object(subclouds, 'db_api') def test_post_subcloud_bad_subnet(self, mock_db_api, mock_rpc_client): data = copy.copy(FAKE_SUBCLOUD_DATA) - data["management-subnet"] = "192.168.101.0/32" + data["management_subnet"] = "192.168.101.0/32" six.assertRaisesRegex(self, webtest.app.AppError, "400 *", self.app.post_json, FAKE_URL, headers=FAKE_HEADERS, params=data) @@ -129,9 +134,9 @@ class TestSubclouds(testroot.DCManagerApiTest): @mock.patch.object(subclouds, 'db_api') def test_post_subcloud_bad_start_ip(self, mock_db_api, mock_rpc_client): data = copy.copy(FAKE_SUBCLOUD_DATA) - data["management-subnet"] = "192.168.101.0/24" - data["management-start-ip"] = "192.168.100.2" - data["management-end-ip"] = "192.168.100.50" + data["management_subnet"] = "192.168.101.0/24" + data["management_start_address"] = "192.168.100.2" + data["management_end_address"] = "192.168.100.50" six.assertRaisesRegex(self, webtest.app.AppError, "400 *", self.app.post_json, FAKE_URL, headers=FAKE_HEADERS, params=data) @@ -140,8 +145,8 @@ class TestSubclouds(testroot.DCManagerApiTest): @mock.patch.object(subclouds, 'db_api') def test_post_subcloud_bad_end_ip(self, mock_db_api, mock_rpc_client): data = copy.copy(FAKE_SUBCLOUD_DATA) - data["management-start-ip"] = "192.168.101.2" - data["management-end-ip"] = "192.168.100.100" + data["management_start_address"] = "192.168.101.2" + data["management_end_address"] = "192.168.100.100" six.assertRaisesRegex(self, webtest.app.AppError, "400 *", self.app.post_json, FAKE_URL, headers=FAKE_HEADERS, params=data) @@ -150,8 +155,8 @@ class TestSubclouds(testroot.DCManagerApiTest): @mock.patch.object(subclouds, 'db_api') def test_post_subcloud_short_ip_range(self, mock_db_api, mock_rpc_client): data = copy.copy(FAKE_SUBCLOUD_DATA) - data["management-start-ip"] = "192.168.101.2" - data["management-end-ip"] = "192.168.101.4" + data["management_start_address"] = "192.168.101.2" + data["management_end_address"] = "192.168.101.4" six.assertRaisesRegex(self, webtest.app.AppError, "400 *", self.app.post_json, FAKE_URL, headers=FAKE_HEADERS, params=data) @@ -160,8 +165,8 @@ class TestSubclouds(testroot.DCManagerApiTest): @mock.patch.object(subclouds, 'db_api') def test_post_subcloud_invert_ip_range(self, mock_db_api, mock_rpc_client): data = copy.copy(FAKE_SUBCLOUD_DATA) - data["management-start-ip"] = "192.168.101.20" - data["management-end-ip"] = "192.168.101.4" + data["management_start_address"] = "192.168.101.20" + data["management_end_address"] = "192.168.101.4" six.assertRaisesRegex(self, webtest.app.AppError, "400 *", self.app.post_json, FAKE_URL, headers=FAKE_HEADERS, params=data) @@ -188,18 +193,6 @@ class TestSubclouds(testroot.DCManagerApiTest): self.app.post_json, FAKE_URL, headers=FAKE_HEADERS, params=data) - @mock.patch.object(subclouds.SubcloudsController, - '_create_subcloud_config_file') - @mock.patch.object(rpc_client, 'ManagerClient') - @mock.patch.object(subclouds, 'db_api') - def test_post_subcloud_config(self, mock_db_api, mock_rpc_client, - mock_create_config): - mock_create_config.return_value = "Some\n long multiline config data" - post_url = FAKE_URL + '/' + FAKE_ID + '/config' - self.app.post(post_url, headers=FAKE_HEADERS) - self.assertEqual(1, mock_db_api.subcloud_get.call_count) - self.assertEqual(1, mock_create_config.call_count) - @mock.patch.object(rpc_client, 'ManagerClient') @mock.patch.object(subclouds, 'db_api') def test_delete_subcloud(self, mock_db_api, mock_rpc_client): diff --git a/dcmanager/tests/unit/db/test_subcloud_db_api.py b/dcmanager/tests/unit/db/test_subcloud_db_api.py index c3a9d8674..9a6e6ebc4 100644 --- a/dcmanager/tests/unit/db/test_subcloud_db_api.py +++ b/dcmanager/tests/unit/db/test_subcloud_db_api.py @@ -85,6 +85,7 @@ class DBAPISubcloudTest(base.DCManagerTestCase): 'management_start_ip': "192.168.101.2", 'management_end_ip': "192.168.101.50", 'systemcontroller_gateway_ip': "192.168.204.101", + 'deploy_status': "not-deployed", } values.update(kwargs) return db_api.subcloud_create(ctxt, **values) @@ -96,11 +97,13 @@ class DBAPISubcloudTest(base.DCManagerTestCase): 'description': data['description'], 'location': data['location'], 'software_version': data['software-version'], - 'management_subnet': data['management-subnet'], - 'management_gateway_ip': data['management-gateway-ip'], - 'management_start_ip': data['management-start-ip'], - 'management_end_ip': data['management-end-ip'], - 'systemcontroller_gateway_ip': data['systemcontroller-gateway-ip'], + 'management_subnet': data['management_subnet'], + 'management_gateway_ip': data['management_gateway_address'], + 'management_start_ip': data['management_start_address'], + 'management_end_ip': data['management_end_address'], + 'systemcontroller_gateway_ip': data[ + 'systemcontroller_gateway_address'], + 'deploy_status': "not-deployed", } return db_api.subcloud_create(ctxt, **values) diff --git a/dcmanager/tests/unit/manager/test_subcloud_manager.py b/dcmanager/tests/unit/manager/test_subcloud_manager.py index 8806174a3..cb91af06f 100644 --- a/dcmanager/tests/unit/manager/test_subcloud_manager.py +++ b/dcmanager/tests/unit/manager/test_subcloud_manager.py @@ -41,11 +41,15 @@ FAKE_ID = '1' FAKE_SUBCLOUD_DATA = {"name": "subcloud1", "description": "subcloud1 description", "location": "subcloud1 location", - "management-subnet": "192.168.101.0/24", - "management-start-ip": "192.168.101.3", - "management-end-ip": "192.168.101.4", - "management-gateway-ip": "192.168.101.1", - "systemcontroller-gateway-ip": "192.168.204.101"} + "system_mode": "duplex", + "management_subnet": "192.168.101.0/24", + "management_start_address": "192.168.101.3", + "management_end_address": "192.168.101.4", + "management_gateway_address": "192.168.101.1", + "systemcontroller_gateway_address": "192.168.204.101", + "external_oam_subnet": "10.10.10.0/24", + "external_oam_gateway_address": "10.10.10.1", + "external_oam_floating_address": "10.10.10.12"} class Controller(object): @@ -72,11 +76,17 @@ class Subcloud(object): else: self.availability_status = consts.AVAILABILITY_OFFLINE - self.management_subnet = data['management-subnet'] - self.management_gateway_ip = data['management-gateway-ip'] - self.management_start_ip = data['management-start-ip'] - self.management_end_ip = data['management-end-ip'] - self.systemcontroller_gateway_ip = data['systemcontroller-gateway-ip'] + 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() @@ -106,7 +116,15 @@ class TestSubcloudManager(base.DCManagerTestCase): @mock.patch.object(subcloud_manager, 'SysinvClient') @mock.patch.object(subcloud_manager.SubcloudManager, '_create_addn_hosts_dc') - def test_add_subcloud(self, value, + @mock.patch.object(subcloud_manager.SubcloudManager, + '_update_subcloud_inventory') + @mock.patch.object(subcloud_manager.SubcloudManager, + '_write_subcloud_ansible_config') + @mock.patch.object(subcloud_manager, + 'keyring') + def test_add_subcloud(self, value, mock_keyring, + mock_write_subcloud_ansible_config, + mock_update_subcloud_inventory, mock_create_addn_hosts, mock_sysinv_client, mock_db_api, mock_keystone_client, mock_context, mock_dcorch_rpc_client): @@ -119,6 +137,7 @@ class TestSubcloudManager(base.DCManagerTestCase): mock_sysinv_client().get_controller_hosts.return_value = controllers mock_keystone_client().services_list = services + mock_keyring.get_password.return_value = "testpassword" sm = subcloud_manager.SubcloudManager() sm.add_subcloud(self.ctxt, payload=value) @@ -127,6 +146,10 @@ class TestSubcloudManager(base.DCManagerTestCase): mock_sysinv_client().create_route.assert_called() mock_dcorch_rpc_client().add_subcloud.assert_called_once() mock_create_addn_hosts.assert_called_once() + mock_update_subcloud_inventory.assert_called_once() + mock_write_subcloud_ansible_config.assert_called_once() + mock_db_api.subcloud_update.assert_called() + mock_keyring.get_password.assert_called() @file_data(utils.get_data_filepath('dcmanager', 'subclouds')) @mock.patch.object(dcorch_rpc_client, 'EngineClient') diff --git a/dcmanager/tests/utils.py b/dcmanager/tests/utils.py index 273eca52a..df3734e35 100644 --- a/dcmanager/tests/utils.py +++ b/dcmanager/tests/utils.py @@ -116,18 +116,22 @@ def create_subcloud_dict(data_list): 'software-version': data_list[4], 'management-state': data_list[5], 'availability-status': data_list[6], - 'management-subnet': data_list[7], - 'management-gateway-ip': data_list[8], - 'management-start-ip': data_list[9], - 'management-end-ip': data_list[10], - 'systemcontroller-gateway-ip': data_list[11], + 'management_subnet': data_list[7], + 'management_gateway_address': data_list[8], + 'management_start_address': data_list[9], + 'management_end_address': data_list[10], + 'systemcontroller_gateway_address': data_list[11], 'audit-fail-count': data_list[12], 'reserved-1': data_list[13], 'reserved-2': data_list[14], 'created-at': data_list[15], 'updated-at': data_list[16], 'deleted-at': data_list[17], - 'deleted': data_list[18]} + 'deleted': data_list[18], + 'external_oam_subnet': data_list[19], + 'external_oam_gateway_address': data_list[20], + 'external_oam_floating_address': data_list[21], + 'subcloud_password': data_list[22]} def create_route_dict(data_list):