Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSM Automation document graph visualization to help with branching in complex workflows #24

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
4 changes: 4 additions & 0 deletions Documents/Automation/DetachEBSVolumes/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ TARGET_DIR = "./Output"
documents: targetdir createdocuments
@echo "Done making documents"

graph: targetdir createdocuments
python ./Setup/create_document_graph.py > ./Output/aws-DetachEBSVolume.dot
@echo "Done making document graph"

targetdir:
@echo "Making $(TARGET_DIR)"
mkdir -p ./Output
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify,
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
import os
import sys

DOC_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
REPO_ROOT = os.path.dirname(DOC_DIR)

# Import shared testing code
sys.path.append(os.path.join(REPO_ROOT, 'Testing'))
import ssm_testing # noqa pylint: disable=import-error,wrong-import-position


def process():
ssm_doc_name = "aws-DetachEBSVolume"
print(ssm_testing.SSMTester.convert_document_to_dot_graph(doc_filename=os.path.join(DOC_DIR,
'Output',
('{}.json'.format(ssm_doc_name)))))


if __name__ == '__main__':
process()

102 changes: 102 additions & 0 deletions Documents/Automation/Testing/ssm_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#!/usr/bin/env python
"""Testing support module for SSM documents."""

from collections import OrderedDict
import json
import logging
import time

Expand Down Expand Up @@ -146,6 +148,106 @@ def destroy(self):
"""Delete SSM document."""
self.ssm_client.delete_document(Name=self.doc_name)

@staticmethod
def convert_document_to_dot_graph(doc_filename):
"""Create a graph representation of the SSM document
in dot language to visualize when and how branching occurs."""
# Loading the document as json
with open(doc_filename, 'r') as jsonfile:
json_doc = json.load(jsonfile, object_pairs_hook=OrderedDict)

# Initializating the graph variable with the document description and the default Start and End nodes
graph = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 160-164 could be replaced with a single list literal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend choosing a different variable name. "graph" has its own meaning in the context of programming/computer science and the type of this variable is actually a list. Consider "dot_lines", "document_lines", or something else.

graph.append("// {}".format(json_doc["description"]))
graph.append("digraph {")
graph.append(" Start [label=Start]")
graph.append(" End [label=End]")

# If the document step does not explicitly define the next step on failure and on success,
# then the next step from the document will use the following variables to create the edge
add_edge_from_previous_step = False
label = ""
previous_step_name = ""

for index, step in enumerate(json_doc["mainSteps"]):
if add_edge_from_previous_step:
graph.append(" {} -> {} [label={}]".format(
previous_step_name, step["name"], label))
add_edge_from_previous_step = False

# Create the edge from the Start node if this is the first node of the document
if index == 0:
graph.append(" {} -> {}".format("Start", step["name"]))
# Create the two edges to the End node if this is the last node of the document, then exit the loop
elif index == (len(json_doc["mainSteps"]) - 1):
graph.append(" {} -> {} [label={}]".format(step["name"], "End", "onSuccess"))
graph.append(" {} -> {} [label={}]".format(step["name"], "End", "onFailure"))
break

# If action is aws:branch, checking all choices to visualize each branch
if step["action"] == "aws:branch":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSM's branching feature supports the and, or, and not logical operators. I think it would be valuable to support them here as well.

for choice in step["inputs"]["Choices"]:
next_step = choice["NextStep"]
del choice["NextStep"]
# Removing first and last character from the choice (that removes the curly brackets),
# escaping and adding a new line for each comma)
label = "\"{}\"".format(json.dumps(choice)[1:-1].replace('", "','"\\l"').replace('"','\\"'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8: missing spaces after the commas.

graph.append(" {} -> {} [label={}]".format(
step["name"], next_step, label))

if "Default" in step["inputs"]:
graph.append(" {} -> {} [label={}]".format(
step["name"], step["inputs"]["Default"], "Default"))
else:
# If nextStep is used in the step, using it to create the edge,
# else we save the current step information to be able to create the edge when inspecting the next available step
if "nextStep" in step:
graph.append(" {} -> {} [label={}]".format(
step["name"], step["nextStep"], "onSuccess"))
# When isEnd is true, create an edge to the End node
elif "isEnd" in step:
if step["isEnd"] == "true":
graph.append(" {} -> {} [label={}]".format(step["name"], "End", "onSuccess"))
else:
add_edge_from_previous_step = True
label = "onSuccess"
previous_step_name = step["name"]

# If onFailure is Abort or not specified, create an edge to the End node.
if "onFailure" in step:
if step["onFailure"] == "Abort":
graph.append(" {} -> {} [label={} color=\"red\"]".format(
step["name"], "End", "onFailure"))
# If onFailure is Continue, we look for nextStep,
# or save the current step information to be able to create the edge when inspecting the next available step
elif step["onFailure"] == "Continue":
if "nextStep" in step:
label="onFailure color=\"red\""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8: missing spaces around the = operator.

if "isCritical" in step:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two if lines could be condensed into one:

if "isCritical" in step and step["isCritical"] == "false":

This also applies to lines 238-239.

if step["isCritical"] == "false":
label="onFailure"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8: missing spaces around the = operator.

graph.append(" {} -> {} [label={}]".format(
step["name"], step["nextStep"], label))
else:
add_edge_from_previous_step = True
label="onFailure color=\"red\""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8: missing spaces around the = operator.

previous_step_name = step["name"]
# Lastly, retrieve the next step from onFailure directly
else:
label="onFailure color=\"red\""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8: missing spaces around the = operator.

if "isCritical" in step:
if step["isCritical"] == "false":
label="onFailure"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8: missing spaces around the = operator.

graph.append(" {} -> {} [label={}]".format(
step["name"], step["onFailure"].replace("step:", ""), label))
else:
graph.append(" {} -> {} [label={}]".format(
step["name"], "End", "onFailure color=\"red\""))

graph.append("}")

return "\n".join(graph)

@staticmethod
def automation_execution_status(ssm_client, execution_id,
block_on_waiting=True, status_callback=None, poll_interval=10):
Expand Down