290 lines
11 KiB
Python
290 lines
11 KiB
Python
#
|
|
# Copyright (c) 2023 Wind River Systems, Inc.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
|
|
import http.client as httpclient
|
|
import os
|
|
|
|
from oslo_log import log as logging
|
|
from oslo_messaging import RemoteError
|
|
import pecan
|
|
import tsconfig.tsconfig as tsc
|
|
import yaml
|
|
|
|
from dcmanager.api.controllers import restcomm
|
|
from dcmanager.api.policies import phased_subcloud_deploy as \
|
|
phased_subcloud_deploy_policy
|
|
from dcmanager.api import policy
|
|
from dcmanager.common import consts
|
|
from dcmanager.common.context import RequestContext
|
|
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.db.sqlalchemy import models
|
|
from dcmanager.rpc import client as rpc_client
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
LOCK_NAME = 'PhasedSubcloudDeployController'
|
|
|
|
SUBCLOUD_CREATE_REQUIRED_PARAMETERS = (
|
|
consts.BOOTSTRAP_VALUES,
|
|
consts.BOOTSTRAP_ADDRESS
|
|
)
|
|
|
|
# The consts.DEPLOY_CONFIG is missing here because it's handled differently
|
|
# by the upload_deploy_config_file() function
|
|
SUBCLOUD_CREATE_GET_FILE_CONTENTS = (
|
|
consts.BOOTSTRAP_VALUES,
|
|
consts.INSTALL_VALUES,
|
|
)
|
|
|
|
SUBCLOUD_BOOTSTRAP_GET_FILE_CONTENTS = (
|
|
consts.BOOTSTRAP_VALUES,
|
|
)
|
|
|
|
SUBCLOUD_CONFIG_GET_FILE_CONTENTS = (
|
|
consts.DEPLOY_CONFIG,
|
|
)
|
|
|
|
VALID_STATES_FOR_DEPLOY_BOOTSTRAP = [
|
|
consts.DEPLOY_STATE_INSTALLED,
|
|
consts.DEPLOY_STATE_BOOTSTRAP_FAILED,
|
|
consts.DEPLOY_STATE_BOOTSTRAP_ABORTED,
|
|
consts.DEPLOY_STATE_BOOTSTRAPPED,
|
|
# The subcloud can be installed manually (without remote install) so we need
|
|
# to allow the bootstrap operation when the state == DEPLOY_STATE_CREATED
|
|
consts.DEPLOY_STATE_CREATED
|
|
]
|
|
|
|
# TODO(vgluzrom): remove deploy_failed once 'subcloud reconfig'
|
|
# has been deprecated
|
|
VALID_STATES_FOR_DEPLOY_CONFIG = (
|
|
consts.DEPLOY_STATE_DONE,
|
|
consts.DEPLOY_STATE_PRE_CONFIG_FAILED,
|
|
consts.DEPLOY_STATE_CONFIG_FAILED,
|
|
consts.DEPLOY_STATE_DEPLOY_FAILED,
|
|
consts.DEPLOY_STATE_BOOTSTRAPPED
|
|
)
|
|
|
|
|
|
def get_create_payload(request: pecan.Request) -> dict:
|
|
payload = dict()
|
|
|
|
for f in SUBCLOUD_CREATE_GET_FILE_CONTENTS:
|
|
if f in request.POST:
|
|
file_item = request.POST[f]
|
|
file_item.file.seek(0, os.SEEK_SET)
|
|
data = yaml.safe_load(file_item.file.read().decode('utf8'))
|
|
if f == consts.BOOTSTRAP_VALUES:
|
|
payload.update(data)
|
|
else:
|
|
payload.update({f: data})
|
|
del request.POST[f]
|
|
payload.update(request.POST)
|
|
|
|
return payload
|
|
|
|
|
|
class PhasedSubcloudDeployController(object):
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.dcmanager_rpc_client = rpc_client.ManagerClient()
|
|
|
|
def _deploy_create(self, context: RequestContext, request: pecan.Request):
|
|
policy.authorize(phased_subcloud_deploy_policy.POLICY_ROOT % "create",
|
|
{}, restcomm.extract_credentials_for_policy())
|
|
psd_common.check_required_parameters(
|
|
request, SUBCLOUD_CREATE_REQUIRED_PARAMETERS)
|
|
|
|
payload = get_create_payload(request)
|
|
|
|
if not payload:
|
|
pecan.abort(400, _('Body required'))
|
|
|
|
psd_common.validate_bootstrap_values(payload)
|
|
|
|
# If a subcloud release is not passed, use the current
|
|
# system controller software_version
|
|
payload['software_version'] = payload.get('release', tsc.SW_VERSION)
|
|
|
|
psd_common.validate_subcloud_name_availability(context, payload['name'])
|
|
|
|
psd_common.validate_system_controller_patch_status("create")
|
|
|
|
psd_common.validate_subcloud_config(context, payload)
|
|
|
|
psd_common.validate_install_values(payload)
|
|
|
|
psd_common.validate_k8s_version(payload)
|
|
|
|
psd_common.format_ip_address(payload)
|
|
|
|
# Upload the deploy config files if it is included in the request
|
|
# It has a dependency on the subcloud name, and it is called after
|
|
# the name has been validated
|
|
psd_common.upload_deploy_config_file(request, payload)
|
|
|
|
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...
|
|
subcloud = self.dcmanager_rpc_client.subcloud_deploy_create(
|
|
context, subcloud.id, payload)
|
|
return subcloud
|
|
|
|
except RemoteError as e:
|
|
pecan.abort(httpclient.UNPROCESSABLE_ENTITY, e.value)
|
|
except Exception:
|
|
LOG.exception("Unable to create subcloud %s" % payload.get('name'))
|
|
pecan.abort(httpclient.INTERNAL_SERVER_ERROR,
|
|
_('Unable to create subcloud'))
|
|
|
|
def _deploy_bootstrap(self, context: RequestContext,
|
|
request: pecan.Request,
|
|
subcloud: models.Subcloud):
|
|
if subcloud.deploy_status not in VALID_STATES_FOR_DEPLOY_BOOTSTRAP:
|
|
valid_states_str = ', '.join(VALID_STATES_FOR_DEPLOY_BOOTSTRAP)
|
|
pecan.abort(400, _('Subcloud deploy status must be either: %s')
|
|
% valid_states_str)
|
|
|
|
has_bootstrap_values = consts.BOOTSTRAP_VALUES in request.POST
|
|
payload = {}
|
|
|
|
# Try to load the existing override values
|
|
override_file = psd_common.get_config_file_path(subcloud.name)
|
|
if os.path.exists(override_file):
|
|
psd_common.populate_payload_with_pre_existing_data(
|
|
payload, subcloud, SUBCLOUD_BOOTSTRAP_GET_FILE_CONTENTS)
|
|
elif not has_bootstrap_values:
|
|
msg = _("Required bootstrap-values file was not provided and it was"
|
|
" not previously available at %s") % (override_file)
|
|
pecan.abort(400, msg)
|
|
|
|
request_data = psd_common.get_request_data(
|
|
request, subcloud, SUBCLOUD_BOOTSTRAP_GET_FILE_CONTENTS)
|
|
|
|
# Update the existing values with new ones from the request
|
|
payload.update(request_data)
|
|
|
|
psd_common.validate_sysadmin_password(payload)
|
|
|
|
if has_bootstrap_values:
|
|
# Need to validate the new values
|
|
playload_name = payload.get('name')
|
|
if playload_name != subcloud.name:
|
|
pecan.abort(400, _('The bootstrap-values "name" value (%s) '
|
|
'must match the current subcloud name (%s)' %
|
|
(playload_name, subcloud.name)))
|
|
|
|
# Verify if payload contains all required bootstrap values
|
|
psd_common.validate_bootstrap_values(payload)
|
|
|
|
# It's ok for the management subnet to conflict with itself since we
|
|
# are only going to update it if it was modified, conflicts with
|
|
# other subclouds are still verified.
|
|
psd_common.validate_subcloud_config(context, payload,
|
|
ignore_conflicts_with=subcloud)
|
|
psd_common.format_ip_address(payload)
|
|
|
|
# Patch status and fresh_install_k8s_version may have been changed
|
|
# between deploy create and deploy bootstrap commands. Validate them
|
|
# again:
|
|
psd_common.validate_system_controller_patch_status("bootstrap")
|
|
psd_common.validate_k8s_version(payload)
|
|
|
|
try:
|
|
# Ask dcmanager-manager to bootstrap the subcloud.
|
|
self.dcmanager_rpc_client.subcloud_deploy_bootstrap(
|
|
context, subcloud.id, payload)
|
|
return db_api.subcloud_db_model_to_dict(subcloud)
|
|
|
|
except RemoteError as e:
|
|
pecan.abort(httpclient.UNPROCESSABLE_ENTITY, e.value)
|
|
except Exception:
|
|
LOG.exception("Unable to bootstrap subcloud %s" %
|
|
payload.get('name'))
|
|
pecan.abort(httpclient.INTERNAL_SERVER_ERROR,
|
|
_('Unable to bootstrap subcloud'))
|
|
|
|
def _deploy_config(self, context: RequestContext,
|
|
request: pecan.Request, subcloud):
|
|
payload = psd_common.get_request_data(
|
|
request, subcloud, SUBCLOUD_CONFIG_GET_FILE_CONTENTS)
|
|
if not payload:
|
|
pecan.abort(400, _('Body required'))
|
|
|
|
if not (subcloud.deploy_status in VALID_STATES_FOR_DEPLOY_CONFIG or
|
|
prestage.is_deploy_status_prestage(subcloud.deploy_status)):
|
|
allowed_states_str = ', '.join(VALID_STATES_FOR_DEPLOY_CONFIG)
|
|
pecan.abort(400, _('Subcloud deploy status must be either '
|
|
'%s or prestage-...') % allowed_states_str)
|
|
|
|
psd_common.populate_payload_with_pre_existing_data(
|
|
payload, subcloud, SUBCLOUD_CONFIG_GET_FILE_CONTENTS)
|
|
|
|
psd_common.validate_sysadmin_password(payload)
|
|
|
|
try:
|
|
subcloud = self.dcmanager_rpc_client.subcloud_deploy_config(
|
|
context, subcloud.id, payload)
|
|
return subcloud
|
|
except RemoteError as e:
|
|
pecan.abort(422, e.value)
|
|
except Exception:
|
|
LOG.exception("Unable to configure subcloud %s" % subcloud.name)
|
|
pecan.abort(500, _('Unable to configure subcloud'))
|
|
|
|
@pecan.expose(generic=True, template='json')
|
|
def index(self):
|
|
# Route the request to specific methods with parameters
|
|
pass
|
|
|
|
@utils.synchronized(LOCK_NAME)
|
|
@index.when(method='POST', template='json')
|
|
def post(self):
|
|
context = restcomm.extract_context_from_environ()
|
|
return self._deploy_create(context, pecan.request)
|
|
|
|
@utils.synchronized(LOCK_NAME)
|
|
@index.when(method='PATCH', template='json')
|
|
def patch(self, subcloud_ref=None, verb=None):
|
|
"""Modify the subcloud deployment.
|
|
|
|
:param subcloud_ref: ID or name of subcloud to update
|
|
|
|
:param verb: Specifies the patch action to be taken
|
|
or subcloud operation
|
|
"""
|
|
|
|
policy.authorize(phased_subcloud_deploy_policy.POLICY_ROOT % "modify", {},
|
|
restcomm.extract_credentials_for_policy())
|
|
context = restcomm.extract_context_from_environ()
|
|
|
|
if not subcloud_ref:
|
|
pecan.abort(400, _('Subcloud ID required'))
|
|
|
|
try:
|
|
if subcloud_ref.isdigit():
|
|
subcloud = db_api.subcloud_get(context, subcloud_ref)
|
|
else:
|
|
subcloud = db_api.subcloud_get_by_name(context, subcloud_ref)
|
|
except (exceptions.SubcloudNotFound, exceptions.SubcloudNameNotFound):
|
|
pecan.abort(404, _('Subcloud not found'))
|
|
|
|
if verb == 'bootstrap':
|
|
subcloud = self._deploy_bootstrap(context, pecan.request, subcloud)
|
|
elif verb == 'configure':
|
|
subcloud = self._deploy_config(context, pecan.request, subcloud)
|
|
else:
|
|
pecan.abort(400, _('Invalid request'))
|
|
|
|
return subcloud
|