ironic/ironic/common/fsm.py

159 lines
5.9 KiB
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 functools
from automaton import exceptions as automaton_exceptions
from automaton import machines
"""State machine modelling.
This is being used in the implementation of:
http://specs.openstack.org/openstack/ironic-specs/specs/kilo/new-ironic-state-machine.html
"""
from ironic.common import exception as excp
from ironic.common.i18n import _
def _translate_excp(func):
"""Decorator to translate automaton exceptions into ironic exceptions."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except (automaton_exceptions.InvalidState,
automaton_exceptions.NotInitialized,
automaton_exceptions.FrozenMachine,
automaton_exceptions.NotFound) as e:
raise excp.InvalidState(str(e))
except automaton_exceptions.Duplicate as e:
raise excp.Duplicate(str(e))
return wrapper
class FSM(machines.FiniteMachine):
"""An ironic state-machine class with some ironic specific additions."""
def __init__(self):
super(FSM, self).__init__()
self._target_state = None
# For now make these raise ironic state machine exceptions until
# a later period where these should(?) be using the raised automaton
# exceptions directly.
add_transition = _translate_excp(machines.FiniteMachine.add_transition)
@property
def target_state(self):
return self._target_state
def is_stable(self, state):
"""Is the state stable?
:param state: the state of interest
:raises: InvalidState if the state is invalid
:returns: True if it is a stable state; False otherwise
"""
try:
return self._states[state]['stable']
except KeyError:
raise excp.InvalidState(_("State '%s' does not exist") % state)
@_translate_excp
def add_state(self, state, on_enter=None, on_exit=None,
target=None, terminal=None, stable=False):
"""Adds a given state to the state machine.
: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'
Further arguments are interpreted as for parent method ``add_state``.
"""
self._validate_target_state(target)
super(FSM, self).add_state(state, terminal=terminal,
on_enter=on_enter, on_exit=on_exit)
self._states[state].update({
'stable': stable,
'target': target,
})
def _post_process_event(self, event, result):
# Clear '_target_state' if we've reached it
if (self._target_state is not None
and self._target_state == self._current.name):
self._target_state = None
# If new state has a different target, update the '_target_state'
if self._states[self._current.name]['target'] is not None:
self._target_state = self._states[self._current.name]['target']
def _validate_target_state(self, target):
"""Validate the target state.
A target state must be a valid state that is 'stable'.
:param target: The target state
:raises: exception.InvalidState if it is an invalid target state
"""
if target is None:
return
if target not in self._states:
raise excp.InvalidState(
_("Target state '%s' does not exist") % target)
if not self.is_stable(target):
raise excp.InvalidState(
_("Target state '%s' is not a 'stable' state") % target)
@_translate_excp
def initialize(self, start_state=None, target_state=None):
"""Initialize the FSM.
:param start_state: the FSM is initialized to start from this state
:param target_state: if specified, the FSM is initialized to this
target state. Otherwise use the default target
state
"""
super(FSM, self).initialize(start_state=start_state)
current_state = self._current.name
self._validate_target_state(target_state)
self._target_state = (target_state
or self._states[current_state]['target'])
@_translate_excp
def process_event(self, event, target_state=None):
"""process the event.
:param event: the event to be processed
:param target_state: if specified, the 'final' target state for the
event. Otherwise, use the default target state
"""
super(FSM, self).process_event(event)
if target_state:
# NOTE(rloo): _post_process_event() was invoked at the end of
# the above super().process_event() call. At this
# point, the default target state is being used but
# we want to use the specified state instead.
self._validate_target_state(target_state)
self._target_state = target_state