mirror of
https://github.com/ansible/awx.git
synced 2026-04-10 20:49:24 -02:30
Merge pull request #8364 from sean-m-sullivan/workflow_approval
Workflow approval Reviewed-by: Bianca Henderson <beeankha@gmail.com> https://github.com/beeankha
This commit is contained in:
@@ -337,7 +337,7 @@ class TowerAPIModule(TowerModule):
|
|||||||
# If we have neither of these, then we can try un-authenticated access
|
# If we have neither of these, then we can try un-authenticated access
|
||||||
self.authenticated = True
|
self.authenticated = True
|
||||||
|
|
||||||
def delete_if_needed(self, existing_item, on_delete=None):
|
def delete_if_needed(self, existing_item, on_delete=None, auto_exit=True):
|
||||||
# This will exit from the module on its own.
|
# This will exit from the module on its own.
|
||||||
# If the method successfully deletes an item and on_delete param is defined,
|
# If the method successfully deletes an item and on_delete param is defined,
|
||||||
# the on_delete parameter will be called as a method pasing in this object and the json from the response
|
# the on_delete parameter will be called as a method pasing in this object and the json from the response
|
||||||
@@ -363,6 +363,10 @@ class TowerAPIModule(TowerModule):
|
|||||||
self.json_output['changed'] = True
|
self.json_output['changed'] = True
|
||||||
self.json_output['id'] = item_id
|
self.json_output['id'] = item_id
|
||||||
self.exit_json(**self.json_output)
|
self.exit_json(**self.json_output)
|
||||||
|
if auto_exit:
|
||||||
|
self.exit_json(**self.json_output)
|
||||||
|
else:
|
||||||
|
return self.json_output
|
||||||
else:
|
else:
|
||||||
if 'json' in response and '__all__' in response['json']:
|
if 'json' in response and '__all__' in response['json']:
|
||||||
self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['json']['__all__'][0]))
|
self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['json']['__all__'][0]))
|
||||||
@@ -375,7 +379,10 @@ class TowerAPIModule(TowerModule):
|
|||||||
else:
|
else:
|
||||||
self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code']))
|
self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code']))
|
||||||
else:
|
else:
|
||||||
self.exit_json(**self.json_output)
|
if auto_exit:
|
||||||
|
self.exit_json(**self.json_output)
|
||||||
|
else:
|
||||||
|
return self.json_output
|
||||||
|
|
||||||
def modify_associations(self, association_endpoint, new_association_list):
|
def modify_associations(self, association_endpoint, new_association_list):
|
||||||
# if we got None instead of [] we are not modifying the association_list
|
# if we got None instead of [] we are not modifying the association_list
|
||||||
@@ -403,7 +410,7 @@ class TowerAPIModule(TowerModule):
|
|||||||
else:
|
else:
|
||||||
self.fail_json(msg="Failed to associate item {0}".format(response['json'].get('detail', response['json'])))
|
self.fail_json(msg="Failed to associate item {0}".format(response['json'].get('detail', response['json'])))
|
||||||
|
|
||||||
def create_if_needed(self, existing_item, new_item, endpoint, on_create=None, item_type='unknown', associations=None):
|
def create_if_needed(self, existing_item, new_item, endpoint, on_create=None, auto_exit=True, item_type='unknown', associations=None):
|
||||||
|
|
||||||
# This will exit from the module on its own
|
# This will exit from the module on its own
|
||||||
# If the method successfully creates an item and on_create param is defined,
|
# If the method successfully creates an item and on_create param is defined,
|
||||||
@@ -430,7 +437,9 @@ class TowerAPIModule(TowerModule):
|
|||||||
item_name = self.get_item_name(new_item, allow_unknown=True)
|
item_name = self.get_item_name(new_item, allow_unknown=True)
|
||||||
|
|
||||||
response = self.post_endpoint(endpoint, **{'data': new_item})
|
response = self.post_endpoint(endpoint, **{'data': new_item})
|
||||||
if response['status_code'] == 201:
|
|
||||||
|
# 200 is response from approval node creation on tower 3.7.3 or awx 15.0.0 or earlier.
|
||||||
|
if response['status_code'] in [200, 201]:
|
||||||
self.json_output['name'] = 'unknown'
|
self.json_output['name'] = 'unknown'
|
||||||
for key in ('name', 'username', 'identifier', 'hostname'):
|
for key in ('name', 'username', 'identifier', 'hostname'):
|
||||||
if key in response['json']:
|
if key in response['json']:
|
||||||
@@ -455,8 +464,11 @@ class TowerAPIModule(TowerModule):
|
|||||||
# If we have an on_create method and we actually changed something we can call on_create
|
# If we have an on_create method and we actually changed something we can call on_create
|
||||||
if on_create is not None and self.json_output['changed']:
|
if on_create is not None and self.json_output['changed']:
|
||||||
on_create(self, response['json'])
|
on_create(self, response['json'])
|
||||||
else:
|
elif auto_exit:
|
||||||
self.exit_json(**self.json_output)
|
self.exit_json(**self.json_output)
|
||||||
|
else:
|
||||||
|
last_data = response['json']
|
||||||
|
return last_data
|
||||||
|
|
||||||
def _encrypted_changed_warning(self, field, old, warning=False):
|
def _encrypted_changed_warning(self, field, old, warning=False):
|
||||||
if not warning:
|
if not warning:
|
||||||
@@ -518,7 +530,7 @@ class TowerAPIModule(TowerModule):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update_if_needed(self, existing_item, new_item, on_update=None, associations=None):
|
def update_if_needed(self, existing_item, new_item, on_update=None, auto_exit=True, associations=None):
|
||||||
# This will exit from the module on its own
|
# This will exit from the module on its own
|
||||||
# If the method successfully updates an item and on_update param is defined,
|
# If the method successfully updates an item and on_update param is defined,
|
||||||
# the on_update parameter will be called as a method pasing in this object and the json from the response
|
# the on_update parameter will be called as a method pasing in this object and the json from the response
|
||||||
@@ -578,14 +590,23 @@ class TowerAPIModule(TowerModule):
|
|||||||
else:
|
else:
|
||||||
last_data = response['json']
|
last_data = response['json']
|
||||||
on_update(self, last_data)
|
on_update(self, last_data)
|
||||||
else:
|
elif auto_exit:
|
||||||
self.exit_json(**self.json_output)
|
self.exit_json(**self.json_output)
|
||||||
|
|
||||||
def create_or_update_if_needed(self, existing_item, new_item, endpoint=None, item_type='unknown', on_create=None, on_update=None, associations=None):
|
|
||||||
if existing_item:
|
|
||||||
return self.update_if_needed(existing_item, new_item, on_update=on_update, associations=associations)
|
|
||||||
else:
|
else:
|
||||||
return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, associations=associations)
|
if response is None:
|
||||||
|
last_data = existing_item
|
||||||
|
else:
|
||||||
|
last_data = response['json']
|
||||||
|
return last_data
|
||||||
|
|
||||||
|
def create_or_update_if_needed(
|
||||||
|
self, existing_item, new_item, endpoint=None, item_type='unknown', on_create=None, on_update=None, auto_exit=True, associations=None
|
||||||
|
):
|
||||||
|
if existing_item:
|
||||||
|
return self.update_if_needed(existing_item, new_item, on_update=on_update, auto_exit=auto_exit, associations=associations)
|
||||||
|
else:
|
||||||
|
return self.create_if_needed(
|
||||||
|
existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, auto_exit=auto_exit, associations=associations)
|
||||||
|
|
||||||
def logout(self):
|
def logout(self):
|
||||||
if self.authenticated and self.oauth_token_id:
|
if self.authenticated and self.oauth_token_id:
|
||||||
|
|||||||
@@ -91,8 +91,28 @@ options:
|
|||||||
description:
|
description:
|
||||||
- Name of unified job template to run in the workflow.
|
- Name of unified job template to run in the workflow.
|
||||||
- Can be a job template, project, inventory source, etc.
|
- Can be a job template, project, inventory source, etc.
|
||||||
- Omit if creating an approval node (not yet implemented).
|
- Omit if creating an approval node.
|
||||||
|
- This parameter is mutually exclusive with C(approval_node).
|
||||||
type: str
|
type: str
|
||||||
|
approval_node:
|
||||||
|
description:
|
||||||
|
- A dictionary of Name, description, and timeout values for the approval node.
|
||||||
|
- This parameter is mutually exclusive with C(unified_job_template).
|
||||||
|
type: dict
|
||||||
|
suboptions:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of this workflow approval template.
|
||||||
|
type: str
|
||||||
|
required: True
|
||||||
|
description:
|
||||||
|
description:
|
||||||
|
- Optional description of this workflow approval template.
|
||||||
|
type: str
|
||||||
|
timeout:
|
||||||
|
description:
|
||||||
|
- The amount of time (in seconds) before the approval node expires and fails.
|
||||||
|
type: int
|
||||||
all_parents_must_converge:
|
all_parents_must_converge:
|
||||||
description:
|
description:
|
||||||
- If enabled then the node will only run if all of the parent nodes have met the criteria to reach this node
|
- If enabled then the node will only run if all of the parent nodes have met the criteria to reach this node
|
||||||
@@ -176,6 +196,7 @@ def main():
|
|||||||
diff_mode=dict(type='bool'),
|
diff_mode=dict(type='bool'),
|
||||||
verbosity=dict(choices=['0', '1', '2', '3', '4', '5']),
|
verbosity=dict(choices=['0', '1', '2', '3', '4', '5']),
|
||||||
unified_job_template=dict(),
|
unified_job_template=dict(),
|
||||||
|
approval_node=dict(type='dict'),
|
||||||
all_parents_must_converge=dict(type='bool'),
|
all_parents_must_converge=dict(type='bool'),
|
||||||
success_nodes=dict(type='list', elements='str'),
|
success_nodes=dict(type='list', elements='str'),
|
||||||
always_nodes=dict(type='list', elements='str'),
|
always_nodes=dict(type='list', elements='str'),
|
||||||
@@ -183,14 +204,20 @@ def main():
|
|||||||
credentials=dict(type='list', elements='str'),
|
credentials=dict(type='list', elements='str'),
|
||||||
state=dict(choices=['present', 'absent'], default='present'),
|
state=dict(choices=['present', 'absent'], default='present'),
|
||||||
)
|
)
|
||||||
|
mutually_exclusive = [("unified_job_template", "approval_node")]
|
||||||
|
required_one_of = [["unified_job_template", "approval_node", "success_nodes", "always_nodes", "failure_nodes"]]
|
||||||
|
|
||||||
# Create a module for ourselves
|
# Create a module for ourselves
|
||||||
module = TowerAPIModule(argument_spec=argument_spec)
|
module = TowerAPIModule(
|
||||||
|
argument_spec=argument_spec,
|
||||||
|
mutually_exclusive=mutually_exclusive,
|
||||||
|
required_one_of=required_one_of,
|
||||||
|
)
|
||||||
|
|
||||||
# Extract our parameters
|
# Extract our parameters
|
||||||
identifier = module.params.get('identifier')
|
identifier = module.params.get('identifier')
|
||||||
state = module.params.get('state')
|
state = module.params.get('state')
|
||||||
|
approval_node = module.params.get('approval_node')
|
||||||
new_fields = {}
|
new_fields = {}
|
||||||
search_fields = {'identifier': identifier}
|
search_fields = {'identifier': identifier}
|
||||||
|
|
||||||
@@ -264,10 +291,43 @@ def main():
|
|||||||
# If the state was present and we can let the module build or update the existing item, this will return on its own
|
# If the state was present and we can let the module build or update the existing item, this will return on its own
|
||||||
module.create_or_update_if_needed(
|
module.create_or_update_if_needed(
|
||||||
existing_item, new_fields,
|
existing_item, new_fields,
|
||||||
endpoint='workflow_job_template_nodes', item_type='workflow_job_template_node',
|
endpoint='workflow_job_template_nodes', item_type='workflow_job_template_node', auto_exit=not approval_node,
|
||||||
associations=association_fields
|
associations=association_fields
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create approval node unified template or update existing
|
||||||
|
if approval_node:
|
||||||
|
# Set Approval Fields
|
||||||
|
new_fields = {}
|
||||||
|
|
||||||
|
# Extract Parameters
|
||||||
|
if approval_node.get('name') is None:
|
||||||
|
module.fail_json(msg="Approval node name is required to create approval node.")
|
||||||
|
if approval_node.get('name') is not None:
|
||||||
|
new_fields['name'] = approval_node['name']
|
||||||
|
if approval_node.get('description') is not None:
|
||||||
|
new_fields['description'] = approval_node['description']
|
||||||
|
if approval_node.get('timeout') is not None:
|
||||||
|
new_fields['timeout'] = approval_node['timeout']
|
||||||
|
|
||||||
|
# Find created workflow node ID
|
||||||
|
search_fields = {'identifier': identifier}
|
||||||
|
search_fields['workflow_job_template'] = workflow_job_template_id
|
||||||
|
workflow_job_template_node = module.get_one('workflow_job_template_nodes', **{'data': search_fields})
|
||||||
|
workflow_job_template_node_id = workflow_job_template_node['id']
|
||||||
|
module.json_output['workflow_node_id'] = workflow_job_template_node_id
|
||||||
|
existing_item = None
|
||||||
|
# Due to not able to lookup workflow_approval_templates, find the existing item in another place
|
||||||
|
if workflow_job_template_node['related'].get('unified_job_template') is not None:
|
||||||
|
existing_item = module.get_endpoint(workflow_job_template_node['related']['unified_job_template'])['json']
|
||||||
|
approval_endpoint = 'workflow_job_template_nodes/{0}/create_approval_template/'.format(workflow_job_template_node_id)
|
||||||
|
module.create_or_update_if_needed(
|
||||||
|
existing_item, new_fields,
|
||||||
|
endpoint=approval_endpoint, item_type='workflow_job_template_approval_node',
|
||||||
|
associations=association_fields
|
||||||
|
)
|
||||||
|
module.exit_json(**module.json_output)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ no_api_parameter_ok = {
|
|||||||
# We take an organization here to help with the lookups only
|
# We take an organization here to help with the lookups only
|
||||||
'tower_job_template': ['survey_spec', 'organization'],
|
'tower_job_template': ['survey_spec', 'organization'],
|
||||||
'tower_inventory_source': ['organization'],
|
'tower_inventory_source': ['organization'],
|
||||||
# Organization is how we are looking up job templates
|
# Organization is how we are looking up job templates, Approval node is for workflow_approval_templates
|
||||||
'tower_workflow_job_template_node': ['organization'],
|
'tower_workflow_job_template_node': ['organization', 'approval_node'],
|
||||||
# Survey is how we handle associations
|
# Survey is how we handle associations
|
||||||
'tower_workflow_job_template': ['survey'],
|
'tower_workflow_job_template': ['survey'],
|
||||||
# ad hoc commands support interval and timeout since its more like tower_job_launc
|
# ad hoc commands support interval and timeout since its more like tower_job_launc
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ __metaclass__ = type
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from awx.main.models import WorkflowJobTemplateNode, WorkflowJobTemplate, JobTemplate
|
from awx.main.models import WorkflowJobTemplateNode, WorkflowJobTemplate, JobTemplate, UnifiedJobTemplate
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -53,27 +53,26 @@ def test_create_workflow_job_template_node(run_module, admin_user, wfjt, job_tem
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_create_workflow_job_template_node_no_template(run_module, admin_user, wfjt, job_template):
|
def test_create_workflow_job_template_node_approval_node(run_module, admin_user, wfjt, job_template):
|
||||||
"""This is a part of the API contract for creating approval nodes
|
"""This is a part of the API contract for creating approval nodes"""
|
||||||
and at some point in the future, tha feature will be supported by the collection
|
|
||||||
"""
|
|
||||||
this_identifier = '42🐉'
|
this_identifier = '42🐉'
|
||||||
result = run_module('tower_workflow_job_template_node', {
|
result = run_module('tower_workflow_job_template_node', {
|
||||||
'identifier': this_identifier,
|
'identifier': this_identifier,
|
||||||
'workflow_job_template': wfjt.name,
|
'workflow_job_template': wfjt.name,
|
||||||
'organization': wfjt.organization.name,
|
'organization': wfjt.organization.name,
|
||||||
|
'approval_node': {'name': 'foo-jt-approval'}
|
||||||
}, admin_user)
|
}, admin_user)
|
||||||
assert not result.get('failed', False), result.get('msg', result)
|
assert not result.get('failed', False), result.get('msg', result)
|
||||||
assert result.get('changed', False), result
|
assert result.get('changed', False), result
|
||||||
|
|
||||||
node = WorkflowJobTemplateNode.objects.get(pk=result['id'])
|
node = WorkflowJobTemplateNode.objects.get(identifier=this_identifier)
|
||||||
# node = WorkflowJobTemplateNode.objects.first()
|
approval_node = UnifiedJobTemplate.objects.get(name='foo-jt-approval')
|
||||||
|
|
||||||
assert result['id'] == node.id
|
assert result['id'] == approval_node.id
|
||||||
|
|
||||||
assert node.identifier == this_identifier
|
assert node.identifier == this_identifier
|
||||||
assert node.workflow_job_template_id == wfjt.id
|
assert node.workflow_job_template_id == wfjt.id
|
||||||
assert node.unified_job_template_id is None
|
assert node.unified_job_template_id is approval_node.id
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
demo_project_name: "AWX-Collection-tests-tower_workflow_job_template-proj-{{ test_id }}"
|
demo_project_name: "AWX-Collection-tests-tower_workflow_job_template-proj-{{ test_id }}"
|
||||||
jt1_name: "AWX-Collection-tests-tower_workflow_job_template-jt1-{{ test_id }}"
|
jt1_name: "AWX-Collection-tests-tower_workflow_job_template-jt1-{{ test_id }}"
|
||||||
jt2_name: "AWX-Collection-tests-tower_workflow_job_template-jt2-{{ test_id }}"
|
jt2_name: "AWX-Collection-tests-tower_workflow_job_template-jt2-{{ test_id }}"
|
||||||
|
approval_node_name: "AWX-Collection-tests-tower_workflow_approval_node-{{ test_id }}"
|
||||||
lab1: "AWX-Collection-tests-tower_job_template-lab1-{{ test_id }}"
|
lab1: "AWX-Collection-tests-tower_job_template-lab1-{{ test_id }}"
|
||||||
wfjt_name: "AWX-Collection-tests-tower_workflow_job_template-wfjt-{{ test_id }}"
|
wfjt_name: "AWX-Collection-tests-tower_workflow_job_template-wfjt-{{ test_id }}"
|
||||||
email_not: "AWX-Collection-tests-tower_job_template-email-not-{{ test_id }}"
|
email_not: "AWX-Collection-tests-tower_job_template-email-not-{{ test_id }}"
|
||||||
@@ -145,6 +146,44 @@
|
|||||||
unified_job_template: "{{ jt1_name }}"
|
unified_job_template: "{{ jt1_name }}"
|
||||||
workflow: "{{ wfjt_name }}"
|
workflow: "{{ wfjt_name }}"
|
||||||
|
|
||||||
|
- name: Fail if no name is set for approval
|
||||||
|
tower_workflow_job_template_node:
|
||||||
|
identifier: approval_test
|
||||||
|
approval_node:
|
||||||
|
description: "{{ approval_node_name }}"
|
||||||
|
workflow: "{{ wfjt_name }}"
|
||||||
|
register: no_name_results
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- "no_name_results.msg == 'Approval node name is required to create approval node.'"
|
||||||
|
|
||||||
|
- name: Create approval node
|
||||||
|
tower_workflow_job_template_node:
|
||||||
|
identifier: approval_test
|
||||||
|
approval_node:
|
||||||
|
name: "{{ approval_node_name }}"
|
||||||
|
timeout: 900
|
||||||
|
workflow: "{{ wfjt_name }}"
|
||||||
|
|
||||||
|
- name: Create link for root node
|
||||||
|
tower_workflow_job_template_node:
|
||||||
|
identifier: root
|
||||||
|
workflow: "{{ wfjt_name }}"
|
||||||
|
success_nodes:
|
||||||
|
- approval_test
|
||||||
|
always_nodes:
|
||||||
|
- leaf
|
||||||
|
|
||||||
|
- name: Delete approval node
|
||||||
|
tower_workflow_job_template_node:
|
||||||
|
identifier: approval_test
|
||||||
|
approval_node:
|
||||||
|
name: "{{ approval_node_name }}"
|
||||||
|
state: absent
|
||||||
|
workflow: "{{ wfjt_name }}"
|
||||||
|
|
||||||
- name: Add started notifications to workflow job template
|
- name: Add started notifications to workflow job template
|
||||||
tower_workflow_job_template:
|
tower_workflow_job_template:
|
||||||
name: "{{ wfjt_name }}"
|
name: "{{ wfjt_name }}"
|
||||||
|
|||||||
Reference in New Issue
Block a user