Merge "Add redeploy_server processing"
This commit is contained in:
commit
7bd1b050bc
8
Makefile
8
Makefile
|
@ -69,13 +69,13 @@ docs: clean build_docs
|
||||||
|
|
||||||
.PHONY: security
|
.PHONY: security
|
||||||
security:
|
security:
|
||||||
cd $(BUILD_CTX)/shipyard_airflow; tox -e bandit
|
|
||||||
cd $(BUILD_CTX)/shipyard_client; tox -e bandit
|
cd $(BUILD_CTX)/shipyard_client; tox -e bandit
|
||||||
|
cd $(BUILD_CTX)/shipyard_airflow; tox -e bandit
|
||||||
|
|
||||||
.PHONY: tests
|
.PHONY: tests
|
||||||
tests:
|
tests:
|
||||||
cd $(BUILD_CTX)/shipyard_airflow; tox
|
|
||||||
cd $(BUILD_CTX)/shipyard_client; tox
|
cd $(BUILD_CTX)/shipyard_client; tox
|
||||||
|
cd $(BUILD_CTX)/shipyard_airflow; tox
|
||||||
|
|
||||||
# Make targets intended for use by the primary targets above.
|
# Make targets intended for use by the primary targets above.
|
||||||
|
|
||||||
|
@ -130,13 +130,13 @@ clean:
|
||||||
rm -rf $(BUILD_DIR)/*
|
rm -rf $(BUILD_DIR)/*
|
||||||
rm -rf build
|
rm -rf build
|
||||||
rm -rf docs/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_client; rm -rf build
|
||||||
|
cd $(BUILD_CTX)/shipyard_airflow; rm -rf build
|
||||||
|
|
||||||
.PHONY: pep8
|
.PHONY: pep8
|
||||||
pep8:
|
pep8:
|
||||||
cd $(BUILD_CTX)/shipyard_airflow; tox -e pep8
|
|
||||||
cd $(BUILD_CTX)/shipyard_client; tox -e pep8
|
cd $(BUILD_CTX)/shipyard_client; tox -e pep8
|
||||||
|
cd $(BUILD_CTX)/shipyard_airflow; tox -e pep8
|
||||||
|
|
||||||
.PHONY: helm_lint
|
.PHONY: helm_lint
|
||||||
helm_lint: clean helm-init
|
helm_lint: clean helm-init
|
||||||
|
|
|
@ -245,7 +245,7 @@ id of the action invoked so that it can be queried subsequently.
|
||||||
[--allow-intermediate-commits]
|
[--allow-intermediate-commits]
|
||||||
|
|
||||||
Example:
|
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"
|
shipyard create action update_site --param="continue-on-fail=true"
|
||||||
|
|
||||||
<action_command>
|
<action_command>
|
||||||
|
|
|
@ -63,3 +63,19 @@
|
||||||
# GET /api/v1.0/site_statuses
|
# GET /api/v1.0/site_statuses
|
||||||
#"workflow_orchestrator:get_site_statuses": "rule:admin_required"
|
#"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"
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,47 @@
|
||||||
Action Commands
|
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
|
Supported actions
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
These actions are currently supported using the Action API
|
These actions are currently supported using the Action API and CLI
|
||||||
|
|
||||||
.. _deploy_site:
|
.. _deploy_site:
|
||||||
|
|
||||||
|
@ -70,30 +107,47 @@ configuration documents. Steps, conceptually:
|
||||||
#. Armada build
|
#. Armada build
|
||||||
Orchestrates Armada to configure software on the nodes as designed.
|
Orchestrates Armada to configure software on the nodes as designed.
|
||||||
|
|
||||||
Actions under development
|
.. _redeploy_server:
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
These actions are under active development
|
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.
|
||||||
|
|
||||||
- redeploy_server
|
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`.
|
||||||
|
|
||||||
Using parameters to indicate which server(s) triggers a redeployment of those
|
Like other `target actions` that will use a baremetal or Kubernetes node as
|
||||||
servers to the last-known-good design and secrets
|
a target, the `target_nodes` parameter will be used to list the names of the
|
||||||
|
nodes that will be acted upon.
|
||||||
|
|
||||||
|
.. 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
|
Future actions
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
These actions are anticipated for development
|
These actions are anticipated for development
|
||||||
|
|
||||||
- test region
|
test region
|
||||||
|
|
||||||
Invoke site validation testing - perhaps a baseline is an invocation of all
|
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.
|
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
|
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
|
used to interrogate a particular platform component to ensure it is in a
|
||||||
working state, and that its own downstream dependencies are also
|
working state, and that its own downstream dependencies are also
|
||||||
operational
|
operational
|
||||||
|
|
||||||
|
update labels
|
||||||
|
Triggers an update to the Kubernetes node labels for specified server(s)
|
|
@ -26,7 +26,7 @@ control plane life-cycle management, and is part of the `Airship`_ platform.
|
||||||
|
|
||||||
sampleconf
|
sampleconf
|
||||||
API
|
API
|
||||||
API-action-commands
|
action-commands
|
||||||
CLI
|
CLI
|
||||||
site-definition-documents
|
site-definition-documents
|
||||||
client-user-guide
|
client-user-guide
|
||||||
|
|
|
@ -63,3 +63,18 @@
|
||||||
# GET /api/v1.0/site_statuses
|
# GET /api/v1.0/site_statuses
|
||||||
#"workflow_orchestrator:get_site_statuses": "rule:admin_required"
|
#"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"
|
||||||
|
|
|
@ -18,35 +18,37 @@ there are any validation failures.
|
||||||
"""
|
"""
|
||||||
import logging
|
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 import service_clients
|
||||||
from shipyard_airflow.control.validators.validate_deployment_configuration \
|
from shipyard_airflow.control.validators.validate_committed_revision import \
|
||||||
import ValidateDeploymentConfigurationBasic
|
ValidateCommittedRevision
|
||||||
from shipyard_airflow.control.validators.validate_deployment_configuration \
|
from shipyard_airflow.control.validators.validate_deployment_action import \
|
||||||
import ValidateDeploymentConfigurationFull
|
ValidateDeploymentAction
|
||||||
from shipyard_airflow.errors import ApiError
|
from shipyard_airflow.control.validators.validate_intermediate_commit import \
|
||||||
|
ValidateIntermediateCommit
|
||||||
|
from shipyard_airflow.control.validators.validate_target_nodes import \
|
||||||
|
ValidateTargetNodes
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
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
|
"""Validates that the deployment configuration is correctly set up
|
||||||
|
|
||||||
Checks:
|
Checks:
|
||||||
|
|
||||||
- The deployment configuration from Deckhand using the design version
|
- The deployment configuration from Deckhand using the design version
|
||||||
|
|
||||||
- If the deployment configuration is missing, error
|
- If the deployment configuration is missing, error
|
||||||
|
|
||||||
- The deployment strategy from the deployment configuration.
|
- The deployment strategy from the deployment configuration.
|
||||||
|
|
||||||
- If the deployment strategy is specified, but is missing, error.
|
- If the deployment strategy is specified, but is missing, error.
|
||||||
- Check that there are no cycles in the groups
|
- Check that there are no cycles in the groups
|
||||||
"""
|
"""
|
||||||
validator = _SiteActionValidator(
|
validator = ValidateDeploymentAction(
|
||||||
dh_client=service_clients.deckhand_client(),
|
dh_client=service_clients.deckhand_client(),
|
||||||
action=action,
|
action=action,
|
||||||
full_validation=True
|
full_validation=True
|
||||||
|
@ -54,16 +56,14 @@ def validate_site_action_full(action):
|
||||||
validator.validate()
|
validator.validate()
|
||||||
|
|
||||||
|
|
||||||
def validate_site_action_basic(action):
|
def validate_deployment_action_basic(action, **kwargs):
|
||||||
"""Validates that the DeploymentConfiguration is present
|
"""Validates that the DeploymentConfiguration is present
|
||||||
|
|
||||||
Checks:
|
Checks:
|
||||||
|
|
||||||
- The deployment configuration from Deckhand using the design version
|
- The deployment configuration from Deckhand using the design version
|
||||||
|
|
||||||
- If the deployment configuration is missing, error
|
- If the deployment configuration is missing, error
|
||||||
"""
|
"""
|
||||||
validator = _SiteActionValidator(
|
validator = ValidateDeploymentAction(
|
||||||
dh_client=service_clients.deckhand_client(),
|
dh_client=service_clients.deckhand_client(),
|
||||||
action=action,
|
action=action,
|
||||||
full_validation=False
|
full_validation=False
|
||||||
|
@ -71,72 +71,22 @@ def validate_site_action_basic(action):
|
||||||
validator.validate()
|
validator.validate()
|
||||||
|
|
||||||
|
|
||||||
class _SiteActionValidator:
|
def validate_intermediate_commits(action, configdocs_helper, **kwargs):
|
||||||
"""The validator object used by the validate_site_action_<x> functions
|
"""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):
|
validator = ValidateIntermediateCommit(
|
||||||
self.action = action
|
action=action, configdocs_helper=configdocs_helper)
|
||||||
self.doc_revision = self._get_doc_revision()
|
validator.validate()
|
||||||
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):
|
def validate_target_nodes(action, **kwargs):
|
||||||
"""Retrieve the value of the specified parameter or None if it doesn't
|
"""Validates the target_nodes parameter
|
||||||
exist
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self.action['parameters'][p_name]
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_doc_revision(self):
|
Ensures the target_nodes is present and properly specified.
|
||||||
"""Finds the revision id for the committed revision"""
|
"""
|
||||||
doc_revision = self.action.get('committed_rev_id')
|
validator = ValidateTargetNodes(action=action)
|
||||||
if doc_revision is None:
|
validator.validate()
|
||||||
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
|
|
||||||
|
|
|
@ -45,19 +45,39 @@ def _action_mappings():
|
||||||
return {
|
return {
|
||||||
'deploy_site': {
|
'deploy_site': {
|
||||||
'dag': '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': {
|
'update_site': {
|
||||||
'dag': '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': {
|
'update_software': {
|
||||||
'dag': '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': {
|
'redeploy_server': {
|
||||||
'dag': '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'])
|
resp.location = '/api/v1.0/actions/{}'.format(action['id'])
|
||||||
|
|
||||||
def create_action(self, action, context, allow_intermediate_commits=False):
|
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.
|
# use uuid assigned for this request as the id of the action.
|
||||||
action['id'] = ulid.ulid()
|
action['id'] = ulid.ulid()
|
||||||
# the invoking user
|
# the invoking user
|
||||||
|
@ -109,12 +128,18 @@ class ActionsResource(BaseResource):
|
||||||
action['timestamp'] = str(datetime.utcnow())
|
action['timestamp'] = str(datetime.utcnow())
|
||||||
# validate that action is supported.
|
# validate that action is supported.
|
||||||
LOG.info("Attempting action: %s", action['name'])
|
LOG.info("Attempting action: %s", action['name'])
|
||||||
|
action_mappings = _action_mappings()
|
||||||
if action['name'] not in action_mappings:
|
if action['name'] not in action_mappings:
|
||||||
raise ApiError(
|
raise ApiError(
|
||||||
title='Unable to start action',
|
title='Unable to start action',
|
||||||
description='Unsupported Action: {}'.format(action['name']))
|
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
|
action['dag_id'] = dag
|
||||||
|
|
||||||
# Set up configdocs_helper
|
# Set up configdocs_helper
|
||||||
|
@ -122,18 +147,19 @@ class ActionsResource(BaseResource):
|
||||||
|
|
||||||
# Retrieve last committed design revision
|
# Retrieve last committed design revision
|
||||||
action['committed_rev_id'] = self.get_committed_design_version()
|
action['committed_rev_id'] = self.get_committed_design_version()
|
||||||
|
# Set if intermediate commits are ignored
|
||||||
# Check for intermediate commit
|
action['allow_intermediate_commits'] = allow_intermediate_commits
|
||||||
self.check_intermediate_commit_revision(allow_intermediate_commits)
|
|
||||||
|
|
||||||
# populate action parameters if they are not set
|
# populate action parameters if they are not set
|
||||||
if 'parameters' not in action:
|
if 'parameters' not in action:
|
||||||
action['parameters'] = {}
|
action['parameters'] = {}
|
||||||
|
|
||||||
# validate if there is any validation to do
|
for validator in action_cfg['validators']:
|
||||||
for validator in action_mappings.get(action['name'])['validators']:
|
# validators will raise ApiError if they fail validation.
|
||||||
# validators will raise ApiError if they are not validated.
|
# validators are expected to accept action as a parameter, but
|
||||||
validator(action)
|
# 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
|
# invoke airflow, get the dag's date
|
||||||
dag_execution_date = self.invoke_airflow_dag(
|
dag_execution_date = self.invoke_airflow_dag(
|
||||||
|
@ -347,43 +373,16 @@ class ActionsResource(BaseResource):
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_committed_design_version(self):
|
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(
|
committed_rev_id = self.configdocs_helper.get_revision_id(
|
||||||
configdocs_helper.COMMITTED
|
configdocs_helper.COMMITTED
|
||||||
)
|
)
|
||||||
|
|
||||||
if committed_rev_id:
|
if committed_rev_id:
|
||||||
LOG.info("The committed revision in Deckhand is %d",
|
LOG.info("The committed revision in Deckhand is %d",
|
||||||
committed_rev_id)
|
committed_rev_id)
|
||||||
|
|
||||||
return committed_rev_id
|
return committed_rev_id
|
||||||
|
LOG.info("No committed revision found in Deckhand")
|
||||||
else:
|
return None
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -23,6 +23,7 @@ try:
|
||||||
from airflow.operators import DeckhandRetrieveRenderedDocOperator
|
from airflow.operators import DeckhandRetrieveRenderedDocOperator
|
||||||
from airflow.operators import DeploymentConfigurationOperator
|
from airflow.operators import DeploymentConfigurationOperator
|
||||||
from airflow.operators import DeckhandCreateSiteActionTagOperator
|
from airflow.operators import DeckhandCreateSiteActionTagOperator
|
||||||
|
from airflow.operators import DrydockDestroyNodeOperator
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# for local testing, they are loaded from their source directory
|
# for local testing, they are loaded from their source directory
|
||||||
from shipyard_airflow.plugins.concurrency_check_operator import \
|
from shipyard_airflow.plugins.concurrency_check_operator import \
|
||||||
|
@ -33,6 +34,8 @@ except ImportError:
|
||||||
DeploymentConfigurationOperator
|
DeploymentConfigurationOperator
|
||||||
from shipyard_airflow.plugins.deckhand_create_site_action_tag import \
|
from shipyard_airflow.plugins.deckhand_create_site_action_tag import \
|
||||||
DeckhandCreateSiteActionTagOperator
|
DeckhandCreateSiteActionTagOperator
|
||||||
|
from shipyard_airflow.plugins.drydock_destroy_nodes import \
|
||||||
|
DrydockDestroyNodeOperator
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# modules reside in a flat directory when deployed with dags
|
# 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
|
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
|
"""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.parent_dag_name = parent_dag_name
|
||||||
self.dag = dag
|
self.dag = dag
|
||||||
self.default_args = default_args
|
self.default_args = default_args
|
||||||
|
self.action_type = action_type or 'default'
|
||||||
|
|
||||||
def get_action_xcom(self, task_id=dn.ACTION_XCOM):
|
def get_action_xcom(self, task_id=dn.ACTION_XCOM):
|
||||||
"""Generate the action_xcom step
|
"""Generate the action_xcom step
|
||||||
|
@ -81,11 +92,13 @@ class CommonStepFactory(object):
|
||||||
|
|
||||||
Defines a push function to store the content of 'action' that is
|
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
|
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',
|
kwargs['ti'].xcom_push(key='action',
|
||||||
value=kwargs['dag_run'].conf['action'])
|
value=kwargs['dag_run'].conf['action'])
|
||||||
|
kwargs['ti'].xcom_push(key='action_type',
|
||||||
|
value=self.action_type)
|
||||||
|
|
||||||
return PythonOperator(task_id=task_id,
|
return PythonOperator(task_id=task_id,
|
||||||
dag=self.dag,
|
dag=self.dag,
|
||||||
|
@ -189,6 +202,21 @@ class CommonStepFactory(object):
|
||||||
on_failure_callback=step_failure_handler,
|
on_failure_callback=step_failure_handler,
|
||||||
dag=self.dag)
|
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):
|
def get_destroy_server(self, task_id=dn.DESTROY_SERVER_DAG_NAME):
|
||||||
"""Generate a destroy server step
|
"""Generate a destroy server step
|
||||||
|
|
||||||
|
|
|
@ -28,3 +28,4 @@ DEPLOYMENT_CONFIGURATION = 'deployment_configuration'
|
||||||
GET_RENDERED_DOC = 'get_rendered_doc'
|
GET_RENDERED_DOC = 'get_rendered_doc'
|
||||||
SKIP_UPGRADE_AIRFLOW = 'skip_upgrade_airflow'
|
SKIP_UPGRADE_AIRFLOW = 'skip_upgrade_airflow'
|
||||||
UPGRADE_AIRFLOW = 'upgrade_airflow'
|
UPGRADE_AIRFLOW = 'upgrade_airflow'
|
||||||
|
DESTROY_SERVER = 'destroy_nodes'
|
||||||
|
|
|
@ -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,
|
step_factory = CommonStepFactory(parent_dag_name=PARENT_DAG_NAME,
|
||||||
dag=dag,
|
dag=dag,
|
||||||
default_args=default_args)
|
default_args=default_args,
|
||||||
|
action_type='site')
|
||||||
|
|
||||||
action_xcom = step_factory.get_action_xcom()
|
action_xcom = step_factory.get_action_xcom()
|
||||||
concurrency_check = step_factory.get_concurrency_check()
|
concurrency_check = step_factory.get_concurrency_check()
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from airflow.models import DAG
|
from airflow.models import DAG
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -18,13 +18,14 @@ from airflow import DAG
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from common_step_factory import CommonStepFactory
|
from common_step_factory import CommonStepFactory
|
||||||
|
from validate_site_design import BAREMETAL
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from shipyard_airflow.dags.common_step_factory import CommonStepFactory
|
from shipyard_airflow.dags.common_step_factory import CommonStepFactory
|
||||||
|
from shipyard_airflow.dags.validate_site_design import BAREMETAL
|
||||||
|
|
||||||
"""redeploy_server
|
"""redeploy_server
|
||||||
|
|
||||||
The top-level orchestration DAG for redeploying a server using the Undercloud
|
The top-level orchestration DAG for redeploying server(s).
|
||||||
platform.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PARENT_DAG_NAME = 'redeploy_server'
|
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,
|
step_factory = CommonStepFactory(parent_dag_name=PARENT_DAG_NAME,
|
||||||
dag=dag,
|
dag=dag,
|
||||||
default_args=default_args)
|
default_args=default_args,
|
||||||
|
action_type='targeted')
|
||||||
|
|
||||||
|
|
||||||
action_xcom = step_factory.get_action_xcom()
|
action_xcom = step_factory.get_action_xcom()
|
||||||
concurrency_check = step_factory.get_concurrency_check()
|
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()
|
deployment_configuration = step_factory.get_deployment_configuration()
|
||||||
validate_site_design = step_factory.get_validate_site_design()
|
validate_site_design = step_factory.get_validate_site_design(
|
||||||
destroy_server = step_factory.get_destroy_server()
|
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()
|
drydock_build = step_factory.get_drydock_build()
|
||||||
|
|
||||||
# DAG Wiring
|
# DAG Wiring
|
||||||
concurrency_check.set_upstream(action_xcom)
|
deployment_configuration.set_upstream(action_xcom)
|
||||||
preflight.set_upstream(concurrency_check)
|
validate_site_design.set_upstream([
|
||||||
get_rendered_doc.set_upstream(preflight)
|
concurrency_check,
|
||||||
deployment_configuration.set_upstream(get_rendered_doc)
|
deployment_configuration
|
||||||
validate_site_design.set_upstream(deployment_configuration)
|
])
|
||||||
destroy_server.set_upstream(validate_site_design)
|
destroy_server.set_upstream(validate_site_design)
|
||||||
drydock_build.set_upstream(destroy_server)
|
drydock_build.set_upstream(destroy_server)
|
||||||
|
|
|
@ -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,
|
step_factory = CommonStepFactory(parent_dag_name=PARENT_DAG_NAME,
|
||||||
dag=dag,
|
dag=dag,
|
||||||
default_args=default_args)
|
default_args=default_args,
|
||||||
|
action_type='site')
|
||||||
|
|
||||||
action_xcom = step_factory.get_action_xcom()
|
action_xcom = step_factory.get_action_xcom()
|
||||||
concurrency_check = step_factory.get_concurrency_check()
|
concurrency_check = step_factory.get_concurrency_check()
|
||||||
|
|
|
@ -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,
|
step_factory = CommonStepFactory(parent_dag_name=PARENT_DAG_NAME,
|
||||||
dag=dag,
|
dag=dag,
|
||||||
default_args=default_args)
|
default_args=default_args,
|
||||||
|
action_type='site')
|
||||||
|
|
||||||
action_xcom = step_factory.get_action_xcom()
|
action_xcom = step_factory.get_action_xcom()
|
||||||
concurrency_check = step_factory.get_concurrency_check()
|
concurrency_check = step_factory.get_concurrency_check()
|
||||||
|
|
|
@ -81,7 +81,7 @@ class ArmadaBaseOperator(UcpBaseOperator):
|
||||||
self.xcom_pusher = XcomPusher(self.task_instance)
|
self.xcom_pusher = XcomPusher(self.task_instance)
|
||||||
|
|
||||||
# Logs uuid of action performed by the Operator
|
# 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
|
# Set up armada client
|
||||||
self.armada_client = self._init_armada_client(
|
self.armada_client = self._init_armada_client(
|
||||||
|
|
|
@ -93,7 +93,7 @@ class DeckhandBaseOperator(UcpBaseOperator):
|
||||||
|
|
||||||
# Logs uuid of Shipyard action
|
# Logs uuid of Shipyard action
|
||||||
LOG.info("Executing Shipyard Action %s",
|
LOG.info("Executing Shipyard Action %s",
|
||||||
self.action_info['id'])
|
self.action_id)
|
||||||
|
|
||||||
# Retrieve Endpoint Information
|
# Retrieve Endpoint Information
|
||||||
self.deckhand_svc_endpoint = self.endpoints.endpoint_by_name(
|
self.deckhand_svc_endpoint = self.endpoints.endpoint_by_name(
|
||||||
|
|
|
@ -17,7 +17,6 @@ import logging
|
||||||
import time
|
import time
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from airflow.exceptions import AirflowException
|
|
||||||
from airflow.plugins_manager import AirflowPlugin
|
from airflow.plugins_manager import AirflowPlugin
|
||||||
from airflow.utils.decorators import apply_defaults
|
from airflow.utils.decorators import apply_defaults
|
||||||
|
|
||||||
|
@ -51,13 +50,11 @@ LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DrydockBaseOperator(UcpBaseOperator):
|
class DrydockBaseOperator(UcpBaseOperator):
|
||||||
|
|
||||||
"""Drydock Base Operator
|
"""Drydock Base Operator
|
||||||
|
|
||||||
All drydock related workflow operators will use the drydock
|
All drydock related workflow operators will use the drydock
|
||||||
base operator as the parent and inherit attributes and methods
|
base operator as the parent and inherit attributes and methods
|
||||||
from this class
|
from this class
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@apply_defaults
|
@apply_defaults
|
||||||
|
@ -85,7 +82,6 @@ class DrydockBaseOperator(UcpBaseOperator):
|
||||||
the action and the deployment configuration
|
the action and the deployment configuration
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super(DrydockBaseOperator,
|
super(DrydockBaseOperator,
|
||||||
self).__init__(
|
self).__init__(
|
||||||
pod_selector_pattern=[{'pod_pattern': 'drydock-api',
|
pod_selector_pattern=[{'pod_pattern': 'drydock-api',
|
||||||
|
@ -97,40 +93,36 @@ class DrydockBaseOperator(UcpBaseOperator):
|
||||||
self.redeploy_server = redeploy_server
|
self.redeploy_server = redeploy_server
|
||||||
self.svc_session = svc_session
|
self.svc_session = svc_session
|
||||||
self.svc_token = svc_token
|
self.svc_token = svc_token
|
||||||
|
self.target_nodes = None
|
||||||
|
|
||||||
def run_base(self, context):
|
def run_base(self, context):
|
||||||
|
"""Base setup/processing for Drydock operators
|
||||||
|
|
||||||
# Logs uuid of action performed by the Operator
|
:param context: the context supplied by the dag_run in Airflow
|
||||||
LOG.info("DryDock Operator for action %s", self.action_info['id'])
|
"""
|
||||||
|
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()
|
||||||
|
|
||||||
# Skip workflow if health checks on Drydock failed and continue-on-fail
|
def _continue_processing_flag(self):
|
||||||
# option is turned on
|
"""Checks if this processing should continue or not
|
||||||
|
|
||||||
|
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():
|
if self.xcom_puller.get_check_drydock_continue_on_fail():
|
||||||
LOG.info("Skipping %s as health checks on Drydock have "
|
LOG.info("Skipping %s as health checks on Drydock have "
|
||||||
"failed and continue-on-fail option has been "
|
"failed and continue-on-fail option has been "
|
||||||
"turned on", self.__class__.__name__)
|
"turned on", self.__class__.__name__)
|
||||||
|
|
||||||
# Set continue processing to False
|
# Set continue processing to False
|
||||||
self.continue_processing = False
|
self.continue_processing = False
|
||||||
|
|
||||||
return
|
return self.continue_processing
|
||||||
|
|
||||||
# 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__)
|
|
||||||
|
|
||||||
|
def _setup_drydock_client(self):
|
||||||
|
"""Setup the drydock client for use by this operator"""
|
||||||
# Retrieve Endpoint Information
|
# Retrieve Endpoint Information
|
||||||
self.drydock_svc_endpoint = self.endpoints.endpoint_by_name(
|
self.drydock_svc_endpoint = self.endpoints.endpoint_by_name(
|
||||||
service_endpoint.DRYDOCK
|
service_endpoint.DRYDOCK
|
||||||
|
@ -145,31 +137,25 @@ class DrydockBaseOperator(UcpBaseOperator):
|
||||||
# information.
|
# information.
|
||||||
# The DrydockSession will care for TCP connection pooling
|
# The DrydockSession will care for TCP connection pooling
|
||||||
# and header management
|
# and header management
|
||||||
LOG.info("Build DryDock Session")
|
|
||||||
dd_session = session.DrydockSession(drydock_url.hostname,
|
dd_session = session.DrydockSession(drydock_url.hostname,
|
||||||
port=drydock_url.port,
|
port=drydock_url.port,
|
||||||
auth_gen=self._auth_gen)
|
auth_gen=self._auth_gen)
|
||||||
|
|
||||||
# Raise Exception if we are not able to set up the session
|
# Raise Exception if we are not able to set up the session
|
||||||
if dd_session:
|
if not dd_session:
|
||||||
LOG.info("Successfully Set Up DryDock Session")
|
|
||||||
else:
|
|
||||||
raise DrydockClientUseFailureException(
|
raise DrydockClientUseFailureException(
|
||||||
"Failed to set up Drydock Session!"
|
"Failed to set up Drydock Session!"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use the DrydockSession to build a DrydockClient that can
|
# Use the DrydockSession to build a DrydockClient that can
|
||||||
# be used to make one or more API calls
|
# be used to make one or more API calls
|
||||||
LOG.info("Create DryDock Client")
|
|
||||||
self.drydock_client = client.DrydockClient(dd_session)
|
self.drydock_client = client.DrydockClient(dd_session)
|
||||||
|
|
||||||
# Raise Exception if we are not able to build the client
|
# Raise Exception if we are not able to build the client
|
||||||
if self.drydock_client:
|
if not self.drydock_client:
|
||||||
LOG.info("Successfully Set Up DryDock client")
|
|
||||||
else:
|
|
||||||
raise DrydockClientUseFailureException(
|
raise DrydockClientUseFailureException(
|
||||||
"Failed to set up Drydock Client!"
|
"Failed to set up Drydock Client!"
|
||||||
)
|
)
|
||||||
|
LOG.info("Drydock Session and Client etablished.")
|
||||||
|
|
||||||
@shipyard_service_token
|
@shipyard_service_token
|
||||||
def _auth_gen(self):
|
def _auth_gen(self):
|
||||||
|
@ -376,6 +362,115 @@ class DrydockBaseOperator(UcpBaseOperator):
|
||||||
"Unable to retrieve subtask info!"
|
"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):
|
class DrydockBaseOperatorPlugin(AirflowPlugin):
|
||||||
|
|
||||||
|
|
|
@ -11,38 +11,91 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
"""Invoke the Drydock steps for destroying a node."""
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
|
|
||||||
|
from airflow.exceptions import AirflowException
|
||||||
from airflow.plugins_manager import AirflowPlugin
|
from airflow.plugins_manager import AirflowPlugin
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from drydock_base_operator import DrydockBaseOperator
|
from drydock_base_operator import DrydockBaseOperator
|
||||||
|
from drydock_base_operator import gen_node_name_filter
|
||||||
|
from drydock_errors import (
|
||||||
|
DrydockTaskFailedException,
|
||||||
|
DrydockTaskTimeoutException
|
||||||
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from shipyard_airflow.plugins.drydock_base_operator import \
|
from shipyard_airflow.plugins.drydock_base_operator import \
|
||||||
DrydockBaseOperator
|
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__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DrydockDestroyNodeOperator(DrydockBaseOperator):
|
class DrydockDestroyNodeOperator(DrydockBaseOperator):
|
||||||
|
|
||||||
"""Drydock Destroy Node Operator
|
"""Drydock Destroy Node Operator
|
||||||
|
|
||||||
This operator will trigger drydock to destroy a bare metal
|
This operator will trigger drydock to destroy a bare metal
|
||||||
node
|
node
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def do_execute(self):
|
def do_execute(self):
|
||||||
|
self.successes = []
|
||||||
|
|
||||||
# NOTE: This is a PlaceHolder function. The 'destroy_node'
|
LOG.info("Destroying nodes [%s]", ", ".join(self.target_nodes))
|
||||||
# functionalities in DryDock is being worked on and is not
|
self.setup_configured_values()
|
||||||
# ready at the moment.
|
self.node_filter = gen_node_name_filter(self.target_nodes)
|
||||||
LOG.info("Destroying node %s from cluster...",
|
self.execute_destroy()
|
||||||
self.redeploy_server)
|
self.successes = self.get_successes_for_task(self.drydock_task_id)
|
||||||
time.sleep(15)
|
self.report_summary()
|
||||||
LOG.info("Successfully deleted node %s", self.redeploy_server)
|
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 =====")
|
||||||
|
|
||||||
|
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):
|
class DrydockDestroyNodeOperatorPlugin(AirflowPlugin):
|
||||||
|
|
|
@ -36,6 +36,7 @@ from shipyard_airflow.common.deployment_group.node_lookup import NodeLookup
|
||||||
try:
|
try:
|
||||||
import check_k8s_node_status
|
import check_k8s_node_status
|
||||||
from drydock_base_operator import DrydockBaseOperator
|
from drydock_base_operator import DrydockBaseOperator
|
||||||
|
from drydock_base_operator import gen_node_name_filter
|
||||||
from drydock_errors import (
|
from drydock_errors import (
|
||||||
DrydockTaskFailedException,
|
DrydockTaskFailedException,
|
||||||
DrydockTaskTimeoutException
|
DrydockTaskTimeoutException
|
||||||
|
@ -44,6 +45,8 @@ except ImportError:
|
||||||
from shipyard_airflow.plugins import check_k8s_node_status
|
from shipyard_airflow.plugins import check_k8s_node_status
|
||||||
from shipyard_airflow.plugins.drydock_base_operator import \
|
from shipyard_airflow.plugins.drydock_base_operator import \
|
||||||
DrydockBaseOperator
|
DrydockBaseOperator
|
||||||
|
from shipyard_airflow.plugins.drydock_base_operator import \
|
||||||
|
gen_node_name_filter
|
||||||
from shipyard_airflow.plugins.drydock_errors import (
|
from shipyard_airflow.plugins.drydock_errors import (
|
||||||
DrydockTaskFailedException,
|
DrydockTaskFailedException,
|
||||||
DrydockTaskTimeoutException
|
DrydockTaskTimeoutException
|
||||||
|
@ -61,9 +64,8 @@ class DrydockNodesOperator(DrydockBaseOperator):
|
||||||
|
|
||||||
def do_execute(self):
|
def do_execute(self):
|
||||||
self._setup_configured_values()
|
self._setup_configured_values()
|
||||||
# setup self.strat_name and self.strategy
|
# setup self.strategy
|
||||||
self.strategy = {}
|
self.strategy = self.get_deployment_strategy()
|
||||||
self._setup_deployment_strategy()
|
|
||||||
dgm = _get_deployment_group_manager(
|
dgm = _get_deployment_group_manager(
|
||||||
self.strategy['groups'],
|
self.strategy['groups'],
|
||||||
_get_node_lookup(self.drydock_client, self.design_ref)
|
_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)
|
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',
|
return self._execute_task('prepare_nodes',
|
||||||
self.prep_interval,
|
self.prep_interval,
|
||||||
self.prep_timeout)
|
self.prep_timeout)
|
||||||
|
@ -132,7 +134,7 @@ class DrydockNodesOperator(DrydockBaseOperator):
|
||||||
"""
|
"""
|
||||||
LOG.info("Group %s is deploying nodes", group.name)
|
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',
|
task_result = self._execute_task('deploy_nodes',
|
||||||
self.dep_interval,
|
self.dep_interval,
|
||||||
self.dep_timeout)
|
self.dep_timeout)
|
||||||
|
@ -223,103 +225,76 @@ class DrydockNodesOperator(DrydockBaseOperator):
|
||||||
# Other AirflowExceptions will fail the whole task - let them do this.
|
# Other AirflowExceptions will fail the whole task - let them do this.
|
||||||
|
|
||||||
# find successes
|
# 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
|
return result
|
||||||
|
|
||||||
def _get_successes_for_task(self, task_id, extend_success=True):
|
def get_deployment_strategy(self):
|
||||||
"""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):
|
|
||||||
"""Determine the deployment strategy
|
"""Determine the deployment strategy
|
||||||
|
|
||||||
Uses the specified strategy from the deployment configuration
|
Uses the specified strategy from the deployment configuration
|
||||||
or returns a default configuration of 'all-at-once'
|
or returns a default configuration of 'all-at-once'
|
||||||
"""
|
"""
|
||||||
self.strat_name = self.dc['physical_provisioner.deployment_strategy']
|
if self.target_nodes:
|
||||||
if self.strat_name:
|
# Set up a strategy with one group with the list of nodes, so those
|
||||||
# if there is a deployment strategy specified, get it and use it
|
# nodes are the only nodes processed.
|
||||||
self.strategy = self.get_unique_doc(
|
LOG.info("Seting up deployment strategy using targeted nodes")
|
||||||
name=self.strat_name,
|
strat_name = 'targeted nodes'
|
||||||
schema="shipyard/DeploymentStrategy/v1"
|
strategy = gen_simple_deployment_strategy(name='target-group',
|
||||||
)
|
nodes=self.target_nodes)
|
||||||
else:
|
else:
|
||||||
# The default behavior is to deploy all nodes, and fail if
|
# Otherwise, do a strategy for the site - either from the
|
||||||
# any nodes fail to deploy.
|
# configdocs or a default "everything".
|
||||||
self.strat_name = 'all-at-once (defaulted)'
|
strat_name = self.dc['physical_provisioner.deployment_strategy']
|
||||||
self.strategy = _default_deployment_strategy()
|
if strat_name:
|
||||||
|
# if there is a deployment strategy specified, use it
|
||||||
|
strategy = self.get_unique_doc(
|
||||||
|
name=strat_name,
|
||||||
|
schema="shipyard/DeploymentStrategy/v1"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# The default behavior is to deploy all nodes, and fail if
|
||||||
|
# any nodes fail to deploy.
|
||||||
|
strat_name = 'all-at-once (defaulted)'
|
||||||
|
strategy = gen_simple_deployment_strategy()
|
||||||
LOG.info("Strategy Name: %s has %s groups",
|
LOG.info("Strategy Name: %s has %s groups",
|
||||||
self.strat_name,
|
strat_name,
|
||||||
len(self.strategy.get('groups', [])))
|
len(strategy.get('groups', [])))
|
||||||
|
return strategy
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Functions supporting the nodes operator class
|
# Functions supporting the nodes operator class
|
||||||
#
|
#
|
||||||
|
def gen_simple_deployment_strategy(name=None, nodes=None):
|
||||||
|
"""Generates a single group deployment strategy
|
||||||
|
|
||||||
|
:param name: the name of the single group. Defaults to 'default'
|
||||||
|
:param nodes: the list of node_names to be used. Defaults to []
|
||||||
|
"""
|
||||||
|
target_name = name or 'default'
|
||||||
|
target_nodes = list(nodes) if nodes else []
|
||||||
|
|
||||||
|
return {
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'name': target_name,
|
||||||
|
'critical': True,
|
||||||
|
'depends_on': [],
|
||||||
|
'selectors': [
|
||||||
|
{
|
||||||
|
'node_names': target_nodes,
|
||||||
|
'node_labels': [],
|
||||||
|
'node_tags': [],
|
||||||
|
'rack_names': [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'success_criteria': {
|
||||||
|
'percent_successful_nodes': 100
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_node_lookup(drydock_client, design_ref):
|
def _get_node_lookup(drydock_client, design_ref):
|
||||||
"""Return a NodeLookup suitable for the DeploymentGroupManager
|
"""Return a NodeLookup suitable for the DeploymentGroupManager
|
||||||
|
@ -409,71 +384,6 @@ def _process_deployment_groups(dgm, prepare_func, deploy_func):
|
||||||
dgm.evaluate_group_succ_criteria(group.name, Stage.DEPLOYED)
|
dgm.evaluate_group_succ_criteria(group.name, Stage.DEPLOYED)
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def _default_deployment_strategy():
|
|
||||||
"""The default deployment strategy for 'all-at-once'"""
|
|
||||||
return {
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'name': 'default',
|
|
||||||
'critical': True,
|
|
||||||
'depends_on': [],
|
|
||||||
'selectors': [
|
|
||||||
{
|
|
||||||
'node_names': [],
|
|
||||||
'node_labels': [],
|
|
||||||
'node_tags': [],
|
|
||||||
'rack_names': [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'success_criteria': {
|
|
||||||
'percent_successful_nodes': 100
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class QueryTaskResult:
|
class QueryTaskResult:
|
||||||
"""Represents a summarized query result from a task"""
|
"""Represents a summarized query result from a task"""
|
||||||
def __init__(self, task_id, task_name):
|
def __init__(self, task_id, task_name):
|
||||||
|
|
|
@ -13,9 +13,8 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from airflow.utils.decorators import apply_defaults
|
|
||||||
from airflow.plugins_manager import AirflowPlugin
|
from airflow.plugins_manager import AirflowPlugin
|
||||||
from airflow.exceptions import AirflowException
|
from airflow.utils.decorators import apply_defaults
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import service_endpoint
|
import service_endpoint
|
||||||
|
@ -63,20 +62,7 @@ class PromenadeBaseOperator(UcpBaseOperator):
|
||||||
def run_base(self, context):
|
def run_base(self, context):
|
||||||
|
|
||||||
# Logs uuid of Shipyard action
|
# Logs uuid of Shipyard action
|
||||||
LOG.info("Executing Shipyard Action %s", self.action_info['id'])
|
LOG.info("Executing Shipyard Action %s", self.action_id)
|
||||||
|
|
||||||
# Retrieve information of the server that we want to redeploy
|
|
||||||
# if user executes the 'redeploy_server' dag
|
|
||||||
if self.action_info['dag_id'] == 'redeploy_server':
|
|
||||||
self.redeploy_server = self.action_info['parameters'].get(
|
|
||||||
'server-name')
|
|
||||||
|
|
||||||
if self.redeploy_server:
|
|
||||||
LOG.info("Server to be redeployed is %s", self.redeploy_server)
|
|
||||||
else:
|
|
||||||
raise AirflowException('%s was unable to retrieve the '
|
|
||||||
'server to be redeployed.'
|
|
||||||
% self.__class__.__name__)
|
|
||||||
|
|
||||||
# Retrieve promenade endpoint
|
# Retrieve promenade endpoint
|
||||||
self.promenade_svc_endpoint = self.endpoints.endpoint_by_name(
|
self.promenade_svc_endpoint = self.endpoints.endpoint_by_name(
|
||||||
|
|
|
@ -122,9 +122,15 @@ class UcpBaseOperator(BaseOperator):
|
||||||
# Set up and retrieve values from xcom
|
# Set up and retrieve values from xcom
|
||||||
self.xcom_puller = XcomPuller(self.main_dag_name, self.task_instance)
|
self.xcom_puller = XcomPuller(self.main_dag_name, self.task_instance)
|
||||||
self.action_info = self.xcom_puller.get_action_info()
|
self.action_info = self.xcom_puller.get_action_info()
|
||||||
|
self.action_type = self.xcom_puller.get_action_type()
|
||||||
self.dc = self.xcom_puller.get_deployment_configuration()
|
self.dc = self.xcom_puller.get_deployment_configuration()
|
||||||
|
|
||||||
|
# Set up other common-use values
|
||||||
|
self.action_id = self.action_info['id']
|
||||||
self.revision_id = self.action_info['committed_rev_id']
|
self.revision_id = self.action_info['committed_rev_id']
|
||||||
|
self.action_params = self.action_info.get('parameters', {})
|
||||||
self.design_ref = self._deckhand_design_ref()
|
self.design_ref = self._deckhand_design_ref()
|
||||||
|
self._setup_target_nodes()
|
||||||
|
|
||||||
def get_k8s_logs(self):
|
def get_k8s_logs(self):
|
||||||
"""Retrieve Kubernetes pod/container logs specified by an opererator
|
"""Retrieve Kubernetes pod/container logs specified by an opererator
|
||||||
|
@ -155,6 +161,35 @@ class UcpBaseOperator(BaseOperator):
|
||||||
else:
|
else:
|
||||||
LOG.debug("There are no pod logs specified to retrieve")
|
LOG.debug("There are no pod logs specified to retrieve")
|
||||||
|
|
||||||
|
def _setup_target_nodes(self):
|
||||||
|
"""Sets up the target nodes field for this action
|
||||||
|
|
||||||
|
When managing a targeted action, this step needs to resolve the
|
||||||
|
target node. If there are no targets found (should be caught before
|
||||||
|
invocation of the DAG), then raise an exception so that it does not
|
||||||
|
try to take action on more nodes than targeted.
|
||||||
|
Later, when creating the deployment group, if this value
|
||||||
|
(self.target_nodes) is set, it will be used in lieu of the design
|
||||||
|
based deployment strategy.
|
||||||
|
target_nodes will be a comma separated string provided as part of the
|
||||||
|
parameters to an action on input to Shipyard.
|
||||||
|
"""
|
||||||
|
if self.action_type == 'targeted':
|
||||||
|
t_nodes = self.action_params.get('target_nodes', '')
|
||||||
|
self.target_nodes = [n.strip() for n in t_nodes.split(',')]
|
||||||
|
if not self.target_nodes:
|
||||||
|
raise AirflowException(
|
||||||
|
'{} ({}) requires targeted nodes, but was unable to '
|
||||||
|
'resolve any targets in {}'.format(
|
||||||
|
self.main_dag_name, self.action_id,
|
||||||
|
self.__class__.__name__
|
||||||
|
)
|
||||||
|
)
|
||||||
|
LOG.info("Target Nodes for action: [%s]",
|
||||||
|
', '.join(self.target_nodes))
|
||||||
|
else:
|
||||||
|
self.target_nodes = None
|
||||||
|
|
||||||
def _deckhand_design_ref(self):
|
def _deckhand_design_ref(self):
|
||||||
"""Assemble a deckhand design_ref"""
|
"""Assemble a deckhand design_ref"""
|
||||||
# Retrieve DeckHand Endpoint Information
|
# Retrieve DeckHand Endpoint Information
|
||||||
|
|
|
@ -74,7 +74,7 @@ class XcomPuller(object):
|
||||||
key=key)
|
key=key)
|
||||||
|
|
||||||
def get_action_info(self):
|
def get_action_info(self):
|
||||||
"""Retrive the action and action parameter info dictionary
|
"""Retrieve the action and action parameter info dictionary
|
||||||
|
|
||||||
Extract information related to current workflow. This is a dictionary
|
Extract information related to current workflow. This is a dictionary
|
||||||
that contains information about the workflow such as action_id, name
|
that contains information about the workflow such as action_id, name
|
||||||
|
@ -87,6 +87,15 @@ class XcomPuller(object):
|
||||||
dag_id=source_dag,
|
dag_id=source_dag,
|
||||||
key=key)
|
key=key)
|
||||||
|
|
||||||
|
def get_action_type(self):
|
||||||
|
"""Retrieve the action type"""
|
||||||
|
source_task = 'action_xcom'
|
||||||
|
source_dag = None
|
||||||
|
key = 'action_type'
|
||||||
|
return self._get_xcom(source_task=source_task,
|
||||||
|
dag_id=source_dag,
|
||||||
|
key=key)
|
||||||
|
|
||||||
def get_check_drydock_continue_on_fail(self):
|
def get_check_drydock_continue_on_fail(self):
|
||||||
"""Check if 'drydock_continue_on_fail' key exists"""
|
"""Check if 'drydock_continue_on_fail' key exists"""
|
||||||
source_task = 'ucp_preflight_check'
|
source_task = 'ucp_preflight_check'
|
||||||
|
|
|
@ -41,6 +41,10 @@ GET_RENDEREDCONFIGDOCS = 'workflow_orchestrator:get_renderedconfigdocs'
|
||||||
LIST_WORKFLOWS = 'workflow_orchestrator:list_workflows'
|
LIST_WORKFLOWS = 'workflow_orchestrator:list_workflows'
|
||||||
GET_WORKFLOW = 'workflow_orchestrator:get_workflow'
|
GET_WORKFLOW = 'workflow_orchestrator:get_workflow'
|
||||||
GET_SITE_STATUSES = 'workflow_orchestrator:get_site_statuses'
|
GET_SITE_STATUSES = 'workflow_orchestrator:get_site_statuses'
|
||||||
|
ACTION_DEPLOY_SITE = 'workflow_orchestrator:action_deploy_site'
|
||||||
|
ACTION_UPDATE_SITE = 'workflow_orchestrator:action_update_site'
|
||||||
|
ACTION_UPDATE_SOFTWARE = 'workflow_orchestrator:action_update_software'
|
||||||
|
ACTION_REDEPLOY_SERVER = 'workflow_orchestrator:action_redeploy_server'
|
||||||
|
|
||||||
|
|
||||||
class ShipyardPolicy(object):
|
class ShipyardPolicy(object):
|
||||||
|
@ -76,6 +80,8 @@ class ShipyardPolicy(object):
|
||||||
'method': 'GET'
|
'method': 'GET'
|
||||||
}]
|
}]
|
||||||
),
|
),
|
||||||
|
# See below for finer grained action access. This controls access
|
||||||
|
# to being able to create any actions.
|
||||||
policy.DocumentedRuleDefault(
|
policy.DocumentedRuleDefault(
|
||||||
CREATE_ACTION,
|
CREATE_ACTION,
|
||||||
RULE_ADMIN_REQUIRED,
|
RULE_ADMIN_REQUIRED,
|
||||||
|
@ -207,6 +213,45 @@ class ShipyardPolicy(object):
|
||||||
'method': 'GET'
|
'method': 'GET'
|
||||||
}]
|
}]
|
||||||
),
|
),
|
||||||
|
# Specific actions - can be controlled independently. See above for
|
||||||
|
# overall access to creating an action. This controls the ability to
|
||||||
|
# create specific actions (invoke specific workflows)
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
ACTION_DEPLOY_SITE,
|
||||||
|
RULE_ADMIN_REQUIRED,
|
||||||
|
'Create a workflow action to deploy the site',
|
||||||
|
[{
|
||||||
|
'path': '/api/v1.0/actions',
|
||||||
|
'method': 'POST'
|
||||||
|
}]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
ACTION_UPDATE_SITE,
|
||||||
|
RULE_ADMIN_REQUIRED,
|
||||||
|
'Create a workflow action to update the site',
|
||||||
|
[{
|
||||||
|
'path': '/api/v1.0/actions',
|
||||||
|
'method': 'POST'
|
||||||
|
}]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
ACTION_UPDATE_SOFTWARE,
|
||||||
|
RULE_ADMIN_REQUIRED,
|
||||||
|
'Create a workflow action to update the site software',
|
||||||
|
[{
|
||||||
|
'path': '/api/v1.0/actions',
|
||||||
|
'method': 'POST'
|
||||||
|
}]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
ACTION_REDEPLOY_SERVER,
|
||||||
|
RULE_ADMIN_REQUIRED,
|
||||||
|
'Create a workflow action to redeploy target servers',
|
||||||
|
[{
|
||||||
|
'path': '/api/v1.0/actions',
|
||||||
|
'method': 'POST'
|
||||||
|
}]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Regions Policy
|
# Regions Policy
|
||||||
|
@ -235,63 +280,66 @@ class ApiEnforcer(object):
|
||||||
def __call__(self, f):
|
def __call__(self, f):
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
def secure_handler(slf, req, resp, *args, **kwargs):
|
def secure_handler(slf, req, resp, *args, **kwargs):
|
||||||
ctx = req.context
|
check_auth(ctx=req.context, rule=self.action)
|
||||||
policy_eng = ctx.policy_engine
|
return f(slf, req, resp, *args, **kwargs)
|
||||||
LOG.info("Policy Engine: %s", policy_eng.__class__.__name__)
|
|
||||||
# perform auth
|
|
||||||
LOG.info("Enforcing policy %s on request %s",
|
|
||||||
self.action, ctx.request_id)
|
|
||||||
# policy engine must be configured
|
|
||||||
if policy_eng is None:
|
|
||||||
LOG.error(
|
|
||||||
"Error-Policy engine required-action: %s", self.action)
|
|
||||||
raise AppError(
|
|
||||||
title="Auth is not being handled by any policy engine",
|
|
||||||
status=falcon.HTTP_500,
|
|
||||||
retry=False
|
|
||||||
)
|
|
||||||
authorized = False
|
|
||||||
try:
|
|
||||||
if policy_eng.authorize(self.action, ctx):
|
|
||||||
# authorized
|
|
||||||
LOG.info("Request is authorized")
|
|
||||||
authorized = True
|
|
||||||
except:
|
|
||||||
# couldn't service the auth request
|
|
||||||
LOG.exception(
|
|
||||||
"Error - Expectation Failed - action: %s", self.action)
|
|
||||||
raise ApiError(
|
|
||||||
title="Expectation Failed",
|
|
||||||
status=falcon.HTTP_417,
|
|
||||||
retry=False
|
|
||||||
)
|
|
||||||
if authorized:
|
|
||||||
return f(slf, req, resp, *args, **kwargs)
|
|
||||||
else:
|
|
||||||
LOG.error("Auth check failed. Authenticated:%s",
|
|
||||||
ctx.authenticated)
|
|
||||||
# raise the appropriate response exeception
|
|
||||||
if ctx.authenticated:
|
|
||||||
LOG.error("Error: Forbidden access - action: %s",
|
|
||||||
self.action)
|
|
||||||
raise ApiError(
|
|
||||||
title="Forbidden",
|
|
||||||
status=falcon.HTTP_403,
|
|
||||||
description="Credentials do not permit access",
|
|
||||||
retry=False
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
LOG.error("Error - Unauthenticated access")
|
|
||||||
raise ApiError(
|
|
||||||
title="Unauthenticated",
|
|
||||||
status=falcon.HTTP_401,
|
|
||||||
description="Credentials are not established",
|
|
||||||
retry=False
|
|
||||||
)
|
|
||||||
|
|
||||||
return secure_handler
|
return secure_handler
|
||||||
|
|
||||||
|
|
||||||
|
def check_auth(ctx, rule):
|
||||||
|
"""Checks the authorization to the requested rule
|
||||||
|
|
||||||
|
:param ctx: the request context for the action being performed
|
||||||
|
:param rule: the name of the policy rule to validate the user in the
|
||||||
|
context against
|
||||||
|
|
||||||
|
Returns if authorized, otherwise raises an ApiError.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
policy_eng = ctx.policy_engine
|
||||||
|
LOG.info("Policy Engine: %s", policy_eng.__class__.__name__)
|
||||||
|
# perform auth
|
||||||
|
LOG.info("Enforcing policy %s on request %s", rule, ctx.request_id)
|
||||||
|
# policy engine must be configured
|
||||||
|
if policy_eng is None:
|
||||||
|
LOG.error(
|
||||||
|
"Error-Policy engine required-action: %s", rule)
|
||||||
|
raise AppError(
|
||||||
|
title="Auth is not being handled by any policy engine",
|
||||||
|
status=falcon.HTTP_500,
|
||||||
|
retry=False
|
||||||
|
)
|
||||||
|
if policy_eng.authorize(rule, ctx):
|
||||||
|
# authorized - log and return
|
||||||
|
LOG.info("Request to %s is authorized", rule)
|
||||||
|
return
|
||||||
|
except Exception as ex:
|
||||||
|
# couldn't service the auth request
|
||||||
|
LOG.exception("Error - Expectation Failed - action: %s", rule)
|
||||||
|
raise ApiError(
|
||||||
|
title="Expectation Failed",
|
||||||
|
status=falcon.HTTP_417,
|
||||||
|
retry=False
|
||||||
|
)
|
||||||
|
# raise the appropriate response exeception
|
||||||
|
if ctx.authenticated:
|
||||||
|
# authenticated but not authorized
|
||||||
|
LOG.error("Error: Forbidden access - action: %s", rule)
|
||||||
|
raise ApiError(
|
||||||
|
title="Forbidden",
|
||||||
|
status=falcon.HTTP_403,
|
||||||
|
description="Credentials do not permit access",
|
||||||
|
retry=False
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
LOG.error("Error - Unauthenticated access")
|
||||||
|
raise ApiError(
|
||||||
|
title="Unauthenticated",
|
||||||
|
status=falcon.HTTP_401,
|
||||||
|
description="Credentials are not established",
|
||||||
|
retry=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_policies():
|
def list_policies():
|
||||||
default_policy = []
|
default_policy = []
|
||||||
default_policy.extend(ShipyardPolicy.base_rules)
|
default_policy.extend(ShipyardPolicy.base_rules)
|
||||||
|
|
|
@ -24,8 +24,11 @@ from shipyard_airflow.common.deployment_group.errors import (
|
||||||
InvalidDeploymentGroupNodeLookupError
|
InvalidDeploymentGroupNodeLookupError
|
||||||
)
|
)
|
||||||
from shipyard_airflow.control.action.action_validators import (
|
from shipyard_airflow.control.action.action_validators import (
|
||||||
validate_site_action_basic,
|
validate_committed_revision,
|
||||||
validate_site_action_full
|
validate_deployment_action_basic,
|
||||||
|
validate_deployment_action_full,
|
||||||
|
validate_intermediate_commits,
|
||||||
|
validate_target_nodes
|
||||||
)
|
)
|
||||||
from shipyard_airflow.errors import ApiError
|
from shipyard_airflow.errors import ApiError
|
||||||
from tests.unit.common.deployment_group.node_lookup_stubs import node_lookup
|
from tests.unit.common.deployment_group.node_lookup_stubs import node_lookup
|
||||||
|
@ -76,10 +79,10 @@ class TestActionValidator:
|
||||||
@mock.patch("shipyard_airflow.control.validators."
|
@mock.patch("shipyard_airflow.control.validators."
|
||||||
"validate_deployment_strategy._get_node_lookup",
|
"validate_deployment_strategy._get_node_lookup",
|
||||||
return_value=node_lookup)
|
return_value=node_lookup)
|
||||||
def test_validate_site_action_full(self, *args):
|
def test_validate_deployment_action_full(self, *args):
|
||||||
"""Test the function that runs the validator class"""
|
"""Test the function that runs the validator class"""
|
||||||
try:
|
try:
|
||||||
validate_site_action_full({
|
validate_deployment_action_full({
|
||||||
'id': '123',
|
'id': '123',
|
||||||
'name': 'deploy_site',
|
'name': 'deploy_site',
|
||||||
'committed_rev_id': 1
|
'committed_rev_id': 1
|
||||||
|
@ -93,12 +96,12 @@ class TestActionValidator:
|
||||||
@mock.patch("shipyard_airflow.control.validators."
|
@mock.patch("shipyard_airflow.control.validators."
|
||||||
"validate_deployment_strategy._get_node_lookup",
|
"validate_deployment_strategy._get_node_lookup",
|
||||||
return_value=node_lookup)
|
return_value=node_lookup)
|
||||||
def test_validate_site_action_full_cycle(self, *args):
|
def test_validate_deployment_action_full_cycle(self, *args):
|
||||||
"""Test the function that runs the validator class with a
|
"""Test the function that runs the validator class with a
|
||||||
deployment strategy that has a cycle in the groups
|
deployment strategy that has a cycle in the groups
|
||||||
"""
|
"""
|
||||||
with pytest.raises(ApiError) as apie:
|
with pytest.raises(ApiError) as apie:
|
||||||
validate_site_action_full({
|
validate_deployment_action_full({
|
||||||
'id': '123',
|
'id': '123',
|
||||||
'name': 'deploy_site',
|
'name': 'deploy_site',
|
||||||
'committed_rev_id': 1
|
'committed_rev_id': 1
|
||||||
|
@ -113,12 +116,12 @@ class TestActionValidator:
|
||||||
@mock.patch("shipyard_airflow.control.validators."
|
@mock.patch("shipyard_airflow.control.validators."
|
||||||
"validate_deployment_strategy._get_node_lookup",
|
"validate_deployment_strategy._get_node_lookup",
|
||||||
return_value=node_lookup)
|
return_value=node_lookup)
|
||||||
def test_validate_site_action_full_missing_dep_strat(self, *args):
|
def test_validate_deployment_action_full_missing_dep_strat(self, *args):
|
||||||
"""Test the function that runs the validator class with a missing
|
"""Test the function that runs the validator class with a missing
|
||||||
deployment strategy - specified, but not present
|
deployment strategy - specified, but not present
|
||||||
"""
|
"""
|
||||||
with pytest.raises(ApiError) as apie:
|
with pytest.raises(ApiError) as apie:
|
||||||
validate_site_action_full({
|
validate_deployment_action_full({
|
||||||
'id': '123',
|
'id': '123',
|
||||||
'name': 'deploy_site',
|
'name': 'deploy_site',
|
||||||
'committed_rev_id': 1
|
'committed_rev_id': 1
|
||||||
|
@ -131,12 +134,12 @@ class TestActionValidator:
|
||||||
@mock.patch("shipyard_airflow.control.validators."
|
@mock.patch("shipyard_airflow.control.validators."
|
||||||
"validate_deployment_strategy._get_node_lookup",
|
"validate_deployment_strategy._get_node_lookup",
|
||||||
return_value=node_lookup)
|
return_value=node_lookup)
|
||||||
def test_validate_site_action_full_default_dep_strat(self, *args):
|
def test_validate_deployment_action_full_default_dep_strat(self, *args):
|
||||||
"""Test the function that runs the validator class with a defaulted
|
"""Test the function that runs the validator class with a defaulted
|
||||||
deployment strategy (not specified)
|
deployment strategy (not specified)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
validate_site_action_full({
|
validate_deployment_action_full({
|
||||||
'id': '123',
|
'id': '123',
|
||||||
'name': 'deploy_site',
|
'name': 'deploy_site',
|
||||||
'committed_rev_id': 1
|
'committed_rev_id': 1
|
||||||
|
@ -145,33 +148,17 @@ class TestActionValidator:
|
||||||
# any exception is a failure
|
# any exception is a failure
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
@mock.patch("shipyard_airflow.control.service_clients.deckhand_client",
|
|
||||||
return_value=fake_dh_doc_client('clean'), ds_name='defaulted')
|
|
||||||
@mock.patch("shipyard_airflow.control.validators."
|
|
||||||
"validate_deployment_strategy._get_node_lookup",
|
|
||||||
return_value=node_lookup)
|
|
||||||
def test_validate_site_missing_rev(self, *args):
|
|
||||||
"""Test the function that runs the validator class with a
|
|
||||||
deployment strategy that has a cycle in the groups
|
|
||||||
"""
|
|
||||||
with pytest.raises(ApiError) as apie:
|
|
||||||
validate_site_action_full({
|
|
||||||
'id': '123',
|
|
||||||
'name': 'deploy_site'
|
|
||||||
})
|
|
||||||
assert apie.value.description == 'InvalidDocumentRevision'
|
|
||||||
|
|
||||||
@mock.patch("shipyard_airflow.control.service_clients.deckhand_client",
|
@mock.patch("shipyard_airflow.control.service_clients.deckhand_client",
|
||||||
return_value=fake_dh_doc_client('clean', ds_name='not-there'))
|
return_value=fake_dh_doc_client('clean', ds_name='not-there'))
|
||||||
@mock.patch("shipyard_airflow.control.validators."
|
@mock.patch("shipyard_airflow.control.validators."
|
||||||
"validate_deployment_strategy._get_node_lookup",
|
"validate_deployment_strategy._get_node_lookup",
|
||||||
return_value=node_lookup)
|
return_value=node_lookup)
|
||||||
def test_validate_site_action_full_continue_failure(self, *args):
|
def test_validate_deployment_action_full_continue_failure(self, *args):
|
||||||
"""Test the function that runs the validator class with a missing
|
"""Test the function that runs the validator class with a missing
|
||||||
deployment strategy (not specified), but continue-on-fail specified
|
deployment strategy (not specified), but continue-on-fail specified
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
validate_site_action_full({
|
validate_deployment_action_full({
|
||||||
'id': '123',
|
'id': '123',
|
||||||
'name': 'deploy_site',
|
'name': 'deploy_site',
|
||||||
'committed_rev_id': 1,
|
'committed_rev_id': 1,
|
||||||
|
@ -186,13 +173,13 @@ class TestActionValidator:
|
||||||
@mock.patch("shipyard_airflow.control.validators."
|
@mock.patch("shipyard_airflow.control.validators."
|
||||||
"validate_deployment_strategy._get_node_lookup",
|
"validate_deployment_strategy._get_node_lookup",
|
||||||
return_value=node_lookup)
|
return_value=node_lookup)
|
||||||
def test_validate_site_action_basic_missing_dep_strat(self, *args):
|
def test_validate_deployment_action_basic_missing_dep_strat(self, *args):
|
||||||
"""Test the function that runs the validator class with a missing
|
"""Test the function that runs the validator class with a missing
|
||||||
deployment strategy - specified, but not present. This should be
|
deployment strategy - specified, but not present. This should be
|
||||||
ignored by the basic valdiator
|
ignored by the basic valdiator
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
validate_site_action_basic({
|
validate_deployment_action_basic({
|
||||||
'id': '123',
|
'id': '123',
|
||||||
'name': 'deploy_site',
|
'name': 'deploy_site',
|
||||||
'committed_rev_id': 1
|
'committed_rev_id': 1
|
||||||
|
@ -206,7 +193,7 @@ class TestActionValidator:
|
||||||
@mock.patch("shipyard_airflow.control.validators."
|
@mock.patch("shipyard_airflow.control.validators."
|
||||||
"validate_deployment_strategy._get_node_lookup",
|
"validate_deployment_strategy._get_node_lookup",
|
||||||
return_value=node_lookup)
|
return_value=node_lookup)
|
||||||
def test_validate_site_action_dep_strategy_exceptions(self, *args):
|
def test_validate_deployment_action_dep_strategy_exceptions(self, *args):
|
||||||
"""Test the function that runs the validator class for exceptions"""
|
"""Test the function that runs the validator class for exceptions"""
|
||||||
to_catch = [InvalidDeploymentGroupNodeLookupError,
|
to_catch = [InvalidDeploymentGroupNodeLookupError,
|
||||||
InvalidDeploymentGroupError, DeploymentGroupCycleError]
|
InvalidDeploymentGroupError, DeploymentGroupCycleError]
|
||||||
|
@ -217,7 +204,7 @@ class TestActionValidator:
|
||||||
side_effect=exc()
|
side_effect=exc()
|
||||||
):
|
):
|
||||||
with pytest.raises(ApiError) as apie:
|
with pytest.raises(ApiError) as apie:
|
||||||
validate_site_action_full({
|
validate_deployment_action_full({
|
||||||
'id': '123',
|
'id': '123',
|
||||||
'name': 'deploy_site',
|
'name': 'deploy_site',
|
||||||
'committed_rev_id': 1
|
'committed_rev_id': 1
|
||||||
|
@ -233,10 +220,10 @@ class TestActionValidator:
|
||||||
@mock.patch("shipyard_airflow.control.validators."
|
@mock.patch("shipyard_airflow.control.validators."
|
||||||
"validate_deployment_strategy._get_deployment_group_manager",
|
"validate_deployment_strategy._get_deployment_group_manager",
|
||||||
side_effect=TypeError())
|
side_effect=TypeError())
|
||||||
def test_validate_site_action_dep_strategy_exception_other(self, *args):
|
def test_validate_deployment_action_dep_strategy_exc_oth(self, *args):
|
||||||
"""Test the function that runs the validator class"""
|
"""Test the function that runs the validator class"""
|
||||||
with pytest.raises(ApiError) as apie:
|
with pytest.raises(ApiError) as apie:
|
||||||
validate_site_action_full({
|
validate_deployment_action_full({
|
||||||
'id': '123',
|
'id': '123',
|
||||||
'name': 'deploy_site',
|
'name': 'deploy_site',
|
||||||
'committed_rev_id': 1
|
'committed_rev_id': 1
|
||||||
|
@ -244,3 +231,70 @@ class TestActionValidator:
|
||||||
assert apie.value.description == 'InvalidConfigurationDocuments'
|
assert apie.value.description == 'InvalidConfigurationDocuments'
|
||||||
assert apie.value.error_list[0]['name'] == (
|
assert apie.value.error_list[0]['name'] == (
|
||||||
'DocumentValidationProcessingError')
|
'DocumentValidationProcessingError')
|
||||||
|
|
||||||
|
def _action(self, params_field, comm_rev=1, allow=False):
|
||||||
|
action = {
|
||||||
|
'id': '123',
|
||||||
|
'name': 'redeploy_server',
|
||||||
|
'allow_intermediate_commits': allow
|
||||||
|
}
|
||||||
|
if comm_rev:
|
||||||
|
action['committed_rev_id'] = comm_rev
|
||||||
|
if params_field:
|
||||||
|
action['parameters'] = params_field
|
||||||
|
return action
|
||||||
|
|
||||||
|
def test_validate_target_nodes(self, *args):
|
||||||
|
"""Test the validate_target_nodes/ValidateTargetNodes validator"""
|
||||||
|
# pass - basic case
|
||||||
|
validate_target_nodes(self._action({'target_nodes': ['node1']}))
|
||||||
|
# missing parameter
|
||||||
|
with pytest.raises(ApiError) as apie:
|
||||||
|
validate_target_nodes(self._action(None))
|
||||||
|
assert apie.value.title == 'Invalid target_nodes parameter'
|
||||||
|
# no nodes
|
||||||
|
with pytest.raises(ApiError) as apie:
|
||||||
|
validate_target_nodes(self._action({'target_nodes': []}))
|
||||||
|
assert apie.value.title == 'Invalid target_nodes parameter'
|
||||||
|
# other parameter than target_nodes
|
||||||
|
with pytest.raises(ApiError) as apie:
|
||||||
|
validate_target_nodes(self._action({'no_nodes': ['what']}))
|
||||||
|
assert apie.value.title == 'Invalid target_nodes parameter'
|
||||||
|
# not a list-able target_nodes
|
||||||
|
with pytest.raises(ApiError) as apie:
|
||||||
|
validate_target_nodes(self._action({'target_nodes': pytest}))
|
||||||
|
assert apie.value.title == 'Invalid target_nodes parameter'
|
||||||
|
# not a list-able target_nodes
|
||||||
|
with pytest.raises(ApiError) as apie:
|
||||||
|
validate_target_nodes(
|
||||||
|
self._action({'target_nodes': [{'not': 'string'}]})
|
||||||
|
)
|
||||||
|
assert apie.value.title == 'Invalid target_nodes parameter'
|
||||||
|
|
||||||
|
def test_validate_committed_revision(self, *args):
|
||||||
|
"""Test the committed revision validator"""
|
||||||
|
validate_committed_revision(self._action(None))
|
||||||
|
with pytest.raises(ApiError) as apie:
|
||||||
|
validate_committed_revision(self._action(None, comm_rev=None))
|
||||||
|
assert apie.value.title == 'No committed configdocs'
|
||||||
|
|
||||||
|
def test_validate_intermediate_commits(self, *args):
|
||||||
|
"""Test the intermediate commit validator"""
|
||||||
|
ch_fail = CfgdHelperIntermediateCommit()
|
||||||
|
ch_success = CfgdHelperIntermediateCommit(commits=False)
|
||||||
|
validate_intermediate_commits(self._action(None), ch_success)
|
||||||
|
with pytest.raises(ApiError) as apie:
|
||||||
|
validate_intermediate_commits(self._action(None), ch_fail)
|
||||||
|
assert apie.value.title == 'Intermediate commit detected'
|
||||||
|
# bypass flag - no api error
|
||||||
|
validate_intermediate_commits(
|
||||||
|
self._action(None, allow=True), ch_fail
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CfgdHelperIntermediateCommit():
|
||||||
|
def __init__(self, commits=True):
|
||||||
|
self.commits = commits
|
||||||
|
|
||||||
|
def check_intermediate_commit(self):
|
||||||
|
return self.commits
|
||||||
|
|
|
@ -43,10 +43,6 @@ CONF = cfg.CONF
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def CHECK_INTERMEDIATE_COMMIT(allow_intermediate_commits):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def create_req(ctx, body):
|
def create_req(ctx, body):
|
||||||
'''creates a falcon request'''
|
'''creates a falcon request'''
|
||||||
env = testing.create_environ(
|
env = testing.create_environ(
|
||||||
|
@ -300,7 +296,8 @@ def test_get_all_actions():
|
||||||
assert action['dag_status'] == 'SUCCESS'
|
assert action['dag_status'] == 'SUCCESS'
|
||||||
|
|
||||||
|
|
||||||
def test_create_action():
|
def _gen_action_resource_stubbed():
|
||||||
|
# TODO(bryan-strassner): mabye subclass this instead?
|
||||||
action_resource = ActionsResource()
|
action_resource = ActionsResource()
|
||||||
action_resource.get_all_actions_db = actions_db
|
action_resource.get_all_actions_db = actions_db
|
||||||
action_resource.get_all_dag_runs_db = dag_runs_db
|
action_resource.get_all_dag_runs_db = dag_runs_db
|
||||||
|
@ -309,97 +306,243 @@ def test_create_action():
|
||||||
action_resource.insert_action = insert_action_stub
|
action_resource.insert_action = insert_action_stub
|
||||||
action_resource.audit_control_command_db = audit_control_command_db
|
action_resource.audit_control_command_db = audit_control_command_db
|
||||||
action_resource.get_committed_design_version = lambda: DESIGN_VERSION
|
action_resource.get_committed_design_version = lambda: DESIGN_VERSION
|
||||||
action_resource.check_intermediate_commit_revision = (
|
return action_resource
|
||||||
CHECK_INTERMEDIATE_COMMIT)
|
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_deployment_action_basic')
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_deployment_action_full')
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_intermediate_commits')
|
||||||
|
def test_create_action_invalid_input(ic_val, full_val, basic_val):
|
||||||
|
action_resource = _gen_action_resource_stubbed()
|
||||||
# with invalid input. fail.
|
# with invalid input. fail.
|
||||||
with mock.patch('shipyard_airflow.control.action.action_validators'
|
with pytest.raises(ApiError):
|
||||||
'.validate_site_action_full') as validator:
|
action = action_resource.create_action(
|
||||||
try:
|
action={'name': 'broken',
|
||||||
action = action_resource.create_action(
|
'parameters': {
|
||||||
action={'name': 'broken',
|
'a': 'aaa'
|
||||||
'parameters': {
|
}},
|
||||||
'a': 'aaa'
|
context=context,
|
||||||
}},
|
allow_intermediate_commits=False)
|
||||||
context=context,
|
assert not ic_val.called
|
||||||
allow_intermediate_commits=False)
|
assert not full_val.called
|
||||||
assert False, 'Should throw an ApiError'
|
assert not basic_val.called
|
||||||
except ApiError:
|
|
||||||
# expected
|
|
||||||
pass
|
|
||||||
assert not validator.called
|
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('shipyard_airflow.policy.check_auth')
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_deployment_action_basic')
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_deployment_action_full')
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_intermediate_commits')
|
||||||
|
def test_create_action_valid_input_and_params(ic_val, full_val, *args):
|
||||||
|
action_resource = _gen_action_resource_stubbed()
|
||||||
# with valid input and some parameters
|
# with valid input and some parameters
|
||||||
with mock.patch('shipyard_airflow.control.action.action_validators'
|
try:
|
||||||
'.validate_site_action_full') as validator:
|
action = action_resource.create_action(
|
||||||
try:
|
action={'name': 'deploy_site',
|
||||||
action = action_resource.create_action(
|
'parameters': {
|
||||||
action={'name': 'deploy_site',
|
'a': 'aaa'
|
||||||
'parameters': {
|
}},
|
||||||
'a': 'aaa'
|
context=context,
|
||||||
}},
|
allow_intermediate_commits=False)
|
||||||
context=context,
|
assert action['timestamp']
|
||||||
allow_intermediate_commits=False)
|
assert action['id']
|
||||||
assert action['timestamp']
|
assert len(action['id']) == 26
|
||||||
assert action['id']
|
assert action['dag_execution_date'] == '2017-09-06 14:10:08.528402'
|
||||||
assert len(action['id']) == 26
|
assert action['dag_status'] == 'SCHEDULED'
|
||||||
assert action['dag_execution_date'] == '2017-09-06 14:10:08.528402'
|
assert action['committed_rev_id'] == 1
|
||||||
assert action['dag_status'] == 'SCHEDULED'
|
except ApiError:
|
||||||
assert action['committed_rev_id'] == 1
|
assert False, 'Should not raise an ApiError'
|
||||||
except ApiError:
|
full_val.assert_called_once_with(
|
||||||
assert False, 'Should not raise an ApiError'
|
action=action, configdocs_helper=action_resource.configdocs_helper)
|
||||||
validator.assert_called_once_with(action)
|
ic_val.assert_called_once_with(
|
||||||
|
action=action, configdocs_helper=action_resource.configdocs_helper)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('shipyard_airflow.policy.check_auth')
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_deployment_action_basic')
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_deployment_action_full')
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_intermediate_commits')
|
||||||
|
def test_create_action_valid_input_no_params(ic_val, full_val, *args):
|
||||||
|
action_resource = _gen_action_resource_stubbed()
|
||||||
# with valid input and no parameters
|
# with valid input and no parameters
|
||||||
with mock.patch('shipyard_airflow.control.action.action_validators'
|
try:
|
||||||
'.validate_site_action_full') as validator:
|
action = action_resource.create_action(
|
||||||
try:
|
action={'name': 'deploy_site'},
|
||||||
action = action_resource.create_action(
|
context=context,
|
||||||
action={'name': 'deploy_site'},
|
allow_intermediate_commits=False)
|
||||||
context=context,
|
assert action['timestamp']
|
||||||
allow_intermediate_commits=False)
|
assert action['id']
|
||||||
assert action['timestamp']
|
assert len(action['id']) == 26
|
||||||
assert action['id']
|
assert action['dag_execution_date'] == '2017-09-06 14:10:08.528402'
|
||||||
assert len(action['id']) == 26
|
assert action['dag_status'] == 'SCHEDULED'
|
||||||
assert action['dag_execution_date'] == '2017-09-06 14:10:08.528402'
|
assert action['committed_rev_id'] == 1
|
||||||
assert action['dag_status'] == 'SCHEDULED'
|
except ApiError:
|
||||||
assert action['committed_rev_id'] == 1
|
assert False, 'Should not raise an ApiError'
|
||||||
except ApiError:
|
full_val.assert_called_once_with(
|
||||||
assert False, 'Should not raise an ApiError'
|
action=action, configdocs_helper=action_resource.configdocs_helper)
|
||||||
validator.assert_called_once_with(action)
|
ic_val.assert_called_once_with(
|
||||||
|
action=action, configdocs_helper=action_resource.configdocs_helper)
|
||||||
|
|
||||||
|
|
||||||
def test_create_action_validator_error():
|
@mock.patch('shipyard_airflow.policy.check_auth')
|
||||||
action_resource = ActionsResource()
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
action_resource.get_all_actions_db = actions_db
|
'.validate_deployment_action_basic')
|
||||||
action_resource.get_all_dag_runs_db = dag_runs_db
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
action_resource.get_all_tasks_db = tasks_db
|
'.validate_deployment_action_full',
|
||||||
action_resource.invoke_airflow_dag = airflow_stub
|
side_effect=ApiError(title='bad'))
|
||||||
action_resource.insert_action = insert_action_stub
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
action_resource.audit_control_command_db = audit_control_command_db
|
'.validate_intermediate_commits')
|
||||||
action_resource.get_committed_design_version = lambda: DESIGN_VERSION
|
def test_create_action_validator_error(*args):
|
||||||
action_resource.check_intermediate_commit_revision = (
|
action_resource = _gen_action_resource_stubbed()
|
||||||
CHECK_INTERMEDIATE_COMMIT)
|
|
||||||
|
|
||||||
# with valid input and some parameters
|
# with valid input and some parameters
|
||||||
with mock.patch('shipyard_airflow.control.action.action_validators'
|
with pytest.raises(ApiError) as apie:
|
||||||
'.validate_site_action_full',
|
action = action_resource.create_action(
|
||||||
side_effect=ApiError(title='bad')):
|
action={'name': 'deploy_site',
|
||||||
with pytest.raises(ApiError) as apie:
|
'parameters': {
|
||||||
|
'a': 'aaa'
|
||||||
|
}},
|
||||||
|
context=context,
|
||||||
|
allow_intermediate_commits=False)
|
||||||
|
assert action['timestamp']
|
||||||
|
assert action['id']
|
||||||
|
assert len(action['id']) == 26
|
||||||
|
assert action['dag_execution_date'] == '2017-09-06 14:10:08.528402'
|
||||||
|
assert action['dag_status'] == 'SCHEDULED'
|
||||||
|
assert action['committed_rev_id'] == 1
|
||||||
|
|
||||||
|
assert apie.value.title == 'bad'
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('shipyard_airflow.policy.check_auth')
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_deployment_action_basic')
|
||||||
|
def test_create_targeted_action_valid_input_and_params(basic_val, *args):
|
||||||
|
action_resource = _gen_action_resource_stubbed()
|
||||||
|
# with valid input and some parameters
|
||||||
|
try:
|
||||||
|
action = action_resource.create_action(
|
||||||
|
action={'name': 'redeploy_server',
|
||||||
|
'parameters': {
|
||||||
|
'target_nodes': ['node1']
|
||||||
|
}},
|
||||||
|
context=context,
|
||||||
|
allow_intermediate_commits=False)
|
||||||
|
assert action['timestamp']
|
||||||
|
assert action['id']
|
||||||
|
assert len(action['id']) == 26
|
||||||
|
assert action['dag_execution_date'] == '2017-09-06 14:10:08.528402'
|
||||||
|
assert action['dag_status'] == 'SCHEDULED'
|
||||||
|
assert action['committed_rev_id'] == 1
|
||||||
|
except ApiError:
|
||||||
|
assert False, 'Should not raise an ApiError'
|
||||||
|
basic_val.assert_called_once_with(
|
||||||
|
action=action, configdocs_helper=action_resource.configdocs_helper)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('shipyard_airflow.policy.check_auth')
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_deployment_action_basic')
|
||||||
|
def test_create_targeted_action_valid_input_missing_target(basic_val, *args):
|
||||||
|
action_resource = _gen_action_resource_stubbed()
|
||||||
|
# with valid input and some parameters
|
||||||
|
with pytest.raises(ApiError) as apie:
|
||||||
|
action = action_resource.create_action(
|
||||||
|
action={'name': 'redeploy_server',
|
||||||
|
'parameters': {
|
||||||
|
'target_nodes': []
|
||||||
|
}},
|
||||||
|
context=context,
|
||||||
|
allow_intermediate_commits=False)
|
||||||
|
assert action['timestamp']
|
||||||
|
assert action['id']
|
||||||
|
assert len(action['id']) == 26
|
||||||
|
assert action['dag_execution_date'] == '2017-09-06 14:10:08.528402'
|
||||||
|
assert action['dag_status'] == 'SCHEDULED'
|
||||||
|
assert action['committed_rev_id'] == 1
|
||||||
|
assert apie.value.title == 'Invalid target_nodes parameter'
|
||||||
|
assert not basic_val.called
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('shipyard_airflow.policy.check_auth')
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_deployment_action_basic')
|
||||||
|
def test_create_targeted_action_valid_input_missing_param(basic_val, *args):
|
||||||
|
action_resource = _gen_action_resource_stubbed()
|
||||||
|
# with valid input and some parameters
|
||||||
|
with pytest.raises(ApiError) as apie:
|
||||||
|
action = action_resource.create_action(
|
||||||
|
action={'name': 'redeploy_server'},
|
||||||
|
context=context,
|
||||||
|
allow_intermediate_commits=False)
|
||||||
|
assert action['timestamp']
|
||||||
|
assert action['id']
|
||||||
|
assert len(action['id']) == 26
|
||||||
|
assert action['dag_execution_date'] == '2017-09-06 14:10:08.528402'
|
||||||
|
assert action['dag_status'] == 'SCHEDULED'
|
||||||
|
assert action['committed_rev_id'] == 1
|
||||||
|
assert apie.value.title == 'Invalid target_nodes parameter'
|
||||||
|
assert not basic_val.called
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('shipyard_airflow.policy.check_auth')
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_deployment_action_basic')
|
||||||
|
def test_create_targeted_action_no_committed(basic_val, *args):
|
||||||
|
action_resource = _gen_action_resource_stubbed()
|
||||||
|
action_resource.get_committed_design_version = lambda: None
|
||||||
|
# with valid input and some parameters
|
||||||
|
with pytest.raises(ApiError) as apie:
|
||||||
|
action = action_resource.create_action(
|
||||||
|
action={'name': 'redeploy_server',
|
||||||
|
'parameters': {
|
||||||
|
'target_nodes': ['node1']
|
||||||
|
}},
|
||||||
|
context=context,
|
||||||
|
allow_intermediate_commits=False)
|
||||||
|
assert action['timestamp']
|
||||||
|
assert action['id']
|
||||||
|
assert len(action['id']) == 26
|
||||||
|
assert action['dag_execution_date'] == '2017-09-06 14:10:08.528402'
|
||||||
|
assert action['dag_status'] == 'SCHEDULED'
|
||||||
|
assert action['committed_rev_id'] == 1
|
||||||
|
assert apie.value.title == 'No committed configdocs'
|
||||||
|
assert not basic_val.called
|
||||||
|
|
||||||
|
|
||||||
|
# Purposefully raising Exception to test only the value passed to auth
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_deployment_action_basic',
|
||||||
|
side_effect=Exception('purposeful'))
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_deployment_action_full',
|
||||||
|
side_effect=Exception('purposeful'))
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_intermediate_commits',
|
||||||
|
side_effect=Exception('purposeful'))
|
||||||
|
@mock.patch('shipyard_airflow.control.action.action_validators'
|
||||||
|
'.validate_target_nodes',
|
||||||
|
side_effect=Exception('purposeful'))
|
||||||
|
@mock.patch('shipyard_airflow.policy.check_auth')
|
||||||
|
def test_auth_alignment(auth, *args):
|
||||||
|
action_resource = _gen_action_resource_stubbed()
|
||||||
|
for action_name, action_cfg in actions_api._action_mappings().items():
|
||||||
|
with pytest.raises(Exception) as ex:
|
||||||
action = action_resource.create_action(
|
action = action_resource.create_action(
|
||||||
action={'name': 'deploy_site',
|
action={'name': action_name},
|
||||||
'parameters': {
|
|
||||||
'a': 'aaa'
|
|
||||||
}},
|
|
||||||
context=context,
|
context=context,
|
||||||
allow_intermediate_commits=False)
|
allow_intermediate_commits=False)
|
||||||
assert action['timestamp']
|
assert 'purposeful' in str(ex)
|
||||||
assert action['id']
|
assert auth.called_with(action_cfg['rbac_policy'])
|
||||||
assert len(action['id']) == 26
|
assert (action_cfg['rbac_policy'] ==
|
||||||
assert action['dag_execution_date'] == '2017-09-06 14:10:08.528402'
|
'workflow_orchestrator:action_{}'.format(action_name))
|
||||||
assert action['dag_status'] == 'SCHEDULED'
|
|
||||||
assert action['committed_rev_id'] == 1
|
|
||||||
assert apie.value.title == 'bad'
|
|
||||||
|
|
||||||
|
|
||||||
@patch('shipyard_airflow.db.shipyard_db.ShipyardDbAccess.'
|
@patch('shipyard_airflow.db.shipyard_db.ShipyardDbAccess.'
|
||||||
|
@ -536,12 +679,8 @@ def test_get_committed_design_version(*args):
|
||||||
|
|
||||||
@mock.patch.object(ConfigdocsHelper, 'get_revision_id', return_value=None)
|
@mock.patch.object(ConfigdocsHelper, 'get_revision_id', return_value=None)
|
||||||
def test_get_committed_design_version_missing(*args):
|
def test_get_committed_design_version_missing(*args):
|
||||||
with pytest.raises(ApiError) as apie:
|
act_resource = ActionsResource()
|
||||||
act_resource = ActionsResource()
|
act_resource.configdocs_helper = ConfigdocsHelper(
|
||||||
act_resource.configdocs_helper = ConfigdocsHelper(
|
ShipyardRequestContext()
|
||||||
ShipyardRequestContext()
|
)
|
||||||
)
|
assert act_resource.get_committed_design_version() is None
|
||||||
act_resource.get_committed_design_version()
|
|
||||||
assert apie.value.status == falcon.HTTP_404
|
|
||||||
assert apie.value.title == ('Unable to locate any committed revision in '
|
|
||||||
'Deckhand')
|
|
||||||
|
|
|
@ -0,0 +1,224 @@
|
||||||
|
# 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.
|
||||||
|
"""Tests for drydock_destroy_nodes operator functions"""
|
||||||
|
import os
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from airflow.exceptions import AirflowException
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from shipyard_airflow.plugins.drydock_destroy_nodes import \
|
||||||
|
DrydockDestroyNodeOperator
|
||||||
|
from shipyard_airflow.plugins.drydock_errors import (
|
||||||
|
DrydockTaskFailedException,
|
||||||
|
DrydockTaskTimeoutException
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CONF_FILE = os.path.join(os.path.dirname(__file__), 'test.conf')
|
||||||
|
ALL_SUCCESES = ['node1', 'node2', 'node3']
|
||||||
|
|
||||||
|
# The top level result should have all successes specified
|
||||||
|
TASK_DICT = {
|
||||||
|
'0': {
|
||||||
|
'result': {
|
||||||
|
'successes': ['node1', 'node2', 'node3'],
|
||||||
|
'status': 'success',
|
||||||
|
},
|
||||||
|
'subtask_id_list': ['1'],
|
||||||
|
'status': 'complete'
|
||||||
|
},
|
||||||
|
'1': {
|
||||||
|
'result': {
|
||||||
|
'successes': ['node3'],
|
||||||
|
'status': 'success',
|
||||||
|
},
|
||||||
|
'subtask_id_list': ['2', '3'],
|
||||||
|
'status': 'complete'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_get_task_dict(task_id):
|
||||||
|
return TASK_DICT[task_id]
|
||||||
|
|
||||||
|
|
||||||
|
class TestDrydockDestroyNodesOperator:
|
||||||
|
def test_setup_configured_values(self):
|
||||||
|
op = DrydockDestroyNodeOperator(main_dag_name="main",
|
||||||
|
shipyard_conf=CONF_FILE,
|
||||||
|
task_id="t1")
|
||||||
|
op.dc = {
|
||||||
|
'physical_provisioner.destroy_interval': 1,
|
||||||
|
'physical_provisioner.destroy_timeout': 10,
|
||||||
|
}
|
||||||
|
op.setup_configured_values()
|
||||||
|
assert op.dest_interval == 1
|
||||||
|
assert op.dest_timeout == 10
|
||||||
|
|
||||||
|
def test_success_functions(self, caplog):
|
||||||
|
op = DrydockDestroyNodeOperator(main_dag_name="main",
|
||||||
|
shipyard_conf=CONF_FILE,
|
||||||
|
task_id="t1")
|
||||||
|
# testing with lists and sets.
|
||||||
|
op.target_nodes = ['n0', 'n1', 'n2']
|
||||||
|
op.successes = ['n1']
|
||||||
|
caplog.clear()
|
||||||
|
op.report_summary()
|
||||||
|
assert " Nodes requested: n0, n1, n2" in caplog.text
|
||||||
|
assert " Nodes destroyed: n1" in caplog.text
|
||||||
|
assert " Nodes not destroyed: n0, n2" in caplog.text
|
||||||
|
assert "===== End Destroy" in caplog.text
|
||||||
|
assert not op.is_destroy_successful()
|
||||||
|
|
||||||
|
op.target_nodes = set(['n0', 'n1', 'n2'])
|
||||||
|
op.successes = []
|
||||||
|
caplog.clear()
|
||||||
|
op.report_summary()
|
||||||
|
assert " Nodes requested: n0, n1, n2" in caplog.text
|
||||||
|
assert " Nodes destroyed: " in caplog.text
|
||||||
|
assert " Nodes not destroyed: n0, n1, n2" in caplog.text
|
||||||
|
assert "===== End Destroy" in caplog.text
|
||||||
|
assert not op.is_destroy_successful()
|
||||||
|
|
||||||
|
op.target_nodes = set(['n0', 'n1', 'n2'])
|
||||||
|
op.successes = set(['n0', 'n1', 'n2'])
|
||||||
|
caplog.clear()
|
||||||
|
op.report_summary()
|
||||||
|
assert " Nodes requested: n0, n1, n2" in caplog.text
|
||||||
|
assert " Nodes destroyed: n0, n1, n2" in caplog.text
|
||||||
|
assert " Nodes not destroyed: " in caplog.text
|
||||||
|
assert "===== End Destroy" in caplog.text
|
||||||
|
assert op.is_destroy_successful()
|
||||||
|
|
||||||
|
@mock.patch.object(
|
||||||
|
DrydockDestroyNodeOperator, 'create_task'
|
||||||
|
)
|
||||||
|
@mock.patch.object(
|
||||||
|
DrydockDestroyNodeOperator, 'query_task'
|
||||||
|
)
|
||||||
|
def test_execute_destroy_simple_success(self, qt, ct, caplog):
|
||||||
|
op = DrydockDestroyNodeOperator(main_dag_name="main",
|
||||||
|
shipyard_conf=CONF_FILE,
|
||||||
|
task_id="t1")
|
||||||
|
op.dc = {
|
||||||
|
'physical_provisioner.destroy_interval': 1,
|
||||||
|
'physical_provisioner.destroy_timeout': 10,
|
||||||
|
}
|
||||||
|
op.setup_configured_values()
|
||||||
|
op.execute_destroy()
|
||||||
|
assert qt.called
|
||||||
|
assert ct.called
|
||||||
|
assert not caplog.records
|
||||||
|
|
||||||
|
@mock.patch.object(
|
||||||
|
DrydockDestroyNodeOperator, 'create_task'
|
||||||
|
)
|
||||||
|
@mock.patch.object(
|
||||||
|
DrydockDestroyNodeOperator, 'query_task',
|
||||||
|
side_effect=DrydockTaskFailedException("test")
|
||||||
|
)
|
||||||
|
def test_execute_destroy_query_fail(self, qt, ct, caplog):
|
||||||
|
op = DrydockDestroyNodeOperator(main_dag_name="main",
|
||||||
|
shipyard_conf=CONF_FILE,
|
||||||
|
task_id="t1")
|
||||||
|
op.dc = {
|
||||||
|
'physical_provisioner.destroy_interval': 1,
|
||||||
|
'physical_provisioner.destroy_timeout': 10,
|
||||||
|
}
|
||||||
|
op.setup_configured_values()
|
||||||
|
op.execute_destroy()
|
||||||
|
assert qt.called
|
||||||
|
assert ct.called
|
||||||
|
assert "Task destroy_nodes has failed." in caplog.text
|
||||||
|
|
||||||
|
@mock.patch.object(
|
||||||
|
DrydockDestroyNodeOperator, 'create_task'
|
||||||
|
)
|
||||||
|
@mock.patch.object(
|
||||||
|
DrydockDestroyNodeOperator, 'query_task',
|
||||||
|
side_effect=DrydockTaskTimeoutException("test")
|
||||||
|
)
|
||||||
|
def test_execute_destroy_query_timeout(self, qt, ct, caplog):
|
||||||
|
op = DrydockDestroyNodeOperator(main_dag_name="main",
|
||||||
|
shipyard_conf=CONF_FILE,
|
||||||
|
task_id="t1")
|
||||||
|
op.dc = {
|
||||||
|
'physical_provisioner.destroy_interval': 1,
|
||||||
|
'physical_provisioner.destroy_timeout': 10,
|
||||||
|
}
|
||||||
|
op.setup_configured_values()
|
||||||
|
op.execute_destroy()
|
||||||
|
assert qt.called
|
||||||
|
assert ct.called
|
||||||
|
assert "Task destroy_nodes has timed out after 10 seconds." in (
|
||||||
|
caplog.text)
|
||||||
|
|
||||||
|
@mock.patch.object(
|
||||||
|
DrydockDestroyNodeOperator, 'get_successes_for_task',
|
||||||
|
return_value=['n0', 'n1']
|
||||||
|
)
|
||||||
|
@mock.patch.object(
|
||||||
|
DrydockDestroyNodeOperator, 'create_task'
|
||||||
|
)
|
||||||
|
@mock.patch.object(
|
||||||
|
DrydockDestroyNodeOperator, 'query_task',
|
||||||
|
side_effect=DrydockTaskTimeoutException("test")
|
||||||
|
)
|
||||||
|
def test_do_execute_fail(self, qt, ct, gs, caplog):
|
||||||
|
op = DrydockDestroyNodeOperator(main_dag_name="main",
|
||||||
|
shipyard_conf=CONF_FILE,
|
||||||
|
task_id="t1")
|
||||||
|
op.dc = {
|
||||||
|
'physical_provisioner.destroy_interval': 1,
|
||||||
|
'physical_provisioner.destroy_timeout': 10,
|
||||||
|
}
|
||||||
|
op.target_nodes = ['n0', 'n1', 'n2']
|
||||||
|
with pytest.raises(AirflowException) as ae:
|
||||||
|
op.do_execute()
|
||||||
|
assert qt.called
|
||||||
|
assert ct.called
|
||||||
|
assert gs.called
|
||||||
|
assert "Task destroy_nodes has timed out after 10 seconds." in (
|
||||||
|
caplog.text)
|
||||||
|
assert ("One or more nodes requested for destruction failed to "
|
||||||
|
"destroy") in str(ae.value)
|
||||||
|
|
||||||
|
@mock.patch.object(
|
||||||
|
DrydockDestroyNodeOperator, 'get_successes_for_task',
|
||||||
|
return_value=['n0', 'n1', 'n2']
|
||||||
|
)
|
||||||
|
@mock.patch.object(
|
||||||
|
DrydockDestroyNodeOperator, 'create_task'
|
||||||
|
)
|
||||||
|
@mock.patch.object(
|
||||||
|
DrydockDestroyNodeOperator, 'query_task',
|
||||||
|
)
|
||||||
|
def test_do_execute(self, qt, ct, gs, caplog):
|
||||||
|
op = DrydockDestroyNodeOperator(main_dag_name="main",
|
||||||
|
shipyard_conf=CONF_FILE,
|
||||||
|
task_id="t1")
|
||||||
|
op.dc = {
|
||||||
|
'physical_provisioner.destroy_interval': 1,
|
||||||
|
'physical_provisioner.destroy_timeout': 10,
|
||||||
|
}
|
||||||
|
op.target_nodes = ['n0', 'n1', 'n2']
|
||||||
|
op.do_execute()
|
||||||
|
assert qt.called
|
||||||
|
assert ct.called
|
||||||
|
assert gs.called
|
||||||
|
assert " Nodes requested: n0, n1, n2" in caplog.text
|
||||||
|
assert " Nodes destroyed: n0, n1, n2" in caplog.text
|
||||||
|
assert " Nodes not destroyed: " in caplog.text
|
||||||
|
assert "===== End Destroy" in caplog.text
|
|
@ -30,10 +30,13 @@ from shipyard_airflow.common.deployment_group.deployment_group_manager import (
|
||||||
DeploymentGroupManager
|
DeploymentGroupManager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from shipyard_airflow.plugins.drydock_base_operator import (
|
||||||
|
gen_node_name_filter,
|
||||||
|
)
|
||||||
|
|
||||||
from shipyard_airflow.plugins.drydock_nodes import (
|
from shipyard_airflow.plugins.drydock_nodes import (
|
||||||
_default_deployment_strategy,
|
|
||||||
_gen_node_name_filter,
|
|
||||||
DrydockNodesOperator,
|
DrydockNodesOperator,
|
||||||
|
gen_simple_deployment_strategy,
|
||||||
_process_deployment_groups,
|
_process_deployment_groups,
|
||||||
QueryTaskResult
|
QueryTaskResult
|
||||||
)
|
)
|
||||||
|
@ -176,7 +179,7 @@ DEP_STRAT = {'groups': yaml.safe_load(tdgm.GROUPS_YAML)}
|
||||||
|
|
||||||
|
|
||||||
def _fake_setup_ds(self):
|
def _fake_setup_ds(self):
|
||||||
self.strategy = DEP_STRAT
|
return DEP_STRAT
|
||||||
|
|
||||||
|
|
||||||
def _fake_get_task_dict(task_id):
|
def _fake_get_task_dict(task_id):
|
||||||
|
@ -217,7 +220,7 @@ class TestDrydockNodesOperator:
|
||||||
critical, has no selector values, and an all-or-nothing success
|
critical, has no selector values, and an all-or-nothing success
|
||||||
criteria
|
criteria
|
||||||
"""
|
"""
|
||||||
s = _default_deployment_strategy()
|
s = gen_simple_deployment_strategy()
|
||||||
assert s['groups'][0]['name'] == 'default'
|
assert s['groups'][0]['name'] == 'default'
|
||||||
assert s['groups'][0]['critical']
|
assert s['groups'][0]['critical']
|
||||||
assert s['groups'][0]['selectors'][0]['node_names'] == []
|
assert s['groups'][0]['selectors'][0]['node_names'] == []
|
||||||
|
@ -228,10 +231,24 @@ class TestDrydockNodesOperator:
|
||||||
'percent_successful_nodes': 100
|
'percent_successful_nodes': 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_targeted_deployment_strategy(self):
|
||||||
|
"""Test a deployment strategy used for a targeted deployment"""
|
||||||
|
s = gen_simple_deployment_strategy(name="targeted", nodes=['a', 'b'])
|
||||||
|
assert s['groups'][0]['name'] == 'targeted'
|
||||||
|
assert s['groups'][0]['critical']
|
||||||
|
assert s['groups'][0]['selectors'][0]['node_names'] == ['a', 'b']
|
||||||
|
assert s['groups'][0]['selectors'][0]['node_labels'] == []
|
||||||
|
assert s['groups'][0]['selectors'][0]['node_tags'] == []
|
||||||
|
assert s['groups'][0]['selectors'][0]['rack_names'] == []
|
||||||
|
assert s['groups'][0]['success_criteria'] == {
|
||||||
|
'percent_successful_nodes': 100
|
||||||
|
}
|
||||||
|
assert len(s['groups']) == 1
|
||||||
|
|
||||||
def test_gen_node_name_filter(self):
|
def test_gen_node_name_filter(self):
|
||||||
"""Test that a node name filter with only node_names is created"""
|
"""Test that a node name filter with only node_names is created"""
|
||||||
nodes = ['node1', 'node2']
|
nodes = ['node1', 'node2']
|
||||||
f = _gen_node_name_filter(nodes)
|
f = gen_node_name_filter(nodes)
|
||||||
assert f['filter_set'][0]['node_names'] == nodes
|
assert f['filter_set'][0]['node_names'] == nodes
|
||||||
assert len(f['filter_set']) == 1
|
assert len(f['filter_set']) == 1
|
||||||
|
|
||||||
|
@ -242,7 +259,7 @@ class TestDrydockNodesOperator:
|
||||||
assert op is not None
|
assert op is not None
|
||||||
|
|
||||||
@mock.patch.object(DrydockNodesOperator, "get_unique_doc")
|
@mock.patch.object(DrydockNodesOperator, "get_unique_doc")
|
||||||
def test_setup_deployment_strategy(self, udoc):
|
def get_deployment_strategy(self, udoc):
|
||||||
"""Assert that the base class method get_unique_doc would be invoked
|
"""Assert that the base class method get_unique_doc would be invoked
|
||||||
"""
|
"""
|
||||||
op = DrydockNodesOperator(main_dag_name="main",
|
op = DrydockNodesOperator(main_dag_name="main",
|
||||||
|
@ -252,7 +269,7 @@ class TestDrydockNodesOperator:
|
||||||
DeploymentConfigurationOperator.config_keys_defaults
|
DeploymentConfigurationOperator.config_keys_defaults
|
||||||
)
|
)
|
||||||
op.dc['physical_provisioner.deployment_strategy'] = 'taco-salad'
|
op.dc['physical_provisioner.deployment_strategy'] = 'taco-salad'
|
||||||
op._setup_deployment_strategy()
|
op.setup_deployment_strategy()
|
||||||
udoc.assert_called_once_with(
|
udoc.assert_called_once_with(
|
||||||
name='taco-salad',
|
name='taco-salad',
|
||||||
schema="shipyard/DeploymentStrategy/v1"
|
schema="shipyard/DeploymentStrategy/v1"
|
||||||
|
@ -353,21 +370,21 @@ class TestDrydockNodesOperator:
|
||||||
assert 'node4 failed to join Kubernetes' in caplog.text
|
assert 'node4 failed to join Kubernetes' in caplog.text
|
||||||
assert len(task_res.successes) == 2
|
assert len(task_res.successes) == 2
|
||||||
|
|
||||||
def test_get_successess_for_task(self):
|
def test_get_successes_for_task(self):
|
||||||
op = DrydockNodesOperator(main_dag_name="main",
|
op = DrydockNodesOperator(main_dag_name="main",
|
||||||
shipyard_conf=CONF_FILE,
|
shipyard_conf=CONF_FILE,
|
||||||
task_id="t1")
|
task_id="t1")
|
||||||
op.get_task_dict = _fake_get_task_dict
|
op.get_task_dict = _fake_get_task_dict
|
||||||
s = op._get_successes_for_task('0')
|
s = op.get_successes_for_task('0')
|
||||||
for i in range(1, 3):
|
for i in range(1, 3):
|
||||||
assert "node{}".format(i) in s
|
assert "node{}".format(i) in s
|
||||||
|
|
||||||
def test_get_successess_for_task_more_logging(self):
|
def test_get_successes_for_task_more_logging(self):
|
||||||
op = DrydockNodesOperator(main_dag_name="main",
|
op = DrydockNodesOperator(main_dag_name="main",
|
||||||
shipyard_conf=CONF_FILE,
|
shipyard_conf=CONF_FILE,
|
||||||
task_id="t1")
|
task_id="t1")
|
||||||
op.get_task_dict = _fake_get_task_dict
|
op.get_task_dict = _fake_get_task_dict
|
||||||
s = op._get_successes_for_task('99')
|
s = op.get_successes_for_task('99')
|
||||||
for i in range(97, 98):
|
for i in range(97, 98):
|
||||||
assert "node{}".format(i) in s
|
assert "node{}".format(i) in s
|
||||||
assert "node2" not in s
|
assert "node2" not in s
|
||||||
|
@ -430,7 +447,7 @@ class TestDrydockNodesOperator:
|
||||||
'_execute_deployment',
|
'_execute_deployment',
|
||||||
new=_gen_pe_func('all-success')
|
new=_gen_pe_func('all-success')
|
||||||
)
|
)
|
||||||
@mock.patch.object(DrydockNodesOperator, '_setup_deployment_strategy',
|
@mock.patch.object(DrydockNodesOperator, 'get_deployment_strategy',
|
||||||
new=_fake_setup_ds)
|
new=_fake_setup_ds)
|
||||||
def test_do_execute_with_dgm(self, nl, caplog):
|
def test_do_execute_with_dgm(self, nl, caplog):
|
||||||
op = DrydockNodesOperator(main_dag_name="main",
|
op = DrydockNodesOperator(main_dag_name="main",
|
||||||
|
|
|
@ -40,7 +40,7 @@ DESC_ACTION = """
|
||||||
id of the action invoked so that it can be queried subsequently. \n
|
id of the action invoked so that it can be queried subsequently. \n
|
||||||
FORMAT: shipyard create action <action command> --param=<parameter>
|
FORMAT: shipyard create action <action command> --param=<parameter>
|
||||||
(repeatable) [--allow-intermediate-commits] \n
|
(repeatable) [--allow-intermediate-commits] \n
|
||||||
EXAMPLE: shipyard create action redeploy_server --param="server-name=mcp"
|
EXAMPLE: shipyard create action redeploy_server --param="target_nodes=mcp"
|
||||||
shipyard create action update_site --param="continue-on-fail=true"
|
shipyard create action update_site --param="continue-on-fail=true"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -30,13 +30,13 @@ def test_create_action():
|
||||||
"""test create_action works with action id and param input"""
|
"""test create_action works with action id and param input"""
|
||||||
|
|
||||||
action_name = 'redeploy_server'
|
action_name = 'redeploy_server'
|
||||||
param = '--param="server-name=mcp"'
|
param = '--param="target_nodes=mcp"'
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
with patch.object(CreateAction, '__init__') as mock_method:
|
with patch.object(CreateAction, '__init__') as mock_method:
|
||||||
runner.invoke(shipyard,
|
runner.invoke(shipyard,
|
||||||
[auth_vars, 'create', 'action', action_name, param])
|
[auth_vars, 'create', 'action', action_name, param])
|
||||||
mock_method.assert_called_once_with(ANY, action_name,
|
mock_method.assert_called_once_with(ANY, action_name,
|
||||||
{'"server-name': 'mcp"'}, False)
|
{'"target_nodes': 'mcp"'}, False)
|
||||||
|
|
||||||
|
|
||||||
def test_create_action_negative():
|
def test_create_action_negative():
|
||||||
|
|
|
@ -32,7 +32,7 @@ run_action () {
|
||||||
|
|
||||||
# Define Variables
|
# Define Variables
|
||||||
action=$1
|
action=$1
|
||||||
server=$2
|
servers=$2
|
||||||
|
|
||||||
# Define Color
|
# Define Color
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
@ -49,11 +49,11 @@ run_action () {
|
||||||
|
|
||||||
# Note that deploy and update site do not require additional parameter
|
# Note that deploy and update site do not require additional parameter
|
||||||
# to be passed in while redeploy_server requires user to indicate which
|
# to be passed in while redeploy_server requires user to indicate which
|
||||||
# server to redeploy
|
# servers to redeploy
|
||||||
if ! [[ ${server} ]] && [[ ${action} ]]; then
|
if ! [[ ${server} ]] && [[ ${action} ]]; then
|
||||||
${base_docker_command} ${SHIPYARD_IMAGE} create action ${action}
|
${base_docker_command} ${SHIPYARD_IMAGE} create action ${action}
|
||||||
elif [[ ${action} == 'redeploy_server' && ${server} ]]; then
|
elif [[ ${action} == 'redeploy_server' && ${servers} ]]; then
|
||||||
${base_docker_command} ${SHIPYARD_IMAGE} create action redeploy_server --param="server-name=${server}"
|
${base_docker_command} ${SHIPYARD_IMAGE} create action redeploy_server --param="target_nodes=${servers}"
|
||||||
else
|
else
|
||||||
echo "Invalid Input!"
|
echo "Invalid Input!"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
@ -23,15 +23,15 @@ set -ex
|
||||||
# $ ./redeploy_server.sh controller01
|
# $ ./redeploy_server.sh controller01
|
||||||
#
|
#
|
||||||
if [[ -z "$1" ]]; then
|
if [[ -z "$1" ]]; then
|
||||||
echo -e "Please specify the server name!"
|
echo -e "Please specify the server names as a comma separated string."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Define Variables
|
# Define Variables
|
||||||
server=$1
|
servers=$1
|
||||||
|
|
||||||
# Source environment variables
|
# Source environment variables
|
||||||
source set_env
|
source set_env
|
||||||
|
|
||||||
# Execute shipyard action for redeploy_server
|
# Execute shipyard action for redeploy_server
|
||||||
bash execute_shipyard_action.sh 'redeploy_server' ${server}
|
bash execute_shipyard_action.sh 'redeploy_server' ${servers}
|
||||||
|
|
Loading…
Reference in New Issue