Expect Abstraction Library

Introduction

Expect Abstraction Library (EAL), as the name suggests, is a python based avatar of Tcl/Expect library. This package attempts to bring in most of the useful features of Expect in a pythonic flavour.

EAL provides classes and structures required to programatically control any interactive command. For interactive programs, whose order of interactions varies based on the user input, we can use dialogs. Dialogs can dymamically invoke different callbacks based on the corresponding pattern match.

EAL provides the lower most abstraction level to Unicon to perform device interactions. This library can be used even outside the context of device connection, for invocation of general shell commands; for example invoking an interactive shell program on a linux system.

This library brings in following major API’s and settings.

  • spawn

  • expect

  • send

  • log_user

  • no_transfer

  • exp_continue

  • dialogs

  • timeout

This is how a simple EAL program could look like.

1
2
3
4
5
6
7
8
9
 from unicon.eal.expect import Spawn
 prompt = r"^.*bash\$$\s?"
 s = Spawn(spawn_command="telnet 1.2.3.4")
 s.expect([r"username:"])
 s.send("admin\r")
 s.expect([r"password:"])
 s.sendline("lab") # same as send but doesn't require carriage return
 s.expect([prompt])
 s.close()

Challenges

Implementing an Expect like library is a bit of task in Python becuase of following two reasons:

Event Driven: Python, unlike Tcl is not event driven language at core. Becuase of this, python lacks asyncronous event loops. We need asyncronous event loops for precise tracking of timeouts.

Note

Python 3 included asyncio as a core library for carrying out asyncronous tasks.

Eval: Python does not encourage evaluation of arbitrary code. Yes it is allowed, but it should be only used only in situation when standard python techniques do not work. Whereas it is quite common in Tcl to pass chunks of code as arguments, which the receiving function can invoke in caller’s context. Because of this self imposed limitation, it is difficult to created nested Expect blocks containing patterns and action/callback pairs.

Globals: Globals are strongly discouraged in python. In absence of global variables we need some special ways to handle situations where we need our callback functions to communicate with each other.

EAL tries its best to overcome these problems and provide an intuitive set of APIs to handle interactive shell commands.

Why Not Pexpect

One common question we often receive is:

Why not pexpect !

In our benchmark tests we found pexpect to be significantly slower than Tcl/Expect. The order of difference was enough for us to consider different possible options. It also lacks concept of Dialogs, without which, it is difficult to scale pexpect programs.

We also included the following libraries in our benchmark tests.

Under The Hood

EAL is developed based on pty library. Pty is a standard python package for in-memory handling of pseudo terminals.

Pty library forks a process and provides socket like objects for communicating

with those processes.

EAL uses this as follows:

  • fork a process.

  • in the forked process, exec the ssh command which will connect to localhost.

  • once we have the shell, issue the command which needs to be spawned.

  • forked process returns a file descriptor, use this for inter process communication.

  • destroy the process when the scope of spawned command is over.

Currently this option is looking scalable and provides extremely good performance, almost at par with Tcl/Expect or sometimes even better.

Spawn

You can Spawn any command to interact with it. Once the command is spawned you can interact with it using APIs like send and expect.

This is how, in a nutshell, it works

from unicon.eal.expect import Spawn
s = Spawn("telnet 1.2.3.4 1000")
# now we have spawn object s

s.send("\r")
ret = s.expect(['pattern'])

Example Shell Script

Since we do not find interactive commands commonly on linux platforms, hence we will use the following shell program during all our subsequent examples in this chapter. Please make sure you save the following shell program as router.sh on your Linux/Mac system. All the example which will follow from here will spawn router.sh. You may require to give it execute permission:

chmod 755 router.sh

Credentials for the router:

username: admin
password: lab
enable password: lablab

Here is the source code of router.sh:

 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#!/bin/bash
hostname="sim-router"
disable_prompt="$hostname>"
enable_prompt="$hostname#"
config_prompt="$hostname(config)#"
echo "Trying X.X.X.X ...
Escape character is '^]'.
Press enter to continue ..."
read escape_char

if [[ $escape_char == "" ]]
then
    echo -n "username: "
    read username
    if [[ $username == "admin" ]]
    then
        echo -n "password: "
        read -s password
        if [[ $password == "lab" ]]
        then
            echo
            #echo -n "$disable_prompt"
        else
            echo "bad password"
            exit 1
        fi
    else
        echo "wrong username"
        exit 1
    fi
fi

prompt=$disable_prompt
while true
do
    echo -n $prompt
    read resp
    # enable command
    if [[ $resp == "enable" || $resp == "en" ]]
    then
        password=""
        echo -n "password: "
        read  password
        if [[ $password == "lablab" ]]
        then
            prompt=$enable_prompt
        else
            echo "Bad Password"
        fi
    # show clock command
    elif [[ $resp == "show clock" || $resp == "sh clock" ]]
    then
        echo $(date)

    # config mode.
    elif [[ $resp == "config" || $resp == "config term" ]]
    then
        # check if we are in enable mode
        if [[ $prompt == $enable_prompt ]]
        then
            echo -n "Configuring from terminal, memory, or network [terminal]? "
            read resp
            if [[ $resp == "" ]]
            then
                prompt=$config_prompt
            fi
        else
            echo "you need to be in enable mode"
        fi
    # config end
    elif [[ $resp == "end" ]]
    then
        # check if are in config mode
        if [[ $prompt == $config_prompt ]]
        then
            prompt=$enable_prompt
        else
            echo "you need to be in config mode"
        fi
    # going to disable mode.
    elif [[ $resp == "disable" ]]
    then
        # check if we are in enable mode first
        if [[ $prompt == $enable_prompt ]]
        then
            prompt=$disable_prompt
        else
            echo "you need to be in enable mode"
        fi
    fi
done

This is a sample run of this script. It is just a minimal script to simulate a router kind of stuff:

$ ./router.sh
Trying X.X.X.X ...
Escape character is '^]'.
Press enter to continue ...

username: admin
password:
sim-router>enable
password: lab
sim-router#show clock
Fri Oct 23 01:55:16 IST 2015
sim-router#

It is only capable to doing following things which is just enough for our purpose.

  • perform a login.

  • going to enable mode with enable command.

  • running show clock command.

Spawning Our First Command

Now let us spawn the router.sh. This is how it can be done. We are assuming that router.sh is in the current directory, or else you can provide the fully qualified path.

import os
from unicon.eal.expect import Spawn
router_command = os.path.join(os.getcwd(), 'router.sh')
s = Spawn(router_command)

Following events happen when above code is executed.

  • an ssh session to localhost is created. This will be manifested a minimal lag.

  • a new tty session is created inside the ssh connection.

  • router.sh is invoked.

Note

You may also see the login banner of localhost, which is normal. This has nothing to do with the spawned command.

Using Send Command

In case you have executed the router.sh, you will notice that it waits for you to press the ENTER button, before it can show the username prompt. This is the exact place where it waits:

Press enter to continue ...

Hence let us send the the carriage return.

s.sendline()
# we can also do it like this.
s.send("\r")
# both are equivalent.

send/sendline methods do not return anything, even if they do, it is irrelevant. Either your command will be sent or an exception will be raised.

Expect The Expected

After the sending the carriage return we expect the username: prompt. Hence let us write a pattern to handle this.

ret = s.expect([r'username:\s?$'])

If the above pattern is not received within the specified amount of time, then a TimeoutError is raised. By default, the timeout value is 10. Let us reduce it since we know our router.sh will take almost no time to show the username prompt.

ret = s.expect([r'username:\s?$'], timeout=5)

Let us generalise the above program a bit. We may come across some routers where username prompt doesn’t look like username:, it may also show up like login::. The good news is, expect method can take a list of patterns.

ret = s.expect([r'username:\s?$', r'login:\s?$'], timeout=5)

By default, match_mode_detect is enabled. Detect rules are as below:

  1. search whole buffer with re.DOTALL if:

  • pattern contains any of: \r, \n

  • pattern equals to any of: .*, ^.*$, .*$, ^.*, .+, ^.+$, .+$, ^.+

  1. If pattern ends with $ but not $, will only match last line

  2. In other situations, search whole buffer with re.DOTALL

Now let’s introspect on the return object. The return object contains the following:

  • last_match: the re match object.

  • match_output: the exact text which matched in the buffer.

  • last_match_index: the index of pattern in the list which matched.

  • last_match_mode: the match mode eg. search whole buffer with re.DOTALL, only match last line

Generally you will be interested in the match_output.

Now lets sum it up and complete the above program to login and run a command. show clock. Most of the program is self explanatory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
from unicon.eal.expect import Spawn, TimeoutError
router_command = os.path.join(os.getcwd(), 'router.sh')
prompt = 'sim-router'
enable_prompt = prompt + '#'
disable_prompt = prompt + '>'
s = Spawn(router_command)
try:
    s.sendline()
    s.expect([r'username:\s?$', r'login:\s?$'], timeout=5)
    s.sendline('admin')
    s.expect([r'password:\s?$'], timeout=5)
    s.sendline('lab')
    s.expect([disable_prompt])
    s.sendline('enable')
    s.expect([r'password:\s?$'], timeout=5)
    s.sendline('lablab')
    s.expect([enable_prompt])
    s.sendline('show clock')
    s.expect([enable_prompt])
except TimeoutError as err:
    print('errored becuase of timeout')

Note

A note on pattern matching and buffer size. The default search size is 8K which is used to search up to 8K bytes at the end of the buffer. This speeds up pattern matching for very large command output. To specify a different search size, use the search_size parameter. Using 0 will search the complete buffer.

You can check and set the default search size using the SEARCH_SIZE setting.

ret = s.expect([r'huge pattern .* matching more than 8K'], timeout=60, search_size=16000)

>>> s.settings.SEARCH_SIZE
8192
>>>
>>> s.settings.SEARCH_SIZE = 16000
>>> s.settings.SEARCH_SIZE
16000
>>>

EOF Exception

If the spawn connection has terminated/closed (like someone clear console line or close() is called on spawn) then any call to send/expect will raise an EOF exception.

from unicon.eal.expect import Spawn
s = Spawn("telnet 127.0.0.1 15000")
s.close()
s.expect([r"username:"]) # This will raise EOF
s.send('some data') # this will raise EOF
# Spawn again if EOF raise
from unicon.core.errors import EOF
try:
    s.expect(r'.*')
except EOF as e:
    print('Spawn not available, Re-Spawn.')
    s = Spawn('telnet 127.0.0.1 15000')

Need For Dialogs

Above programs looks complete, but it has few limitations. We can use send/expect pair when we know for sure, that sequence of interaction will never change. Think of a hypothetical situation, in the above example, if the router.sh prompts for password before username ! In such situation, above program will timeout, even though it knows how to handle the password prompt. The order of interaction cannot be taken for granted in all the situations.

We need to interact with commands which prompts for different things based on the user input, and our program should be able to handle it. The better example could be copy command on the router. On different platforms, and with different copy protocols we see different questions being asked. And it is expected from our API’s to handle all such variations, in order to produce a platform agnostic API.

Dialogs provide a way to handle exactly the same situation. It allows us to club all the anticipated interactions in one structure. It is agnostic to sequence of interaction as long as dialog knows how to handle it. At semantic level this how it looks.

d = Dialog([statement_1,
            statement_2,
            ...,
            ...,
            statement_n])
# to execute or process a dialog.
d.process(s)
# here s is the spawn instance on which this dialog has to
# be targeted.

In EAL Dialog is a class which is constituted of Statements. Before we go forward, lets study Statement class, the building block of a dialog.

Statements

Statements are building blocks of Dialogs. It has following constituents.

  • pattern: pattern for which the statments get triggered. (mandatory)

  • action: any callable which needs to be called once the pattern is matched. (optional)

  • args: a dict which contains arguments to action, if any. (default value None)

  • loop_continue: whether to continue the dialog after this statement match. (default value False)

  • continue_timer: the dialog timeout gets reset after every match. (default value True)

  • debug_statement: log the matched pattern if set to True. (default value False)

  • trim_buffer: whether to remove match content from buffer. (default value True)

  • matched_retries: retry times if statement pattern is matched. (default value 0)

  • matched_retry_sleep: sleep between retries. (default value 0.02 seconds)

This is how an statement can be constructed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 # create a simple callback function
 def send_password(spawn, password):
     spawn.sendline(password)

 from unicon.eal.dialogs import Statement
 s = Statement(pattern=r'password:',
               action=send_password,
               args={'password': 10},
               loop_continue=True,
               continue_timer=False,
               trim_buffer=True,
               debug_statement=True)

Feel free to use lambdas in case you find it convenient for simple operations

# By using lambda, same thing can be written as below.
# in this we don't need to define the callback functions.
from unicon.eal.dialogs import Statement
s = Statement(pattern=r'password:',
              action=lambda spawn, password: spawn.sendline(password)
              args={'password': 10},
              loop_continue=True,
              continue_timer=False)

Notice the args in both the examples. We have not supplied any value for the argument spawn even though the callback function (or the lambda) depends on it. EAL performs dependency injection for few thinngs which cannot be determined while contructing the Statement object. We will see it in detail in the next section.

Note

Mention args, loop_continue, continue_timer only if you want to change the the default values. This will help reducting the clutter.

Timeout Statement: By default, if none of Statement patterns get match within timeout period TimeoutError execption gets raised. If we want to add some custom action when timeout occurs before TimeoutError execption, this can be done by adding a Statement with pattern set as __timeout__. Action set for this Statement will get invoke if timeout occurs.

def custom_timeout_method(spawn):
    print('None of patterns matched within timeout period.')

s = Statement(pattern='__timeout__',
              action=custom_timeout_method,
              loop_continue=False,
              continue_timer=False)

Note

Make sure to set continue_timer as False for timeout statement, else it may will end up in infinite loop. If continue_timer set as True, then Dialog will start trying to match all patterns again and timeout period will be reset to original one.

Dependency Injection in Statements

Few things which cannot be determined at the time of construction of Statement objects, are dependency injected by the EAL framework. There are three such things.

  • spawn

  • context (an attribute dict)

  • session (an attribute dict)

spawn: Since same dialog instance can be used on multiple spawns instances, hence user cannot determine its (spawn) value at the time creating Statement instance. If your callback requires spawn then, just mention it in signature. You dont’t need to provide its value in the args section.

context: It is possible to have a situation when the value of some of the arguments of the callback needs to be determined at the runtime. One good example could be fetching the password from some config file, on which the developer has no control. In such situations, same callback function could be written like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 def send_password(spawn, context):
     spawn.sendline(context.password)

 from unicon.utils import AttributeDict
 ctx = AttributeDict({'password': 'lab'})

 from unicon.eal.dialogs import Statement
 s = Statement(pattern=r'password:',
               action=send_password,
               loop_continue=True,
               continue_timer=False)
 # we are assuming we have more statements s1 and s3
 # also we have one spawn instance named s.
 d = Dialog([s, s1, s3])
 d.process(s, context=ctx)

Note

we don’t need args in above statement as both the values will be injected in runtime.

session: It is used for scenarios where different callback functions in a dialog would require to communicate with each other. session provides a way for inter callback communication. It is an AttributeDict which can be treated as dictionary. It is also required if the same statement matched more than once during an interaction and the callback function is expected to behave differently in both the entries. We will have an example for this later.

Whenever a dialog processing begins, a blank session dict is initialized. Any callback function can add or access any value to it. Since it is a dictionary, hence all the rules for handling dict*s are applicable. It is strongly recommended to check for the presence of a key before accessing it. Becuase it can always happen that statement callback function which was supposed to *set the value has not been invoked yet. This precaution will help avoiding KeyError.

To be able to use the session dict we need to mention it in the callback signature, else it will not be injected.

We will use these concepts in the later part of the document to make things clear.

Dialogs Revisited

In this section we will cover two different ways the dialogs can be created.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 # as said, dialog is a list of statements
 d = Dialog([
     Statement(pattern=r'^pat1',
               action=first_callback,
               args=dict(a=1),
               loop_continue=True,
               continue_timer=False),
     Statement(pattern=r'^pat2',
               action=second_callback,
               args=None,
               loop_continue=False,
               continue_timer=False),
    Statement(pattern=r'^pat3',
              action=third_callback,
              args=None,
              loop_continue=True,
              continue_timer=False),
 ])

As we can see there is a lot of typing involved. We can also use a shorthand notation. Same dialog can also be represented as follows.

1
2
3
4
5
 d = Dialog([
     [r'^pat1', first_callback, {'a':1}, True, False],
     [r'^pat2', second_callback, None, False, False],
     [r'^pat3', third_callback, None, True, False],
 ])

Above style is a lot compact. Here we only need to provide arguments required by the Statement class as a list. But while using above notation please make sure to provide all the default arguments in case any of the default values are changed.

Note

Please make sure to have at least one statement in the dialog having its loop_continue value as False, else the dialog will run into infinite loop, till it times out. We can’t call it a bug becuase sometimes it is a desired feature. But almost always you will not want an infinite loop.

Dialog Shorthand Notation

New in version 1.1.0.

As you can see above, we are required to write callback function even for very trivial operations like sending a character y or yes. Sometimes writting even little lambda functions also cause a lot of clutter.

It is good to know how callback functions work but for very trivial operations you can use special string notation to get the the job done. For example if you need to send a “yes” followed by a new line character. You can do it like this:

Dialog([
    [r'pattern', 'sendline(yes)', None, False, False]
])

As you can see in the above example, you don’t need to define sendline function. We have more such string based callbacks. You can send any string by changing the string inside the parenthesis. For example to send xx you can write it as sendline(xx).

Note

Please make sure you don’t use any quotations line '' or "" inside the parenthesis.

String Callbacks

Description

sendline(x)

sends the x followed by a new line character

send(x)

sends the x without a new line character

send_ctx(x)

sends the value in the context dict with key x, without a new line character.

sendline_ctx(x)

sends the value in the context dict with key x, follwed by a new line character

send_session(x)

sends the value in the session dict with key x, without a new line character.

sendline_session(x)

sends the value in the session dict with key x, followed by a new line character.

sendline_cred_user(x)

sends the username for credential with key x, followed by a new line character.

sendline_cred_pass(x)

sends the password for credential with key x, followed by a new line character.

In the next section we would see how to use this in practice.

Putting It All Together

Let us now try to put all the above concepts to work. First we will try the following assigenment:

login to the router to reach the disable prompt

The program to handle this could look like this. We will call it our version 1.

 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
import os
from unicon.eal.expect import Spawn, TimeoutError
from unicon.eal.dialogs import Statement, Dialog

router_command = os.path.join(os.getcwd(), 'router.sh')
prompt = 'sim-router'
enable_prompt = prompt + '#'
disable_prompt = prompt + '>'

# callback to send password
def send_password(spawn, password='lab'):
    spawn.sendline(password)

# callback to send username
def send_username(spawn, username="admin"):
    spawn.sendline(username)

# callback to send new line
def send_new_line(spawn):
    spawn.sendline()

# construct the dialog
d = Dialog([
    [r'enter to continue \.\.\.', send_new_line, None, True, False],
    [r'username:\s?$', send_username, None, True, False],
    [r'password:\s?$', send_password, None, True, False],
    [disable_prompt, None, None, False, False],
])

s = Spawn(router_command)

# at this stage we are anticipating the program to wait for a new line
d.process(s)
s.close()

One thing we can quickly notice here, is that all the callback functions look a like. In the first glance we can say that there is scope for some optimization. Rather that writting three callback functions, all of which look alike, we can improve it by using only one callaback function.

Let’s see our version 2, this is more DRY than the previous.

 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
import os
from unicon.eal.expect import Spawn, TimeoutError
from unicon.eal.dialogs import Statement, Dialog

router_command = os.path.join(os.getcwd(), 'router.sh')
prompt = 'sim-router'
enable_prompt = prompt + '#'
disable_prompt = prompt + '>'

# callback to send any command or a new line character
def send_command(spawn, command=None):
    if command is not None:
        spawn.sendline(command)
    else:
        spawn.sendline()

# construct the dialog
d = Dialog([
    [r'enter to continue \.\.\.', send_command, None, True, False],
    [r'username:\s?$', send_command, {'command': 'admin'}, True, False],
    [r'password:\s?$', send_command, {'command': 'lab'}, True, False],
    [disable_prompt, None, None, False, False],
])

s = Spawn(router_command)

# at this stage we are anticipating the program to wait for a new line
d.process(s)

s.close()

But there is still room for improvement. In fact, our lone callback function is essentially performing a very trivial task, i.e sending a command. We can actually write it inline using lambda functions. Our version 3

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os
from unicon.eal.expect import Spawn, TimeoutError
from unicon.eal.dialogs import Statement, Dialog

router_command = os.path.join(os.getcwd(), 'router.sh')
prompt = 'sim-router'
enable_prompt = prompt + '#'
disable_prompt = prompt + '>'

# construct the dialog
d = Dialog([
    [r'enter to continue \.\.\.', lambda spawn: spawn.sendline(), None, True, False],
    [r'username:\s?$', lambda spawn: spawn.sendline("admin"), None, True, False],
    [r'password:\s?$', lambda spawn: spawn.sendline("lab"), None, True, False],
    [disable_prompt, None, None, False, False],
])

s = Spawn(router_command)

# at this stage we are anticipating the program to wait for a new line
d.process(s)
s.close()

Now let’s use the shorthand notation which we learnt in the last section. This can make the overall composition look even more compact and lucid. Here is version 4

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
from unicon.eal.expect import Spawn, TimeoutError
from unicon.eal.dialogs import Statement, Dialog

router_command = os.path.join(os.getcwd(), 'router.sh')
prompt = 'sim-router'
enable_prompt = prompt + '#'
disable_prompt = prompt + '>'

# construct the dialog
# we can see how shorthand notation makes the code look even more leaner.
d = Dialog([
    [r'enter to continue \.\.\.', 'sendline()', None, True, False],
    [r'username:\s?$', 'sendline(admin)', None, True, False],
    [r'password:\s?$', 'sendline(lab)', None, True, False],
    [disable_prompt, None, None, False, False],
])

s = Spawn(router_command)

# at this stage we are anticipating the program to wait for a new line
d.process(s)
s.close()

Based on your preference you can use either of version 2 or 3 or 4. But we will strongly recommed to use version 4, i.e. the one which follows shorthand notation, whenever and whereever possible. It reduces the chances or including an erroneous callback function and also avoids code duplication.

Using Session

Now lets extend the problem a bit:

What if we have to take the router till enable mode, unlike the previous
example where we are only going till disable mode.

In the first glance it may just look like a linear extension to the previous problem, but it is not. It may tempt us to solve it by just adding one more statement in the dialog. But notice the fact that login password prompt and enable password prompt look the same. Hence the following statement will trigger twice:

[r'password:\s?$', send_command, {'command': 'lab'}, False, False]

But on the second occassion it has to send the enable password. We can’t have two statements having the same pattern in a dialog. We need to solve this by doing something at the callaback level. Our callback must have a way to understand whether it has been called for the first time or the second time, in order to decide which password to send. Here we can use session to our rescue.

 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
import os
from unicon.eal.expect import Spawn, TimeoutError
from unicon.eal.dialogs import Statement, Dialog

router_command = os.path.join(os.getcwd(), 'router.sh')
prompt = 'sim-router'
enable_prompt = prompt + '#'
disable_prompt = prompt + '>'

# callback to send password
def send_passwd(spawn, session, enablepw, loginpw):
    if 'flag' not in session:
        # this is first entry hence we need to send login password.
        session.flag = True
        spawn.sendline(loginpw)
    else:
        # if we come here that means it is second entry and here.
        # we need to send the enable password.
        spawn.sendline(enablepw)

# construct the dialog
d = Dialog([
    [r'enter to continue \.\.\.', lambda spawn: spawn.sendline(), None, True, False],
    [r'username:\s?$', lambda spawn: spawn.sendline("admin"), None, True, False],
    [r'password:\s?$', send_passwd, {'enablepw': 'lablab', 'loginpw': 'lab'}, True, False],
    [disable_prompt, lambda spawn: spawn.sendline("enable"), None, True, False],
    [enable_prompt, None, None, False, False],
])

s = Spawn(router_command)

# at this stage we are anticipating the program to wait for a new line
d.process(s)

s.close()

Similar approch can be taken to solve situations where two callaback in two different callabacks have to communicate with each other. session is unique to the whole dialog context.

The same code can be also we re-written using shorthand notation as follows. We would recommed you to use this version over the one which was just illustrated.

 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
import os
from unicon.eal.expect import Spawn, TimeoutError
from unicon.eal.dialogs import Statement, Dialog

router_command = os.path.join(os.getcwd(), 'router.sh')
prompt = 'sim-router'
enable_prompt = prompt + '#'
disable_prompt = prompt + '>'

# callback to send password, we still need this callback 
# because shorthand notation is for handling trivial payloads.
# this function does little more than that.
def send_passwd(spawn, session, enablepw, loginpw):
    if 'flag' not in session:
        # this is first entry hence we need to send login password.
        session.flag = True
        spawn.sendline(loginpw)
    else:
        # if we come here that means it is second entry and here.
        # we need to send the enable password.
        spawn.sendline(enablepw)

# construct the dialog.
# here we see how shorthand notation can make the code look leaner.
d = Dialog([
    [r'enter to continue \.\.\.', 'sendline()', None, True, False],
    [r'username:\s?$', "sendline(admin)", None, True, False],
    [r'password:\s?$', send_passwd, {'enablepw': 'lablab', 'loginpw': 'lab'}, True, False],
    [disable_prompt, 'sendline(enable)', None, True, False],
    [enable_prompt, None, None, False, False],
])

s = Spawn(router_command)

# at this stage we are anticipating the program to wait for a new line
d.process(s)

s.close()

Prompt Recovery Feature

Prompt recovery feature will try to recover device after normal dialog timeout occurs. This is just an attempt to bring device to stable state and this does not guarantee to bring device to stable state in every scenario.

Use case: Once device booted up with image, console messages displayed over terminal, because of these console log messages over terminal unicon is unable to match the device prompt. Sending a enter key to device bring the device prompt at front and unicon matches device prompt. After reload, console messages can interfere with prompt matching, especially during reload and configuration operations

This feature is available for Dialog, Connect and Services.

Usage

By default this feature is disabled. To enable it, use it in this way:

Dialog.process(spawn, prompt_recovery=True)
# In Unicon
device = Connection(hostname='R1', start=['telnet x.x.x.x'], prompt_recovery=True]
device.connect()
# In pyATS
device.connect(prompt_recovery=True)
device.service(command, prompt_recovery=True)

Prompt recovery configurations

prompt_recovery can be configure using below 3 settings:

  • PROMPT_RECOVERY_COMMANDS : List of prompt recovery commands. Default value: ['\r', '\025', '\032', '\r', '\x1E']. ‘\025’ is Ctrl-U, ‘\032’ is Ctrl-Z and ‘\x1E’ is Ctrl-^ For Linux connection type default command list is: ['\r', '\x03', '\r'] \x03 is Ctrl-C.

  • PROMPT_RECOVERY_INTERVAL : Timeout period after sending each prompt recovery command, in secs. Default value: 10 secs

  • PROMPT_RECOVERY_RETRIES : Number of prompt recovery retires to perform. Default value: 1

Users can also alter these values at run time by setting these values as dialog context.

Example:

from unicon.utils import AttributeDict
ctx = AttributeDict()
ctx.prompt_recovery_interval = 30
dialog.process(dev.spawn, context = ctx)

dialog is Dialog object. dev is device connection object.

Working of prompt recovery feature

When prompt_recovery is enable, below steps followed:

  1. After normal Dialog Timeout occurs. Unicon will not return Timeout exception at that moment, it will try to recover it to known state. Here known state means, try to match all the patterns in dialog again after sending PROMPT_RECOVERY_COMMANDS to device.

  2. List of command which are set to PROMPT_RECOVERY_COMMANDS are send to device, one at a time and new timeout period is set, value of this new timeout period is PROMPT_RECOVERY_INTERVAL.

  3. After sending each PROMPT_RECOVERY_COMMANDS command, unicon waits if device comes to any known stable state. If device comes to any of known stable state, Dialog processing is complete and dialog process is considered as successful.

  4. After sending all PROMPT_RECOVERY_COMMANDS commands to device, one at a time, if device does not comes to known stable state then Timeout exception will be raised.

  5. Step 2 will get repeated PROMPT_RECOVERY_RETRIES times. Example, Value of 1 to PROMPT_RECOVERY_RETRIES means, all commands set to PROMPT_RECOVERY_COMMANDS will be sent to device once. If its set as 2, then all commands will be send two times and the sequence of commands will be like below

PROMPT_RECOVERY_COMMANDS = [cmd1, cmd2, cmd3]
PROMPT_RECOVERY_RETRIES = 2

Commands to device will be send in below sequence to device

cmd1, cmd2, cmd3, cmd1, cmd2, cmd3