Using Topology Objects¶
As previously indicated, topology objects are interconnected together via object attributes & property references (pointers), representing the physical testbed topology. This section provides more details on how this can be done.
Device Connection Manager¶
Device
class natively only contains the meta-data and the interconnect
information related to testbed device. For example, device connections
attribute is simply a dictionary describing the many ways to connect to the
actual device it represent. With the help of ConnectionManager
class,
Device
objects are effectively transformed into a compound object that both
handles the above, as well as the actual connections (Eg. telnet/ssh).
Each Device
object is instantiated with a connectionmgr
attribute that
contains an instance of ConnectionManager
, and contains all of the current
active connections to the managed device.
# Example
# -------
#
# connecting to device
# using the sample topology file from
import os
from pyats import topology
testbedfile = os.path.join(os.path.dirname(topology.__file__),
'sampleTestbed.yaml')
testbed = topology.loader.load(testbedfile)
# pick a device
n7k5 = testbed.devices['ott-tb1-n7k5']
# connect to it by calling connect()
# this is actually a ConnectionManager method. the compound object
# redirects the call to Device.connectionmgr.connect(), and:
# - defaults to using connection alias "default"
# - and creates the connection
#
# in effect, this is the same as calling
# n7k5.connectionmgr.connect()
n7k5.connect == n7k5.connectionmgr.connect
# True
n7k5.connect()
n7k5.is_connected()
# True
# since the connection above is aliased with default
# we can make calls to it directly.
n7k5.default
# ott-tb1-n7k5(default)
n7k5.default.execute('show clock')
# 00:56:54.569 EST Sat Mar 07 2015
# default connections are also shortcut to the device level, so users
# don't always have to type the default alias
n7k5.execute == n7k5.default.execute == n7k5.connectionmgr.default.execute
# True
n7k5.execute('show clock')
# 00:58:27.069 EST Sat Mar 07 2015
# the connection manager is capable of handling multiple connections
# each connection is referred to via its alias, and the connection object
# is again compounded to be callable directly under the device object.
# if supported, you can always create extra connections to
# the same connection type, as long as this is supported by the device
# creating a new connection to the alt (mgmt) connection definition
# and calling it 'mgmt'. After connecting, you can make calls to it.
n7k5.connect(alias = 'mgmt', via = 'alt')
n7k5.mgmt.execute('show version')
# ... etc
To summarize this behavior:
device connections are handled by a
ConnectionManager
instance, underdevice.connectionmgr
attributedevice attributes are compounded with attributes of this connection manager. Eg. calling
device.connect()
is the same as callingdevice.connectionmgr.connect()
, and calling each connection usingdevice.<alias>.execute()
is the same asdevice.connectionmgr.<alias>.execute()
For more details of the functionality of ConnectionManager
instances, refer
to Connections Library documentation.
Connect To All Devices¶
Testbed
object also provides a convenience function, Testbed.connect()
,
allowing you to establish asynchronous connection to multiple testbed devices
at the same time.
# Example
# -------
#
# connecting to devices in parallel
# using the sample topology file from
from pyats import topology
testbedfile = os.path.join('sampleTestbed.yaml')
testbed = topology.loader.load(testbedfile)
# connect to all devices in this testbed
testbed.connect()
# connect to some devices in this testbed
testbed.connect(testbed.devices['uut'],
testbed.devices['helper'])
# connect to some devices in this testbed
# and provide unique vias
testbed.connect(testbed.devices['uut'],
testbed.devices['helper'],
vias = {'uut': 'cli',
'helper': 'console'})
# connect to some devices in this testbed
# using unique vias per device, and shared kwargs (eg, log_stdout = False)
# shared keyword-arguments will be passed to every single connection
testbed.connect(testbed.devices['uut'],
testbed.devices['helper'],
vias = {'uut': 'cli',
'helper': 'console'},
log_stdout = False)
This is a convenience function, as under the hood it uses threads perform
per device .connect()
asynchronously. All other behavior follows that of
the above connection manager concept.
Disconnect From All Devices¶
Testbed
object provides a convenience function, Testbed.disconnect()
,
allowing you to make asynchronous disconnection from multiple testbed devices
at the same time.
# Example
# -------
#
# disconnecting from devices in parallel
# using the sample topology file from
from pyats import topology
testbed = topology.loader.load('your-testbed-file.yaml')
# connect to all devices in this testbed
testbed.connect()
# disconnect from all devices in this testbed
testbed.disconnect()
# disconnect from some devices in this testbed
testbed.disconnect(testbed.devices['uut'],
testbed.devices['helper'])
# disconnect from some devices in this testbed
# and provide unique vias
testbed.disconnect(testbed.devices['uut'],
testbed.devices['helper'],
vias = {'uut': 'cli',
'helper': 'console'})
# disconnect from some devices in this testbed
# using unique vias per device, and shared kwargs
# shared keyword-arguments will be passed to every single connection
testbed.disconnect(testbed.devices['uut'],
testbed.devices['helper'],
vias = {'uut': 'cli',
'helper': 'console'},
log_stdout = False)
This is a convenience function using threads under the hood to perform per
device .disconnect()
asynchronously.
After disconnecting from devices, the connection objects will remain,
and can be checked by command .conectionmgr.connections
, the same objects
will be used to connect to device again.
Destroy connections to all device¶
Testbed
object provides Testbed.destroy()
function, allowing you
to destroy connections asynchronously to multiple testbed devices. Unlike
.disconnect()
, if .destroy()
is used, then all connection objects
will be completely removed.
# Example
# -------
#
# destroy devices in parallel
# using the sample topology file from
from pyats import topology
testbed = topology.loader.load('your-testbed-file.yaml')
# connect to all devices in this testbed
testbed.connect()
# destroy all devices in this testbed
testbed.destroy()
# destroy some devices in this testbed
testbed.destroy(testbed.devices['uut'],
testbed.devices['helper'])
# destroy some devices in this testbed
# and provide unique vias
testbed.destroy(testbed.devices['uut'],
testbed.devices['helper'],
vias = {'uut': 'cli',
'helper': 'console'})
# destroy some devices in this testbed
# using unique vias per device, and shared kwargs
# shared keyword-arguments will be passed to every single connection
testbed.destroy(testbed.devices['uut'],
testbed.devices['helper'],
vias = {'uut': 'cli',
'helper': 'console'},
log_stdout = False)
This is a convenience function using threads under the hood to perform
per device .destroy()
asynchronously.
Querying Topology¶
The basic concept is simple:
Testbed
contains one or moreDevice
Device
contains zero or moreInterface
Interface
may be connected to aLink
Link
connects one or moreInterface
together.
It may be useful to refer to Everything is an Object page for detailed object attributes and how everything is tailored together.
# Example
# -------
#
# topology querying
import os
# import the topology module
from pyats import topology
# load the sample testbed supplied as part of topology module
# (example testbed file page for reference)
testbedfile = os.path.join(os.path.dirname(topology.__file__),
'sampleTestbed.yaml')
testbed = topology.loader.load(testbedfile)
# confirming that this is indeed a testbed object
type(testbed) is topology.Testbed
# True
# check that our expected devices are part of the testbed
'ott-tb1-n7k4' in testbed and 'ott-tb1-n7k5' in testbed
# True
# access the actual device object from Testbed.devices attribute
testbed.devices
# AttrDict({'ott-tb1-n7k4': <Device ott-tb1-n7k4 at 0xf77190cc>,
# 'ott-tb1-n7k5': <Device ott-tb1-n7k5 at 0xf744e16c>})
# see how many links this testbed contains:
for link in testbed.links:
print(repr(link))
# <Link rtr1-rtr2-1 at 0xf744d16c>
# <Link rtr1-rtr2-2 at 0xf744ef8c>
# <Link ethernet-1 at 0xf744ee8c>
# <Link ethernet-2 at 0xf744efac>
# grab both device
n7k4 = testbed.devices['ott-tb1-n7k4']
n7k5 = testbed.devices['ott-tb1-n7k5']
# confirm that this is a Device object
type(n7k4) is Device and type(n7k5) is Device
# True
# note that you can check whether devices exists in a testbed
# by using either the device object or its name
n7k4 in testbed and n7k5 in testbed
# True
# find the links connecting n7k5 from n7k4
# using Device.find_links()
for link in n7k4.find_links(n7k5):
print(repr(link))
# <Link rtr1-rtr2-2 at 0xf744ef8c>
# <Link rtr1-rtr2-1 at 0xf744d16c>
# loop through interfaces and find a interface that connects
# to a particular link
for intf in n7k4:
if n7k5 in intf.remote_devices and intf.link.name == 'rtr1-rtr2-2':
break
else:
intf = None
# the entire topology is chained by attributes
intf.link.interfaces
# WeakList([<Interface Ethernet4/2 at 0xf744eeac>,
# <Interface Ethernet5/2 at 0xf744d74c>])
intf.link.interfaces[1].device
# <Device ott-tb1-n7k5 at 0xf744e16c>
intf.link.interfaces[1].device.testbed
# <pyats.topology.testbed.Testbed object at 0xf76b5f0c>
intf.link.interfaces[1].device is n7k5
# True
# and all properties are computed on the fly
n7k4.find_links(n7k4) & set(testbed.links)
# <Link ethernet-1 at 0xf744ee8c>
# <Link ethernet-2 at 0xf744efac>
Device & Interface Aliases¶
Every topology object is a subclass of TopologyObject
base class: each one
comes with its own name
(mandatory) and alias
(optional, defaults to
name
).
# Example
# -------
#
# Topology objects have names and aliases
from pyats import topology
# testbed object example
testbed = Testbed('myTestbed')
testbed.name
# myTestbed
testbed.alias
# myTestbed
# Link object example
link = Link('myLink', alias = 'newLink')
link.name
# myLink
link.alias
# newLink
In an effort to abstract out hostnames/interfaces and allow users to reference
to testbed objects using their aliases (indirect access), Testbed.devices
and Device.interfaces
object attributes have been rigged to allow accesses
using aliases as an added feature.
# Example
# -------
#
# topology alias access
# continuing to use the same sample topology file from
# the previous example
import os
from pyats import topology
testbedfile = os.path.join(os.path.dirname(topology.__file__),
'sampleTestbed.yaml')
testbed = topology.loader.load(testbedfile)
# testbed has alias
testbed.alias
# topologySampleTestbed
# testbed devices have aliases
testbed.devices['ott-tb1-n7k4'].alias
# device-1
testbed.devices['ott-tb1-n7k5'].alias
# device-2
# you can refer to devices within a testbed using its alias name instead
# of the actual device name. this yields the same device object
testbed.devices['device-1'] is testbed.devices['ott-tb1-n7k4']
testbed.devices['device-2'] is testbed.devices['ott-tb1-n7k5']
device_1 = testbed.devices['device-1']
device_2 = testbed.devices['device-2']
# if the device name or alias is a valid python name,
# the object can be accessed via attribute syntax
device1 = testbed.devices.device1
device1 = testbed.devices.alias1
# device in testbed check also works using alias
'device-1' in testbed.devices
# True
# testbed device interfaces also have aliases
device_1.interfaces['Ethernet4/1'].alias
# device1-intf1
device_1.interfaces['Ethernet4/2'].alias
# device1-intf2
# same as devices in testbed, interface access in device can be
# done using their aliases, including in operator
device_1.interfaces['device1-intf1'] is device_1.interfaces['Ethernet4/1']
True
device_1.interfaces['device1-intf2'] is device_1.interfaces['Ethernet4/2']
True
'device1-intf1' in device_1.interfaces
True
# if the interface name or alias is a valid python name,
# the object can be accessed via attribute syntax
device_1.interfaces.Eth0
device_1.interfaces.alias_eth0
# in addition, to test if something is an alias or not, use is_alias()
device_1.interfaces.is_alias('device1-intf1')
# True
testbed.devices.is_alias('ott-tb1-n7k4')
# False
Thus, as long as the testscript does not hard-code device and interface names, and instead refers to them using aliases, the script would remain agnostic, and run on any similarly configured testbeds with the same topology.
Tip
Device and interface objects can be accessed via attribute syntax if the name
or alias is a valid python name. For example: testbed.devices.uut
instead of
testbed.devices['uut']
.
Note
Link
and Testbed
also have aliases, but since they are not stored as
MutableMappings
like Testbed.devices
and Device.interfaces
, they
need to be accessed directly and tested for their alias instead. Example,
if intf.link.alias == 'linkAlias'
.
Add, Modify & Delete¶
Testbed objects are mutable and non-singletons. This means that at anytime, you can modify their attributes & connection properties as needed. Keep in mind that the following rules still apply:
Testbed device names must be unique (within the testbed)
Testbed link names must be unique (within the testbed)
Device interface names must be unique (within the device)
As well, because the topology is represented by physical relationships, their contained objects move with them. Eg, if you move an interface object from one device to another, the link that is connected to that interface moves with it. Ditto for devices and their interfaces, etc.
# Example
# -------
#
# topology adding/deleting and modifying
# continuing to use the same sample topology file from
# the first example
import os
from pyats import topology
testbedfile = os.path.join(os.path.dirname(topology.__file__),
'sampleTestbed.yaml')
testbed = topology.loader.load(testbedfile)
# add a new device
# note - could also do this with
# topology.Device('myNewDevice', testbed = testbed)
new_device = topology.Device('myNewDevice')
testbed.add_device(new_device)
testbed.devices
# AttrDict({'ott-tb1-n7k5': <Device ott-tb1-n7k5 at 0xf76990cc>,
# 'ott-tb1-n7k4': <Device ott-tb1-n7k4 at 0xf73ce2ac>,
# 'myNewDevice': <Device myNewDevice at 0xf74aa78c>})
# modify a testbed alias
testbed.alias = 'newAlias'
# add new interfaces (to existing device)
n7k4 = testbed.devices['ott-tb1-n7k4']
interface = topology.Interface('Ethernet9/40', 'ethernet')
interface.link = topology.Link('newLink')
n7k4.add_interface(interface)
n7k4.interfaces
# AttrDict({'Ethernet9/40': <Interface Ethernet9/40 at 0xf73ce98c>,
# 'Ethernet4/45': <Interface Ethernet4/45 at 0xf73ce28c>,
# 'Ethernet4/46': <Interface Ethernet4/46 at 0xf73ce36c>,
# 'Ethernet4/2': <Interface Ethernet4/2 at 0xf73ce24c>,
# 'Ethernet4/6': <Interface Ethernet4/6 at 0xf73ce18c>,
# 'Ethernet4/7': <Interface Ethernet4/7 at 0xf73ce2ec>,
# 'Ethernet4/1': <Interface Ethernet4/1 at 0xf73ce34c>})
# modify device information
n7k4.custom['newCustomInfo'] = 'new information that did not exist before'
# removing interfaces
n7k5 = testbed.devices['ott-tb1-n7k5']
n7k5.remove_interface('Ethernet5/1')
# now the number of connections changed:
for link in n7k4.find_links(n7k5):
print(repr(link))
# <Link rtr1-rtr2-2 at 0xf73ce0ec>
# let's create a new testbed and move n7k5 over to it.
new_testbed = topology.Testbed('newTestbed')
n7k5.testbed = new_testbed
# notice how everything changed over
n7k4.interfaces['Ethernet4/2'].link.interfaces[0].device
# <Device ott-tb1-n7k5 at 0xf76990cc>
n7k4.interfaces['Ethernet4/2'].link.interfaces[0].device.testbed.name
# 'newTestbed'
# since now link rtr1-rtr2-2 connects 2 testbeds, it is contained in
# both sides
link = n7k4.interfaces['Ethernet4/2'].link
link in testbed.links and link in new_testbed.links
# True
# let's squeeze a topology
# (reduce a topology to a wanted list of devices and/or links,
# aliases are respected, interfaces not connected to wanted links
# are removed):
testbed = topology.loader.load(testbedfile)
testbed.squeeze('device-1', 'rtr1-rtr2-1', extend_devices_from_links=True)
[device.name for device in testbed]
# ['ott-tb1-n7k4', 'ott-tb1-n7k5']
[link.name for link in testbed.links]
# ['rtr1-rtr2-1']
[interface.name for interface in testbed.devices['device-1']]
# ['Ethernet4/1']
[interface.name for interface in testbed.devices['device-2']]
# ['Ethernet5/1']
The above example may be elaborate (involving new testbeds), but is only used
as an example to show how everything works together. Attribute collection (such
as links
) is a combined iterative computation of parent/child relationships,
as shown above.
The simplest way to think about this relationship is to visualize it: if you move a linecard from one device to another without disconnecting the cables first, then the cables would follow through and be connected to interfaces of that linecard on the new parent device. The same applies with topology objects.
References and Weak References¶
topology
module is designed to avoid circular object references (eg, devices
referring to parent testbed and testbed containing devices).
Testbed
containsDevice
Device
refer to parent testbed as a weak reference.
Device
containInterface
Interface
refer to parent device as a weak reference
Interface
containsLink
Link
refer to their connectedInterface
as weak references
# Example
# -------
#
# demonstration of where weak references apply
# import objects
from pyats.topology import Testbed, Device, Interface, Link
# create the objects
testbed = Testbed('exampleTestbed')
device = Device('exampleDevice')
interface = Interface('exampleInterface', 'ethernet')
link = Link('exampleLink')
# hook up the relationship
testbed.add_device(device)
device.add_interface(interface)
link.connect_interface(interface)
# testbed contains devices as actual references
testbed.devices
# AttrDict({'exampleDevice': <Device exampleDevice at 0xf763c70c>})
# device.testbed is internally stored as a weak reference to testbed,
# even though it returns the actual testbed object
testbed.device
# <pyats.topology.testbed.Testbed object at 0xf763c6ac>
# device.interfaces contains interfaces as actual references
device.interfaces
# AttrDict({'exampleInterface': <Interface exampleInterface at 0xf763c74c>})
# interface.device is internally stored as a weak reference
# even though it returns the actual device object
interface.device
# <Device exampleDevice at 0xf763c70c>
# interface connects to links as an actual reference
interface.link
# <Link exampleLink at 0xf763ca8c>
# links contain only weak references to interfaces
# even though when you access it, it returns actual objects
link.interfaces
WeakList([<Interface exampleInterface at 0xf763c74c>])
link.interfaces[0]
# <Interface exampleInterface at 0xf763c74c>
Therefore, all rules of python garbage collection apply. For example, if you
dereference a device object entirely, all of its interface objects will be gone.
However, the only exception is Link
objects: if more than one interface
connects to the same link, then unless all of those interfaces are removed, the
link will continue to exist.
# Example
# -------
#
# weak ref deletions
# using the sample topology file from
import os
from pyats import topology
testbedfile = os.path.join(os.path.dirname(topology.__file__),
'sampleTestbed.yaml')
testbed = topology.loader.load(testbedfile)
# let's see how many devices and links we started with
testbed.devices
# AttrDict({'ott-tb1-n7k4': <Device ott-tb1-n7k4 at 0xf73f712c>,
# 'ott-tb1-n7k5': <Device ott-tb1-n7k5 at 0xf765ef0c>})
testbed.links
# {<Link rtr1-rtr2-2 at 0xf73fa58c>,
# <Link ethernet-2 at 0xf73f8fac>,
# <Link ethernet-1 at 0xf73f8b6c>,
# <Link rtr1-rtr2-1 at 0xf73fa4ec>}
# now remove a device
testbed.devices.pop('ott-tb1-n7k4')
# look at devices and lists again in the testbed. it's all gone
testbed.devices
# AttrDict({'ott-tb1-n7k5': <Device ott-tb1-n7k5 at 0xf765ef0c>})
testbed.links
# {<Link rtr1-rtr2-2 at 0xf73fa58c>,
# <Link rtr1-rtr2-1 at 0xf73fa4ec>}
Tip
if it’s not cleaned up, something’s holding it up. Likely you’ve stored a reference to that object or to its parent object somewhere else.
Tip
read up on python garbage collection.