Browse Source

Add redeploy_server processing

Adds the functionality to redeploy a server in an unguarded fashion,
meaning that the server will not be pre-validated to be in a state that
workloads have been removed.

This is the first targeted action for Shipyard, so a refactoring of the
validators to support more flexibility has been done.

Also adds RBAC controls for specific actions being created, rather than
a binary create action privilege.

Change-Id: I39e3dab6595c5187affb9f2b6edadd0f5f925b0c
changes/93/583793/19
Bryan Strassner 3 years ago
parent
commit
f3749ca3f9
  1. 8
      Makefile
  2. 2
      docs/source/CLI.rst
  3. 16
      docs/source/_static/shipyard.policy.yaml.sample
  4. 78
      docs/source/action-commands.rst
  5. 2
      docs/source/index.rst
  6. 15
      src/bin/shipyard_airflow/etc/shipyard/policy.yaml.sample
  7. 124
      src/bin/shipyard_airflow/shipyard_airflow/control/action/action_validators.py
  8. 89
      src/bin/shipyard_airflow/shipyard_airflow/control/action/actions_api.py
  9. 41
      src/bin/shipyard_airflow/shipyard_airflow/control/validators/validate_committed_revision.py
  10. 76
      src/bin/shipyard_airflow/shipyard_airflow/control/validators/validate_deployment_action.py
  11. 51
      src/bin/shipyard_airflow/shipyard_airflow/control/validators/validate_intermediate_commit.py
  12. 66
      src/bin/shipyard_airflow/shipyard_airflow/control/validators/validate_target_nodes.py
  13. 34
      src/bin/shipyard_airflow/shipyard_airflow/dags/common_step_factory.py
  14. 1
      src/bin/shipyard_airflow/shipyard_airflow/dags/dag_names.py
  15. 3
      src/bin/shipyard_airflow/shipyard_airflow/dags/deploy_site.py
  16. 1
      src/bin/shipyard_airflow/shipyard_airflow/dags/preflight_checks.py
  17. 31
      src/bin/shipyard_airflow/shipyard_airflow/dags/redeploy_server.py
  18. 3
      src/bin/shipyard_airflow/shipyard_airflow/dags/update_site.py
  19. 3
      src/bin/shipyard_airflow/shipyard_airflow/dags/update_software.py
  20. 2
      src/bin/shipyard_airflow/shipyard_airflow/plugins/armada_base_operator.py
  21. 2
      src/bin/shipyard_airflow/shipyard_airflow/plugins/deckhand_base_operator.py
  22. 165
      src/bin/shipyard_airflow/shipyard_airflow/plugins/drydock_base_operator.py
  23. 75
      src/bin/shipyard_airflow/shipyard_airflow/plugins/drydock_destroy_nodes.py
  24. 216
      src/bin/shipyard_airflow/shipyard_airflow/plugins/drydock_nodes.py
  25. 18
      src/bin/shipyard_airflow/shipyard_airflow/plugins/promenade_base_operator.py
  26. 35
      src/bin/shipyard_airflow/shipyard_airflow/plugins/ucp_base_operator.py
  27. 11
      src/bin/shipyard_airflow/shipyard_airflow/plugins/xcom_puller.py
  28. 156
      src/bin/shipyard_airflow/shipyard_airflow/policy.py
  29. 122
      src/bin/shipyard_airflow/tests/unit/control/test_action_validators.py
  30. 333
      src/bin/shipyard_airflow/tests/unit/control/test_actions_api.py
  31. 224
      src/bin/shipyard_airflow/tests/unit/plugins/test_drydock_destroy_nodes_operator.py
  32. 41
      src/bin/shipyard_airflow/tests/unit/plugins/test_drydock_nodes_operator.py
  33. 2
      src/bin/shipyard_client/shipyard_client/cli/create/commands.py
  34. 4
      src/bin/shipyard_client/tests/unit/cli/create/test_create_commands.py
  35. 8
      tools/execute_shipyard_action.sh
  36. 6
      tools/redeploy_server.sh

8
Makefile

@ -69,13 +69,13 @@ docs: clean build_docs
.PHONY: security
security:
cd $(BUILD_CTX)/shipyard_airflow; tox -e bandit
cd $(BUILD_CTX)/shipyard_client; tox -e bandit
cd $(BUILD_CTX)/shipyard_airflow; tox -e bandit
.PHONY: tests
tests:
cd $(BUILD_CTX)/shipyard_airflow; tox
cd $(BUILD_CTX)/shipyard_client; tox
cd $(BUILD_CTX)/shipyard_airflow; tox
# Make targets intended for use by the primary targets above.
@ -130,13 +130,13 @@ clean:
rm -rf $(BUILD_DIR)/*
rm -rf build
rm -rf docs/build
cd $(BUILD_CTX)/shipyard_airflow; rm -rf build
cd $(BUILD_CTX)/shipyard_client; rm -rf build
cd $(BUILD_CTX)/shipyard_airflow; rm -rf build
.PHONY: pep8
pep8:
cd $(BUILD_CTX)/shipyard_airflow; tox -e pep8
cd $(BUILD_CTX)/shipyard_client; tox -e pep8
cd $(BUILD_CTX)/shipyard_airflow; tox -e pep8
.PHONY: helm_lint
helm_lint: clean helm-init

2
docs/source/CLI.rst

@ -245,7 +245,7 @@ id of the action invoked so that it can be queried subsequently.
[--allow-intermediate-commits]
Example:
shipyard create action redeploy_server --param="server-name=mcp"
shipyard create action redeploy_server --param="target_nodes=mcp"
shipyard create action update_site --param="continue-on-fail=true"
<action_command>

16
docs/source/_static/shipyard.policy.yaml.sample

@ -63,3 +63,19 @@
# GET /api/v1.0/site_statuses
#"workflow_orchestrator:get_site_statuses": "rule:admin_required"
# Create a workflow action to deploy the site
# POST /api/v1.0/actions
#"workflow_orchestrator:action_deploy_site": "rule:admin_required"
# Create a workflow action to update the site
# POST /api/v1.0/actions
#"workflow_orchestrator:action_update_site": "rule:admin_required"
# Create a workflow action to update the site software
# POST /api/v1.0/actions
#"workflow_orchestrator:action_update_software": "rule:admin_required"
# Create a workflow action to redeploy target servers
# POST /api/v1.0/actions
#"workflow_orchestrator:action_redeploy_server": "rule:admin_required"

78
docs/source/API-action-commands.rst → docs/source/action-commands.rst

@ -19,10 +19,47 @@
Action Commands
===============
Example invocation
------------------
API input to create an action follows this pattern, varying the name field:
Without Parmeters::
POST /v1.0/actions
{"name" : "update_site"}
With Parameters::
POST /v1.0/actions
{
"name": "redeploy_server",
"parameters": {
"target_nodes": ["node1", "node2"]
}
}
POST /v1.0/actions
{
"name": "update_site",
"parameters": {
"continue-on-fail": "true"
}
}
Analogous CLI commands::
shipyard create action update_site
shipyard create action redeploy_server --param="target_nodes=node1,node2"
shipyard create action update_site --param="continue-on-fail=true"
Supported actions
-----------------
These actions are currently supported using the Action API
These actions are currently supported using the Action API and CLI
.. _deploy_site:
@ -70,30 +107,47 @@ configuration documents. Steps, conceptually:
#. Armada build
Orchestrates Armada to configure software on the nodes as designed.
Actions under development
~~~~~~~~~~~~~~~~~~~~~~~~~
.. _redeploy_server:
redeploy_server
~~~~~~~~~~~~~~~
Using parameters to indicate which server(s) triggers a teardown and
subsequent deployment of those servers to restore them to the current
committed design.
These actions are under active development
This action is a `target action`, and does not apply the `site action`
labels to the revision of documents in Deckhand. Application of site action
labels is reserved for site actions such as `deploy_site` and `update_site`.
- redeploy_server
Like other `target actions` that will use a baremetal or Kubernetes node as
a target, the `target_nodes` parameter will be used to list the names of the
nodes that will be acted upon.
Using parameters to indicate which server(s) triggers a redeployment of those
servers to the last-known-good design and secrets
.. danger::
At this time, there are no safeguards with regard to the running workload
in place before tearing down a server and the result may be *very*
disruptive to a working site. Users are cautioned to ensure the server
being torn down is not running a critical workload.
To support controlling this, the Shipyard service allows actions to be
associated with RBAC rules. A deployment of Shipyard can restrict access
to this action to help prevent unexpected disaster.
Future actions
~~~~~~~~~~~~~~
These actions are anticipated for development
- test region
test region
Invoke site validation testing - perhaps a baseline is an invocation of all
component's exposed tests or extended health checks. This test would be used
components' exposed tests or extended health checks. This test would be used
as a preflight-style test to ensure all components are in a working state.
- test component
test component
Invoke a particular platform component to test it. This test would be
used to interrogate a particular platform component to ensure it is in a
working state, and that its own downstream dependencies are also
operational
update labels
Triggers an update to the Kubernetes node labels for specified server(s)

2
docs/source/index.rst

@ -26,7 +26,7 @@ control plane life-cycle management, and is part of the `Airship`_ platform.
sampleconf
API
API-action-commands
action-commands
CLI
site-definition-documents
client-user-guide

15
src/bin/shipyard_airflow/etc/shipyard/policy.yaml.sample

@ -63,3 +63,18 @@
# GET /api/v1.0/site_statuses
#"workflow_orchestrator:get_site_statuses": "rule:admin_required"
# Create a workflow action to deploy the site
# POST /api/v1.0/actions
#"workflow_orchestrator:action_deploy_site": "rule:admin_required"
# Create a workflow action to update the site
# POST /api/v1.0/actions
#"workflow_orchestrator:action_update_site": "rule:admin_required"
# Create a workflow action to update the site software
# POST /api/v1.0/actions
#"workflow_orchestrator:action_update_software": "rule:admin_required"
# Create a workflow action to redeploy target servers
# POST /api/v1.0/actions
#"workflow_orchestrator:action_redeploy_server": "rule:admin_required"

124
src/bin/shipyard_airflow/shipyard_airflow/control/action/action_validators.py

@ -18,35 +18,37 @@ there are any validation failures.
"""
import logging
import falcon
from shipyard_airflow.common.document_validators.document_validator_manager \
import DocumentValidationManager
from shipyard_airflow.control import service_clients
from shipyard_airflow.control.validators.validate_deployment_configuration \
import ValidateDeploymentConfigurationBasic
from shipyard_airflow.control.validators.validate_deployment_configuration \
import ValidateDeploymentConfigurationFull
from shipyard_airflow.errors import ApiError
from shipyard_airflow.control.validators.validate_committed_revision import \
ValidateCommittedRevision
from shipyard_airflow.control.validators.validate_deployment_action import \
ValidateDeploymentAction
from shipyard_airflow.control.validators.validate_intermediate_commit import \
ValidateIntermediateCommit
from shipyard_airflow.control.validators.validate_target_nodes import \
ValidateTargetNodes
LOG = logging.getLogger(__name__)
def validate_site_action_full(action):
def validate_committed_revision(action, **kwargs):
"""Invokes a validation that the committed revision of site design exists
"""
validator = ValidateCommittedRevision(action=action)
validator.validate()
def validate_deployment_action_full(action, **kwargs):
"""Validates that the deployment configuration is correctly set up
Checks:
- The deployment configuration from Deckhand using the design version
- If the deployment configuration is missing, error
- The deployment strategy from the deployment configuration.
- If the deployment strategy is specified, but is missing, error.
- Check that there are no cycles in the groups
"""
validator = _SiteActionValidator(
validator = ValidateDeploymentAction(
dh_client=service_clients.deckhand_client(),
action=action,
full_validation=True
@ -54,16 +56,14 @@ def validate_site_action_full(action):
validator.validate()
def validate_site_action_basic(action):
def validate_deployment_action_basic(action, **kwargs):
"""Validates that the DeploymentConfiguration is present
Checks:
- The deployment configuration from Deckhand using the design version
- If the deployment configuration is missing, error
"""
validator = _SiteActionValidator(
validator = ValidateDeploymentAction(
dh_client=service_clients.deckhand_client(),
action=action,
full_validation=False
@ -71,72 +71,22 @@ def validate_site_action_basic(action):
validator.validate()
class _SiteActionValidator:
"""The validator object used by the validate_site_action_<x> functions
def validate_intermediate_commits(action, configdocs_helper, **kwargs):
"""Validates that intermediate commits don't exist
Prevents the execution of an action if there are intermediate commits
since the last site action. If 'allow_intermediate_commits' is set on the
action, allows the action to continue
"""
def __init__(self, dh_client, action, full_validation=True):
self.action = action
self.doc_revision = self._get_doc_revision()
self.cont_on_fail = str(self._action_param(
'continue-on-fail')).lower() == 'true'
if full_validation:
# Perform a complete validation
self.doc_val_mgr = DocumentValidationManager(
dh_client,
self.doc_revision,
[(ValidateDeploymentConfigurationFull,
'deployment-configuration')]
)
else:
# Perform a basic validation only
self.doc_val_mgr = DocumentValidationManager(
dh_client,
self.doc_revision,
[(ValidateDeploymentConfigurationBasic,
'deployment-configuration')]
)
def validate(self):
results = self.doc_val_mgr.validate()
if self.doc_val_mgr.errored:
if self.cont_on_fail:
LOG.warn("Validation failures occured, but 'continue-on-fail' "
"is set to true. Processing continues")
else:
raise ApiError(
title='Document validation failed',
description='InvalidConfigurationDocuments',
status=falcon.HTTP_400,
error_list=results,
retry=False,
)
def _action_param(self, p_name):
"""Retrieve the value of the specified parameter or None if it doesn't
exist
"""
try:
return self.action['parameters'][p_name]
except KeyError:
return None
def _get_doc_revision(self):
"""Finds the revision id for the committed revision"""
doc_revision = self.action.get('committed_rev_id')
if doc_revision is None:
raise ApiError(
title='Invalid document revision',
description='InvalidDocumentRevision',
status=falcon.HTTP_400,
error_list=[{
'message': (
'Action {} with id {} was unable to find a valid '
'committed document revision'.format(
self.action.get('name'),
self.action.get('id')
)
)
}],
retry=False,
)
return doc_revision
validator = ValidateIntermediateCommit(
action=action, configdocs_helper=configdocs_helper)
validator.validate()
def validate_target_nodes(action, **kwargs):
"""Validates the target_nodes parameter
Ensures the target_nodes is present and properly specified.
"""
validator = ValidateTargetNodes(action=action)
validator.validate()

89
src/bin/shipyard_airflow/shipyard_airflow/control/action/actions_api.py

@ -45,19 +45,39 @@ def _action_mappings():
return {
'deploy_site': {
'dag': 'deploy_site',
'validators': [action_validators.validate_site_action_full]
'rbac_policy': policy.ACTION_DEPLOY_SITE,
'validators': [
action_validators.validate_committed_revision,
action_validators.validate_intermediate_commits,
action_validators.validate_deployment_action_full,
]
},
'update_site': {
'dag': 'update_site',
'validators': [action_validators.validate_site_action_full]
'rbac_policy': policy.ACTION_UPDATE_SITE,
'validators': [
action_validators.validate_committed_revision,
action_validators.validate_intermediate_commits,
action_validators.validate_deployment_action_full,
]
},
'update_software': {
'dag': 'update_software',
'validators': [action_validators.validate_site_action_basic]
'rbac_policy': policy.ACTION_UPDATE_SOFTWARE,
'validators': [
action_validators.validate_committed_revision,
action_validators.validate_intermediate_commits,
action_validators.validate_deployment_action_basic,
]
},
'redeploy_server': {
'dag': 'redeploy_server',
'validators': []
'rbac_policy': policy.ACTION_REDEPLOY_SERVER,
'validators': [
action_validators.validate_target_nodes,
action_validators.validate_committed_revision,
action_validators.validate_deployment_action_basic,
]
}
}
@ -100,7 +120,6 @@ class ActionsResource(BaseResource):
resp.location = '/api/v1.0/actions/{}'.format(action['id'])
def create_action(self, action, context, allow_intermediate_commits=False):
action_mappings = _action_mappings()
# use uuid assigned for this request as the id of the action.
action['id'] = ulid.ulid()
# the invoking user
@ -109,12 +128,18 @@ class ActionsResource(BaseResource):
action['timestamp'] = str(datetime.utcnow())
# validate that action is supported.
LOG.info("Attempting action: %s", action['name'])
action_mappings = _action_mappings()
if action['name'] not in action_mappings:
raise ApiError(
title='Unable to start action',
description='Unsupported Action: {}'.format(action['name']))
dag = action_mappings.get(action['name'])['dag']
action_cfg = action_mappings.get(action['name'])
# check access to specific actions - lack of access will exception out
policy.check_auth(context, action_cfg['rbac_policy'])
dag = action_cfg['dag']
action['dag_id'] = dag
# Set up configdocs_helper
@ -122,18 +147,19 @@ class ActionsResource(BaseResource):
# Retrieve last committed design revision
action['committed_rev_id'] = self.get_committed_design_version()
# Check for intermediate commit
self.check_intermediate_commit_revision(allow_intermediate_commits)
# Set if intermediate commits are ignored
action['allow_intermediate_commits'] = allow_intermediate_commits
# populate action parameters if they are not set
if 'parameters' not in action:
action['parameters'] = {}
# validate if there is any validation to do
for validator in action_mappings.get(action['name'])['validators']:
# validators will raise ApiError if they are not validated.
validator(action)
for validator in action_cfg['validators']:
# validators will raise ApiError if they fail validation.
# validators are expected to accept action as a parameter, but
# handle all other kwargs (e.g. def vdtr(action, **kwargs): even if
# they don't use that parameter.
validator(action=action, configdocs_helper=self.configdocs_helper)
# invoke airflow, get the dag's date
dag_execution_date = self.invoke_airflow_dag(
@ -347,43 +373,16 @@ class ActionsResource(BaseResource):
)
def get_committed_design_version(self):
"""Retrieves the committed design version from Deckhand.
LOG.info("Checking for committed revision in Deckhand...")
Returns None if there is no committed version
"""
committed_rev_id = self.configdocs_helper.get_revision_id(
configdocs_helper.COMMITTED
)
if committed_rev_id:
LOG.info("The committed revision in Deckhand is %d",
committed_rev_id)
return committed_rev_id
else:
raise ApiError(
title='Unable to locate any committed revision in Deckhand',
description='No committed version found in Deckhand',
status=falcon.HTTP_404,
retry=False)
def check_intermediate_commit_revision(self,
allow_intermediate_commits=False):
LOG.info("Checking for intermediate committed revision in Deckhand...")
intermediate_commits = (
self.configdocs_helper.check_intermediate_commit())
if intermediate_commits and not allow_intermediate_commits:
raise ApiError(
title='Intermediate Commit Detected',
description=(
'The current committed revision of documents has '
'other prior commits that have not been used as '
'part of a site action, e.g. update_site. If you '
'are aware and these other commits are intended, '
'please rerun this action with the option '
'`allow-intermediate-commits=True`'),
status=falcon.HTTP_409,
retry=False
)
LOG.info("No committed revision found in Deckhand")
return None

41
src/bin/shipyard_airflow/shipyard_airflow/control/validators/validate_committed_revision.py

@ -0,0 +1,41 @@
# Copyright 2018 AT&T Intellectual Property. All other 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.
import logging
import falcon
from shipyard_airflow.errors import ApiError
LOG = logging.getLogger(__name__)
class ValidateCommittedRevision:
"""Validate that the committed revision was found in Deckhand
Does not perform the actual lookup - only validates that the action has
the value populated with a valid value other than `None`
"""
def __init__(self, action):
self.action = action
def validate(self):
if self.action.get('committed_rev_id') is None:
raise ApiError(
title='No committed configdocs',
description=(
'Unable to locate a committed revision in Deckhand'
),
status=falcon.HTTP_400,
retry=False
)

76
src/bin/shipyard_airflow/shipyard_airflow/control/validators/validate_deployment_action.py

@ -0,0 +1,76 @@
# Copyright 2018 AT&T Intellectual Property. All other 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.
import logging
import falcon
from .validate_deployment_configuration \
import ValidateDeploymentConfigurationBasic
from .validate_deployment_configuration \
import ValidateDeploymentConfigurationFull
from shipyard_airflow.common.document_validators.document_validator_manager \
import DocumentValidationManager
from shipyard_airflow.errors import ApiError
LOG = logging.getLogger(__name__)
class ValidateDeploymentAction:
"""The validator used by the validate_deployment_action_<x> functions
"""
def __init__(self, dh_client, action, full_validation=True):
self.action = action
self.doc_revision = self.action.get('committed_rev_id')
self.cont_on_fail = str(self._action_param(
'continue-on-fail')).lower() == 'true'
if full_validation:
# Perform a complete validation
self.doc_val_mgr = DocumentValidationManager(
dh_client,
self.doc_revision,
[(ValidateDeploymentConfigurationFull,
'deployment-configuration')]
)
else:
# Perform a basic validation only
self.doc_val_mgr = DocumentValidationManager(
dh_client,
self.doc_revision,
[(ValidateDeploymentConfigurationBasic,
'deployment-configuration')]
)
def validate(self):
results = self.doc_val_mgr.validate()
if self.doc_val_mgr.errored:
if self.cont_on_fail:
LOG.warn("Validation failures occured, but 'continue-on-fail' "
"is set to true. Processing continues")
else:
raise ApiError(
title='Document validation failed',
description='InvalidConfigurationDocuments',
status=falcon.HTTP_400,
error_list=results,
retry=False,
)
def _action_param(self, p_name):
"""Retrieve the value of the specified parameter or None if it doesn't
exist
"""
try:
return self.action['parameters'][p_name]
except KeyError:
return None

51
src/bin/shipyard_airflow/shipyard_airflow/control/validators/validate_intermediate_commit.py

@ -0,0 +1,51 @@
# Copyright 2018 AT&T Intellectual Property. All other 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.
import logging
import falcon
from shipyard_airflow.errors import ApiError
LOG = logging.getLogger(__name__)
class ValidateIntermediateCommit:
"""Validtor to ensure that intermediate commits are not present
If allow_intermediate_commits is set on the action, this validator will
not check.
"""
def __init__(self, action, configdocs_helper):
self.action = action
self.configdocs_helper = configdocs_helper
def validate(self):
if self.action.get('allow_intermediate_commits'):
LOG.debug("Intermediate commit check skipped due to user input")
else:
intermediate_commits = (
self.configdocs_helper.check_intermediate_commit())
if intermediate_commits:
raise ApiError(
title='Intermediate commit detected',
description=(
'The current committed revision of documents has '
'other prior commits that have not been used as '
'part of a site action, e.g. update_site. If you '
'are aware and these other commits are intended, '
'please rerun this action with the option '
'`allow-intermediate-commits=True`'),
status=falcon.HTTP_409,
retry=False
)

66
src/bin/shipyard_airflow/shipyard_airflow/control/validators/validate_target_nodes.py

@ -0,0 +1,66 @@
# Copyright 2018 AT&T Intellectual Property. All other 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.
import logging
import falcon
from shipyard_airflow.errors import ApiError
LOG = logging.getLogger(__name__)
class ValidateTargetNodes:
"""Validate that the target_nodes parameter has values in it
For actions that target nodes, this parameter must have at least one value
in it, and each value should be a string
"""
def __init__(self, action):
self.action = action
def validate(self):
parameters = self.action.get('parameters')
valid = parameters is not None
if valid:
# target_nodes parameter should exist
nodes = parameters.get('target_nodes')
valid = nodes is not None
if valid:
# should be able to listify the nodes
try:
node_list = list(nodes)
valid = len(node_list) > 0
except TypeError:
valid = False
if valid:
# each entry should be a string
for s in node_list:
if not isinstance(s, str):
valid = False
break
if valid:
# all valid
return
# something was invalid
raise ApiError(
title='Invalid target_nodes parameter',
description=(
'The target_nodes parameter for this action '
'should be a list with one or more string values '
'representing node names'
),
status=falcon.HTTP_400,
retry=False
)

34
src/bin/shipyard_airflow/shipyard_airflow/dags/common_step_factory.py

@ -23,6 +23,7 @@ try:
from airflow.operators import DeckhandRetrieveRenderedDocOperator
from airflow.operators import DeploymentConfigurationOperator
from airflow.operators import DeckhandCreateSiteActionTagOperator
from airflow.operators import DrydockDestroyNodeOperator
except ImportError:
# for local testing, they are loaded from their source directory
from shipyard_airflow.plugins.concurrency_check_operator import \
@ -33,6 +34,8 @@ except ImportError:
DeploymentConfigurationOperator
from shipyard_airflow.plugins.deckhand_create_site_action_tag import \
DeckhandCreateSiteActionTagOperator
from shipyard_airflow.plugins.drydock_destroy_nodes import \
DrydockDestroyNodeOperator
try:
# modules reside in a flat directory when deployed with dags
@ -61,14 +64,22 @@ class CommonStepFactory(object):
A factory to generate steps that are reused among multiple dags
"""
def __init__(self, parent_dag_name, dag, default_args):
def __init__(self, parent_dag_name, dag, default_args, action_type):
"""Creates a factory
Uses the specified parent_dag_name
:param parent_dag_name: the name of the base DAG that this step
factory will service
:param dag: the dag object
:param default_args: the default args from the dag that will be used
by steps in lieu of overridden values.
:action_type: defines the type of action - site, targeted, possibly
others that will be stored on xcom if the action_xcom step is used.
This can then be used to drive behavior in later steps.
"""
self.parent_dag_name = parent_dag_name
self.dag = dag
self.default_args = default_args
self.action_type = action_type or 'default'
def get_action_xcom(self, task_id=dn.ACTION_XCOM):
"""Generate the action_xcom step
@ -81,11 +92,13 @@ class CommonStepFactory(object):
Defines a push function to store the content of 'action' that is
defined via 'dag_run' in XCOM so that it can be used by the
Operators
Operators. Includes action-related information for later steps.
"""
kwargs['ti'].xcom_push(key='action',
value=kwargs['dag_run'].conf['action'])
kwargs['ti'].xcom_push(key='action_type',
value=self.action_type)
return PythonOperator(task_id=task_id,
dag=self.dag,
@ -189,6 +202,21 @@ class CommonStepFactory(object):
on_failure_callback=step_failure_handler,
dag=self.dag)
def get_unguarded_destroy_servers(self, task_id=dn.DESTROY_SERVER):
"""Generates an unguarded destroy server step.
This version of destroying servers does no pre-validations or extra
shutdowns of anything. It unconditionally triggers Drydock to destroy
the server. The counterpart to this step is the subdag returned by the
get_destroy_server method below.
"""
return DrydockDestroyNodeOperator(
shipyard_conf=config_path,
main_dag_name=self.parent_dag_name,
task_id=task_id,
on_failure_callback=step_failure_handler,
dag=self.dag)
def get_destroy_server(self, task_id=dn.DESTROY_SERVER_DAG_NAME):
"""Generate a destroy server step

1
src/bin/shipyard_airflow/shipyard_airflow/dags/dag_names.py

@ -28,3 +28,4 @@ DEPLOYMENT_CONFIGURATION = 'deployment_configuration'
GET_RENDERED_DOC = 'get_rendered_doc'
SKIP_UPGRADE_AIRFLOW = 'skip_upgrade_airflow'
UPGRADE_AIRFLOW = 'upgrade_airflow'
DESTROY_SERVER = 'destroy_nodes'

3
src/bin/shipyard_airflow/shipyard_airflow/dags/deploy_site.py

@ -45,7 +45,8 @@ dag = DAG(PARENT_DAG_NAME, default_args=default_args, schedule_interval=None)
step_factory = CommonStepFactory(parent_dag_name=PARENT_DAG_NAME,
dag=dag,
default_args=default_args)
default_args=default_args,
action_type='site')
action_xcom = step_factory.get_action_xcom()
concurrency_check = step_factory.get_concurrency_check()

1
src/bin/shipyard_airflow/shipyard_airflow/dags/preflight_checks.py

@ -11,7 +11,6 @@
# 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.
from airflow.models import DAG
try:

31
src/bin/shipyard_airflow/shipyard_airflow/dags/redeploy_server.py

@ -18,13 +18,14 @@ from airflow import DAG
try:
from common_step_factory import CommonStepFactory
from validate_site_design import BAREMETAL
except ImportError:
from shipyard_airflow.dags.common_step_factory import CommonStepFactory
from shipyard_airflow.dags.validate_site_design import BAREMETAL
"""redeploy_server
The top-level orchestration DAG for redeploying a server using the Undercloud
platform.
The top-level orchestration DAG for redeploying server(s).
"""
PARENT_DAG_NAME = 'redeploy_server'
@ -45,23 +46,29 @@ dag = DAG(PARENT_DAG_NAME, default_args=default_args, schedule_interval=None)
step_factory = CommonStepFactory(parent_dag_name=PARENT_DAG_NAME,
dag=dag,
default_args=default_args)
default_args=default_args,
action_type='targeted')
action_xcom = step_factory.get_action_xcom()
concurrency_check = step_factory.get_concurrency_check()
preflight = step_factory.get_preflight()
get_rendered_doc = step_factory.get_get_rendered_doc()
deployment_configuration = step_factory.get_deployment_configuration()
validate_site_design = step_factory.get_validate_site_design()
destroy_server = step_factory.get_destroy_server()
validate_site_design = step_factory.get_validate_site_design(
targets=[BAREMETAL]
)
# TODO(bryan-strassner): When the rest of the necessary functionality is in
# place, this step may need to be replaced with the guarded version of
# destroying servers.
# For now, this is the unguarded action, which will tear down the server
# without concern for any workload.
destroy_server = step_factory.get_unguarded_destroy_servers()
drydock_build = step_factory.get_drydock_build()
# DAG Wiring
concurrency_check.set_upstream(action_xcom)
preflight.set_upstream(concurrency_check)
get_rendered_doc.set_upstream(preflight)
deployment_configuration.set_upstream(get_rendered_doc)
validate_site_design.set_upstream(deployment_configuration)
deployment_configuration.set_upstream(action_xcom)
validate_site_design.set_upstream([
concurrency_check,
deployment_configuration
])
destroy_server.set_upstream(validate_site_design)
drydock_build.set_upstream(destroy_server)

3
src/bin/shipyard_airflow/shipyard_airflow/dags/update_site.py

@ -49,7 +49,8 @@ dag = DAG(PARENT_DAG_NAME, default_args=default_args, schedule_interval=None)
step_factory = CommonStepFactory(parent_dag_name=PARENT_DAG_NAME,
dag=dag,
default_args=default_args)
default_args=default_args,
action_type='site')
action_xcom = step_factory.get_action_xcom()
concurrency_check = step_factory.get_concurrency_check()

3
src/bin/shipyard_airflow/shipyard_airflow/dags/update_software.py

@ -46,7 +46,8 @@ dag = DAG(PARENT_DAG_NAME, default_args=default_args, schedule_interval=None)
step_factory = CommonStepFactory(parent_dag_name=PARENT_DAG_NAME,
dag=dag,
default_args=default_args)
default_args=default_args,
action_type='site')
action_xcom = step_factory.get_action_xcom()
concurrency_check = step_factory.get_concurrency_check()

2
src/bin/shipyard_airflow/shipyard_airflow/plugins/armada_base_operator.py

@ -81,7 +81,7 @@ class ArmadaBaseOperator(UcpBaseOperator):
self.xcom_pusher = XcomPusher(self.task_instance)
# Logs uuid of action performed by the Operator
LOG.info("Armada Operator for action %s", self.action_info['id'])
LOG.info("Armada Operator for action %s", self.action_id)
# Set up armada client
self.armada_client = self._init_armada_client(

2
src/bin/shipyard_airflow/shipyard_airflow/plugins/deckhand_base_operator.py

@ -93,7 +93,7 @@ class DeckhandBaseOperator(UcpBaseOperator):
# Logs uuid of Shipyard action
LOG.info("Executing Shipyard Action %s",
self.action_info['id'])
self.action_id)
# Retrieve Endpoint Information
self.deckhand_svc_endpoint = self.endpoints.endpoint_by_name(

165
src/bin/shipyard_airflow/shipyard_airflow/plugins/drydock_base_operator.py

@ -17,7 +17,6 @@ import logging
import time
from urllib.parse import urlparse
from airflow.exceptions import AirflowException
from airflow.plugins_manager import AirflowPlugin
from airflow.utils.decorators import apply_defaults
@ -51,13 +50,11 @@ LOG = logging.getLogger(__name__)
class DrydockBaseOperator(UcpBaseOperator):
"""Drydock Base Operator
All drydock related workflow operators will use the drydock
base operator as the parent and inherit attributes and methods
from this class
"""
@apply_defaults
@ -85,7 +82,6 @@ class DrydockBaseOperator(UcpBaseOperator):
the action and the deployment configuration
"""
super(DrydockBaseOperator,
self).__init__(
pod_selector_pattern=[{'pod_pattern': 'drydock-api',
@ -97,40 +93,36 @@ class DrydockBaseOperator(UcpBaseOperator):
self.redeploy_server = redeploy_server
self.svc_session = svc_session
self.svc_token = svc_token
self.target_nodes = None
def run_base(self, context):
"""Base setup/processing for Drydock operators
# Logs uuid of action performed by the Operator
LOG.info("DryDock Operator for action %s", self.action_info['id'])
:param context: the context supplied by the dag_run in Airflow
"""
LOG.debug("Drydock Operator for action %s", self.action_id)
# if continue processing is false, don't bother setting up things.
if self._continue_processing_flag():
self._setup_drydock_client()
def _continue_processing_flag(self):
"""Checks if this processing should continue or not
# Skip workflow if health checks on Drydock failed and continue-on-fail
# option is turned on
Skip workflow if health checks on Drydock failed and continue-on-fail
option is turned on.
Returns the self.continue_processing value.
"""
if self.xcom_puller.get_check_drydock_continue_on_fail():
LOG.info("Skipping %s as health checks on Drydock have "
"failed and continue-on-fail option has been "
"turned on", self.__class__.__name__)
# Set continue processing to False
self.continue_processing = False
return
# Retrieve information of the server that we want to redeploy if user
# executes the 'redeploy_server' dag
# Set node filter to be the server that we want to redeploy
if self.action_info['dag_id'] == 'redeploy_server':
self.redeploy_server = (
self.action_info['parameters']['server-name'])
if self.redeploy_server:
LOG.info("Server to be redeployed is %s",
self.redeploy_server)
self.node_filter = self.redeploy_server
else:
raise AirflowException('%s was unable to retrieve the '
'server to be redeployed.'
% self.__class__.__name__)
return self.continue_processing
def _setup_drydock_client(self):
"""Setup the drydock client for use by this operator"""
# Retrieve Endpoint Information
self.drydock_svc_endpoint = self.endpoints.endpoint_by_name(
service_endpoint.DRYDOCK
@ -145,31 +137,25 @@ class DrydockBaseOperator(UcpBaseOperator):
# information.
# The DrydockSession will care for TCP connection pooling
# and header management
LOG.info("Build DryDock Session")
dd_session = session.DrydockSession(drydock_url.hostname,
port=drydock_url.port,
auth_gen=self._auth_gen)
# Raise Exception if we are not able to set up the session
if dd_session:
LOG.info("Successfully Set Up DryDock Session")
else:
if not dd_session:
raise DrydockClientUseFailureException(
"Failed to set up Drydock Session!"
)
# Use the DrydockSession to build a DrydockClient that can
# be used to make one or more API calls
LOG.info("Create DryDock Client")
self.drydock_client = client.DrydockClient(dd_session)
# Raise Exception if we are not able to build the client
if self.drydock_client:
LOG.info("Successfully Set Up DryDock client")
else:
if not self.drydock_client:
raise DrydockClientUseFailureException(
"Failed to set up Drydock Client!"
)
LOG.info("Drydock Session and Client etablished.")
@shipyard_service_token
def _auth_gen(self):
@ -376,6 +362,115 @@ class DrydockBaseOperator(UcpBaseOperator):
"Unable to retrieve subtask info!"
)
def get_successes_for_task(self, task_id, extend_success=True):
"""Discover the successful nodes based on the current task id.
:param task_id: The id of the task
:param extend_successes: determines if this result extends successes
or simply reports on the task.
Gets the set of successful nodes by examining the self.drydock_task_id.
The children are traversed recursively to display each sub-task's
information.
Only a reported success at the parent task indicates success of the
task. Drydock is assumed to roll up overall success to the top level.
"""
success_nodes = []
try:
task_dict = self.get_task_dict(task_id)
task_status = task_dict.get('status', "Not Specified")
task_result = task_dict.get('result')
if task_result is None:
LOG.warn("Task result is missing for task %s, with status %s."
" Neither successes nor further details can be"
" extracted from this result",
task_id, task_status)
else:
if extend_success:
try:
# successes and failures on the task result drive the
# interpretation of success or failure for this
# workflow.
# - Any node that is _only_ success for a task is a
# success to us.
# - Any node that is listed as a failure is a failure.
# This implies that a node listed as a success and a
# failure is a failure. E.g. some subtasks succeeded
# and some failed
t_successes = task_result.get('successes', [])
t_failures = task_result.get('failures', [])
actual_successes = set(t_successes) - set(t_failures)
# acquire the successes from success nodes
success_nodes.extend(actual_successes)
LOG.info("Nodes <%s> added as successes for task %s",
", ".join(success_nodes), task_id)
except KeyError:
# missing key on the path to getting nodes - don't add
LOG.warn(
"Missing successes field on result of task %s, "
"but a success field was expected. No successes"
" can be extracted from this result", task_id
)
pass
_report_task_info(task_id, task_result, task_status)
# for each child, report only the step info, do not add to overall
# success list.
for ch_task_id in task_dict.get('subtask_id_list', []):
success_nodes.extend(
self.get_successes_for_task(ch_task_id,
extend_success=False)
)
except Exception:
# since we are reporting task results, if we can't get the
# results, do not block the processing.
LOG.warn("Failed to retrieve a result for task %s. Exception "
"follows:", task_id, exc_info=True)
# deduplicate and return
return set(success_nodes)
def gen_node_name_filter(node_names):
"""Generates a drydock compatible node filter using only node names
:param node_names: the nodes with which to create a filter
"""
return {
'filter_set_type': 'union',
'filter_set': [
{
'filter_type': 'union',
'node_names': node_names
}
]
}
def _report_task_info(task_id, task_result, task_status):
"""Logs information regarding a task.
:param task_id: id of the task
:param task_result: The result dictionary of the task
:param task_status: The status for the task
"""
# setup fields, or defaults if missing values
task_failures = task_result.get('failures', [])
task_successes = task_result.get('successes', [])
result_details = task_result.get('details', {'messageList': []})
result_status = task_result.get('status', "No status supplied")
LOG.info("Task %s with status %s/%s reports successes: [%s] and"
" failures: [%s]", task_id, task_status, result_status,
", ".join(task_successes), ", ".join(task_failures))
for message_item in result_details['messageList']:
context_type = message_item.get('context_type', 'N/A')
context_id = message_item.get('context', 'N/A')
message = message_item.get('message', "No message text supplied")
error = message_item.get('error', False)
timestamp = message_item.get('ts', 'No timestamp supplied')
LOG.info(" - Task %s for item %s:%s has message: %s [err=%s, at %s]",
task_id, context_type, context_id, message, error, timestamp)
class DrydockBaseOperatorPlugin(AirflowPlugin):

75
src/bin/shipyard_airflow/shipyard_airflow/plugins/drydock_destroy_nodes.py

@ -11,38 +11,91 @@
# 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.
"""Invoke the Drydock steps for destroying a node."""
import logging
import time
from airflow.exceptions import AirflowException
from airflow.plugins_manager import AirflowPlugin
try:
from drydock_base_operator import DrydockBaseOperator
from drydock_base_operator import gen_node_name_filter
from drydock_errors import (
DrydockTaskFailedException,
DrydockTaskTimeoutException
)
except ImportError:
from shipyard_airflow.plugins.drydock_base_operator import \
DrydockBaseOperator
from shipyard_airflow.plugins.drydock_base_operator import \
gen_node_name_filter
from shipyard_airflow.plugins.drydock_errors import (
DrydockTaskFailedException,
DrydockTaskTimeoutException
)
LOG = logging.getLogger(__name__)
class DrydockDestroyNodeOperator(DrydockBaseOperator):
"""Drydock Destroy Node Operator
This operator will trigger drydock to destroy a bare metal
node
"""
def do_execute(self):
self.successes = []
LOG.info("Destroying nodes [%s]", ", ".join(self.target_nodes))
self.setup_configured_values()
self.node_filter = gen_node_name_filter(self.target_nodes)
self.execute_destroy()
self.successes = self.get_successes_for_task(self.drydock_task_id)
self.report_summary()
if not self.is_destroy_successful():
raise AirflowException(
"One or more nodes requested for destruction failed to destroy"
)
def setup_configured_values(self):
"""Retrieve and localize the interval and timeout values for destroy
"""
self.dest_interval = self.dc['physical_provisioner.destroy_interval']
self.dest_timeout = self.dc['physical_provisioner.destroy_timeout']
def execute_destroy(self):
"""Run the task to destroy the nodes specified in the node_filter
:param node_filter: The Drydock node filter with the nodes to destroy
"""
task_name = 'destroy_nodes'
self.create_task(task_name)
try:
self.query_task(self.dest_interval, self.dest_timeout)
except DrydockTaskFailedException:
LOG.exception("Task %s has failed. Some nodes may have been "
"destroyed. The report at the end of processing "
"this step contains the results", task_name)
except DrydockTaskTimeoutException:
LOG.warn("Task %s has timed out after %s seconds. Some nodes may "
"have been destroyed. The report at the end of "
"processing this step contains the results", task_name,
self.dest_timeout)
def report_summary(self):
"""Reports the successfully destroyed nodes"""
failed = list(set(self.target_nodes) - set(self.successes))
LOG.info("===== Destroy Nodes Summary =====")
LOG.info(" Nodes requested: %s", ", ".join(sorted(self.target_nodes)))
LOG.info(" Nodes destroyed: %s ", ", ".join(sorted(self.successes)))
LOG.info(" Nodes not destroyed: %s", ", ".join(sorted(failed)))
LOG.info("===== End Destroy Nodes Summary =====")
# NOTE: This is a PlaceHolder function. The 'destroy_node'
# functionalities in DryDock is being worked on and is not
# ready at the moment.
LOG.info("Destroying node %s from cluster...",
self.redeploy_server)
time.sleep(15)
LOG.info("Successfully deleted node %s", self.redeploy_server)
def is_destroy_successful(self):
"""Boolean if the destroy nodes was completely succesful."""
failed = set(self.target_nodes) - set(self.successes)
return not failed
class DrydockDestroyNodeOperatorPlugin(AirflowPlugin):

216
src/bin/shipyard_airflow/shipyard_airflow/plugins/drydock_nodes.py

@ -36,6 +36,7 @@ from shipyard_airflow.common.deployment_group.node_lookup import NodeLookup
try:
import check_k8s_node_status
from drydock_base_operator import DrydockBaseOperator
from drydock_base_operator import gen_node_name_filter
from drydock_errors import (
DrydockTaskFailedException,
DrydockTaskTimeoutException
@ -44,6 +45,8 @@ except ImportError:
from shipyard_airflow.plugins import check_k8s_node_status
from shipyard_airflow.plugins.drydock_base_operator import \
DrydockBaseOperator
from shipyard_airflow.plugins.drydock_base_operator import \
gen_node_name_filter
from shipyard_airflow.plugins.drydock_errors import (
DrydockTaskFailedException,
DrydockTaskTimeoutException
@ -61,9 +64,8 @@ class DrydockNodesOperator(DrydockBaseOperator):
def do_execute(self):
self._setup_configured_values()
# setup self.strat_name and self.strategy
self.strategy = {}
self._setup_deployment_strategy()
# setup self.strategy
self.strategy = self.get_deployment_strategy()
dgm = _get_deployment_group_manager(
self.strategy['groups'],
_get_node_lookup(self.drydock_client, self.design_ref)
@ -119,7 +121,7 @@ class DrydockNodesOperator(DrydockBaseOperator):
"""
LOG.info("Group %s is preparing nodes", group.name)
self.node_filter = _gen_node_name_filter(group.actionable_nodes)
self.node_filter = gen_node_name_filter(group.actionable_nodes)
return self._execute_task('prepare_nodes',
self.prep_interval,
self.prep_timeout)
@ -132,7 +134,7 @@ class DrydockNodesOperator(DrydockBaseOperator):
"""
LOG.info("Group %s is deploying nodes", group.name)
self.node_filter = _gen_node_name_filter(group.actionable_nodes)
self.node_filter = gen_node_name_filter(group.actionable_nodes)
task_result = self._execute_task('deploy_nodes',
self.dep_interval,
self.dep_timeout)
@ -223,103 +225,76 @@ class DrydockNodesOperator(DrydockBaseOperator):
# Other AirflowExceptions will fail the whole task - let them do this.
# find successes
result.successes = self._get_successes_for_task(self.drydock_task_id)
result.successes = self.get_successes_for_task(self.drydock_task_id)
return result
def _get_successes_for_task(self, task_id, extend_success=True):
"""Discover the successful nodes based on the current task id.
:param task_id: The id of the task
:param extend_successes: determines if this result extends successes
or simply reports on the task.
Gets the set of successful nodes by examining the self.drydock_task_id.
The children are traversed recursively to display each sub-task's
information.
Only a reported success at the parent task indicates success of the
task. Drydock is assumed to roll up overall success to the top level.
"""
success_nodes = []
try:
task_dict = self.get_task_dict(task_id)
task_status = task_dict.get('status', "Not Specified")
task_result = task_dict.get('result')
if task_result is None:
LOG.warn("Task result is missing for task %s, with status %s."
" Neither successes nor further details can be"
" extracted from this result",
task_id, task_status)
else:
if extend_success:
try:
# successes and failures on the task result drive the
# interpretation of success or failure for this
# workflow.
# - Any node that is _only_ success for a task is a
# success to us.
# - Any node that is listed as a failure is a failure.
# This implies that a node listed as a success and a
# failure is a failure. E.g. some subtasks succeeded
# and some failed
t_successes = task_result.get('successes', [])
t_failures = task_result.get('failures', [])
actual_successes = set(t_successes) - set(t_failures)
# acquire the successes from success nodes
success_nodes.extend(actual_successes)
LOG.info("Nodes <%s> added as successes for task %s",
", ".join(success_nodes), task_id)
except KeyError:
# missing key on the path to getting nodes - don't add
LOG.warn(
"Missing successes field on result of task %s, "
"but a success field was expected. No successes"
" can be extracted from this result", task_id
)
pass
_report_task_info(task_id, task_result, task_status)
# for each child, report only the step info, do not add to overall
# success list.
for ch_task_id in task_dict.get('subtask_id_list', []):
success_nodes.extend(
self._get_successes_for_task(ch_task_id,
extend_success=False)
)
except Exception:
# since we are reporting task results, if we can't get the
# results, do not block the processing.
LOG.warn("Failed to retrieve a result for task %s. Exception "
"follows:", task_id, exc_info=True)
# deduplicate and return
return set(success_nodes)
def _setup_deployment_strategy(self):
def get_deployment_strategy(self):
"""Determine the deployment strategy
Uses the specified strategy from the deployment configuration
or returns a default configuration of 'all-at-once'
"""
self.strat_name = self.dc['physical_provisioner.deployment_strategy']
if self.strat_name:
# if there is a deployment strategy specified, get it and use it
self.strategy = self.get_unique_doc(
name=self.strat_name,
schema="shipyard/DeploymentStrategy/v1"
)
if self.target_nodes:
# Set up a strategy with one group with the list of nodes, so those
# nodes are the only nodes processed.
LOG.info("Seting up deployment strategy using targeted nodes")
strat_name = 'targeted nodes'
strategy = gen_simple_deployment_strategy(name='target-group',
nodes=self.target_nodes)
else:
# The default behavior is to deploy all nodes, and fail if
# any nodes fail to deploy.
self.strat_name = 'all-at-once (defaulted)'
self.strategy = _default_deployment_strategy()
# Otherwise, do a strategy for the site - either from the
# configdocs or a default "everything".
strat_name = self.dc['physical_provisioner.deployment_strategy']
if strat_name:
# if there is a deployment strategy specified, use it
strategy = self.get_unique_doc(
name=strat_name,