fix completeness

This commit is contained in:
sean-m-ssullivan 2021-04-22 13:46:59 -05:00
parent 4cd4845617
commit ec312358e2
5 changed files with 494 additions and 127 deletions

View File

@ -740,3 +740,37 @@ class TowerAPIModule(TowerModule):
def wait_output(self, response):
for k in ('id', 'status', 'elapsed', 'started', 'finished'):
self.json_output[k] = response['json'].get(k)
def wait_on_workflow_node_url(self, url, object_name, object_type, timeout=30, interval=10, **kwargs):
# Grab our start time to compare against for the timeout
start = time.time()
result = self.get_endpoint(url, **kwargs)
while result["json"]["count"] == 0:
# If we are past our time out fail with a message
if timeout and timeout < time.time() - start:
# Account for Legacy messages
self.json_output["msg"] = "Monitoring of {0} - {1} aborted due to timeout, {2}".format(object_type, object_name, url)
self.wait_output(result)
self.fail_json(**self.json_output)
# Put the process to sleep for our interval
time.sleep(interval)
result = self.get_endpoint(url, **kwargs)
if object_type == "Workflow Approval":
# Approval jobs have no elapsed time so return
return result["json"]["results"][0]
else:
# Removed time so far from timeout.
revised_timeout = timeout - (time.time() - start)
# Now that Job has been found, wait for it to finish
result = self.wait_on_url(
url=result["json"]["results"][0]["related"]["job"],
object_name=object_name,
object_type=object_type,
timeout=revised_timeout,
interval=interval,
)
self.json_output["job_data"] = result["json"]
return result

View File

@ -0,0 +1,125 @@
#!/usr/bin/python
# coding: utf-8 -*-
# (c) 2021, Sean Sullivan
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
"metadata_version": "1.1",
"status": ["preview"],
"supported_by": "community",
}
DOCUMENTATION = """
---
module: tower_workflow_approval
author: "Sean Sullivan (@sean-m-sullivan)"
short_description: Approve an approval node in a workflow job.
description:
- Approve an approval node in a workflow job. See
U(https://www.ansible.com/tower) for an overview.
options:
workflow_job_id:
description:
- ID of the workflow job to monitor for approval.
required: True
type: int
name:
description:
- Name of the Approval node to approve or deny.
required: True
type: str
action:
description:
- Type of action to take.
choices: ["approve", "deny"]
default: "approve"
type: str
interval:
description:
- The interval in sections, to request an update from Tower.
required: False
default: 1
type: float
timeout:
description:
- Maximum time in seconds to wait for a workflow job to to reach approval node.
default: 10
type: int
extends_documentation_fragment: awx.awx.auth
"""
EXAMPLES = """
- name: Launch a workflow with a timeout of 10 seconds
tower_workflow_launch:
workflow_template: "Test Workflow"
wait: False
register: workflow
- name: Wait for approval node to activate and approve
tower_workflow_approval:
workflow_job_id: "{{ workflow.id }}"
name: Approve Me
interval: 10
timeout: 20
action: deny
"""
RETURN = """
"""
from ..module_utils.tower_api import TowerAPIModule
def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict(
workflow_job_id=dict(type="int", required=True),
name=dict(required=True),
action=dict(choices=["approve", "deny"], default="approve"),
timeout=dict(type="int", default=10),
interval=dict(type="float", default=1),
)
# Create a module for ourselves
module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters
workflow_job_id = module.params.get("workflow_job_id")
name = module.params.get("name")
action = module.params.get("action")
timeout = module.params.get("timeout")
interval = module.params.get("interval")
# Attempt to look up workflow job based on the provided id
approval_job = module.wait_on_workflow_node_url(
url="workflow_jobs/{0}/workflow_nodes/".format(workflow_job_id),
object_name=name,
object_type="Workflow Approval",
timeout=timeout,
interval=interval,
**{
"data": {
"job__name": name,
}
}
)
response = module.post_endpoint("{0}{1}".format(approval_job["related"]["job"], action))
if response["status_code"] == 204:
module.json_output["changed"] = True
# Attempt to look up jobs based on the status
module.exit_json(**module.json_output)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,115 @@
#!/usr/bin/python
# coding: utf-8 -*-
# (c) 2021, Sean Sullivan
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
"metadata_version": "1.1",
"status": ["preview"],
"supported_by": "community",
}
DOCUMENTATION = """
---
module: tower_workflow_node_wait
author: "Sean Sullivan (@sean-m-sullivan)"
short_description: Approve an approval node in a workflow job.
description:
- Approve an approval node in a workflow job. See
U(https://www.ansible.com/tower) for an overview.
options:
workflow_job_id:
description:
- ID of the workflow job to monitor for node.
required: True
type: int
name:
description:
- Name of the workflow node to wait on.
required: True
type: str
interval:
description:
- The interval in sections, to request an update from Tower.
required: False
default: 1
type: float
timeout:
description:
- Maximum time in seconds to wait for a workflow job to to reach approval node.
default: 10
type: int
extends_documentation_fragment: awx.awx.auth
"""
EXAMPLES = """
- name: Launch a workflow with a timeout of 10 seconds
tower_workflow_launch:
workflow_template: "Test Workflow"
wait: False
register: workflow
- name: Wait for a workflow node to finish
tower_workflow_node_wait:
workflow_job_id: "{{ workflow.id }}"
name: Approval Data Step
timeout: 120
"""
RETURN = """
"""
from ..module_utils.tower_api import TowerAPIModule
import time
def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict(
workflow_job_id=dict(type="int", required=True),
name=dict(required=True),
timeout=dict(type="int", default=10),
interval=dict(type="float", default=1),
)
# Create a module for ourselves
module = TowerAPIModule(argument_spec=argument_spec)
# Extract our parameters
workflow_job_id = module.params.get("workflow_job_id")
name = module.params.get("name")
timeout = module.params.get("timeout")
interval = module.params.get("interval")
node_url = "workflow_jobs/{0}/workflow_nodes/?job__name={1}".format(workflow_job_id, name)
# Attempt to look up workflow job node based on the provided id
result = module.wait_on_workflow_node_url(
url="workflow_jobs/{0}/workflow_nodes/".format(workflow_job_id),
object_name=name,
object_type="Workflow Node",
timeout=timeout,
interval=interval,
**{
"data": {
"job__name": name,
}
}
)
# Attempt to look up jobs based on the status
module.exit_json(**module.json_output)
if __name__ == "__main__":
main()

View File

@ -36,6 +36,7 @@ no_endpoint_for_module = [
'tower_receive',
'tower_send',
'tower_workflow_launch',
'tower_workflow_node_wait',
'tower_job_cancel',
'tower_workflow_template',
'tower_ad_hoc_command_wait',
@ -64,16 +65,17 @@ no_api_parameter_ok = {
'tower_ad_hoc_command': ['interval', 'timeout', 'wait'],
# tower_group parameters to perserve hosts and children.
'tower_group': ['preserve_existing_children', 'preserve_existing_hosts'],
# tower_workflow_approval parameters that do not apply when approving an approval node.
'tower_workflow_approval': ['action', 'interval', 'timeout', 'workflow_job_id'],
}
# When this tool was created we were not feature complete. Adding something in here indicates a module
# that needs to be developed. If the module is found on the file system it will auto-detect that the
# work is being done and will bypass this check. At some point this module should be removed from this list.
needs_development = [
'tower_workflow_approval',
]
needs_development = ['tower_inventory_script']
needs_param_development = {
'tower_host': ['instance_id'],
'tower_workflow_approval': ['description', 'execution_environment'],
}
# -----------------------------------------------------------------------------------------------------------

View File

@ -7,146 +7,237 @@
- name: Generate names
set_fact:
wfjt_name1: "AWX-Collection-tests-tower_workflow_launch--wfjt1-{{ test_id }}"
wfjt_name2: "AWX-Collection-tests-tower_workflow_launch--wfjt1-{{ test_id }}-2"
approval_node_name: "AWX-Collection-tests-tower_workflow_launch_approval_node-{{ test_id }}"
- name: Create our workflow
tower_workflow_job_template:
name: "{{ wfjt_name1 }}"
state: present
- block:
- name: Add a node
tower_workflow_job_template_node:
workflow_job_template: "{{ wfjt_name1 }}"
unified_job_template: "Demo Job Template"
identifier: leaf
register: new_node
- name: Create our workflow
tower_workflow_job_template:
name: "{{ wfjt_name1 }}"
state: present
- name: Connect to Tower server but request an invalid workflow
tower_workflow_launch:
workflow_template: "Does Not Exist"
ignore_errors: true
register: result
- name: Add a node
tower_workflow_job_template_node:
workflow_job_template: "{{ wfjt_name1 }}"
unified_job_template: "Demo Job Template"
identifier: leaf
register: new_node
- assert:
that:
- result is failed
- "'Unable to find workflow job template' in result.msg"
- name: Connect to Tower server but request an invalid workflow
tower_workflow_launch:
workflow_template: "Does Not Exist"
ignore_errors: true
register: result
- name: Run the workflow without waiting (this should just give us back a job ID)
tower_workflow_launch:
workflow_template: "{{ wfjt_name1 }}"
wait: false
ignore_errors: true
register: result
- assert:
that:
- result is failed
- "'Unable to find workflow job template' in result.msg"
- assert:
that:
- result is not failed
- "'id' in result['job_info']"
- name: Run the workflow without waiting (this should just give us back a job ID)
tower_workflow_launch:
workflow_template: "{{ wfjt_name1 }}"
wait: false
ignore_errors: true
register: result
- name: Kick off a workflow and wait for it, but only for a second
tower_workflow_launch:
workflow_template: "{{ wfjt_name1 }}"
timeout: 1
ignore_errors: true
register: result
- assert:
that:
- result is not failed
- "'id' in result['job_info']"
- assert:
that:
- result is failed
- "'Monitoring of Workflow Job - {{ wfjt_name1 }} aborted due to timeout' in result.msg"
- name: Kick off a workflow and wait for it, but only for a second
tower_workflow_launch:
workflow_template: "{{ wfjt_name1 }}"
timeout: 1
ignore_errors: true
register: result
- name: Kick off a workflow and wait for it
tower_workflow_launch:
workflow_template: "{{ wfjt_name1 }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- "'Monitoring of Workflow Job - {{ wfjt_name1 }} aborted due to timeout' in result.msg"
- assert:
that:
- result is not failed
- "'id' in result['job_info']"
- name: Kick off a workflow and wait for it
tower_workflow_launch:
workflow_template: "{{ wfjt_name1 }}"
ignore_errors: true
register: result
- name: Kick off a workflow with extra_vars but not enabled
tower_workflow_launch:
workflow_template: "{{ wfjt_name1 }}"
extra_vars:
var1: My First Variable
var2: My Second Variable
ignore_errors: true
register: result
- assert:
that:
- result is not failed
- "'id' in result['job_info']"
- assert:
that:
- result is failed
- "'The field extra_vars was specified but the workflow job template does not allow for it to be overridden' in result.errors"
- name: Kick off a workflow with extra_vars but not enabled
tower_workflow_launch:
workflow_template: "{{ wfjt_name1 }}"
extra_vars:
var1: My First Variable
var2: My Second Variable
ignore_errors: true
register: result
- name: Prompt the workflow's with survey
tower_workflow_job_template:
name: "{{ wfjt_name1 }}"
state: present
survey_enabled: true
ask_variables_on_launch: false
survey:
name: ''
description: ''
spec:
- question_name: Basic Name
question_description: Name
required: true
type: text
variable: basic_name
min: 0
max: 1024
default: ''
choices: ''
new_question: true
- question_name: Choose yes or no?
question_description: Choosing yes or no.
required: false
type: multiplechoice
variable: option_true_false
min:
max:
default: 'yes'
choices: |-
yes
no
new_question: true
- assert:
that:
- result is failed
- "'The field extra_vars was specified but the workflow job template does not allow for it to be overridden' in result.errors"
- name: Kick off a workflow with survey
tower_workflow_launch:
workflow_template: "{{ wfjt_name1 }}"
extra_vars:
basic_name: My First Variable
option_true_false: 'no'
ignore_errors: true
register: result
- name: Prompt the workflow's with survey
tower_workflow_job_template:
name: "{{ wfjt_name1 }}"
state: present
survey_enabled: true
ask_variables_on_launch: false
survey:
name: ''
description: ''
spec:
- question_name: Basic Name
question_description: Name
required: true
type: text
variable: basic_name
min: 0
max: 1024
default: ''
choices: ''
new_question: true
- question_name: Choose yes or no?
question_description: Choosing yes or no.
required: false
type: multiplechoice
variable: option_true_false
min:
max:
default: 'yes'
choices: |-
yes
no
new_question: true
- assert:
that:
- result is not failed
- name: Kick off a workflow with survey
tower_workflow_launch:
workflow_template: "{{ wfjt_name1 }}"
extra_vars:
basic_name: My First Variable
option_true_false: 'no'
ignore_errors: true
register: result
- name: Prompt the workflow's extra_vars on launch
tower_workflow_job_template:
name: "{{ wfjt_name1 }}"
state: present
ask_variables_on_launch: true
- assert:
that:
- result is not failed
- name: Kick off a workflow with extra_vars
tower_workflow_launch:
workflow_template: "{{ wfjt_name1 }}"
extra_vars:
basic_name: My First Variable
var1: My First Variable
var2: My Second Variable
ignore_errors: true
register: result
- name: Prompt the workflow's extra_vars on launch
tower_workflow_job_template:
name: "{{ wfjt_name1 }}"
state: present
ask_variables_on_launch: true
- assert:
that:
- result is not failed
- name: Kick off a workflow with extra_vars
tower_workflow_launch:
workflow_template: "{{ wfjt_name1 }}"
extra_vars:
basic_name: My First Variable
var1: My First Variable
var2: My Second Variable
ignore_errors: true
register: result
- name: Clean up test workflow
tower_workflow_job_template:
name: "{{ wfjt_name1 }}"
state: absent
- assert:
that:
- result is not failed
- name: Test waiting for an approval node that doesn't exit on the last workflow for failure.
tower_workflow_approval:
workflow_job_id: "{{ result.id }}"
name: Test workflow approval
interval: 1
timeout: 2
action: deny
register: result
ignore_errors: true
- assert:
that:
- result is failed
- "'Monitoring of Workflow Approval - Test workflow approval aborted due to timeout' in result.msg"
- name: Create new Workflow
tower_workflow_job_template:
name: "{{ wfjt_name2 }}"
state: present
- name: Add a job node
tower_workflow_job_template_node:
workflow_job_template: "{{ wfjt_name2 }}"
unified_job_template: "Demo Job Template"
identifier: leaf
# Test tower_workflow_approval and tower_workflow_node_wait
- name: Create approval node
tower_workflow_job_template_node:
identifier: approval_test
approval_node:
name: "{{ approval_node_name }}"
timeout: 900
workflow: "{{ wfjt_name2 }}"
- name: Create link for approval node
tower_workflow_job_template_node:
identifier: approval_test
workflow: "{{ wfjt_name2 }}"
always_nodes:
- leaf
- name: Run the workflow without waiting This should pause waiting for approval
tower_workflow_launch:
workflow_template: "{{ wfjt_name2 }}"
wait: false
ignore_errors: true
register: wfjt_info
- name: Wait for Job node wait to fail as it is waiting on approval
awx.awx.tower_workflow_node_wait:
workflow_job_id: "{{ wfjt_info.id }}"
name: Demo Job Template
interval: 1
timeout: 5
register: result
ignore_errors: true
- assert:
that:
- result is failed
- "'Monitoring of Workflow Node - Demo Job Template aborted due to timeout' in result.msg"
- name: Wait for approval node to activate and approve
awx.awx.tower_workflow_approval:
workflow_job_id: "{{ wfjt_info.id }}"
name: "{{ approval_node_name }}"
interval: 1
timeout: 10
action: deny
register: result
- assert:
that:
- result is not failed
- result is changed
- name: Wait for workflow job to finish max 120s
tower_job_wait:
job_id: "{{ wfjt_info.id }}"
timeout: 120
job_type: "workflow_jobs"
always:
- name: Clean up test workflow
tower_workflow_job_template:
name: "{{ item }}"
state: absent
with_items:
- "{{ wfjt_name1 }}"
- "{{ wfjt_name2 }}"