
This commit fixes a condition where the playbook fails, but subcloud deployment continues anyway. After the exception is raised, a "False" condition should be returned. This commit also fixes an enroll validation failure where the validation method should verify if the object "install-values" is received but verifies if the string "install-values" is received instead. Test Plan: PASS: Deploy a system controller and add a subcloud passing --enroll parameter and a wrong bmc-password to force a failure in the init- enroll playbook. Verify that the subcloud state is "pre-init-enroll- failed". PASS: Deploy a system controller and successfully enroll a subcloud. PASS: Enroll a subcloud using --deploy config. Verify that the subcloud is successfully enrolled. Closes-bug: 2079853 Change-Id: Ia14979d9d6549b28d5717a57e93ad766009b4b6a Signed-off-by: Gustavo Pereira <gustavo.lyrapereira@windriver.com>
708 lines
28 KiB
Python
708 lines
28 KiB
Python
#
|
|
# Copyright (c) 2023-2024 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
|
|
|
|
from dcmanager.api.controllers import restcomm
|
|
from dcmanager.api.controllers.v1.subclouds import SubcloudsController
|
|
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 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"
|
|
|
|
INSTALL = consts.DEPLOY_PHASE_INSTALL
|
|
BOOTSTRAP = consts.DEPLOY_PHASE_BOOTSTRAP
|
|
CONFIG = consts.DEPLOY_PHASE_CONFIG
|
|
COMPLETE = consts.DEPLOY_PHASE_COMPLETE
|
|
ABORT = consts.DEPLOY_PHASE_ABORT
|
|
RESUME = consts.DEPLOY_PHASE_RESUME
|
|
ENROLL = consts.DEPLOY_PHASE_ENROLL
|
|
|
|
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_INSTALL_GET_FILE_CONTENTS = (consts.INSTALL_VALUES,)
|
|
SUBCLOUD_BOOTSTRAP_GET_FILE_CONTENTS = (consts.BOOTSTRAP_VALUES,)
|
|
SUBCLOUD_ENROLL_GET_FILE_CONTENTS = (consts.BOOTSTRAP_VALUES, consts.INSTALL_VALUES)
|
|
SUBCLOUD_CONFIG_GET_FILE_CONTENTS = (consts.DEPLOY_CONFIG,)
|
|
|
|
VALID_STATES_FOR_DEPLOY_INSTALL = (
|
|
consts.DEPLOY_STATE_CREATED,
|
|
consts.DEPLOY_STATE_PRE_INSTALL_FAILED,
|
|
consts.DEPLOY_STATE_INSTALL_FAILED,
|
|
consts.DEPLOY_STATE_INSTALLED,
|
|
consts.DEPLOY_STATE_INSTALL_ABORTED,
|
|
)
|
|
VALID_STATES_FOR_DEPLOY_BOOTSTRAP = [
|
|
consts.DEPLOY_STATE_INSTALLED,
|
|
consts.DEPLOY_STATE_PRE_BOOTSTRAP_FAILED,
|
|
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,
|
|
]
|
|
VALID_STATES_FOR_DEPLOY_CONFIG = (
|
|
consts.DEPLOY_STATE_DONE,
|
|
consts.DEPLOY_STATE_PRE_CONFIG_FAILED,
|
|
consts.DEPLOY_STATE_CONFIG_FAILED,
|
|
consts.DEPLOY_STATE_BOOTSTRAPPED,
|
|
consts.DEPLOY_STATE_ENROLLED,
|
|
consts.DEPLOY_STATE_CONFIG_ABORTED,
|
|
)
|
|
VALID_STATES_FOR_DEPLOY_COMPLETE = (
|
|
consts.DEPLOY_STATE_BOOTSTRAPPED,
|
|
consts.DEPLOY_STATE_ENROLLED,
|
|
)
|
|
VALID_STATES_FOR_DEPLOY_ABORT = (
|
|
consts.DEPLOY_STATE_INSTALLING,
|
|
consts.DEPLOY_STATE_BOOTSTRAPPING,
|
|
consts.DEPLOY_STATE_CONFIGURING,
|
|
)
|
|
VALID_STATES_FOR_DEPLOY_ENROLL = (
|
|
consts.DEPLOY_STATE_CREATED,
|
|
consts.DEPLOY_STATE_ENROLL_FAILED,
|
|
consts.DEPLOY_STATE_PRE_ENROLL_FAILED,
|
|
consts.DEPLOY_STATE_PRE_INIT_ENROLL_FAILED,
|
|
consts.DEPLOY_STATE_INIT_ENROLL_FAILED,
|
|
)
|
|
|
|
FILES_FOR_RESUME_INSTALL = (
|
|
SUBCLOUD_INSTALL_GET_FILE_CONTENTS
|
|
+ SUBCLOUD_BOOTSTRAP_GET_FILE_CONTENTS
|
|
+ SUBCLOUD_CONFIG_GET_FILE_CONTENTS
|
|
)
|
|
FILES_FOR_RESUME_BOOTSTRAP = (
|
|
SUBCLOUD_BOOTSTRAP_GET_FILE_CONTENTS + SUBCLOUD_CONFIG_GET_FILE_CONTENTS
|
|
)
|
|
FILES_FOR_RESUME_CONFIG = SUBCLOUD_CONFIG_GET_FILE_CONTENTS
|
|
RESUMABLE_STATES = {
|
|
consts.DEPLOY_STATE_CREATED: [INSTALL, BOOTSTRAP, CONFIG],
|
|
consts.DEPLOY_STATE_INSTALLED: [BOOTSTRAP, CONFIG],
|
|
consts.DEPLOY_STATE_PRE_INSTALL_FAILED: [INSTALL, BOOTSTRAP, CONFIG],
|
|
consts.DEPLOY_STATE_INSTALL_FAILED: [INSTALL, BOOTSTRAP, CONFIG],
|
|
consts.DEPLOY_STATE_INSTALL_ABORTED: [INSTALL, BOOTSTRAP, CONFIG],
|
|
consts.DEPLOY_STATE_BOOTSTRAPPED: [CONFIG],
|
|
consts.DEPLOY_STATE_PRE_BOOTSTRAP_FAILED: [BOOTSTRAP, CONFIG],
|
|
consts.DEPLOY_STATE_BOOTSTRAP_FAILED: [BOOTSTRAP, CONFIG],
|
|
consts.DEPLOY_STATE_BOOTSTRAP_ABORTED: [BOOTSTRAP, CONFIG],
|
|
consts.DEPLOY_STATE_PRE_CONFIG_FAILED: [CONFIG],
|
|
consts.DEPLOY_STATE_CONFIG_FAILED: [CONFIG],
|
|
consts.DEPLOY_STATE_CONFIG_ABORTED: [CONFIG],
|
|
}
|
|
DEPLOY_PHASES = [INSTALL, BOOTSTRAP, CONFIG]
|
|
FILES_MAPPING = {
|
|
INSTALL: SUBCLOUD_INSTALL_GET_FILE_CONTENTS,
|
|
BOOTSTRAP: SUBCLOUD_BOOTSTRAP_GET_FILE_CONTENTS,
|
|
CONFIG: SUBCLOUD_CONFIG_GET_FILE_CONTENTS,
|
|
}
|
|
RESUME_PREP_UPDATE_STATUS = {
|
|
INSTALL: consts.DEPLOY_STATE_PRE_INSTALL,
|
|
BOOTSTRAP: consts.DEPLOY_STATE_PRE_BOOTSTRAP,
|
|
CONFIG: consts.DEPLOY_STATE_PRE_CONFIG,
|
|
}
|
|
|
|
|
|
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 = utils.yaml_safe_load(file_item.file.read().decode("utf8"), f)
|
|
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)
|
|
|
|
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 create the subcloud.
|
|
# It will do all the real work...
|
|
subcloud_dict = self.dcmanager_rpc_client.subcloud_deploy_create(
|
|
context, subcloud.id, payload
|
|
)
|
|
|
|
return subcloud_dict
|
|
|
|
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_install(
|
|
self, context: RequestContext, request: pecan.Request, subcloud
|
|
):
|
|
payload = psd_common.get_request_data(
|
|
request, subcloud, SUBCLOUD_INSTALL_GET_FILE_CONTENTS
|
|
)
|
|
if not payload:
|
|
pecan.abort(400, _("Body required"))
|
|
|
|
SubcloudsController.validate_software_deploy_state()
|
|
|
|
if subcloud.deploy_status not in VALID_STATES_FOR_DEPLOY_INSTALL:
|
|
allowed_states_str = ", ".join(VALID_STATES_FOR_DEPLOY_INSTALL)
|
|
pecan.abort(
|
|
400, _("Subcloud deploy status must be either: %s") % allowed_states_str
|
|
)
|
|
|
|
initial_deployment = psd_common.is_initial_deployment(subcloud.name)
|
|
if not initial_deployment:
|
|
pecan.abort(
|
|
400,
|
|
_(
|
|
"The deploy install command can only be used "
|
|
"during initial deployment."
|
|
),
|
|
)
|
|
|
|
unvalidated_sw_version = payload.get("release", subcloud.software_version)
|
|
# get_sw_version will simply return back
|
|
# the passed unvalidated_sw_version after validating it.
|
|
payload["software_version"] = utils.get_sw_version(unvalidated_sw_version)
|
|
|
|
psd_common.populate_payload_with_pre_existing_data(
|
|
payload, subcloud, SUBCLOUD_INSTALL_GET_FILE_CONTENTS
|
|
)
|
|
|
|
psd_common.pre_deploy_install(payload, subcloud)
|
|
|
|
try:
|
|
self.dcmanager_rpc_client.subcloud_deploy_install(
|
|
context,
|
|
subcloud.id,
|
|
payload,
|
|
initial_deployment=True,
|
|
previous_version=subcloud.software_version,
|
|
)
|
|
subcloud_dict = db_api.subcloud_db_model_to_dict(subcloud)
|
|
subcloud_dict["deploy-status"] = consts.DEPLOY_STATE_PRE_INSTALL
|
|
subcloud_dict["software-version"] = payload["software_version"]
|
|
|
|
return subcloud_dict
|
|
except RemoteError as e:
|
|
pecan.abort(422, e.value)
|
|
except Exception:
|
|
LOG.exception("Unable to install subcloud %s" % subcloud.name)
|
|
pecan.abort(500, _("Unable to install 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 = psd_common.get_request_data(
|
|
request, subcloud, SUBCLOUD_BOOTSTRAP_GET_FILE_CONTENTS
|
|
)
|
|
|
|
# Try to load the existing override values
|
|
override_file = psd_common.get_config_file_path(subcloud.name)
|
|
if os.path.exists(override_file):
|
|
if not has_bootstrap_values:
|
|
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)
|
|
|
|
payload["software_version"] = subcloud.software_version
|
|
|
|
psd_common.pre_deploy_bootstrap(
|
|
context, payload, subcloud, has_bootstrap_values
|
|
)
|
|
|
|
try:
|
|
# Ask dcmanager-manager to bootstrap the subcloud.
|
|
self.dcmanager_rpc_client.subcloud_deploy_bootstrap(
|
|
context, subcloud.id, payload, initial_deployment=True
|
|
)
|
|
|
|
# Change the response to correctly display the values
|
|
# that will be updated on the manager.
|
|
subcloud_dict = db_api.subcloud_db_model_to_dict(subcloud)
|
|
subcloud_dict["deploy-status"] = consts.DEPLOY_STATE_PRE_BOOTSTRAP
|
|
subcloud_dict["description"] = payload.get(
|
|
"description", subcloud.description
|
|
)
|
|
subcloud_dict["location"] = payload.get("location", subcloud.location)
|
|
subcloud_dict["management-subnet"] = utils.get_primary_management_subnet(
|
|
payload
|
|
)
|
|
subcloud_dict["management-gateway-ip"] = (
|
|
utils.get_primary_management_gateway_address(payload)
|
|
)
|
|
subcloud_dict["management-start-ip"] = (
|
|
utils.get_primary_management_start_address(payload)
|
|
)
|
|
subcloud_dict["management-end-ip"] = (
|
|
utils.get_primary_management_end_address(payload)
|
|
)
|
|
systemcontroller_primary_gw = (
|
|
utils.get_primary_systemcontroller_gateway_address(payload)
|
|
)
|
|
subcloud_dict["systemcontroller-gateway-ip"] = (
|
|
systemcontroller_primary_gw
|
|
if systemcontroller_primary_gw
|
|
else subcloud.systemcontroller_gateway_ip
|
|
)
|
|
return subcloud_dict
|
|
|
|
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 subcloud.deploy_status not in VALID_STATES_FOR_DEPLOY_CONFIG:
|
|
allowed_states_str = ", ".join(VALID_STATES_FOR_DEPLOY_CONFIG)
|
|
pecan.abort(
|
|
400, _("Subcloud deploy status must be %s") % allowed_states_str
|
|
)
|
|
|
|
if subcloud.prestage_status in consts.STATES_FOR_ONGOING_PRESTAGE:
|
|
pecan.abort(
|
|
400, _("Subcloud prestage is ongoing %s") % subcloud.prestage_status
|
|
)
|
|
|
|
# If the subcloud belongs to a peer group, ensure that
|
|
# it's not being configured in a secondary site.
|
|
if subcloud.peer_group_id is not None:
|
|
peer_group = utils.subcloud_peer_group_get_by_ref(
|
|
context, str(subcloud.peer_group_id)
|
|
)
|
|
if peer_group is not None:
|
|
if peer_group.group_priority != consts.PEER_GROUP_PRIMARY_PRIORITY:
|
|
pecan.abort(
|
|
400,
|
|
_("Subcloud can only be configured in its primary site."),
|
|
)
|
|
|
|
psd_common.populate_payload_with_pre_existing_data(
|
|
payload, subcloud, SUBCLOUD_CONFIG_GET_FILE_CONTENTS
|
|
)
|
|
|
|
psd_common.pre_deploy_config(payload, subcloud)
|
|
|
|
try:
|
|
self.dcmanager_rpc_client.subcloud_deploy_config(
|
|
context, subcloud.id, payload, initial_deployment=True
|
|
)
|
|
subcloud_dict = db_api.subcloud_db_model_to_dict(subcloud)
|
|
subcloud_dict["deploy-status"] = consts.DEPLOY_STATE_PRE_CONFIG
|
|
return subcloud_dict
|
|
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"))
|
|
|
|
def _deploy_complete(self, context: RequestContext, subcloud):
|
|
|
|
# The deployment should be able to be completed when the deploy state
|
|
# is VALID_STATES_FOR_DEPLOY_COMPLETE because the user could have
|
|
# configured the subcloud manually
|
|
if subcloud.deploy_status not in VALID_STATES_FOR_DEPLOY_COMPLETE:
|
|
allowed_states_str = ", ".join(VALID_STATES_FOR_DEPLOY_COMPLETE)
|
|
pecan.abort(
|
|
400,
|
|
_(
|
|
"Subcloud deploy can only be completed when "
|
|
"its deploy status is: %s"
|
|
)
|
|
% allowed_states_str,
|
|
)
|
|
|
|
try:
|
|
# Ask dcmanager-manager to complete the subcloud deployment
|
|
subcloud = self.dcmanager_rpc_client.subcloud_deploy_complete(
|
|
context, subcloud.id
|
|
)
|
|
return subcloud
|
|
|
|
except RemoteError as e:
|
|
pecan.abort(httpclient.UNPROCESSABLE_ENTITY, e.value)
|
|
except Exception:
|
|
LOG.exception("Unable to complete subcloud %s deployment" % subcloud.name)
|
|
pecan.abort(
|
|
httpclient.INTERNAL_SERVER_ERROR,
|
|
_("Unable to complete subcloud deployment"),
|
|
)
|
|
|
|
def _deploy_abort(self, context, subcloud):
|
|
|
|
if subcloud.deploy_status not in VALID_STATES_FOR_DEPLOY_ABORT:
|
|
allowed_states_str = ", ".join(VALID_STATES_FOR_DEPLOY_ABORT)
|
|
pecan.abort(
|
|
400,
|
|
_("Subcloud deploy status must be in one of the following states: %s")
|
|
% allowed_states_str,
|
|
)
|
|
|
|
initial_deployment = psd_common.is_initial_deployment(subcloud.name)
|
|
if not initial_deployment:
|
|
pecan.abort(
|
|
400, _("The subcloud can only be aborted during initial deployment.")
|
|
)
|
|
|
|
try:
|
|
self.dcmanager_rpc_client.subcloud_deploy_abort(
|
|
context, subcloud.id, subcloud.deploy_status
|
|
)
|
|
subcloud_dict = db_api.subcloud_db_model_to_dict(subcloud)
|
|
subcloud_dict["deploy-status"] = utils.ABORT_UPDATE_STATUS[
|
|
subcloud.deploy_status
|
|
]
|
|
return subcloud_dict
|
|
except RemoteError as e:
|
|
pecan.abort(422, e.value)
|
|
except Exception:
|
|
LOG.exception("Unable to abort subcloud %s deployment" % subcloud.name)
|
|
pecan.abort(500, _("Unable to abort subcloud deployment"))
|
|
|
|
def _deploy_resume(self, context: RequestContext, request: pecan.Request, subcloud):
|
|
|
|
if subcloud.deploy_status not in RESUMABLE_STATES:
|
|
allowed_states_str = ", ".join(RESUMABLE_STATES)
|
|
pecan.abort(
|
|
400, _("Subcloud deploy status must be either: %s") % allowed_states_str
|
|
)
|
|
|
|
initial_deployment = psd_common.is_initial_deployment(subcloud.name)
|
|
if not initial_deployment:
|
|
pecan.abort(
|
|
400, _("The subcloud can only be resumed during initial deployment.")
|
|
)
|
|
|
|
# Since both install and config are optional phases,
|
|
# it's necessary to check if they should be executed
|
|
config_file = psd_common.get_config_file_path(
|
|
subcloud.name, consts.DEPLOY_CONFIG
|
|
)
|
|
has_original_install_values = subcloud.data_install
|
|
has_original_config_values = os.path.exists(config_file)
|
|
has_new_install_values = consts.INSTALL_VALUES in request.POST
|
|
has_new_config_values = consts.DEPLOY_CONFIG in request.POST
|
|
has_bootstrap_values = consts.BOOTSTRAP_VALUES in request.POST
|
|
has_config_values = has_original_config_values or has_new_config_values
|
|
has_install_values = has_original_install_values or has_new_install_values
|
|
|
|
base_deploy_states = RESUMABLE_STATES[subcloud.deploy_status]
|
|
if base_deploy_states == [CONFIG] and not has_config_values:
|
|
msg = (
|
|
_(
|
|
"Only deploy phase left is deploy config. "
|
|
"Required %s file was not provided and it was not "
|
|
"previously available. If manually configuring the "
|
|
"subcloud, please run 'dcmanager subcloud deploy complete'"
|
|
)
|
|
% consts.DEPLOY_CONFIG
|
|
)
|
|
pecan.abort(400, msg)
|
|
|
|
# Since the subcloud can be installed manually and the config is optional,
|
|
# skip those phases if the user doesn't provide the install or config values
|
|
# and they are not available from previous executions.
|
|
# Add the deploy complete phase if deploy config is not going to be executed.
|
|
files_for_resume = []
|
|
deploy_states_to_run = []
|
|
for state in base_deploy_states:
|
|
if state == INSTALL and not has_install_values:
|
|
continue
|
|
elif state == CONFIG and not has_config_values:
|
|
deploy_states_to_run.append(COMPLETE)
|
|
else:
|
|
deploy_states_to_run.append(state)
|
|
files_for_resume.extend(FILES_MAPPING[state])
|
|
|
|
payload = psd_common.get_request_data(request, subcloud, files_for_resume)
|
|
|
|
# Only when the deploy state is created, all files should be sent.
|
|
# If the subcloud is in any other state and it receives a file mapping that
|
|
# does not relate to the current state, the deploy resume should be aborted
|
|
states_executed = list(set(DEPLOY_PHASES) - set(base_deploy_states))
|
|
|
|
for state_executed in states_executed:
|
|
if FILES_MAPPING[state_executed][0] in payload.keys():
|
|
message = (
|
|
f"{state_executed.title()} was already executed and "
|
|
f"{FILES_MAPPING[state_executed][0].replace('_', '-')} is not "
|
|
"required"
|
|
)
|
|
pecan.abort(httpclient.BAD_REQUEST, _(message))
|
|
|
|
# Consider the incoming release parameter only if install is one
|
|
# of the pending deploy states
|
|
if INSTALL in deploy_states_to_run:
|
|
SubcloudsController.validate_software_deploy_state()
|
|
unvalidated_sw_version = payload.get("release", subcloud.software_version)
|
|
else:
|
|
LOG.debug(
|
|
"Disregarding release parameter for %s as installation is complete."
|
|
% subcloud.name
|
|
)
|
|
unvalidated_sw_version = subcloud.software_version
|
|
|
|
# get_sw_version will simply return back the passed
|
|
# unvalidated_sw_version after validating it.
|
|
payload["software_version"] = utils.get_sw_version(unvalidated_sw_version)
|
|
|
|
# Need to remove bootstrap_values from the list of files to populate
|
|
# pre existing data so it does not overwrite newly loaded values
|
|
if has_bootstrap_values:
|
|
files_for_resume = [
|
|
f for f in files_for_resume if f not in FILES_MAPPING[BOOTSTRAP]
|
|
]
|
|
psd_common.populate_payload_with_pre_existing_data(
|
|
payload, subcloud, files_for_resume
|
|
)
|
|
|
|
psd_common.validate_sysadmin_password(payload)
|
|
for state in deploy_states_to_run:
|
|
if state == INSTALL:
|
|
psd_common.pre_deploy_install(payload, validate_password=False)
|
|
elif state == BOOTSTRAP:
|
|
psd_common.pre_deploy_bootstrap(
|
|
context,
|
|
payload,
|
|
subcloud,
|
|
has_bootstrap_values,
|
|
validate_password=False,
|
|
)
|
|
elif state == CONFIG:
|
|
psd_common.pre_deploy_config(payload, subcloud, validate_password=False)
|
|
|
|
try:
|
|
self.dcmanager_rpc_client.subcloud_deploy_resume(
|
|
context,
|
|
subcloud.id,
|
|
subcloud.name,
|
|
payload,
|
|
deploy_states_to_run,
|
|
subcloud.software_version,
|
|
)
|
|
|
|
# Change the response to correctly display the values
|
|
# that will be updated on the manager.
|
|
subcloud_dict = db_api.subcloud_db_model_to_dict(subcloud)
|
|
next_deploy_phase = RESUMABLE_STATES[subcloud.deploy_status][0]
|
|
next_deploy_state = RESUME_PREP_UPDATE_STATUS[next_deploy_phase]
|
|
subcloud_dict["deploy-status"] = next_deploy_state
|
|
subcloud_dict["software-version"] = payload["software_version"]
|
|
subcloud_dict["description"] = payload.get(
|
|
"description", subcloud.description
|
|
)
|
|
subcloud_dict["location"] = payload.get("location", subcloud.location)
|
|
subcloud_dict["management-subnet"] = utils.get_primary_management_subnet(
|
|
payload
|
|
)
|
|
subcloud_dict["management-gateway-ip"] = (
|
|
utils.get_primary_management_gateway_address(payload)
|
|
)
|
|
subcloud_dict["management-start-ip"] = (
|
|
utils.get_primary_management_start_address(payload)
|
|
)
|
|
subcloud_dict["management-end-ip"] = (
|
|
utils.get_primary_management_end_address(payload)
|
|
)
|
|
systemcontroller_primary_gw = (
|
|
utils.get_primary_systemcontroller_gateway_address(payload)
|
|
)
|
|
subcloud_dict["systemcontroller-gateway-ip"] = (
|
|
systemcontroller_primary_gw
|
|
if systemcontroller_primary_gw
|
|
else subcloud.systemcontroller_gateway_ip
|
|
)
|
|
return subcloud_dict
|
|
except RemoteError as e:
|
|
pecan.abort(422, e.value)
|
|
except Exception:
|
|
LOG.exception("Unable to resume subcloud %s deployment" % subcloud.name)
|
|
pecan.abort(500, _("Unable to resume subcloud deployment"))
|
|
|
|
def _deploy_enroll(
|
|
self, context: RequestContext, request: pecan.Request, subcloud: models.Subcloud
|
|
):
|
|
if subcloud.deploy_status not in VALID_STATES_FOR_DEPLOY_ENROLL:
|
|
valid_states_str = ", ".join(VALID_STATES_FOR_DEPLOY_ENROLL)
|
|
msg = f"Subcloud deploy status must be either: {valid_states_str}"
|
|
pecan.abort(400, _(msg))
|
|
|
|
has_bootstrap_values = consts.BOOTSTRAP_VALUES in request.POST
|
|
has_install_values = consts.INSTALL_VALUES in request.POST
|
|
|
|
payload = psd_common.get_request_data(
|
|
request, subcloud, SUBCLOUD_ENROLL_GET_FILE_CONTENTS
|
|
)
|
|
|
|
# Try to load the existing override values
|
|
override_file = psd_common.get_config_file_path(subcloud.name)
|
|
if os.path.exists(override_file):
|
|
if not has_bootstrap_values:
|
|
psd_common.populate_payload_with_pre_existing_data(
|
|
payload, subcloud, [consts.BOOTSTRAP_VALUES]
|
|
)
|
|
elif not has_bootstrap_values:
|
|
msg = (
|
|
"Required bootstrap-values file was not provided and it was "
|
|
f"not previously available at {override_file}"
|
|
)
|
|
pecan.abort(400, _(msg))
|
|
|
|
if not has_install_values:
|
|
psd_common.populate_payload_with_pre_existing_data(
|
|
payload, subcloud, [consts.INSTALL_VALUES]
|
|
)
|
|
|
|
psd_common.validate_enroll_parameter(payload)
|
|
|
|
payload["software_version"] = subcloud.software_version
|
|
|
|
# Use bootstrap file verification
|
|
psd_common.pre_deploy_bootstrap(
|
|
context, payload, subcloud, has_bootstrap_values
|
|
)
|
|
|
|
self.dcmanager_rpc_client.subcloud_deploy_enroll(context, subcloud.id, payload)
|
|
subcloud_dict = db_api.subcloud_db_model_to_dict(subcloud)
|
|
|
|
return subcloud_dict
|
|
|
|
@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 == ABORT:
|
|
subcloud = self._deploy_abort(context, subcloud)
|
|
elif verb == RESUME:
|
|
subcloud = self._deploy_resume(context, pecan.request, subcloud)
|
|
elif verb == INSTALL:
|
|
subcloud = self._deploy_install(context, pecan.request, subcloud)
|
|
elif verb == BOOTSTRAP:
|
|
subcloud = self._deploy_bootstrap(context, pecan.request, subcloud)
|
|
elif verb == CONFIG:
|
|
subcloud = self._deploy_config(context, pecan.request, subcloud)
|
|
elif verb == COMPLETE:
|
|
subcloud = self._deploy_complete(context, subcloud)
|
|
elif verb == ENROLL:
|
|
subcloud = self._deploy_enroll(context, pecan.request, subcloud)
|
|
else:
|
|
pecan.abort(400, _("Invalid request"))
|
|
|
|
return subcloud
|