
This commit adds the subcloud deploy complete command to dcmanager. It's used to mark the subcloud deployment as 'complete'. This is useful when the user manually configures the subcloud and wants to finalize the deployment without running 'dcmanager subcloud deploy config'. To run the 'deploy complete' operation deploy status of the subcloud must be 'bootstrap-complete'. This commit also fixes an issue with the value returned from the subcloud_deploy_create function. It was returning the database model object to the RCP call when it should be returning a dictionary. Test Plan: 1. PASS - Bootstrap a subcloud, manually configuring it and then run the deploy complete command (by CLI and directly through the API). Verify that the deploy status updates from 'bootstrap-complete' to 'complete'; 2. PASS - Verify that the command is rejected when the deploy status is not 'bootstrap-complete'. Story: 2010756 Task: 48453 Change-Id: Ie2eca930e4b13a50cc12e8b9eef79bcb5e7c671f Signed-off-by: Gustavo Herzmann <gustavo.herzmann@windriver.com>
484 lines
19 KiB
Python
484 lines
19 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 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'
|
|
|
|
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
|
|
|
|
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_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
|
|
]
|
|
|
|
# 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,
|
|
consts.DEPLOY_STATE_CONFIG_ABORTED
|
|
)
|
|
|
|
VALID_STATES_FOR_DEPLOY_ABORT = (
|
|
consts.DEPLOY_STATE_INSTALLING,
|
|
consts.DEPLOY_STATE_BOOTSTRAPPING,
|
|
consts.DEPLOY_STATE_CONFIGURING
|
|
)
|
|
|
|
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]
|
|
}
|
|
|
|
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 = 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)
|
|
|
|
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'))
|
|
|
|
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)
|
|
|
|
payload['software_version'] = payload.get('release', subcloud.software_version)
|
|
psd_common.populate_payload_with_pre_existing_data(
|
|
payload, subcloud, SUBCLOUD_INSTALL_GET_FILE_CONTENTS)
|
|
|
|
psd_common.pre_deploy_install(payload, subcloud)
|
|
|
|
try:
|
|
# Align the software version of the subcloud with install
|
|
# version. Update the deploy status as pre-install.
|
|
|
|
self.dcmanager_rpc_client.subcloud_deploy_install(
|
|
context, subcloud.id, payload)
|
|
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 = {}
|
|
|
|
# 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.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)
|
|
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:
|
|
self.dcmanager_rpc_client.subcloud_deploy_config(
|
|
context, subcloud.id, payload)
|
|
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 consts.DEPLOY_STATE_BOOTSTRAPPED because the user could have
|
|
# configured the subcloud manually
|
|
if subcloud.deploy_status != consts.DEPLOY_STATE_BOOTSTRAPPED:
|
|
pecan.abort(400, _('Subcloud deploy can only be completed when'
|
|
' its deploy status is: %s')
|
|
% consts.DEPLOY_STATE_BOOTSTRAPPED)
|
|
|
|
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)
|
|
|
|
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 deploy'))
|
|
|
|
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)
|
|
|
|
# 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
|
|
|
|
deploy_states_to_run = RESUMABLE_STATES[subcloud.deploy_status]
|
|
if deploy_states_to_run == [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.") % 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.
|
|
files_for_resume = []
|
|
for state in deploy_states_to_run:
|
|
if state == INSTALL and not has_install_values:
|
|
deploy_states_to_run.remove(state)
|
|
elif state == CONFIG and not has_config_values:
|
|
deploy_states_to_run.remove(state)
|
|
else:
|
|
files_for_resume.extend(FILES_MAPPING[state])
|
|
|
|
payload = psd_common.get_request_data(request, subcloud, files_for_resume)
|
|
|
|
# Consider the incoming release parameter only if install is one
|
|
# of the pending deploy states
|
|
if INSTALL in deploy_states_to_run:
|
|
payload['software_version'] = payload.get('release', subcloud.software_version)
|
|
else:
|
|
payload['software_version'] = subcloud.software_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:
|
|
# Currently the only pre_deploy_config step is validate_sysadmin_password
|
|
# which can't be executed more than once
|
|
pass
|
|
|
|
try:
|
|
self.dcmanager_rpc_client.subcloud_deploy_resume(
|
|
context, subcloud.id, subcloud.name, payload, deploy_states_to_run)
|
|
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']
|
|
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'))
|
|
|
|
@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)
|
|
else:
|
|
pecan.abort(400, _('Invalid request'))
|
|
|
|
return subcloud
|