distcloud/dcmanager/api/controllers/v1/subclouds.py
Tao Liu 38f08a0d98 Decouple Fault Management from stx-config
The fault management (FM) APIs has been removed from the syinv API
service and a new FM API service has been introduced. This update adds
a new fm openstack driver for retrieving each region's alarm summary,
and it also modifies the alarm aggregate manager to use the fm driver.
In addition, it removes get alarm summary routine from sysinv and
adds the  fm user to the subcloud user list

Story: 2002828
Task: 22747

Signed-off-by: Tao Liu <tao.liu@windriver.com>
2018-08-23 14:59:38 -04:00

690 lines
28 KiB
Python

# Copyright (c) 2017 Ericsson AB.
# 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) 2017-2018 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 keyring
from netaddr import IPAddress
from netaddr import IPNetwork
from netaddr import IPRange
from oslo_config import cfg
from oslo_log import log as logging
from oslo_messaging import RemoteError
import pecan
from pecan import expose
from pecan import request
from configutilities.common import crypt
from configutilities.common.exceptions import ValidateFail
from configutilities.common.utils import validate_address_str
from configutilities.common.utils import validate_network_str
from dcorch.drivers.openstack.keystone_v3 import KeystoneClient
from dcmanager.api.controllers import restcomm
from dcmanager.common import consts
from dcmanager.common import exceptions
from dcmanager.common.i18n import _
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__)
# System mode
SYSTEM_MODE_DUPLEX = "duplex"
SYSTEM_MODE_SIMPLEX = "simplex"
SYSTEM_MODE_DUPLEX_DIRECT = "duplex-direct"
class SubcloudsController(object):
VERSION_ALIASES = {
'Newton': '1.0',
}
def __init__(self):
super(SubcloudsController, self).__init__()
self.rpc_client = rpc_client.ManagerClient()
# to do the version compatibility for future purpose
def _determine_version_cap(self, target):
version_cap = 1.0
return version_cap
@expose(generic=True, template='json')
def index(self):
# Route the request to specific methods with parameters
pass
def _validate_subcloud_config(self,
context,
name,
management_subnet_str,
management_start_ip_str,
management_end_ip_str,
management_gateway_ip_str,
systemcontroller_gateway_ip_str):
"""Check whether subcloud config is valid."""
# Validate the name
if name.isdigit():
pecan.abort(400, _("name must contain alphabetic characters"))
if name in [consts.DEFAULT_REGION_NAME,
consts.SYSTEM_CONTROLLER_NAME]:
pecan.abort(400, _("name cannot be %(bad_name1)s or %(bad_name2)s")
% {'bad_name1': consts.DEFAULT_REGION_NAME,
'bad_name2': consts.SYSTEM_CONTROLLER_NAME})
# Parse/validate the management subnet
subcloud_subnets = []
subclouds = db_api.subcloud_get_all(context)
for subcloud in subclouds:
subcloud_subnets.append(IPNetwork(subcloud.management_subnet))
MIN_MANAGEMENT_SUBNET_SIZE = 8
# subtract 3 for network, gateway and broadcast addresses.
MIN_MANAGEMENT_ADDRESSES = MIN_MANAGEMENT_SUBNET_SIZE - 3
management_subnet = None
try:
management_subnet = validate_network_str(
management_subnet_str,
minimum_size=MIN_MANAGEMENT_SUBNET_SIZE,
existing_networks=subcloud_subnets)
except ValidateFail as e:
LOG.exception(e)
pecan.abort(400, _("management-subnet invalid: %s") % e)
# Parse/validate the start/end addresses
management_start_ip = None
try:
management_start_ip = validate_address_str(
management_start_ip_str, management_subnet)
except ValidateFail as e:
LOG.exception(e)
pecan.abort(400, _("management-start-ip invalid: %s") % e)
management_end_ip = None
try:
management_end_ip = validate_address_str(
management_end_ip_str, management_subnet)
except ValidateFail as e:
LOG.exception(e)
pecan.abort(400, _("management-end-ip invalid: %s") % e)
if not management_start_ip < management_end_ip:
pecan.abort(
400,
_("management-start-ip not less than management-end-ip"))
if not len(IPRange(management_start_ip, management_end_ip)) >= \
MIN_MANAGEMENT_ADDRESSES:
pecan.abort(
400,
_("management address range must contain at least %d "
"addresses") % MIN_MANAGEMENT_ADDRESSES)
# Parse/validate the gateway
try:
validate_address_str(
management_gateway_ip_str, management_subnet)
except ValidateFail as e:
LOG.exception(e)
pecan.abort(400, _("management-gateway-ip invalid: %s") % e)
# Ensure subcloud management gateway is not within the actual subcloud
# management subnet address pool for consistency with the
# systemcontroller gateway restriction below. Address collision
# is not a concern as the address is added to sysinv.
subcloud_mgmt_address_start = IPAddress(management_start_ip_str)
subcloud_mgmt_address_end = IPAddress(management_end_ip_str)
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, "
"is within management pool: %(start)s - "
"%(end)s") %
{'start': subcloud_mgmt_address_start,
'end': subcloud_mgmt_address_end})
# Ensure systemcontroller gateway is in the management subnet
# for the systemcontroller region.
management_address_pool = self._get_management_address_pool(context)
systemcontroller_subnet_str = "%s/%d" % (
management_address_pool.network,
management_address_pool.prefix)
systemcontroller_subnet = IPNetwork(systemcontroller_subnet_str)
try:
validate_address_str(
systemcontroller_gateway_ip_str, systemcontroller_subnet)
except ValidateFail as e:
LOG.exception(e)
pecan.abort(400, _("systemcontroller-gateway-ip 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])
mgmt_address_end = IPAddress(management_address_pool.ranges[0][1])
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, "
"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>'
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)
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)
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,
)
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
def _get_subcloud_users(self):
"""Get the subcloud users and passwords from keyring"""
DEFAULT_SERVICE_PROJECT_NAME = 'services'
# First entry is openstack user name, second entry is the user stored
# in keyring. Not sure why heat_admin uses a different keystone name.
SUBCLOUD_USERS = [
('nova', 'nova'),
('placement', 'placement'),
('sysinv', 'sysinv'),
('patching', 'patching'),
('heat', 'heat'),
('ceilometer', 'ceilometer'),
('vim', 'vim'),
('aodh', 'aodh'),
('panko', 'panko'),
('mtce', 'mtce'),
('cinder', 'cinder'),
('glance', 'glance'),
('neutron', 'neutron'),
('heat_admin', 'heat-domain'),
('gnocchi', 'gnocchi'),
('fm', 'fm')
]
user_list = list()
for user in SUBCLOUD_USERS:
password = keyring.get_password(user[1],
DEFAULT_SERVICE_PROJECT_NAME)
if password:
user_dict = dict()
user_dict['name'] = user[0]
user_dict['password'] = password
user_list.append(user_dict)
else:
LOG.error("User %s not found in keyring as %s" % (user[0],
user[1]))
pecan.abort(500, _('System configuration error'))
return user_list
def _get_management_address_pool(self, context):
"""Get the system controller's management address pool"""
session = KeystoneClient().endpoint_cache.get_session_from_token(
context.auth_token, context.project)
sysinv_client = SysinvClient(consts.DEFAULT_REGION_NAME, session)
return sysinv_client.get_management_address_pool()
@index.when(method='GET', template='json')
def get(self, subcloud_ref=None, qualifier=None):
"""Get details about subcloud.
:param subcloud_ref: ID or name of subcloud
"""
context = restcomm.extract_context_from_environ()
if subcloud_ref is None:
# List of subclouds requested
subclouds = db_api.subcloud_get_all_with_status(context)
result = dict()
result['subclouds'] = []
first_time = True
subcloud_list = []
subcloud_status_list = []
# We get back a subcloud, subcloud_status pair for every
# subcloud_status entry corresponding to a subcloud. (Subcloud
# info repeats)
# Aggregate all the sync status for each of the
# endpoints per subcloud into an overall sync status
for subcloud, subcloud_status in subclouds:
subcloud_dict = db_api.subcloud_db_model_to_dict(subcloud)
subcloud_status_dict = db_api.subcloud_status_db_model_to_dict(
subcloud_status)
subcloud_dict.update(subcloud_status_dict)
if not first_time:
if subcloud_list[-1]['id'] == subcloud_dict['id']:
# We have a match for this subcloud id already,
# check if we have a same sync_status
if subcloud_list[-1][consts.SYNC_STATUS] != \
subcloud_dict[consts.SYNC_STATUS]:
subcloud_list[-1][consts.SYNC_STATUS] = \
consts.SYNC_STATUS_OUT_OF_SYNC
if subcloud_status:
subcloud_status_list.append(
db_api.subcloud_endpoint_status_db_model_to_dict( # noqa
subcloud_status))
subcloud_list[-1][
consts.ENDPOINT_SYNC_STATUS] = subcloud_status_list
else:
subcloud_status_list = []
if subcloud_status:
subcloud_status_list.append(
db_api.subcloud_endpoint_status_db_model_to_dict( # noqa
subcloud_status))
subcloud_list.append(subcloud_dict)
else:
if subcloud_status:
subcloud_status_list.append(
db_api.subcloud_endpoint_status_db_model_to_dict(
subcloud_status))
subcloud_list.append(subcloud_dict)
first_time = False
for s in subcloud_list:
result['subclouds'].append(s)
return result
else:
# Single subcloud requested
subcloud = None
subcloud_dict = dict()
subcloud_status_list = []
endpoint_sync_dict = dict()
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'))
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()
# 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
@index.when(method='POST', template='json')
def post(self, subcloud_ref=None, qualifier=None):
"""Create 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')
if not management_subnet:
pecan.abort(400, _('management-subnet required'))
management_start_ip = payload.get('management-start-ip')
if not management_start_ip:
pecan.abort(400, _('management-start-ip required'))
management_end_ip = payload.get('management-end-ip')
if not management_end_ip:
pecan.abort(400, _('management-end-ip required'))
management_gateway_ip = payload.get('management-gateway-ip')
if not management_gateway_ip:
pecan.abort(400, _('management-gateway-ip required'))
systemcontroller_gateway_ip = \
payload.get('systemcontroller-gateway-ip')
if not systemcontroller_gateway_ip:
pecan.abort(400, _('systemcontroller-gateway-ip required'))
self._validate_subcloud_config(context,
name,
management_subnet,
management_start_ip,
management_end_ip,
management_gateway_ip,
systemcontroller_gateway_ip)
try:
# Ask dcmanager-manager to add the subcloud.
# It will do all the real work...
return self.rpc_client.add_subcloud(context, payload)
except RemoteError as e:
pecan.abort(422, e.value)
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'))
@index.when(method='PATCH', template='json')
def patch(self, subcloud_ref=None):
"""Update a subcloud.
:param subcloud_ref: ID or name of subcloud to update
"""
context = restcomm.extract_context_from_environ()
subcloud = None
if subcloud_ref is None:
pecan.abort(400, _('Subcloud ID required'))
payload = eval(request.body)
if not payload:
pecan.abort(400, _('Body required'))
if subcloud_ref.isdigit():
# Look up subcloud as an ID
try:
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'))
subcloud_id = subcloud.id
management_state = payload.get('management-state')
description = payload.get('description')
location = payload.get('location')
if not (management_state or description or location):
pecan.abort(400, _('nothing to update'))
# Syntax checking
if management_state and \
management_state not in [consts.MANAGEMENT_UNMANAGED,
consts.MANAGEMENT_MANAGED]:
pecan.abort(400, _('Invalid management-state'))
try:
# Inform dcmanager-manager that subcloud has been updated.
# It will do all the real work...
subcloud = self.rpc_client.update_subcloud(
context, subcloud_id, management_state=management_state,
description=description, location=location)
return subcloud
except RemoteError as e:
pecan.abort(422, e.value)
except Exception as e:
# additional exceptions.
LOG.exception(e)
pecan.abort(500, _('Unable to update subcloud'))
@index.when(method='delete', template='json')
def delete(self, subcloud_ref):
"""Delete a subcloud.
:param subcloud_ref: ID or name of subcloud to delete.
"""
context = restcomm.extract_context_from_environ()
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'))
subcloud_id = subcloud.id
try:
# Ask dcmanager-manager to delete the subcloud.
# It will do all the real work...
return self.rpc_client.delete_subcloud(context, subcloud_id)
except RemoteError as e:
pecan.abort(422, e.value)
except Exception as e:
LOG.exception(e)
pecan.abort(500, _('Unable to delete subcloud'))