-
Notifications
You must be signed in to change notification settings - Fork 52
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
base: master
Are you sure you want to change the base?
Changes from all commits
57fec28
e2b6d3b
4f2031b
700bb66
13a9b37
9f6bfd8
36ee2c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,8 @@ | |
#!/usr/bin/env python | ||
"""Testing support module for SSM documents.""" | ||
|
||
from collections import OrderedDict | ||
import json | ||
import logging | ||
import time | ||
|
||
|
@@ -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 = [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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('"','\\"')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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\"" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PEP8: missing spaces around the = operator. |
||
if "isCritical" in step: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The two if lines could be condensed into one:
This also applies to lines 238-239. |
||
if step["isCritical"] == "false": | ||
label="onFailure" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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\"" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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\"" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
|
There was a problem hiding this comment.
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.