bff2f0aa2f
It is risky to update a subcloud's network if there is an existing vim strategy: the on-going vim strategy triggers lock/unlock/swact can interrupt the ansible-playbook, leaving the subcloud in an un-recoverable state. To prevent this issue, this commit checks the on-going vim strategy, and blocks the further operation if it exists. Test plan: 1. Passed - deploy a DC with this change. 2. Passed - create a kube-rootca-update orchestration against a subcloud, verify the subcloud update with network reconfiguration is blocked by the ongoing strategy, verify the network reconfiguration is not blocked after the strategy applied. 3. Passed - create a system-config-update strategy by updating the addresspool on the subcloud, verify the subcloud update with network reconfiguration is blocked when applying this strategy, verify this operation is not blocked after the strategy applied. Story: 2010722 Task: 49414 Signed-off-by: Yuxing Jiang <Yuxing.Jiang@windriver.com> Change-Id: I2c3b824ebbd6766996f3422c681f16d03b6063fa
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
|