Python Network Programming
上QQ阅读APP看书,第一时间看更新

Pexpect overview

For our first lab, we will construct a simple network with two IOSv devices connected back to back: 

Lab topology

The devices will each have a loopback address in the 192.16.0.x/24 range and the management IP will be in the 172.16.1.x/24 range. The VIRL topology file is included in the accommodated book downloadable files. You can import the topology to your own VIRL software. If you do not have VIRL, you can also view the necessary information by opening the topology file with a text editor. The file is simply an XML file with each node's information under the node element: 

Lab node information

With the devices ready, let's take a look at how you would interact with the router if you were to Telnet into the device:

echou@ubuntu:~$ telnet 172.16.1.20
Trying 172.16.1.20...
Connected to 172.16.1.20.
Escape character is '^]'.
<skip>
User Access Verification

Username: cisco
Password:

I used VIRL AutoNetkit to automatically generate the initial configuration of the routers, which generated the default username cisco, and the password cisco. Notice that the user is already in privileged mode because of the privilege assigned in the configuration:

iosv-1#sh run | i cisco
enable password cisco
username cisco privilege 15 secret 5 $1$Wiwq$7xt2oE0P9ThdxFS02trFw.
password cisco
password cisco
iosv-1#

The auto-config also generated vty access for both Telnet and SSH:

line vty 0 4
exec-timeout 720 0
password cisco
login local
transport input telnet ssh

Let's see a Pexpect example using the Python interactive shell:

Python 3.5.2 (default, Nov 17 2016, 17:05:23)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pexpect
>>> child = pexpect.spawn('telnet 172.16.1.20')
>>> child.expect('Username')
0
>>> child.sendline('cisco')
6
>>> child.expect('Password')
0
>>> child.sendline('cisco')
6
>>> child.expect('iosv-1#')
0
>>> child.sendline('show version | i V')
19
>>> child.expect('iosv-1#')
0
>>> child.before
b'show version | i VrnCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.6(2)T, RELEASE SOFTWARE (fc2)rnProcessor board ID 9MM4BI7B0DSWK40KV1IIRrn'
>>> child.sendline('exit')
5
>>> exit()
Starting from Pexpect version 4.0, you can run Pexpect on a Windows platform. But, as noted in the Pexpect documentation, running Pexpect on Windows should be considered experimental for now.

In the previous interactive example, Pexpect spawns off a child process and watches over it in an interactive fashion. There are two important methods shown in the example, expect() and sendline(). The expect() line indicates that the string the Pexpect process looks for as an indicator for when the returned string is considered done. This is the expected pattern. In our example, we knew the router had sent us all the information when the hostname prompt (iosv-1#) was returned. The sendline() method indicates which words should be sent to the remote device as the command. There is also a method called send() but sendline() includes a linefeed, which is similar to pressing the Enter key at the end of the words you sent in your previous telnet session. From the router's perspective, it is just as if someone typed in the text from a Terminal. In other words, we are tricking the routers into thinking they are interfacing with a human being when they are actually communicating with a computer.

The before and after properties will be set to the text printed by the child application. The before properties will be set to the text printed by the child application up to the expected pattern. The after string will contain the text that was matched by the expected pattern. In our case, the before text will be set to the output between the two expected matches (iosv-1#), including the show version command. The after text is the router hostname prompt:

>>> child.sendline('show version | i V')
19
>>> child.expect('iosv-1#')
0
>>> child.before
b'show version | i VrnCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.6(2)T, RELEASE SOFTWARE (fc2)rnProcessor board ID 9MM4BI7B0DSWK40KV1IIRrn'
>>> child.after
b'iosv-1#'

What would happen if you expected the wrong term? For example, if you typed in username instead of Username after spawning the child application, then the Pexpect process would look for a string of username from the child process. In that case, the Pexpect process would just hang because the word username would never be returned by the router. The session would eventually timeout, or you could manually exit out via Ctrl + C.

The expect() method waits for the child application to return a given string, so in the previous example, if you wanted to accommodate both lowercase and uppercase u, you could use the following term:

>>> child.expect('[Uu]sername')

The square bracket serves as an or operation that tells the child application to expect a lowercase or uppercase u followed by sername as the string. What we are telling the process is that we will accept either Username or username as the expected string.

For more information on Python regular expressions, go to https://docs.python.org/3.5/library/re.html.

The expect() method can also contain a list of options instead of just a single string; these options can also be regular expression themselves. Going back to the previous example, you can use the following list of options to accommodate the two different possible strings:

>>> child.expect(['Username', 'username'])

Generally speaking, use the regular expression for a single expect string when you can fit the different hostname in a regular expression, whereas use the possible options if you need to catch completely different responses from the router, such as a password rejection. For example, if you use several different passwords for your login, you want to catch % Login invalid as well as the device prompt.

One important difference between Pexpect regular expressions and Python regular expressions is that the Pexpect matching is non-greedy, which means they will match as little as possible when using special characters. Because Pexpect performs regular expression on a stream, you cannot look ahead, as the child process generating the stream may not be finished. This means the special dollar sign character $ typically matching the end of the line is useless because .+ will always return no characters, and the .* pattern will match as little as possible. In general, just keep this in mind and be as specific as you can be on the expect match strings.

Let's consider the following scenario:

>>> child.sendline('show run | i hostname')
22
>>> child.expect('iosv-1')
0
>>> child.before
b'show run | i hostnamernhostname '
>>>

Hmm... Something is not quite right here. Compare it to the Terminal output before; the output you expect would be hostname iosv-1:

iosv-1#show run | i hostname
hostname iosv-1
iosv-1#

Taking a closer look at the expected string will reveal the mistake. In this case, we were missing the hash (#) sign behind the iosv-1 hostname. Therefore, the child application treated the second part of the return string as the expected string:

>>> child.sendline('show run | i hostname')
22
>>> child.expect('iosv-1#')
0
>>> child.before
b'show run | i hostnamernhostname iosv-1rn'
>>>

You can see a pattern emerging from the usage of Pexpect after a few examples. The user maps out the sequence of interactions between the Pexpect process and the child application. With some Python variables and loops, we can start to construct a useful program that will help us gather information and make changes to network devices.