blob: 243d5611ccbb6053ce76b43ecc35e17dcc238dc2 [file] [log] [blame]
#!/usr/bin/env python
"""
salt-state-graph
A tool that ingests the YAML representing the Salt highstate (or sls state) for
a single minion and produces a program written in DOT.
The tool is useful for visualising the dependency graph of a Salt highstate.
"""
from pydot import Dot, Node, Edge
import yaml
import sys
def find(obj, find_key):
"""
Takes a list and a set. Returns a list of all matching objects.
Uses find_inner to recursively traverse the data structure, finding objects
with keyed by find_key.
"""
all_matches = [find_inner(item, find_key) for item in obj]
final = [item for sublist in all_matches for item in sublist]
return final
def find_inner(obj, find_key):
"""
Recursively search through the data structure to find objects
keyed by find_key.
"""
results = []
if not hasattr(obj, '__iter__'):
# not a sequence type - return nothing
# this excludes strings
return results
if isinstance(obj, dict):
# a dict - check each key
for key, prop in obj.iteritems():
if key == find_key:
results.extend(prop)
else:
results.extend(find_inner(prop, find_key))
else:
# a list / tuple - check each item
for i in obj:
results.extend(find_inner(i, find_key))
return results
def make_node_name(state_type, state_label):
return "{0} - {1}".format(state_type.upper(), state_label)
def find_edges(states, relname):
"""
Use find() to recursively find objects at keys matching
relname, yielding a node name for every result.
"""
try:
deps = find(states, relname)
for dep in deps:
for dep_type, dep_name in dep.iteritems():
yield make_node_name(dep_type, dep_name)
except AttributeError as e:
sys.stderr.write("Bad state: {0}\n".format(str(states)))
raise e
def main(input):
state_obj = yaml.load(input)
graph = Dot("states", graph_type='digraph')
rules = {
'require': {'color': 'blue'},
'require_in': {'color': 'blue', 'reverse': True},
'watch': {'color': 'red'},
'watch_in': {'color': 'red', 'reverse': True},
}
for top_key, props in state_obj.iteritems():
# Add a node for each state type embedded in this state
# keys starting with underscores are not candidates
if top_key == '__extend__':
# TODO - merge these into the main states and remove them
sys.stderr.write(
"Removing __extend__ states:\n{0}\n".format(str(props)))
continue
for top_key_type, states in props.iteritems():
if top_key_type[:2] == '__':
continue
node_name = make_node_name(top_key_type, top_key)
graph.add_node(Node(node_name))
for edge_type, ruleset in rules.iteritems():
for relname in find_edges(states, edge_type):
if 'reverse' in ruleset and ruleset['reverse']:
graph.add_edge(Edge(
node_name, relname, color=ruleset['color']))
else:
graph.add_edge(Edge(
relname, node_name, color=ruleset['color']))
graph.write('/dev/stdout')
if __name__ == '__main__':
main(sys.stdin)