From 47b49a009ca676bfced908770bfa4807369690f1 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 18 Dec 2014 11:30:48 -0800 Subject: [PATCH] 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 --- ironic/common/fsm.py | 6 ++ tools/states_to_dot.py | 127 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100755 tools/states_to_dot.py diff --git a/ironic/common/fsm.py b/ironic/common/fsm.py index 69044242e8..0735aaaf89 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 0000000000..0885cf86a5 --- /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()