Michael Pasternak has uploaded a new change for review. Change subject: cli: introducing FiniteStateMachine ......................................................................
cli: introducing FiniteStateMachine This patch moves shell state management to the Deterministic Finite State Machine (DFSA). Change-Id: Ic3ea3cf7fa82eb82c2323d96231de0a5e1094056 Signed-off-by: Michael pasternak <[email protected]> --- M src/cli/error.py M src/ovirtcli/__init__.py M src/ovirtcli/annotations/requires.py A src/ovirtcli/annotations/singleton.py M src/ovirtcli/command/connect.py M src/ovirtcli/command/disconnect.py D src/ovirtcli/listeners/exitlistener.py A src/ovirtcli/meta/__init__.py A src/ovirtcli/meta/singleton.py M src/ovirtcli/shell/engineshell.py A src/ovirtcli/state/__init__.py A src/ovirtcli/state/dfsaevent.py A src/ovirtcli/state/dfsaeventcontext.py A src/ovirtcli/state/dfsastate.py A src/ovirtcli/state/finitestatemachine.py A src/ovirtcli/state/statemachine.py 16 files changed, 884 insertions(+), 77 deletions(-) git pull ssh://gerrit.ovirt.org:29418/ovirt-engine-cli refs/changes/51/20451/1 diff --git a/src/cli/error.py b/src/cli/error.py index 27f4740..013317c 100644 --- a/src/cli/error.py +++ b/src/cli/error.py @@ -16,7 +16,10 @@ from cli import compat +import types +from ovirtcli.annotations.requires import Requires +from ovirtcli.state.dfsastate import DFSAState class Error(Exception): """Base class for python-cli errors.""" @@ -40,3 +43,46 @@ class SyntaxError(Error): # @ReservedAssignment """Illegal syntax.""" + +class StateError(Error): + """raised when state change error occurs.""" +# @Requires([DFSAEvent, types.StringType]) + def __init__(self, destination, current): + """ + @param destination: the destination DFSAEvent + @param current: the current state + """ + super(StateError, self).__init__( + message= + ( + '\n\nMoving to the "%s" state is not allowed,\n' + % + str(DFSAState(destination.get_destination())) + ) + + + ( + 'eligible states from the "%s" state are:\n%s' + % + ( + str(DFSAState(current)), + str([ str(DFSAState(src)) + for src in destination.get_sources() + ] + ) + ) + ) + ) + +class UnknownEventError(Error): + """raised when unregistered event is triggered.""" + @Requires(types.StringType) + def __init__(self, name): + """ + @param name: the name of DFSAEvent + """ + super(UnknownEventError, self).__init__( + message=( + 'Event %s, was not properly registered.' % \ + name + ) + ) diff --git a/src/ovirtcli/__init__.py b/src/ovirtcli/__init__.py index 792d600..e69de29 100644 --- a/src/ovirtcli/__init__.py +++ b/src/ovirtcli/__init__.py @@ -1 +0,0 @@ -# diff --git a/src/ovirtcli/annotations/requires.py b/src/ovirtcli/annotations/requires.py index 5e8c20d..a87e32e 100644 --- a/src/ovirtcli/annotations/requires.py +++ b/src/ovirtcli/annotations/requires.py @@ -13,36 +13,78 @@ # See the License for the specific language governing permissions and # limitations under the License. +import types class Requires(object): """ Checks that method arg is of a given type + + @note: + + 1. checks that method's argument of type DFSAEvent + + @Requires(DFSAEvent) + def method1(self, event): + ... + + 2. checks that method's argument is a List of DFSAEvent + + @Requires([DFSAEvent]) + def method2(self, events): + ... """ - def __init__(self, typ): + def __init__(self, types_to_check): """ Checks that method arg is of a given type - @param typ: the type to validate against + @param types_to_check: the types to validate against + @note: + + 1. checks that method's argument of type DFSAEvent + + @Requires(DFSAEvent) + def method1(self, event): + ... + + 2. checks that method's argument is a List of DFSAEvent + + @Requires([DFSAEvent]) + def method2(self, events): + ... """ - assert typ != None - self.typ = typ + assert types_to_check != None + self.__types_to_check = types_to_check def __call__(self, original_func): decorator_self = self def wrappee(*args, **kwargs): self.__check_list( - args[1], - decorator_self.typ + args[1:], + decorator_self.__types_to_check ) return original_func(*args, **kwargs) return wrappee - def __check_list(self, candidate, typ): - if not isinstance(candidate, typ): - raise TypeError( - "%s instance is expected." - % - typ.__name__ - ) + def __raise_error(self, typ): + raise TypeError( + "%s instance is expected." + % + typ.__name__ + ) + + def __check_list(self, candidates, typs): + if isinstance(typs, types.ListType): + if type(candidates[0]) is types.ListType: + # the list of items of a specific type + if candidates[0]: + for candidate in candidates[0]: + if not isinstance(candidate, typs[0]): + self.__raise_error(typs[0]) + else: + self.__raise_error(types.ListType) + else: + # the items is of a specific type + if not isinstance(candidates[0], typs): + self.__raise_error(typs) diff --git a/src/ovirtcli/annotations/singleton.py b/src/ovirtcli/annotations/singleton.py new file mode 100644 index 0000000..04b0c62 --- /dev/null +++ b/src/ovirtcli/annotations/singleton.py @@ -0,0 +1,29 @@ +# +# Copyright (c) 2013 Red Hat, Inc. +# +# 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. + + +def Singleton(clazz): + """ + The singleton annotation + + applying this annotation on class, will make sure that no + more than single instance of this class can be created. + """ + instances = {} + def fetch_instance(*args, **kwargs): + if clazz not in instances: + instances[clazz] = clazz(*args, **kwargs) + return instances[clazz] + return fetch_instance diff --git a/src/ovirtcli/command/connect.py b/src/ovirtcli/command/connect.py index 68462c4..0c7a27c 100644 --- a/src/ovirtcli/command/connect.py +++ b/src/ovirtcli/command/connect.py @@ -19,12 +19,12 @@ from ovirtcli.command.command import OvirtCommand from ovirtsdk.api import API -from ovirtcli.settings import OvirtCliSettings from ovirtsdk.infrastructure.errors import RequestError, NoCertificatesError, \ ConnectionError from cli.messages import Messages from urlparse import urlparse from ovirtcli.shell.connectcmdshell import ConnectCmdShell +from ovirtcli.state.statemachine import StateMachine class ConnectCommand(OvirtCommand): @@ -115,6 +115,8 @@ ) try: + StateMachine.connecting() # @UndefinedVariable + self.context.set_connection ( API( url=url, @@ -137,11 +139,7 @@ if context.sdk_version < MIN_FORCE_CREDENTIALS_CHECK_VERSION: self.__test_connectivity() - self.context.history.enable() - self.write( - OvirtCliSettings.CONNECTED_TEMPLATE % \ - self.context.settings.get('ovirt-shell:version') - ) + StateMachine.connected() # @UndefinedVariable except RequestError, e: self.__cleanContext() diff --git a/src/ovirtcli/command/disconnect.py b/src/ovirtcli/command/disconnect.py index 989e3e5..762e285 100644 --- a/src/ovirtcli/command/disconnect.py +++ b/src/ovirtcli/command/disconnect.py @@ -16,10 +16,10 @@ from ovirtcli.command.command import OvirtCommand -from cli.context import ExecutionContext -from ovirtcli.settings import OvirtCliSettings -from cli.messages import Messages +from ovirtcli.state.statemachine import StateMachine +from cli.context import ExecutionContext +from cli.messages import Messages class DisconnectCommand(OvirtCommand): @@ -45,12 +45,12 @@ ) return try: + StateMachine.disconnecting() # @UndefinedVariable + self.context._clean_settings() connection.disconnect() self.context.status = ExecutionContext.OK except Exception: self.context.status = ExecutionContext.COMMAND_ERROR finally: - self.context.history.disable() - self.write(OvirtCliSettings.DISCONNECTED_TEMPLATE) - self.context.connection = None + StateMachine.disconnected() # @UndefinedVariable diff --git a/src/ovirtcli/listeners/exitlistener.py b/src/ovirtcli/listeners/exitlistener.py deleted file mode 100644 index 65fee27..0000000 --- a/src/ovirtcli/listeners/exitlistener.py +++ /dev/null @@ -1,43 +0,0 @@ -# -# Copyright (c) 2010 Red Hat, Inc. -# -# 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. -# - -from ovirtcli.listeners.abstractlistener import AbstractListener -from ovirtcli.shell.disconnectcmdshell import DisconnectCmdShell - -class ExitListener(AbstractListener): - ''' - Listens for the exit events - ''' - - def __init__(self, shell): - """ - @param shell: EngineShell instance - """ - assert shell != None - self.__shell = shell - - def onEvent(self, *args, **kwargs): - ''' - fired when exit event is raised - - @param args: a list o args - @param kwargs: a list o kwargs - ''' - - if self.__shell.context.connection: - self.__shell.onecmd( - DisconnectCmdShell.NAME + "\n" - ) diff --git a/src/ovirtcli/meta/__init__.py b/src/ovirtcli/meta/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/ovirtcli/meta/__init__.py diff --git a/src/ovirtcli/meta/singleton.py b/src/ovirtcli/meta/singleton.py new file mode 100644 index 0000000..0c949e5 --- /dev/null +++ b/src/ovirtcli/meta/singleton.py @@ -0,0 +1,32 @@ +# +# Copyright (c) 2013 Red Hat, Inc. +# +# 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. + +class Singleton(type): + """ + The singleton annotation + + applying this annotation on class, will make sure that no + more than single instance of this class can be created. + """ + __all_instances = {} + def __call__(cls, *args, **kwargs): # @NoSelf + if cls not in cls.__all_instances: + cls.__all_instances[cls] = super( + Singleton, + cls).__call__( + *args, + **kwargs + ) + return cls.__all_instances[cls] diff --git a/src/ovirtcli/shell/engineshell.py b/src/ovirtcli/shell/engineshell.py index e7ce88a..2b49a0e 100644 --- a/src/ovirtcli/shell/engineshell.py +++ b/src/ovirtcli/shell/engineshell.py @@ -51,8 +51,9 @@ from ovirtcli.listeners.errorlistener import ErrorListener from ovirtcli.settings import OvirtCliSettings from ovirtcli.prompt import PromptMode -from ovirtcli.listeners.exitlistener import ExitListener from cli.error import CommandError + +from ovirtcli.state.statemachine import StateMachine class EngineShell(cmd.Cmd, ConnectCmdShell, ActionCmdShell, \ ShowCmdShell, ListCmdShell, UpdateCmdShell, \ @@ -82,11 +83,11 @@ SummaryCmdShell.__init__(self, context, parser) CapabilitiesCmdShell.__init__(self, context, parser) - self.onError = Event() - self.onInit = Event() - self.onExit = Event() - self.onPromptChange = Event() - self.onSigInt = Event() + self.onError = Event() # triggered when error occurs + self.onInit = Event() # triggered on init() + self.onExit = Event() # triggered on exit + self.onPromptChange = Event() # triggered onPromptChange + self.onSigInt = Event() # triggered on SigInt fault self.__last_output = '' self.__input_buffer = '' @@ -94,6 +95,7 @@ self.__last_status = -1 self.__register_sys_listeners() + self.__register_dfsm_callbacks() self.__init_promt() cmd.Cmd.doc_header = self.context.settings.get('ovirt-shell:commands') @@ -182,9 +184,43 @@ ########################### SYSTEM ################################# + def __register_dfsm_callbacks(self): + """ + registers StateMachine events callbacks + """ + StateMachine.add_callback("disconnected", self.__on_disconnected_callback) + StateMachine.add_callback("connected", self.__on_connected_callback) + StateMachine.add_callback("exiting", self.__on_exiting_callback) + + def __on_connected_callback(self, **kwargs): + """ + triggered when StateMachine.CONNECTED state is acquired + """ + self.context.history.enable() + self._print( + OvirtCliSettings.CONNECTED_TEMPLATE % \ + self.context.settings.get('ovirt-shell:version') + ) + + def __on_disconnected_callback(self, **kwargs): + """ + triggered when StateMachine.DISCONNECTED state is acquired + """ + self.context.history.disable() + self._print(OvirtCliSettings.DISCONNECTED_TEMPLATE) + self.context.connection = None + + def __on_exiting_callback(self, **kwargs): + """ + triggered when StateMachine.EXITING state is acquired + """ + if self.context.connection: + self.onecmd( + DisconnectCmdShell.NAME + "\n" + ) + def __register_sys_listeners(self): self.onError += ErrorListener(self) - self.onExit += ExitListener(self) def __init_promt(self): self._set_prompt(mode=PromptMode.Disconnected) @@ -422,8 +458,8 @@ Ctrl+D """ - self._print("") self.onExit.fire() + StateMachine.exiting() # @UndefinedVariable return True def do_exit(self, args): @@ -441,6 +477,7 @@ exit """ self.onExit.fire() + StateMachine.exiting() # @UndefinedVariable sys.exit(0) def do_help(self, args): diff --git a/src/ovirtcli/state/__init__.py b/src/ovirtcli/state/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/ovirtcli/state/__init__.py diff --git a/src/ovirtcli/state/dfsaevent.py b/src/ovirtcli/state/dfsaevent.py new file mode 100644 index 0000000..79450fb --- /dev/null +++ b/src/ovirtcli/state/dfsaevent.py @@ -0,0 +1,91 @@ +# +# Copyright (c) 2013 Red Hat, Inc. +# +# 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. +# + + +from ovirtcli.state.dfsastate import DFSAState +from ovirtcli.annotations.requires import Requires +import types + +class DFSAEvent(object): + ''' + Finite-State Automata event + ''' + + def __init__(self, name, sources, destination, callbacks=[]): + ''' + @param name: the state event name + @param sources: the source states from which destination state is eligible + @param destination: the destination state + @param callbacks: collection of callbacks to invoke on this event (TODO) + ''' + self.__id = id(self) + self.__name = name + self.__sources = sources + self.__destination = destination + self.__callbacks = callbacks + + def get_name(self): + """ + @return: the name of DFSAEvent + """ + return self.__name + + def get_sources(self): + """ + @return: the sources of DFSAEvent + """ + return self.__sources + + def get_destination(self): + """ + @return: the destination of DFSAEvent + """ + return self.__destination + + def get_callbacks(self): + """ + @return: the destination of DFSAEvent + """ + return self.__callbacks + + @Requires(types.MethodType) + def add_callback(self, callback): + """ + adds new callback to event + + @param callback: the method to register + """ + if self.__callbacks == None: + self.__callbacks = [] + self.__callbacks.append(callback) + + def __str__(self): + return ( + "DFSAEvent: %s\n" + \ + "name: %s\n" + \ + "sources: %s\n" + \ + "destination: %s\n" + \ + "callbacks: %s") % ( + str(self.__id), + self.get_name(), + str([ + str(DFSAState(src)) + for src in self.get_sources() + ] + ), + str(DFSAState(self.get_destination())), + str([str(callback) for callback in self.get_callbacks()]) + ) diff --git a/src/ovirtcli/state/dfsaeventcontext.py b/src/ovirtcli/state/dfsaeventcontext.py new file mode 100644 index 0000000..1185ffb --- /dev/null +++ b/src/ovirtcli/state/dfsaeventcontext.py @@ -0,0 +1,57 @@ +# +# Copyright (c) 2013 Red Hat, Inc. +# +# 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. +# + +class DFSAEventContext(object): + ''' + Deterministic Finite-State Automata event context + ''' + + + def __init__(self, name, sources, destination, callbacks=[]): + ''' + @param name: the state event name + @param sources: the source states from which destination state is eligible + @param destination: the destination state + @param callbacks: collection of callbacks to invoke on this event (TODO) + ''' + self.__id = id(self) + self.__name = name + self.__source = sources + self.__destination = destination + + def get_name(self): + return self.__name + + def get_sources(self): + return self.__sources + + def get_destination(self): + return self.__destination + + def get_callbacks(self): + return self.__callbacks + + def __str__(self): + print 'DFSAEventContext: %s\n' + \ + 'name: %s\n' + \ + 'sources: %s\n' + \ + 'destination: %s' % \ + ( + str(self.__id), + self.get_name(), + self.get_sources(), + self.get_destination() + ) diff --git a/src/ovirtcli/state/dfsastate.py b/src/ovirtcli/state/dfsastate.py new file mode 100644 index 0000000..db3fabd --- /dev/null +++ b/src/ovirtcli/state/dfsastate.py @@ -0,0 +1,40 @@ +# +# Copyright (c) 2013 Red Hat, Inc. +# +# 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. +# + + +class DFSAState(object): + Disconnecting, Disconnected, Connecting, \ + Connected, Unauthorized, Exiting = range(6) + + def __init__(self, Type): + self.value = Type + + def __str__(self): + if self.value == DFSAState.Disconnected: + return 'Disconnected' + if self.value == DFSAState.Connected: + return 'Connected' + if self.value == DFSAState.Unauthorized: + return 'Unauthorized' + if self.value == DFSAState.Exiting: + return 'Exiting' + if self.value == DFSAState.Disconnecting: + return 'Disconnecting' + if self.value == DFSAState.Connecting: + return 'Connecting' + + def __eq__(self, y): + return self.value == y.value diff --git a/src/ovirtcli/state/finitestatemachine.py b/src/ovirtcli/state/finitestatemachine.py new file mode 100644 index 0000000..c18b234 --- /dev/null +++ b/src/ovirtcli/state/finitestatemachine.py @@ -0,0 +1,340 @@ +# +# Copyright (c) 2013 Red Hat, Inc. +# +# 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. +# + + +from ovirtcli.state.dfsaevent import DFSAEvent +from ovirtcli.state.dfsastate import DFSAState +from ovirtcli.annotations.requires import Requires +from ovirtcli.events.event import Event +from ovirtcli.meta.singleton import Singleton + +from cli.error import StateError, UnknownEventError + +import types + +class FiniteStateMachine(object): + ''' + The Deterministic Finite-State Automata (DFSA) + + =========== state maintains logic =========== + + A=(A, Q, l, q0, F) + + A is alphabetic + _ + Q is a finite set of states |_| + _ _ + l is a state transition function |_| -> |_| + _ + q0 is a initial state ->|_| + + ===== + F is a set of final states || || + ===== + + L = {ab^n E A* : n >= 0} + + + e.g: + + --b------------------------- + | | + --- ===== ---- | + ->|q0|-a--->||q1||-a-->|q2|<--- + -- ===== ---- + | ^ | ^ + b | a,b | + | | | | + ----- ----- + + A = ({{a, b}, {q0,q1,q2},l,{q0}, {q1}) + + e.g: l=(q0,a)=q1 l=(q1,b)=q1 + + =========== usage =========== + + from ovirtcli.state.finitestatemachine import FiniteStateMachine + from ovirtcli.state.dfsaevent import DFSAEvent + from ovirtcli.state.dfsastate import DFSAState + + 1. define the callback for events, with callback you can either + perform onEvent actions or block the StateMachine till your + action is accomplished. + + def onConnectCallback(self, **kwargs): + print "connect callback with:\n%s\n\n" % kwargs['event'] + + def onDisconnectCallback(self, **kwargs): + print "disconnect callback:\n%s\n\n" % kwargs['event'] + + 2. define the StateMachine + + class Test(object): + def run(self): + sm = FiniteStateMachine( # define the StateMachine + events=[ # define events + DFSAEvent( # define StateMachine event + name='disconnect', # event name + sources=[ # source states from which this event is eligible + DFSAState.Connected, + DFSAState.Unauthorized + ], + destination=DFSAState.Disconnected, # destination event state + callbacks=[self.onDisconnectCallback]), # callbacks to invoke after + # this events is triggered + DFSAEvent( + name='connect', + sources=[ + DFSAState.Disconnected, + DFSAState.Unauthorized + ], + destination=DFSAState.Connected, + callbacks=[self.onConnectCallback]), + DFSAEvent( + name='unauthorized', + sources=[ + DFSAState.Connected + ], + destination=DFSAState.Unauthorized, + callbacks=[]), + ] + ) + + 3. StateMachine has own events: + + - onBeforeApplyState: triggered Before ApplyState occurs + - onAfterApplyState : triggered After ApplyState occurs + - onStateChange, : triggered on any StateChange + - onBeforeEvent : triggered Before Event processed + - onAfterEvent : triggered After Event processed + - onCanMove : triggered when CanMove() check is invoked + + you can register to them using event patter: + + sm.onBeforeEvent += OnBeforeEventListener() + + NOTE: + + - EventListener must to implement IListener interface + - All events are provided with 'event' argument to maintain the state + between the events, in some case extra data can be provided such as + [source, destination, result, ...] depending on event's context. + + 4. trigger events on StateMachine + + sm.connect() # 'connect' event + # print sm; print "\n" + sm.disconnect() 'disconnect' event + # print sm; print "\n" + sm.unauthorized() 'unauthorized' event + # print sm; print "\n" + ... + + Test().run() + ''' + + __metaclass__ = Singleton + +# @Requires([DFSAEvent], DFSAEvent) +# TODO: support multi-parameters definition ^ + def __init__(self, events, inital_state=DFSAEvent( + name='init', + sources=[], + destination=DFSAState.Disconnected + ) + ): + ''' + @param events: the list of DFSA events + @param inital_state: the inital state of DFSA (optional) + ''' + + assert events != None + + self.__id = id(self) + self.__current_state_obj = None + self.__current_state = None + self.__events = {} # future use + + self.onBeforeApplyState = Event() + self.onAfterApplyState = Event() + self.onStateChange = Event() + + self.onBeforeEvent = Event() + self.onAfterEvent = Event() + + self.onCanMove = Event() + + self.__register_events(events) + self.__apply_state(inital_state) + + @Requires(DFSAEvent) + def __apply_state(self, event): + """ + applying state + + @raise StateError: when event.destination state is + not applicable from the current_state + """ + if self.can_move(event): + self.onBeforeApplyState.fire( + event=event, + source=self.get_current_state(), + destination=event.get_destination() + ) + self.onStateChange.fire( + event=event, + source=self.get_current_state(), + destination=event.get_destination() + ) + + old_state = self.get_current_state() + + self.__current_state_obj = event + self.__current_state = event.get_destination() + + self.onAfterApplyState.fire( + event=event, + source=old_state, + destination=self.get_current_state() + ) + + else: + self.__raise_state_error(event) + + @Requires([DFSAEvent]) + def __register_events(self, events): + """ + registers events + + @param events: the list of events to register + """ + self.__events = {} + for event in events: + self.__do_add_event(event) + + @Requires(DFSAEvent) + def __produce_event_method(self, event): + """ + produces event method + + @param eevent: event for which the method should + be procured + """ + def event_method(**kwargs): + if self.get_current_state() != event.get_destination(): + self.onBeforeEvent.fire(event=event) + self.__apply_state(event) + if event.get_callbacks(): + for callback in event.get_callbacks(): + # TODO: consider passing all **kwargs + callback(event=event) + self.onAfterEvent.fire(event=event) + else: + return + return event_method + + @Requires(DFSAEvent) + def __raise_state_error(self, event): + """ + @raise StateError: when event.destination state is + not applicable from the current_state + """ + raise StateError( + destination=event, + current=self.get_current_state() + ) + + def __raise_unknown_event(self, name): + """ + @raise UnknownEventError: when is not registered + """ + raise UnknownEventError(name=name) + + @Requires(DFSAEvent) + def __do_add_event(self, event): + """ + registers new event in DFSM + + @param event: event to register + """ + self.__events[event.get_name()] = event + setattr( + self, + event.get_name(), + self.__produce_event_method(event) + ) + + @Requires(types.StringType) + def __get_event(self, name): + if name in self.__events.keys(): + return self.__events[name] + self.__raise_unknown_event(name) + + def __str__(self): + return 'FiniteStateMachine: %s, current state: %s' % ( + str(self.__id), + DFSAState(self.get_current_state()) + ) + + @Requires(DFSAEvent) + def add_event(self, event): + """ + adds or overrides DFSAEvent event to/in DFSA + + @param event: the DFSAEvent to add/override + """ + self.__do_add_event(event) + + # @Requires(types.StringType, types.MethodType) + def add_callback(self, event_name, callback): + """ + adds new callback to event + + @param event_name: the name of even to register + callback for + @param callback: the method to register + """ + self.__get_event(event_name) \ + .get_callbacks() \ + .append(callback) + + def get_current_state(self): + """ + @return: the current State of DFSA + """ + return self.__current_state + + @Requires(DFSAEvent) + def can_move(self, event): + """ + checks if DFSA can move to the given event.destination + + @param event: the destination DFSAEvent + """ + if not self.__current_state_obj: + result = True # can happen during init only! + # TODO: consider restricting this behavior + else: + result = self.__current_state in event.get_sources() + + self.onCanMove.fire( + event=event, + source=self.get_current_state(), + destination=event.get_destination(), + result=result + ) + + return result diff --git a/src/ovirtcli/state/statemachine.py b/src/ovirtcli/state/statemachine.py new file mode 100644 index 0000000..83f452f --- /dev/null +++ b/src/ovirtcli/state/statemachine.py @@ -0,0 +1,139 @@ +# +# Copyright (c) 2013 Red Hat, Inc. +# +# 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. +# + + +from ovirtcli.state.dfsaevent import DFSAEvent +from ovirtcli.state.dfsastate import DFSAState +from ovirtcli.state.finitestatemachine import FiniteStateMachine + +""" + =========== define self instance =========== + + this is singleton instance of FiniteStateMachine + that will be used across the app to maintain the + state. + + =========== state optimization ============= + + q ~ q` = {final states}, {other states} + o + + q ~ q` <=> q~ q` and l(q,a) ~ l(q`,a) + k+1 k k + + ~ = ~ => ~ = ~ + k+1 k k + + e.g: + + --- ===== ===== + ->|1|-a-->||2||-a-->||3||---- + --- ===== ===== | + | | ^ | ^ b + b |_b_| |_a_| | + | ^ | + | | | + v a | + --- __|__ ===== + ->|4|<--b-| 5 |<--------b-||6||-a-- + | --- ----- ===== | + | | ^ | + | a,b |_____| + |__| + + + 1 2 3 4 5 6 + ------------ + a |2 3 3 4 2 6 ~ = {1,4,5}, {2,3,6} + b |4 2 6 4 4 5 o + ~ = {1,5}, {2,3}. {4}, {6} + 1 + + ---- ==== ==== + ->|1,5|-a---||2||-a---||3||-b--- + ---- ==== ==== | + | ^ ^ | ^ | | + b | | b | a | + | | |__| |___| | + | | | + | | ==== | + | -----b-||6||--------------- + V ==== + --- ^ | + |4|-a,b- | a + --- | |___| + ^ | + | | + |______| + +""" + +StateMachine = FiniteStateMachine( + events=[ + DFSAEvent( + name='exiting', + sources=[ + DFSAState.Connecting, + DFSAState.Connected, + DFSAState.Disconnected, + DFSAState.Unauthorized, + ], + destination=DFSAState.Exiting, + callbacks=[]), + DFSAEvent( + name='disconnecting', + sources=[ + DFSAState.Connected, + DFSAState.Unauthorized, + DFSAState.Exiting + ], + destination=DFSAState.Disconnecting, + callbacks=[]), + DFSAEvent( + name='disconnected', + sources=[ + DFSAState.Connected, + DFSAState.Unauthorized, + DFSAState.Disconnecting + ], + destination=DFSAState.Disconnected, + callbacks=[]), + DFSAEvent( + name='connecting', + sources=[ + DFSAState.Disconnected, + DFSAState.Unauthorized + ], + destination=DFSAState.Connecting, + callbacks=[]), + DFSAEvent( + name='connected', + sources=[ + DFSAState.Disconnected, + DFSAState.Unauthorized, + DFSAState.Connecting + ], + destination=DFSAState.Connected, + callbacks=[]), + DFSAEvent( + name='unauthorized', + sources=[ + DFSAState.Connected + ], + destination=DFSAState.Unauthorized, + callbacks=[]), + ] +) -- To view, visit http://gerrit.ovirt.org/20451 To unsubscribe, visit http://gerrit.ovirt.org/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ic3ea3cf7fa82eb82c2323d96231de0a5e1094056 Gerrit-PatchSet: 1 Gerrit-Project: ovirt-engine-cli Gerrit-Branch: master Gerrit-Owner: Michael Pasternak <[email protected]> _______________________________________________ Engine-patches mailing list [email protected] http://lists.ovirt.org/mailman/listinfo/engine-patches
