diff --git a/ironic/common/fsm.py b/ironic/common/fsm.py index f314054962..e293306dcf 100644 --- a/ironic/common/fsm.py +++ b/ironic/common/fsm.py @@ -17,6 +17,9 @@ This work will be turned into a library. See https://github.com/harlowja/automaton + +This is being used in the implementation of: +http://specs.openstack.org/openstack/ironic-specs/specs/kilo/new-ironic-state-machine.html """ from collections import OrderedDict # noqa @@ -71,7 +74,7 @@ class FSM(object): return self._states[self._current.name]['terminal'] def add_state(self, state, on_enter=None, on_exit=None, - target=None, terminal=None): + target=None, terminal=None, stable=False): """Adds a given state to the state machine. The on_enter and on_exit callbacks, if provided will be expected to @@ -79,6 +82,13 @@ class FSM(object): on_exit) or the state being entered (for on_enter) and a second parameter which is the event that is being processed that caused the state transition. + + :param stable: Use this to specify that this state is a stable/passive + state. A state must have been previously defined as + 'stable' before it can be used as a 'target' + :param target: The target state for 'state' to go to. Before a state + can be used as a target it must have been previously + added and specified as 'stable' """ if state in self._states: raise excp.Duplicate(_("State '%s' already defined") % state) @@ -91,6 +101,9 @@ class FSM(object): if target is not None and target not in self._states: raise excp.InvalidState(_("Target state '%s' does not exist") % target) + if target is not None and not self._states[target]['stable']: + raise excp.InvalidState( + _("Target state '%s' is not a 'stable' state") % target) self._states[state] = { 'terminal': bool(terminal), @@ -98,6 +111,7 @@ class FSM(object): 'on_enter': on_enter, 'on_exit': on_exit, 'target': target, + 'stable': stable, } self._transitions[state] = OrderedDict() diff --git a/ironic/common/states.py b/ironic/common/states.py index 042502b55f..97581f3c78 100644 --- a/ironic/common/states.py +++ b/ironic/common/states.py @@ -180,10 +180,10 @@ watchers['on_enter'] = on_enter machine = fsm.FSM() # Add stable states -machine.add_state(MANAGEABLE, **watchers) -machine.add_state(AVAILABLE, **watchers) -machine.add_state(ACTIVE, **watchers) -machine.add_state(ERROR, **watchers) +machine.add_state(MANAGEABLE, stable=True, **watchers) +machine.add_state(AVAILABLE, stable=True, **watchers) +machine.add_state(ACTIVE, stable=True, **watchers) +machine.add_state(ERROR, stable=True, **watchers) # From MANAGEABLE, a node may be made available # TODO(deva): add CLEAN* states to this path diff --git a/ironic/tests/test_fsm.py b/ironic/tests/test_fsm.py index a49b769a65..6e7cb29094 100644 --- a/ironic/tests/test_fsm.py +++ b/ironic/tests/test_fsm.py @@ -115,3 +115,23 @@ class FSMTest(base.TestCase): m.add_state('broken') self.assertRaises(ValueError, m.add_state, 'b', on_enter=2) self.assertRaises(ValueError, m.add_state, 'b', on_exit=2) + + def test_invalid_target_state(self): + # Test to verify that adding a state which has a 'target' state that + # does not exist will raise an exception + self.assertRaises(excp.InvalidState, + self.jumper.add_state, 'jump', target='unknown') + + def test_target_state_not_stable(self): + # Test to verify that adding a state that has a 'target' state which is + # not a 'stable' state will raise an exception + self.assertRaises(excp.InvalidState, + self.jumper.add_state, 'jump', target='down') + + def test_target_state_stable(self): + # Test to verify that adding a new state with a 'target' state pointing + # to a 'stable' state does not raise an exception + m = fsm.FSM('working') + m.add_state('working', stable=True) + m.add_state('foo', target='working') + m.initialize()