1052 lines
46 KiB
Python
1052 lines
46 KiB
Python
# Copyright (c) 2017 Ericsson AB.
|
|
# Copyright (c) 2017-2024 Wind River Systems, Inc.
|
|
# 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.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
|
|
import base64
|
|
import json
|
|
import os
|
|
import re
|
|
|
|
import keyring
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_messaging import RemoteError
|
|
|
|
from requests_toolbelt.multipart import decoder
|
|
|
|
import pecan
|
|
from pecan import expose
|
|
from pecan import request
|
|
|
|
from fm_api.constants import FM_ALARM_ID_UNSYNCHRONIZED_RESOURCE
|
|
|
|
from keystoneauth1 import exceptions as keystone_exceptions
|
|
|
|
from dccommon import consts as dccommon_consts
|
|
from dccommon.drivers.openstack.fm import FmClient
|
|
from dccommon.drivers.openstack.sdk_platform import OpenStackDriver
|
|
from dccommon.drivers.openstack.sysinv_v1 import SysinvClient
|
|
from dccommon.drivers.openstack import vim
|
|
from dccommon import exceptions as dccommon_exceptions
|
|
|
|
from dcmanager.api.controllers import restcomm
|
|
from dcmanager.api.policies import subclouds as subclouds_policy
|
|
from dcmanager.api import policy
|
|
from dcmanager.common import consts
|
|
from dcmanager.common import exceptions
|
|
from dcmanager.common.i18n import _
|
|
from dcmanager.common import phased_subcloud_deploy as psd_common
|
|
from dcmanager.common import prestage
|
|
from dcmanager.common import utils
|
|
from dcmanager.db import api as db_api
|
|
|
|
from dcmanager.rpc import client as rpc_client
|
|
|
|
CONF = cfg.CONF
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
LOCK_NAME = 'SubcloudsController'
|
|
|
|
SUBCLOUD_ADD_GET_FILE_CONTENTS = [
|
|
consts.BOOTSTRAP_VALUES,
|
|
consts.INSTALL_VALUES,
|
|
]
|
|
|
|
SUBCLOUD_REDEPLOY_GET_FILE_CONTENTS = [
|
|
consts.INSTALL_VALUES,
|
|
consts.BOOTSTRAP_VALUES,
|
|
consts.DEPLOY_CONFIG
|
|
]
|
|
|
|
SUBCLOUD_MANDATORY_NETWORK_PARAMS = [
|
|
'management_subnet', 'management_gateway_ip',
|
|
'management_start_ip', 'management_end_ip'
|
|
]
|
|
|
|
|
|
def _get_multipart_field_name(part):
|
|
content = part.headers[b"Content-Disposition"].decode("utf8")
|
|
regex = 'name="([^"]*)"'
|
|
return re.search(regex, content).group(1)
|
|
|
|
|
|
class SubcloudsController(object):
|
|
VERSION_ALIASES = {
|
|
'Newton': '1.0',
|
|
}
|
|
|
|
def __init__(self):
|
|
super(SubcloudsController, self).__init__()
|
|
self.dcmanager_rpc_client = rpc_client.ManagerClient()
|
|
self.dcmanager_state_rpc_client = rpc_client.SubcloudStateClient()
|
|
|
|
# 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
|
|
|
|
@staticmethod
|
|
def _get_patch_data(request):
|
|
payload = dict()
|
|
content_type = request.headers.get("Content-Type")
|
|
multipart_data = decoder.MultipartDecoder(request.body, content_type)
|
|
|
|
for part in multipart_data.parts:
|
|
field_name = _get_multipart_field_name(part)
|
|
field_content = part.text
|
|
|
|
# only the install_values field is yaml, force should be bool
|
|
if field_name in [consts.INSTALL_VALUES, 'force']:
|
|
field_content = utils.yaml_safe_load(field_content, field_name)
|
|
|
|
payload[field_name] = field_content
|
|
|
|
return payload
|
|
|
|
@staticmethod
|
|
def _get_prestage_payload(request):
|
|
fields = ['sysadmin_password', 'force', consts.PRESTAGE_REQUEST_RELEASE]
|
|
payload = {
|
|
'force': False
|
|
}
|
|
try:
|
|
body = json.loads(request.body)
|
|
except Exception:
|
|
pecan.abort(400, _('Request body is malformed.'))
|
|
|
|
for field in fields:
|
|
val = body.get(field)
|
|
if val is None:
|
|
if field == 'sysadmin_password':
|
|
pecan.abort(400, _("%s is required." % field))
|
|
else:
|
|
if field == 'sysadmin_password':
|
|
try:
|
|
base64.b64decode(val).decode('utf-8')
|
|
payload['sysadmin_password'] = val
|
|
except Exception:
|
|
pecan.abort(
|
|
400,
|
|
_('Failed to decode subcloud sysadmin_password, '
|
|
'verify the password is base64 encoded'))
|
|
elif field == 'force':
|
|
if val.lower() in ('true', 'false', 't', 'f'):
|
|
payload['force'] = val.lower() in ('true', 't')
|
|
else:
|
|
pecan.abort(
|
|
400, _('Invalid value for force option: %s' % val))
|
|
elif field == consts.PRESTAGE_REQUEST_RELEASE:
|
|
payload[consts.PRESTAGE_REQUEST_RELEASE] = val
|
|
return payload
|
|
|
|
@staticmethod
|
|
def _get_updatestatus_payload(request):
|
|
"""retrieve payload of a patch request for update_status
|
|
|
|
:param request: request from the http client
|
|
:return: dict object submitted from the http client
|
|
"""
|
|
|
|
payload = dict()
|
|
payload.update(json.loads(request.body))
|
|
return payload
|
|
|
|
def _check_existing_vim_strategy(self, context, subcloud):
|
|
"""Check existing vim strategy by state.
|
|
|
|
An on-going vim strategy may interfere with subcloud reconfiguration
|
|
attempt and result in unrecoverable failure. Check if there is an
|
|
on-going strategy and whether it is in a state that is safe to proceed.
|
|
|
|
:param context: request context object.
|
|
:param subcloud: subcloud object.
|
|
|
|
:returns bool: True if on-going vim strategy found or not searchable,
|
|
otherwise False.
|
|
"""
|
|
|
|
strategy_steps = None
|
|
# Firstly, check the DC orchestrated vim strategies from database
|
|
try:
|
|
strategy_steps = db_api.strategy_step_get(context, subcloud.id)
|
|
except exceptions.StrategyStepNotFound:
|
|
LOG.debug(f"No existing vim strategy steps on subcloud: {subcloud.name}")
|
|
except Exception:
|
|
LOG.exception("Failed to get strategy steps on subcloud: "
|
|
f"{subcloud.name}.")
|
|
return True
|
|
|
|
if strategy_steps and strategy_steps.state not in (
|
|
consts.STRATEGY_STATE_COMPLETE,
|
|
consts.STRATEGY_STATE_ABORTED,
|
|
consts.STRATEGY_STATE_FAILED
|
|
):
|
|
return True
|
|
|
|
# Then check the system config update strategy
|
|
try:
|
|
keystone_client = OpenStackDriver(
|
|
region_name=subcloud.region_name,
|
|
region_clients=None).keystone_client
|
|
vim_client = vim.VimClient(subcloud.region_name,
|
|
keystone_client.session)
|
|
strategy = vim_client.get_strategy(
|
|
strategy_name=vim.STRATEGY_NAME_SYS_CONFIG_UPDATE,
|
|
raise_error_if_missing=False)
|
|
except Exception:
|
|
# Don't block the operation when the vim service is inaccessible
|
|
LOG.warning(f"Openstack admin endpoints on subcloud: {subcloud.name} "
|
|
"are unaccessible")
|
|
return False
|
|
|
|
return strategy and strategy.state in vim.TRANSITORY_STATES
|
|
|
|
# TODO(nicodemos): Check if subcloud is online and network already exist in the
|
|
# subcloud when the lock/unlock is not required for network reconfiguration
|
|
def _validate_network_reconfiguration(self, payload, subcloud):
|
|
if payload.get('management-state'):
|
|
pecan.abort(422, _("Management state and network reconfiguration must "
|
|
"be updated separately"))
|
|
if subcloud.management_state != dccommon_consts.MANAGEMENT_UNMANAGED:
|
|
pecan.abort(422, _("A subcloud must be unmanaged to perform network "
|
|
"reconfiguration"))
|
|
if not payload.get('bootstrap_address'):
|
|
pecan.abort(422, _("The bootstrap_address parameter is required for "
|
|
"network reconfiguration"))
|
|
# Check if all parameters exist
|
|
if not all(payload.get(value) is not None for value in (
|
|
SUBCLOUD_MANDATORY_NETWORK_PARAMS)):
|
|
mandatory_params = ', '.join('--{}'.format(param.replace(
|
|
'_', '-')) for param in SUBCLOUD_MANDATORY_NETWORK_PARAMS)
|
|
abort_msg = (
|
|
"The following parameters are necessary for "
|
|
"subcloud network reconfiguration: {}".format(mandatory_params)
|
|
)
|
|
pecan.abort(422, _(abort_msg))
|
|
|
|
# Check if any network values are already in use
|
|
for param in SUBCLOUD_MANDATORY_NETWORK_PARAMS:
|
|
if payload.get(param) == getattr(subcloud, param):
|
|
pecan.abort(422, _("%s already in use by the subcloud.") % param)
|
|
|
|
# Check password and decode it
|
|
sysadmin_password = payload.get('sysadmin_password')
|
|
if not sysadmin_password:
|
|
pecan.abort(400, _('subcloud sysadmin_password required'))
|
|
try:
|
|
payload['sysadmin_password'] = utils.decode_and_normalize_passwd(
|
|
sysadmin_password)
|
|
except Exception:
|
|
msg = _('Failed to decode subcloud sysadmin_password, '
|
|
'verify the password is base64 encoded')
|
|
LOG.exception(msg)
|
|
pecan.abort(400, msg)
|
|
|
|
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 = [
|
|
('sysinv', 'sysinv'),
|
|
('patching', 'patching'),
|
|
('vim', 'vim'),
|
|
('mtce', 'mtce'),
|
|
('fm', 'fm'),
|
|
('barbican', 'barbican'),
|
|
('smapi', 'smapi'),
|
|
('dcdbsync', 'dcdbsync')
|
|
]
|
|
|
|
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
|
|
|
|
# TODO(gsilvatr): refactor to use implementation from common/utils and test
|
|
def _get_oam_addresses(self, context, subcloud_name, sc_ks_client):
|
|
"""Get the subclouds oam addresses"""
|
|
|
|
# First need to retrieve the Subcloud's Keystone session
|
|
try:
|
|
endpoint = sc_ks_client.endpoint_cache.get_endpoint('sysinv')
|
|
sysinv_client = SysinvClient(subcloud_name,
|
|
sc_ks_client.session,
|
|
endpoint=endpoint)
|
|
return sysinv_client.get_oam_addresses()
|
|
except (keystone_exceptions.EndpointNotFound, IndexError) as e:
|
|
message = ("Identity endpoint for subcloud: %s not found. %s" %
|
|
(subcloud_name, e))
|
|
LOG.error(message)
|
|
except dccommon_exceptions.OAMAddressesNotFound:
|
|
message = ("OAM addresses for subcloud: %s not found." %
|
|
(subcloud_name))
|
|
LOG.error(message)
|
|
return None
|
|
|
|
def _get_deploy_config_sync_status(
|
|
self, context, subcloud_name, keystone_client
|
|
):
|
|
"""Get the deploy configuration insync status of the subcloud """
|
|
detected_alarms = None
|
|
try:
|
|
fm_client = FmClient(subcloud_name, keystone_client.session)
|
|
detected_alarms = fm_client.get_alarms_by_id(
|
|
FM_ALARM_ID_UNSYNCHRONIZED_RESOURCE)
|
|
except Exception as ex:
|
|
LOG.error(str(ex))
|
|
return None
|
|
|
|
out_of_date = False
|
|
if detected_alarms:
|
|
# Check if any alarm.entity_instance_id contains any of the values
|
|
# in MONITORED_ALARM_ENTITIES.
|
|
# We want to scope 260.002 alarms to the host entity only.
|
|
out_of_date = any(
|
|
any(entity_id in alarm.entity_instance_id
|
|
for entity_id in dccommon_consts.MONITORED_ALARM_ENTITIES)
|
|
for alarm in detected_alarms
|
|
)
|
|
sync_status = dccommon_consts.DEPLOY_CONFIG_OUT_OF_DATE if out_of_date \
|
|
else dccommon_consts.DEPLOY_CONFIG_UP_TO_DATE
|
|
return sync_status
|
|
|
|
def _validate_rehome_pending(self, subcloud, management_state):
|
|
unmanaged = dccommon_consts.MANAGEMENT_UNMANAGED
|
|
error_msg = None
|
|
|
|
# Can only set the subcloud to rehome-pending
|
|
# if the deployment is done
|
|
if subcloud.deploy_status != consts.DEPLOY_STATE_DONE:
|
|
error_msg = (
|
|
"The deploy status can only be updated to "
|
|
f"'{consts.DEPLOY_STATE_REHOME_PENDING}' if the current "
|
|
f"deploy status is '{consts.DEPLOY_STATE_DONE}'")
|
|
|
|
# Can only set the subcloud to rehome-pending if the subcloud is
|
|
# being unmanaged or is already unmanaged
|
|
if management_state != unmanaged and (
|
|
management_state or subcloud.management_state != unmanaged
|
|
):
|
|
error_msg = (
|
|
f"Subcloud must be {unmanaged} for its deploy status to "
|
|
f"be updated to '{consts.DEPLOY_STATE_REHOME_PENDING}'")
|
|
|
|
if error_msg:
|
|
pecan.abort(400, error_msg)
|
|
|
|
@staticmethod
|
|
def _append_static_err_content(subcloud):
|
|
err_dict = consts.ERR_MSG_DICT
|
|
status = subcloud.get('deploy-status')
|
|
err_msg = [subcloud.get('error-description')]
|
|
err_code = \
|
|
re.search(r"err_code\s*=\s*(\S*)", err_msg[0], re.IGNORECASE)
|
|
if err_code and err_code.group(1) in err_dict:
|
|
err_msg.append(err_dict.get(err_code.group(1)))
|
|
if status == consts.DEPLOY_STATE_CONFIG_FAILED:
|
|
err_msg.append(err_dict.get(consts.CONFIG_ERROR_MSG))
|
|
elif status == consts.DEPLOY_STATE_BOOTSTRAP_FAILED:
|
|
err_msg.append(err_dict.get(consts.BOOTSTRAP_ERROR_MSG))
|
|
subcloud['error-description'] = '\n'.join(err_msg)
|
|
return None
|
|
|
|
@index.when(method='GET', template='json')
|
|
def get(self, subcloud_ref=None, detail=None):
|
|
"""Get details about subcloud.
|
|
|
|
:param subcloud_ref: ID or name of subcloud
|
|
"""
|
|
policy.authorize(subclouds_policy.POLICY_ROOT % "get", {},
|
|
restcomm.extract_credentials_for_policy())
|
|
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)
|
|
|
|
self._append_static_err_content(subcloud_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] = \
|
|
dccommon_consts.SYNC_STATUS_OUT_OF_SYNC
|
|
|
|
if subcloud_status:
|
|
subcloud_status_list.append(
|
|
db_api.subcloud_endpoint_status_db_model_to_dict(
|
|
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(
|
|
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:
|
|
# This is to reduce changes on cert-mon
|
|
# Overwrites the name value with region
|
|
if utils.is_req_from_cert_mon_agent(request):
|
|
s['name'] = s['region-name']
|
|
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:
|
|
try:
|
|
# When the request comes from the cert-monitor or another
|
|
# DC, it is based on the region name (which is UUID format).
|
|
# Whereas, if the request comes from a client other
|
|
# than cert-monitor, it will do the lookup based on
|
|
# the subcloud name.
|
|
if (utils.is_req_from_cert_mon_agent(request) or
|
|
utils.is_req_from_another_dc(request)):
|
|
subcloud = db_api.\
|
|
subcloud_get_by_region_name(context, subcloud_ref)
|
|
else:
|
|
subcloud = db_api.subcloud_get_by_name(context,
|
|
subcloud_ref)
|
|
except (exceptions.SubcloudRegionNameNotFound,
|
|
exceptions.SubcloudNameNotFound):
|
|
pecan.abort(404, _('Subcloud not found'))
|
|
|
|
subcloud_id = subcloud.id
|
|
|
|
# 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)
|
|
|
|
self._append_static_err_content(subcloud_dict)
|
|
|
|
subcloud_region = subcloud.region_name
|
|
subcloud_dict.pop('region-name')
|
|
if detail is not None:
|
|
oam_floating_ip = "unavailable"
|
|
deploy_config_sync_status = "unknown"
|
|
if (subcloud.availability_status ==
|
|
dccommon_consts.AVAILABILITY_ONLINE):
|
|
|
|
# Get the keystone client that will be used
|
|
# for _get_deploy_config_sync_status and _get_oam_addresses
|
|
sc_ks_client = psd_common.get_ks_client(subcloud_region)
|
|
oam_addresses = self._get_oam_addresses(
|
|
context, subcloud_region, sc_ks_client
|
|
)
|
|
if oam_addresses is not None:
|
|
oam_floating_ip = oam_addresses.oam_floating_ip
|
|
|
|
deploy_config_state = self._get_deploy_config_sync_status(
|
|
context, subcloud_region, sc_ks_client)
|
|
if deploy_config_state is not None:
|
|
deploy_config_sync_status = deploy_config_state
|
|
|
|
extra_details = {
|
|
"oam_floating_ip": oam_floating_ip,
|
|
"deploy_config_sync_status": deploy_config_sync_status,
|
|
"region_name": subcloud_region
|
|
}
|
|
|
|
subcloud_dict.update(extra_details)
|
|
return subcloud_dict
|
|
|
|
@utils.synchronized(LOCK_NAME)
|
|
@index.when(method='POST', template='json')
|
|
def post(self):
|
|
"""Create and deploy a new subcloud."""
|
|
|
|
policy.authorize(subclouds_policy.POLICY_ROOT % "create", {},
|
|
restcomm.extract_credentials_for_policy())
|
|
context = restcomm.extract_context_from_environ()
|
|
|
|
bootstrap_sc_name = psd_common.get_bootstrap_subcloud_name(request)
|
|
|
|
payload = psd_common.get_request_data(request, None,
|
|
SUBCLOUD_ADD_GET_FILE_CONTENTS)
|
|
|
|
psd_common.validate_migrate_parameter(payload, request)
|
|
|
|
psd_common.validate_secondary_parameter(payload, request)
|
|
|
|
# Compares to match both supplied and bootstrap name param
|
|
# of the subcloud if migrate is on
|
|
if payload.get('migrate') == 'true' and bootstrap_sc_name is not None:
|
|
if bootstrap_sc_name != payload.get('name'):
|
|
pecan.abort(400, _('subcloud name does not match the '
|
|
'name defined in bootstrap file'))
|
|
|
|
# No need sysadmin_password when add a secondary subcloud
|
|
if 'secondary' not in payload:
|
|
psd_common.validate_sysadmin_password(payload)
|
|
|
|
# Use the region_name if it has been provided in the payload.
|
|
# The typical scenario is adding a secondary subcloud from
|
|
# peer site where subcloud region_name is known and can be
|
|
# put into the payload of the subcloud add request.
|
|
if 'region_name' not in payload:
|
|
psd_common.subcloud_region_create(payload, context)
|
|
|
|
psd_common.pre_deploy_create(payload, context, request)
|
|
|
|
try:
|
|
# Add the subcloud details to the database
|
|
subcloud = psd_common.add_subcloud_to_database(context, payload)
|
|
|
|
# Ask dcmanager-manager to add the subcloud.
|
|
# It will do all the real work...
|
|
# If the subcloud is secondary, it will be synchronous operation.
|
|
# A normal subcloud add will be asynchronous operation.
|
|
if 'secondary' in payload:
|
|
self.dcmanager_rpc_client.add_secondary_subcloud(
|
|
context, subcloud.id, payload)
|
|
else:
|
|
self.dcmanager_rpc_client.add_subcloud(
|
|
context, subcloud.id, payload)
|
|
|
|
return db_api.subcloud_db_model_to_dict(subcloud)
|
|
except RemoteError as e:
|
|
pecan.abort(422, e.value)
|
|
except Exception:
|
|
LOG.exception(
|
|
"Unable to add subcloud %s" % payload.get('name'))
|
|
pecan.abort(500, _('Unable to add subcloud'))
|
|
|
|
@utils.synchronized(LOCK_NAME)
|
|
@index.when(method='PATCH', template='json')
|
|
def patch(self, subcloud_ref=None, verb=None):
|
|
"""Update a subcloud.
|
|
|
|
:param subcloud_ref: ID or name of subcloud to update
|
|
|
|
:param verb: Specifies the patch action to be taken
|
|
or subcloud update operation
|
|
"""
|
|
|
|
policy.authorize(subclouds_policy.POLICY_ROOT % "modify", {},
|
|
restcomm.extract_credentials_for_policy())
|
|
context = restcomm.extract_context_from_environ()
|
|
subcloud = None
|
|
|
|
if subcloud_ref is None:
|
|
pecan.abort(400, _('Subcloud ID 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:
|
|
try:
|
|
# When the request comes from the cert-monitor or another DC,
|
|
# it is based on the region name (which is UUID format).
|
|
# Whereas, if the request comes from a client other
|
|
# than cert-monitor, it will do the lookup based on
|
|
# the subcloud name.
|
|
if (utils.is_req_from_cert_mon_agent(request) or
|
|
utils.is_req_from_another_dc(request)):
|
|
subcloud = db_api.\
|
|
subcloud_get_by_region_name(context, subcloud_ref)
|
|
else:
|
|
subcloud = db_api.subcloud_get_by_name(context,
|
|
subcloud_ref)
|
|
except (exceptions.SubcloudRegionNameNotFound,
|
|
exceptions.SubcloudNameNotFound):
|
|
pecan.abort(404, _('Subcloud not found'))
|
|
|
|
subcloud_id = subcloud.id
|
|
|
|
if verb is None:
|
|
# subcloud update
|
|
payload = self._get_patch_data(request)
|
|
if not payload:
|
|
pecan.abort(400, _('Body required'))
|
|
|
|
# Rename the subcloud
|
|
new_subcloud_name = payload.get('name')
|
|
if new_subcloud_name is not None:
|
|
# To be renamed the subcloud must be in unmanaged, valid deploy
|
|
# state, and no going prestage
|
|
if (subcloud.management_state !=
|
|
dccommon_consts.MANAGEMENT_UNMANAGED or
|
|
subcloud.deploy_status != consts.DEPLOY_STATE_DONE or
|
|
subcloud.prestage_status in
|
|
consts.STATES_FOR_ONGOING_PRESTAGE):
|
|
msg = ('Subcloud %s must be deployed, unmanaged and '
|
|
'no ongoing prestage for the subcloud rename '
|
|
'operation.' % subcloud.name)
|
|
pecan.abort(400, msg)
|
|
|
|
# Validates new name
|
|
if not utils.is_subcloud_name_format_valid(new_subcloud_name):
|
|
pecan.abort(
|
|
400, _("new name must contain alphabetic characters")
|
|
)
|
|
|
|
# Checks if new subcloud name is the same as the current subcloud
|
|
if new_subcloud_name == subcloud.name:
|
|
pecan.abort(
|
|
400, _('Provided subcloud name %s is the same as the '
|
|
'current subcloud %s. A different name is '
|
|
'required to rename the subcloud' %
|
|
(new_subcloud_name, subcloud.name))
|
|
)
|
|
|
|
error_msg = (
|
|
'Unable to rename subcloud %s with their region %s to %s' %
|
|
(subcloud.name, subcloud.region_name, new_subcloud_name)
|
|
)
|
|
|
|
try:
|
|
LOG.info("Renaming subcloud %s to: %s\n" % (subcloud.name,
|
|
new_subcloud_name))
|
|
sc = self.dcmanager_rpc_client.rename_subcloud(context,
|
|
subcloud_id,
|
|
subcloud.name,
|
|
new_subcloud_name)
|
|
subcloud.name = sc['name']
|
|
except RemoteError as e:
|
|
LOG.error(error_msg)
|
|
pecan.abort(422, e.value)
|
|
except Exception:
|
|
LOG.error(error_msg)
|
|
pecan.abort(500, _('Unable to rename subcloud'))
|
|
|
|
# Check if exist any network reconfiguration parameters
|
|
reconfigure_network = any(payload.get(value) is not None for value in (
|
|
SUBCLOUD_MANDATORY_NETWORK_PARAMS))
|
|
|
|
if reconfigure_network:
|
|
if utils.subcloud_is_secondary_state(subcloud.deploy_status):
|
|
pecan.abort(500, _("Cannot perform on %s "
|
|
"state subcloud" % subcloud.deploy_status))
|
|
system_controller_mgmt_pool = psd_common.get_network_address_pool()
|
|
# Required parameters
|
|
payload['name'] = subcloud.name
|
|
payload['region_name'] = subcloud.region_name
|
|
payload['system_controller_network'] = (
|
|
system_controller_mgmt_pool.network)
|
|
payload['system_controller_network_prefix'] = (
|
|
system_controller_mgmt_pool.prefix
|
|
)
|
|
# Needed for service endpoint reconfiguration
|
|
payload['management_start_address'] = (
|
|
payload.get('management_start_ip', None)
|
|
)
|
|
# Validation
|
|
self._validate_network_reconfiguration(payload, subcloud)
|
|
# Validate there's no on-going vim strategy
|
|
if self._check_existing_vim_strategy(context, subcloud):
|
|
error_msg = (
|
|
"Reconfiguring subcloud network is not allowed while "
|
|
"there is an on-going orchestrated operation in this "
|
|
"subcloud. Please try again after the strategy has "
|
|
"completed."
|
|
)
|
|
pecan.abort(400, error_msg)
|
|
|
|
management_state = payload.get('management-state')
|
|
group_id = payload.get('group_id')
|
|
description = payload.get('description')
|
|
location = payload.get('location')
|
|
bootstrap_values = payload.get('bootstrap_values')
|
|
peer_group = payload.get('peer_group')
|
|
bootstrap_address = payload.get('bootstrap_address')
|
|
|
|
# If the migrate flag is present we need to update the deploy status
|
|
# to consts.DEPLOY_STATE_REHOME_PENDING
|
|
deploy_status = None
|
|
if (payload.get('migrate') == 'true' and subcloud.deploy_status !=
|
|
consts.DEPLOY_STATE_REHOME_PENDING):
|
|
self._validate_rehome_pending(subcloud, management_state)
|
|
deploy_status = consts.DEPLOY_STATE_REHOME_PENDING
|
|
|
|
# Syntax checking
|
|
if management_state and \
|
|
management_state not in [dccommon_consts.MANAGEMENT_UNMANAGED,
|
|
dccommon_consts.MANAGEMENT_MANAGED]:
|
|
pecan.abort(400, _('Invalid management-state'))
|
|
|
|
force_flag = payload.get('force')
|
|
if force_flag is not None:
|
|
if force_flag not in [True, False]:
|
|
pecan.abort(400, _('Invalid force value'))
|
|
elif management_state != dccommon_consts.MANAGEMENT_MANAGED:
|
|
pecan.abort(400, _('Invalid option: force'))
|
|
|
|
# Verify the group_id is valid
|
|
if group_id is not None:
|
|
try:
|
|
# group_id may be passed in the payload as an int or str
|
|
group_id = str(group_id)
|
|
if group_id.isdigit():
|
|
grp = db_api.subcloud_group_get(context, group_id)
|
|
else:
|
|
# replace the group_id (name) with the id
|
|
grp = db_api.subcloud_group_get_by_name(context,
|
|
group_id)
|
|
group_id = grp.id
|
|
except (exceptions.SubcloudGroupNameNotFound,
|
|
exceptions.SubcloudGroupNotFound):
|
|
pecan.abort(400, _('Invalid group'))
|
|
|
|
# Verify the peer_group is valid
|
|
peer_group_id = None
|
|
if peer_group is not None:
|
|
# peer_group may be passed in the payload as an int or str
|
|
peer_group = str(peer_group)
|
|
# Check if user wants to remove a subcloud
|
|
# from a subcloud-peer-group by
|
|
# setting peer_group_id as 'none',
|
|
# then we will pass 'none' string as
|
|
# the peer_group_id,
|
|
# update_subcloud() will handle it and
|
|
# Set the peer_group_id DB into None.
|
|
if peer_group.lower() == 'none':
|
|
peer_group_id = 'none'
|
|
else:
|
|
pgrp = utils.subcloud_peer_group_get_by_ref(context, peer_group)
|
|
if not pgrp:
|
|
pecan.abort(400, _('Invalid peer group'))
|
|
if not utils.is_req_from_another_dc(request):
|
|
if pgrp.group_priority > 0:
|
|
pecan.abort(400, _("Cannot set the subcloud to a peer"
|
|
" group with non-zero priority."))
|
|
elif not (
|
|
subcloud.deploy_status in [
|
|
consts.DEPLOY_STATE_DONE,
|
|
consts.PRESTAGE_STATE_COMPLETE
|
|
] and subcloud.management_state ==
|
|
dccommon_consts.MANAGEMENT_MANAGED
|
|
and subcloud.availability_status ==
|
|
dccommon_consts.AVAILABILITY_ONLINE):
|
|
pecan.abort(400, _("Only subclouds that are "
|
|
"managed and online can be "
|
|
"added to a peer group."))
|
|
peer_group_id = pgrp.id
|
|
|
|
if consts.INSTALL_VALUES in payload:
|
|
# install_values of secondary subclouds are validated on
|
|
# peer site
|
|
if utils.subcloud_is_secondary_state(subcloud.deploy_status) \
|
|
and utils.is_req_from_another_dc(request):
|
|
LOG.debug("Skipping install_values validation for subcloud "
|
|
f"{subcloud.name}. Subcloud is secondary and "
|
|
"request is from a peer site.")
|
|
else:
|
|
psd_common.validate_install_values(payload, subcloud)
|
|
payload['data_install'] = json.dumps(payload[consts.INSTALL_VALUES])
|
|
|
|
try:
|
|
if reconfigure_network:
|
|
self.dcmanager_rpc_client.update_subcloud_with_network_reconfig(
|
|
context, subcloud_id, payload)
|
|
return db_api.subcloud_db_model_to_dict(subcloud)
|
|
subcloud = self.dcmanager_rpc_client.update_subcloud(
|
|
context, subcloud_id, management_state=management_state,
|
|
description=description, location=location,
|
|
group_id=group_id, data_install=payload.get('data_install'),
|
|
force=force_flag,
|
|
peer_group_id=peer_group_id,
|
|
bootstrap_values=bootstrap_values,
|
|
bootstrap_address=bootstrap_address,
|
|
deploy_status=deploy_status)
|
|
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'))
|
|
|
|
elif verb == "redeploy":
|
|
if utils.subcloud_is_secondary_state(subcloud.deploy_status):
|
|
pecan.abort(500, _("Cannot perform on %s "
|
|
"state subcloud" % subcloud.deploy_status))
|
|
config_file = psd_common.get_config_file_path(subcloud.name,
|
|
consts.DEPLOY_CONFIG)
|
|
has_bootstrap_values = consts.BOOTSTRAP_VALUES in request.POST
|
|
has_original_config_values = os.path.exists(config_file)
|
|
has_new_config_values = consts.DEPLOY_CONFIG in request.POST
|
|
has_config_values = has_original_config_values or has_new_config_values
|
|
payload = psd_common.get_request_data(
|
|
request, subcloud, SUBCLOUD_REDEPLOY_GET_FILE_CONTENTS)
|
|
|
|
if (subcloud.availability_status == dccommon_consts.AVAILABILITY_ONLINE
|
|
or subcloud.management_state ==
|
|
dccommon_consts.MANAGEMENT_MANAGED):
|
|
msg = _('Cannot re-deploy an online and/or managed subcloud')
|
|
LOG.warning(msg)
|
|
pecan.abort(400, msg)
|
|
|
|
payload['software_version'] = \
|
|
utils.get_sw_version(payload.get('release'))
|
|
|
|
# Don't load previously stored bootstrap_values if they are present in
|
|
# the request, as this would override the already loaded values from it.
|
|
# As config_values are optional, only attempt to load previously stored
|
|
# values if this phase should be executed.
|
|
files_for_redeploy = SUBCLOUD_REDEPLOY_GET_FILE_CONTENTS.copy()
|
|
if has_bootstrap_values:
|
|
files_for_redeploy.remove(consts.BOOTSTRAP_VALUES)
|
|
if not has_config_values:
|
|
files_for_redeploy.remove(consts.DEPLOY_CONFIG)
|
|
|
|
psd_common.populate_payload_with_pre_existing_data(
|
|
payload, subcloud, files_for_redeploy)
|
|
|
|
payload['bootstrap-address'] = \
|
|
payload['install_values']['bootstrap_address']
|
|
psd_common.validate_sysadmin_password(payload)
|
|
psd_common.pre_deploy_install(payload, validate_password=False)
|
|
psd_common.pre_deploy_bootstrap(context, payload, subcloud,
|
|
has_bootstrap_values,
|
|
validate_password=False)
|
|
if has_config_values:
|
|
psd_common.pre_deploy_config(payload, subcloud,
|
|
validate_password=False)
|
|
|
|
try:
|
|
# Align the software version of the subcloud with redeploy
|
|
# version. Update description, location and group id if offered,
|
|
# update the deploy status as pre-install.
|
|
subcloud = db_api.subcloud_update(
|
|
context,
|
|
subcloud_id,
|
|
description=payload.get('description'),
|
|
location=payload.get('location'),
|
|
software_version=payload['software_version'],
|
|
deploy_status=consts.DEPLOY_STATE_PRE_INSTALL,
|
|
first_identity_sync_complete=False,
|
|
data_install=json.dumps(payload['install_values']))
|
|
|
|
self.dcmanager_rpc_client.redeploy_subcloud(
|
|
context, subcloud_id, payload)
|
|
|
|
return db_api.subcloud_db_model_to_dict(subcloud)
|
|
except RemoteError as e:
|
|
pecan.abort(422, e.value)
|
|
except Exception:
|
|
LOG.exception("Unable to redeploy subcloud %s" % subcloud.name)
|
|
pecan.abort(500, _('Unable to redeploy subcloud'))
|
|
|
|
elif verb == "restore":
|
|
pecan.abort(410, _('This API is deprecated. '
|
|
'Please use /v1.0/subcloud-backup/restore'))
|
|
|
|
elif verb == "reconfigure":
|
|
pecan.abort(
|
|
410, _('This API is deprecated. Please use '
|
|
'/v1.0/phased-subcloud-deploy/{subcloud}/configure')
|
|
)
|
|
|
|
elif verb == "reinstall":
|
|
pecan.abort(410, _('This API is deprecated. '
|
|
'Please use /v1.0/subclouds/{subcloud}/redeploy'))
|
|
|
|
elif verb == 'update_status':
|
|
res = self.updatestatus(subcloud.name, subcloud.region_name)
|
|
return res
|
|
elif verb == 'prestage':
|
|
if utils.subcloud_is_secondary_state(subcloud.deploy_status):
|
|
pecan.abort(500, _("Cannot perform on %s "
|
|
"state subcloud" % subcloud.deploy_status))
|
|
payload = self._get_prestage_payload(request)
|
|
payload['subcloud_name'] = subcloud.name
|
|
try:
|
|
prestage.global_prestage_validate(payload)
|
|
except exceptions.PrestagePreCheckFailedException as exc:
|
|
LOG.exception("global_prestage_validate failed")
|
|
pecan.abort(400, _(str(exc)))
|
|
|
|
try:
|
|
payload['oam_floating_ip'] = \
|
|
prestage.validate_prestage(subcloud, payload)
|
|
except exceptions.PrestagePreCheckFailedException as exc:
|
|
LOG.exception("validate_prestage failed")
|
|
pecan.abort(400, _(str(exc)))
|
|
|
|
prestage_software_version = utils.get_sw_version(
|
|
payload.get(consts.PRESTAGE_REQUEST_RELEASE))
|
|
|
|
try:
|
|
self.dcmanager_rpc_client.prestage_subcloud(context, payload)
|
|
# local update to prestage_status - this is just for
|
|
# CLI response:
|
|
subcloud.prestage_status = consts.PRESTAGE_STATE_PACKAGES
|
|
|
|
subcloud_dict = db_api.subcloud_db_model_to_dict(subcloud)
|
|
subcloud_dict.update(
|
|
{consts.PRESTAGE_SOFTWARE_VERSION: prestage_software_version})
|
|
return subcloud_dict
|
|
except RemoteError as e:
|
|
pecan.abort(422, e.value)
|
|
except Exception:
|
|
LOG.exception("Unable to prestage subcloud %s" % subcloud.name)
|
|
pecan.abort(500, _('Unable to prestage subcloud'))
|
|
|
|
@utils.synchronized(LOCK_NAME)
|
|
@index.when(method='delete', template='json')
|
|
def delete(self, subcloud_ref):
|
|
"""Delete a subcloud.
|
|
|
|
:param subcloud_ref: ID or name of subcloud to delete.
|
|
"""
|
|
policy.authorize(subclouds_policy.POLICY_ROOT % "delete", {},
|
|
restcomm.extract_credentials_for_policy())
|
|
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.dcmanager_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'))
|
|
|
|
def updatestatus(self, subcloud_name, subcloud_region):
|
|
"""Update subcloud sync status
|
|
|
|
:param subcloud_name: name of the subcloud
|
|
:param subcloud_region: name of the subcloud region
|
|
:return: json result object for the operation on success
|
|
"""
|
|
|
|
payload = self._get_updatestatus_payload(request)
|
|
if not payload:
|
|
pecan.abort(400, _('Body required'))
|
|
|
|
endpoint = payload.get('endpoint')
|
|
if not endpoint:
|
|
pecan.abort(400, _('endpoint required'))
|
|
allowed_endpoints = [dccommon_consts.ENDPOINT_TYPE_DC_CERT]
|
|
if endpoint not in allowed_endpoints:
|
|
pecan.abort(400, _('updating endpoint %s status is not allowed'
|
|
% endpoint))
|
|
|
|
status = payload.get('status')
|
|
if not status:
|
|
pecan.abort(400, _('status required'))
|
|
|
|
allowed_status = [dccommon_consts.SYNC_STATUS_IN_SYNC,
|
|
dccommon_consts.SYNC_STATUS_OUT_OF_SYNC,
|
|
dccommon_consts.SYNC_STATUS_UNKNOWN]
|
|
if status not in allowed_status:
|
|
pecan.abort(400, _('status %s in invalid.' % status))
|
|
|
|
LOG.info('update %s set %s=%s' % (subcloud_name, endpoint, status))
|
|
context = restcomm.extract_context_from_environ()
|
|
self.dcmanager_state_rpc_client.update_subcloud_endpoint_status(
|
|
context, subcloud_name, subcloud_region, endpoint, status)
|
|
|
|
result = {'result': 'OK'}
|
|
return result
|