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