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 <tyler.smith@windriver.com>
This commit is contained in:
Tyler Smith 2019-07-11 10:56:45 -04:00
parent a3f595fe18
commit 8c2bd5fa14
12 changed files with 391 additions and 339 deletions

View File

@ -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 = '<EDIT>'
# 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'))

View File

@ -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'

View File

@ -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):

View File

@ -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

View File

@ -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.')

View File

@ -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)

View File

@ -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"""

View File

@ -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"]
}

View File

@ -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):

View File

@ -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)

View File

@ -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')

View File

@ -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):