diff --git a/awx/main/scheduler/dag_simple.py b/awx/main/scheduler/dag_simple.py index 2ebda0375b05..f3120581d10f 100644 --- a/awx/main/scheduler/dag_simple.py +++ b/awx/main/scheduler/dag_simple.py @@ -59,7 +59,7 @@ def __len__(self): def __iter__(self): return self.nodes.__iter__() - def generate_graphviz_plot(self): + def generate_graphviz_plot(self, file_name="/awx_devel/graph.gv"): def run_status(obj): dnr = "RUN" status = "NA" @@ -83,6 +83,8 @@ def run_status(obj): color = 'green' elif status == 'failed': color = 'red' + elif obj.do_not_run is True: + color = 'gray' doc += "%s [color = %s]\n" % ( run_status(n['node_object']), color @@ -96,7 +98,7 @@ def run_status(obj): label, ) doc += "}\n" - gv_file = open('/awx_devel/graph.gv', 'w') + gv_file = open(file_name, 'w') gv_file.write(doc) gv_file.close() diff --git a/awx/main/tests/unit/scheduler/test_dag_workflow.py b/awx/main/tests/unit/scheduler/test_dag_workflow.py index 81b304e2647b..648a089a7943 100644 --- a/awx/main/tests/unit/scheduler/test_dag_workflow.py +++ b/awx/main/tests/unit/scheduler/test_dag_workflow.py @@ -1,5 +1,6 @@ import pytest import uuid +import os from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_text @@ -270,3 +271,48 @@ def test_cancel_still_runs_children(self, workflow_dag_canceled): g.mark_dnr_nodes() assert set([nodes[1], nodes[2]]) == set(g.bfs_nodes_to_run()) + + +@pytest.mark.skip(reason="Run manually to re-generate doc images") +class TestDocsExample(): + @pytest.fixture + def complex_dag(self, wf_node_generator): + g = WorkflowDAG() + nodes = [wf_node_generator() for i in range(10)] + map(lambda n: g.add_node(n), nodes) + + g.add_edge(nodes[0], nodes[1], "failure_nodes") + g.add_edge(nodes[0], nodes[2], "success_nodes") + g.add_edge(nodes[0], nodes[3], "always_nodes") + g.add_edge(nodes[1], nodes[4], "success_nodes") + g.add_edge(nodes[1], nodes[5], "failure_nodes") + + g.add_edge(nodes[2], nodes[6], "failure_nodes") + g.add_edge(nodes[3], nodes[6], "success_nodes") + g.add_edge(nodes[4], nodes[6], "always_nodes") + + g.add_edge(nodes[6], nodes[7], "always_nodes") + g.add_edge(nodes[6], nodes[8], "success_nodes") + g.add_edge(nodes[6], nodes[9], "failure_nodes") + + return (g, nodes) + + def test_dnr_step(self, complex_dag): + (g, nodes) = complex_dag + base_dir = '/awx_devel' + + g.generate_graphviz_plot(file_name=os.path.join(base_dir, "workflow_step0.gv")) + nodes[0].job = Job(status='successful') + g.mark_dnr_nodes() + g.generate_graphviz_plot(file_name=os.path.join(base_dir, "workflow_step1.gv")) + nodes[2].job = Job(status='successful') + nodes[3].job = Job(status='successful') + g.mark_dnr_nodes() + g.generate_graphviz_plot(file_name=os.path.join(base_dir, "workflow_step2.gv")) + nodes[6].job = Job(status='failed') + g.mark_dnr_nodes() + g.generate_graphviz_plot(file_name=os.path.join(base_dir, "workflow_step3.gv")) + nodes[7].job = Job(status='successful') + nodes[9].job = Job(status='successful') + g.mark_dnr_nodes() + g.generate_graphviz_plot(file_name=os.path.join(base_dir, "workflow_step4.gv")) diff --git a/docs/img/workflow_step0.png b/docs/img/workflow_step0.png new file mode 100644 index 000000000000..25495c139a90 Binary files /dev/null and b/docs/img/workflow_step0.png differ diff --git a/docs/img/workflow_step1.png b/docs/img/workflow_step1.png new file mode 100644 index 000000000000..984e31b528bf Binary files /dev/null and b/docs/img/workflow_step1.png differ diff --git a/docs/img/workflow_step2.png b/docs/img/workflow_step2.png new file mode 100644 index 000000000000..d9d9c706065b Binary files /dev/null and b/docs/img/workflow_step2.png differ diff --git a/docs/img/workflow_step3.png b/docs/img/workflow_step3.png new file mode 100644 index 000000000000..56cc7c5445ba Binary files /dev/null and b/docs/img/workflow_step3.png differ diff --git a/docs/img/workflow_step4.png b/docs/img/workflow_step4.png new file mode 100644 index 000000000000..1cf722d2f351 Binary files /dev/null and b/docs/img/workflow_step4.png differ diff --git a/docs/workflow.md b/docs/workflow.md index ceb7f8ee6548..e4becca74fd6 100644 --- a/docs/workflow.md +++ b/docs/workflow.md @@ -64,7 +64,7 @@ As stated, workflow job templates can be created with populated `extra_vars`. Th Job resources spawned by workflow jobs are needed by workflow to run correctly. Therefore deletion of spawned job resources is blocked while the underlying workflow job is executing. -Other than success and failure, a workflow spawned job resource can also end with status 'error' and 'canceled'. When a workflow spawned job resource errors, it is treated the same as failure. Canceling a workflow spawned job resource is also treated as a failure. If the unified job template of the node is null (which could be a result of deleting the unified job template or copying a workflow when the user lacks necessary permissions to use the resource), then the branch should stop executing in this case as well. +Other than success and failure, a workflow spawned job resource can also end with status 'error' and 'canceled'. When a workflow spawned job resource errors or is canceled, it is treated the same as failure. If the unified job template of the node is null (which could be a result of deleting the unified job template or copying a workflow when the user lacks necessary permissions to use the resource), then the node will be treated as 'failed' and the failure paths will continue to execute. A workflow job itself can also be canceled. In this case all its spawned job resources will be canceled if cancelable and following paths stop executing. @@ -92,6 +92,33 @@ Workflow jobs cannot be copied directly, instead a workflow job is implicitly co ### Artifacts Artifact support starts in Ansible and is carried through in Tower. The `set_stats` module is invoked by users, in a playbook, to register facts. Facts are passed in via `data:` argument. Note that the default `set_stats` parameters are the correct ones to work with Tower (i.e. `per_host: no`). Now that facts are registered, we will describe how facts are used. In Ansible, registered facts are "returned" to the callback plugin(s) via the `playbook_on_stats` event. Ansible users can configure whether or not they want the facts displayed through the global `show_custom_stats` configuration. Note that the `show_custom_stats` does not effect the artifacting feature of Tower. This only controls the displaying of `set_stats` fact data in Ansible output (also the output in Ansible playbooks ran in Tower). Tower uses a custom callback plugin that gathers the fact data set via `set_stats` in the `playbook_on_stats` handler and "ships" it back to Tower, saves it in the database, and makes it available on the job endpoint via the variable `artifacts`. The semantics and usage of `artifacts` throughout a workflow is described elsewhere in this document. +### Workflow Run Example +To best understand the nuances of workflow run logic we will look at an example workflow run as it progresses through the 'running' state. In the workflow examples below nodes are labeled `` where `do_not_run` can be `RUN` or `DNR` where `DNR` means 'do not run the node' and `RUN` which means will run the node. Nodes start out with `do_not_run = False` depicted as `RUN` in the pictures below. When nodes are known to not run they will be marked `DNR` and the state will not change. `job_status` is the job's status associated with the node. `node_id` is the unique id for the workflow job node. + +

+ + Workflow before running has started. +

+

+ + Root nodes are selected to run. A root node is a node with no incoming nodes. Node 0 is selected to run and results in a status of 'successful'. Nodes 1, 4, and 5 are marked 'DNR' because they are in the failure path. Node 6 is not marked 'DNR' because nodes 2 and 3 may run and result and node 6 running. The same reasoning is why nodes 7, 8, 9 are not marked 'DNR'. +

+

+ + Nodes 2 and 3 are selected to run and their job results are both 'successful'. Node 6 is not marked 'DNR' because node 3 will trigger node 6. +

+

+ + Node 6 is selected to run and the job results in 'failed'. Node 8 is marked 'DNR' because of the success path. Nodes 7 and 8 will be ran in the next cycle. +

+

+ + Node 7 and 8 are selected to run and their job results are both 'successful'. +

+ +The resulting state of the workflow job run above would be 'successful'. Although individual nodes fail, the overall workflow job status is 'successful' because all individual node failures have error handling paths ('failed_nodes' or 'always_nodes'). + + ## Test Coverage ### CRUD-related * Verify that CRUD operations on all workflow resources are working properly. Note workflow job nodes cannot be created or deleted independently, but verifications are needed to make sure when a workflow job is deleted, all its related workflow job nodes are deleted.