State Machine¶
Statemachine is a major building block of a connection object. It enables the connection handle to smoothly traverse across different router states. This is how it fits into overall scheme of things.
We define router states as different router modes, e.g. enable, disable, config, rommon etc. Hence statemachine provides a software abstraction of all the router modes, and it keeps the connection library always in sync with the actual device mode.
We need to implement the statemachine for all the platform implementations and if this step is done correctly, we can safely assume that at least half of the platform implementation is over.
In this chapter we will go through the important APIs and using an example device we will try to implement a working statemachine.
Before you go further, please make sure you have gone through Expect Abstraction Library
Note
It is not mandatory that states must be a router mode. In dual rp connections, we even treat standby locked also as one of the states. It all depends on how do we want to abstract to the device in software.
Structure¶
The statemachine consists of following two things:
States: Individual states representing one of the router modes.
Paths: Migration paths between the states.
Following is the block diagram for the same.
State¶
As said in the previous section, it depicts one of the router modes. We identify a router mode using the prompt pattern. For example this is how we can define the enable state and disable state.
1 2 3 4 5 6 7 | from unicon.statemachine import State # enable state enable = State('enable', r'^.*(%N-standby|%N-sdby|%N)*#\s?#') # disable state disable = State('disable', r'^.*(%N-standby|%N-sdby|%N)*#\s?>') # config state config = State('config', r'^.(%N\(config\))#\s?') |
What is %N
? Since the hostname of the device is not known at the point of
creating states, hence it is a just a markup indicating hostname. All the %N
would be replaced by the actual hostname of the device during runtime.
API Guide For State¶
-
class
unicon.statemachine.statemachine.
State
(name, pattern) Defines a device state
Example
enable_state = State(name='enable', pattern=r'^.*Router|%N#')
Initializes the state
-
__init__
(name, pattern) Initializes the state
-
add_state_pattern
(pattern_list) Appends state patterns to the existing list
Example
state.add_state_pattern('.*new pattern')
-
restore_state_pattern
() Restore state pattern back to default one.
Example
state.restore_state_pattern()
-
Path¶
Path objects contain all the information for migrating from one state to another. It requires the following arguments.
from_state: state object from which migration will start. (mandatory)
to_state: state object to which migration will end. (mandatory)
command: command required to initiate the migration. (mandatory)
dialog: dialog object for negotiating any interaction because of command (optional)
Continuing from the previous example, lets add a few Path
.
1 2 3 4 5 | from unicon.statemachine import State disable_to_enable = Path(disable, enable, "enable", None) enable_to_disable = Path(enable, disable, "disable", Dialog()) enable_to_config = Path(enable, config, "config term", Dialog()) config_to_enable = Path(config, enable, "end", None) |
Please note that Dialog
in above example will be different in all the lines,
based on the nature of interaction caused by the command
, a blank Dialog
has been used just for example.
The command
option can also be a callable function. This can be used in case
a single command sent with sendline()
is not sufficient. Below example
sends an escape character and quits a telnet session as part of a state transition.
The arguments, statemachine
, spawn
and context
are mandatory.
1 2 3 4 5 6 | def escape_telnet(statemachine, spawn, context): spawn.send('~') spawn.expect(r'telnet>\s?$', timeout=5) spawn.sendline('q') module_console_to_chassis = Path(module_console, chassis, escape_telnet, None) |
Statemachine¶
To create a statemachine class, we need to subclass from StateMachine
,
which is the base class. This base class has all the relevant instrumentation
required for creating shortest paths between any two given states. It uses all
the Path
instances to make way from any given state to any state. It
provides APIs required for state migration, which we shall see shortly.
Let’s create a sample statemachine class. All the State
and the Path
instances, which we created above are eventually fed into the custom
statemachine class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | from unicon.statemachine import StateMachine from unicon.statemachine import State from unicon.statemachine import Path class MyStateMachine(StateMachine): def create(self): # enable state enable = State('enable', r'^.*(%N-standby|%N-sdby|%N)*#\s?#') # disable state disable = State('disable', r'^.*(%N-standby|%N-sdby|%N)*#\s?>') # config state config = State('config', r'^.(%N\(config\))#\s?') # create all the paths disable_to_enable = Path(disable, enable, "enable", None) enable_to_disable = Path(enable, disable, "disable", Dialog()) enable_to_config = Path(enable, config, "config term", Dialog()) config_to_enable = Path(config, enable, "end", None) # add all the states to statemachine self.add_state(enable) self.add_state(disable) self.add_state(config) self.add_state(enable) # add all the paths to statemachine self.add_path(disable_to_enable) self.add_path(enable_to_disable) self.add_path(enable_to_config) self.add_path(config_to_enable) # at the time of creating statemachine instance you should be aware of the # hostname of the device. sm = MyStateMachine("<hostname>") |
Note
Please note that we don’t want same State
and Path
instances to be
reused by different statemachine classes. Hence we create State
and
Path
instances inside the scope of statemachine class.
Using The StateMachine¶
Now that the statemachine instance is created, let’s take it for a spin.
However, before we do so please ensure that you have already connected to the
device (statemachine doesn’t know how to connect) and you have a spawn
instance. It can be used against any spawn
instance which is already
connected to the device.
Here is a sample run from the statemachine of a connection
instance,
connected to a single rp IOS device.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | In [4]: sm = con.sm # con is the connection handle In [5]: s = con.spawn In [6]: # getting the current state In [7]: sm.current_state Out[7]: 'enable' In [8]: # to list all the states in statemachine In [9]: sm.states Out[9]: [enable, disable, config, rommon] In [10]: # to list all the paths, __str__ of paths have to tweaked for better legibility In [11]: sm.paths Out[11]: [enable->disable, enable->config, enable->rommon, disable->enable, config->enable, rommon->disable] In [13]: # to get a state object by name. In [14]: state = sm.get_state('enable') In [15]: type(state) Out[15]: unicon.statemachine.statemachine.State In [16]: state Out[16]: enable In [17]: # to get a path by name In [18]: path = sm.get_path('enable', 'disable') In [19]: type(path) Out[19]: unicon.statemachine.statemachine.Path In [20]: path Out[20]: enable->disable In [28]: # to move to any given state In [29]: sm.go_to('config', s) config term Enter configuration commands, one per line. End with CNTL/Z. si-tvt-7200-28-41(config)# In [30]: sm.go_to('enable', s) end si-tvt-7200-28-41# In [31]: In [31]: # go to any valid state in the state machine. In [32]: sm.go_to('any', s) '' si-tvt-7200-28-41# si-tvt-7200-28-41# In [33]: sm.current_state Out[33]: 'enable' |
learn_hostname
feature support requirements¶
Hostname pattern substitution using the %N
markup is typically required
for routing and switching platforms that expect the hostname passed into the
unicon Connection object (or the pyATS device name being connected to)
to be already configured on the device in order for the state patterns
containing %N
to match.
This feature enables connection to a device with an unknown hostname
configured. It does so by analyzing the device prompt, learning a potentially
different hostname and then using that hostname for all subsequent
%N
markup substitutions.
All state patterns containing a chain of %N
hostname variants must
specify them in a most-to-least-specific manner in order for this feature
to work correctly. A hostname cannot be learned if none of the device’s
state patterns contain the %N
hostname markup.
Here’s a simplified example from the generic plugin patterns illustrating this.
Note the final pattern in the chain is simply %N
self.enable_prompt = r'^(.*?)(%N-standby|%N-stby|%N)(\(boot\))*#\s?$'
The connection provider object’s establish_connection
method is responsible
for driving this feature.
If for some reason, learn_hostname
is unable to detect hostname then unicon
compares unicon buffer with default hostname pattern set to Settings
Attribute DEFAULT_HOSTNAME_PATTERN
. Default value is
r'RouterRP|Router|[Ss]witch|Controller|ios'
API Guide For StateMachine¶
-
class
unicon.statemachine.
StateMachine
(hostname=None, learn_hostname=False) StateMachine for connection
StateMachine class has the detail of all supported device state for any particular platform and the connection between each of this state, and also provide mechanism to change the device state
This class performs the following
Collects and Validates supported device state
Collects path between any two defined state
Computes the transition path from one state to any end state
Keeps in track of the current device state
constructs the Dialog for changing device state on runtime
Enables hostname substitution
Provides methods to change a device state
Each state created outside statemachine has to be add to it
Initializes the state machine
-
__init__
(hostname=None, learn_hostname=False) Initializes the state machine
-
add_default_statements
(statements) Adds default Statements
This statement will be appended to all state transitions
Example
stmt1 = Statement(pattern=r'.*[confirm]', action=lambda spawn:spawn.sendline() loop_continue=True) stmt2 = Statement(pattern=r'.*[yes/no]', action=lambda spawn:spawn.sendline('yes') loop_continue=True) # Adding default statements sm.add_default_statements(stm1) sm.add_default_statements(stm2) # Adding a list of statements sm.add_default_statements([stm1, stmt2])
-
add_path
(path) Add path to the list of available paths
Example
# Create a path instance first path1 = Path('enable', 'config', command='config terminal', dialog=None) # Now add it to SM sm.add_path(path1)
-
add_state
(state) Appends a given state to the state machine
Example
state = State(state_name='name', pattern=None') sm.add_state(state)
-
create
() Create the statemachine
-
create_path
(from_state, to_state, command=None, dialog=None) Creates a Path and add it to the state machine
Example
sm.create_path('enable', 'config', command='config terminal', dialog=None)
-
create_state
(name, pattern) Creates a state and add it to State Machines
Example
sm.create_state(state_name='name', pattern=None')
-
property
current_state
Current device state property getter
-
find_all_paths
(from_state, to_state) Return all possible paths from any given state to any state
Example
sm.find_all_paths('enable', 'disable')
-
get_path
(from_state, to_state) Get the path object for the given from and to state
This API is used for getting paths between directly linked states
- Usage:
sm.get_path(<from_state>, <to_state>)
- Example::
sm.get_path('enable', 'disable')
-
get_shortest_path
(from_state, to_state) Finds the shortest path from the list of possible paths
Example
sm.get_shortest_path('rommon', 'enable')
-
get_state
(state_name) Returns the state object for the given the name
Example
sm.get_state('config')
-
go_to
(to_state, spawn, context={}, dialog=None, timeout=None, hop_wise=False, prompt_recovery=False) Performs device state change
- Parameters
to_state – Destination state, or a list of state or a keyword ‘any’
spawn – Device handle where the state change has to be performed
dialog – Additional dialogs which has to be appended to the SM path
prompt_recovery – Enable/Disable prompt_recovery feature
timeout – timeout for this goto operation
hop_wise – If enabled each path is processed separately Default: False
Example
# Changing to a particular state from the current state # Here sm is the state machine instance sm.go_to(to_state='enable', spawn=dev.spawn, context=dev.context) # Changing state to anyone state from the given list of states sm.go_to(to_state=['enable', 'disable'], spawn=dev.spawn, context=dev.context) # Bring device to 'any' valid available state in state machine # ( Usually used for reading the device state) sm.go_to(to_state='any', spawn=dev.spawn, context=dev.context)
-
remove_path
(from_state, to_state) Removes a particular path from SM
- Usage:
sm.remove_path(<from_state>, <to_state>)
Example
sm.remove_path('enable', 'disable')
-
remove_state
(state) Removes a state from SM, given the string name
Example
sm.remove_state('disable')