#!/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)