Connection Class¶
Connection classes are the workhorse of the connection model: their implementation represents the actual connection pipeline, carrying information back and forth from the scripter’s realm to their testbed devices.
As there are over nine thousand types of different devices and methods to
connect to them, the Connections module does not come with a built-in,
universal connector class. Rather, it features BaseConnection
: a base class
template, offering a set of standards, guidelines, rules & tools for anyone to
build on.
Fundamentals¶
The fundamental idea behind standardizing using BaseConnection
is to
enable basic connector-swapping through duck typing: changing connection
types & classes without impacting the library & user scripts (within reason, of
course). This also gives all connection implementations the same look &
feel, allowing for ease of extension, debugging & maintenance, etc.
Therefore, all connection implementations shall follow the same set of rules & guidelines:
always subclass from
connections.BaseConnection
respect the basic design: arguments, properties, methods & etc.
each instance should only represent a single connection pipe (eg, a single telnet instance, etc)
all connections shall have a maximum timeout limit, and should never block forever.
avoid race conditions & deadlocks when your connection instance is shared between processes/threads: use semaphores.
always implement using a bottom-up approach:
start with the basic building blocks:
send
/receive
then build the next level:
expect
,dialogs
then wrap up with top-levels:
execute
,configure
, etc.
In essence, a connection class models the various ways of communicating with a target device using object methods (also known as services). These methods (services) vasly simplify the underlying protocol/behavior details into arguments, datastructure inputs and returns.
BaseConnection¶
Base class to all connection implementations, BaseConnection
is a simple set
abstract concepts & of bare-minimum methods. The following is a table describing
each attribute/method and how it should be used.
Name |
Description |
---|---|
|
should always be called to instantiate a connection class. |
|
property, storing the connected Device object as weakref |
|
property, returns the dictionary of connection info based
on path ( |
|
property, returns the current connection state boolean |
|
abstract method: open/establish this connection |
|
abstract method: disconnect/close this connection |
|
abstract method: send a text string through this connection |
|
abstract method: receive whatever is currently in the buffer |
|
abstract method: high-level api to execute a command and collect its full return |
|
abstract method: configure the device through this connection |
** abstract methods are to be implemented in user's subclass.
Hint
do not confuse ConnectionManager.connect()
and connection class’
YourConnection.connect()
: the former is a factory class creating new
connection instances; the latter is the action that opens/establishes
the actual connection.
Sample Implementation¶
The following is a pseudo-code implementation of a connection class cooked up under 10 minutes. The idea is to walk you through how writing your own connection class looks & feels like.
# Example
# -------
#
# a very rudimentry implementation of telnet connection
# using python's built-in library
# to handle telnet connections client
import telnetlib
from pyats.connections import BaseConnection
class TelnetConnection(BaseConnection):
'''TelnetConnection
Sample implementation of Telnet connection to linux, based on pyATS
BaseConnection, allowing devices to telnet to end routers
'''
def __init__(self, *args, **kwargs):
'''__init__
instantiate a single connection instance.
'''
# instantiate parent BaseConnection
super().__init__(*args, **kwargs)
# create an instance of telnetlib.Telnet
self._telnet = telnetlib.Telnet()
# let's hard code the expected prompt
# (and assume it's a bash shell prompt)
self._prompt = 'bash$ '
def connect(self):
'''connect
opens the telnet connection and log us in.
'''
# open the telnet session
# self.connection_info is inherited from BaseConnection
self._telnet.open(host = self.connection_info['ip'],
port = self.connection_info['port'])
# process login
self._telnet.read_until(b"login: ")
# send the login name
self.send(self.device.tacacs['username'])
# process password
self._telnet.read_until(b"password: ")
self.send(self.device.passwords['tacacs'])
# find the prompt
self._telnet.read_until(self._prompt.encode('ascii'))
def send(self, text):
'''send
low-level api: sends raw text through telnet session.
'''
# remember to convert string to bytes
return self._telnet.write(text.encode('ascii') + '\n')
def receive(self):
'''receive
low-level api: reads from the telnet session and returns whatever is
currently in the buffer
'''
# remember to convert back from bytes to string
return self._telnet.read_eager().decode('utf-8')
def execute(self, command):
'''execute
high-level api: sends a command through the session, expect it to
be executed, and return back to prompt.
'''
# send the command
self.send(command)
# expect the prompt
output = self._telnet.read_until(self._prompt.encode('ascii'))
# convert the output to string
output = output.decode('utf-8')
# remove the telnet echo of our original command
output.lstrp(command + '\n')
# we're done!
return output
def configure(self, *args, **kwargs):
raise NotImplementedError('just configure using execute api!')
The code above is very rudimentry, but hopefully gives you a basic idea as to how connection classes work, and how it should be implemented.
Word of advice: don’t try to enhance this for production use. I can think of 42 ways it could go wrong, and 47 unhandled corner cases that will cause your script to hang. (Hmm, this makes a great interview question).
“For everything, there is a first time.” - Spock