diff options
author | Joshua Harlow <harlowja@yahoo-inc.com> | 2014-12-18 11:30:48 -0800 |
---|---|---|
committer | Joshua Harlow <harlowja@yahoo-inc.com> | 2014-12-18 11:33:31 -0800 |
commit | 47b49a009ca676bfced908770bfa4807369690f1 (patch) | |
tree | e3a901c762ffdd41ff40314a8b5826568fb9755b | |
parent | d98ce6c38f4e1b411cd653cdbbcb69c49c0f6ad5 (diff) | |
download | ironic-47b49a009ca676bfced908770bfa4807369690f1.tar.gz |
Add a fsm state -> dot diagram generator
Add a tool (derived from taskflow's similar tool)
that can turn the state machine defined in ironic
into a dot diagram.
This diagram is much easier to look at then trying
to mentally create a similar diagram; this makes it
easier to see issues and to visualize the state machines
valid states and transitions.
For this change we need to add back in the state machine
__iter__ method so that we can correctly iterate over the
states and transitions (and events that will cause those
transitions).
Change-Id: I9da09f65a46617aa1c837ae0fc71350276df8bea
-rw-r--r-- | ironic/common/fsm.py | 6 | ||||
-rwxr-xr-x | tools/states_to_dot.py | 127 |
2 files changed, 133 insertions, 0 deletions
diff --git a/ironic/common/fsm.py b/ironic/common/fsm.py index 69044242e..0735aaaf8 100644 --- a/ironic/common/fsm.py +++ b/ironic/common/fsm.py @@ -210,6 +210,12 @@ class FSM(object): """Returns a list of the state names.""" return list(six.iterkeys(self._states)) + def __iter__(self): + """Iterates over (start, event, end) transition tuples.""" + for state in six.iterkeys(self._states): + for event, target in six.iteritems(self._transitions[state]): + yield (state, event, target.name) + @property def events(self): """Returns how many events exist.""" diff --git a/tools/states_to_dot.py b/tools/states_to_dot.py new file mode 100755 index 000000000..0885cf86a --- /dev/null +++ b/tools/states_to_dot.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python + +# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import optparse +import os +import sys + +top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), + os.pardir)) +sys.path.insert(0, top_dir) + +# To get this installed you may have to follow: +# https://code.google.com/p/pydot/issues/detail?id=93 (until fixed). +import pydot + +from ironic.common import states + + +def print_header(text): + print("*" * len(text)) + print(text) + print("*" * len(text)) + + +def map_color(text): + # If the text contains 'error' then we'll return red... + if 'error' in text: + return 'red' + else: + return None + + +def format_state(state): + # Changes a state (mainly NOSTATE which is the None object) into + # a nicer string... + if state == states.NOSTATE: + state = 'no-state' + return state + + +def main(): + parser = optparse.OptionParser() + parser.add_option("-f", "--file", dest="filename", + help="write svg to FILE", metavar="FILE") + parser.add_option("-T", "--format", dest="format", + help="output in given format", + default='png') + parser.add_option("--no-labels", dest="labels", + help="do not include labels", + action='store_false', default=True) + (options, args) = parser.parse_args() + if options.filename is None: + options.filename = 'states.%s' % options.format + + source = states.machine + graph_name = "Ironic states" + g = pydot.Dot(graph_name=graph_name, rankdir='LR', + nodesep='0.25', overlap='false', + ranksep="0.5", size="11x8.5", + splines='true', ordering='in') + node_attrs = { + 'fontsize': '11', + } + nodes = {} + for (start_state, on_event, end_state) in source: + start_state = format_state(start_state) + end_state = format_state(end_state) + if start_state not in nodes: + start_node_attrs = node_attrs.copy() + text_color = map_color(start_state) + if text_color: + start_node_attrs['fontcolor'] = text_color + nodes[start_state] = pydot.Node(start_state, **start_node_attrs) + g.add_node(nodes[start_state]) + if end_state not in nodes: + end_node_attrs = node_attrs.copy() + text_color = map_color(end_state) + if text_color: + end_node_attrs['fontcolor'] = text_color + nodes[end_state] = pydot.Node(end_state, **end_node_attrs) + g.add_node(nodes[end_state]) + edge_attrs = {} + if options.labels: + edge_attrs['label'] = "on_%s" % on_event + edge_color = map_color(on_event) + if edge_color: + edge_attrs['fontcolor'] = edge_color + g.add_edge(pydot.Edge(nodes[start_state], nodes[end_state], + **edge_attrs)) + + # Make nice start states... + starts = [ + format_state(source.start_state), + ] + for i, s in enumerate(starts): + name = "__start_%s__" % i + start = pydot.Node(name, shape="point", width="0.1", + xlabel='start', fontcolor='green', **node_attrs) + g.add_node(start) + g.add_edge(pydot.Edge(start, nodes[s], style='dotted')) + + print_header(graph_name) + print(g.to_string().strip()) + + g.write(options.filename, format=options.format) + print_header("Created %s at '%s'" % (options.format, options.filename)) + + # To make the svg more pretty use the following: + # $ xsltproc ../diagram-tools/notugly.xsl ./states.svg > pretty-states.svg + # Get diagram-tools from https://github.com/vidarh/diagram-tools.git + + +if __name__ == '__main__': + main() |