- Setup and Preparation
- Libraries to Work with Data
- Libraries to Work with APIs
- Other Cool Python Stuff
This lab was written to be run using the DevNet Devbox Sandbox. This sandbox is a basic CentOS 7 workstation with typical development tools and software installed. Specifically used in this lab are Python 3.6 and Vagrant (used to instantiate an IOS XE router for use in the labs.)
If you are doing this lab on your own, you'll need to reserve an instance of this sandbox before beginning. If you are doing this as part of a guided workshop, the instructor will assign you a pod.
-
Using either AnyConnect or OpenConnect, establish a VPN to your pod.
-
SSH to the Devbox at IP
10.10.20.20
using credentialsroot / cisco123
ssh root@10.10.20.20
-
Install the Python 3.6 development libraries.
yum install -y python36u-devel
-
Add the IOS XE 16.9 Vagrant Box to your workstation. Instructions for creating the Box file are available on github at github.com/hpreston/vagrant_net_prog. If you are completing this as part of a guided lab, the instructor will provide details on how to complete this step.
vagrant box add --name iosxe/16.09.01 serial-csr1000v-universalk9.16.09.01.box
-
Clone the code samples to the devbox from GitHub and change into the directory.
git clone https://github.com/hpreston/python_networking cd python_networking
-
Create a Python 3.6 virtual environment and install Python libraries for exercises.
python3.6 -m venv venv source venv/bin/activate pip install -r requirements.txt
-
Start and baseline the IOS XE Vagrant environment.
vagrant up # After it completes python vagrant_device_setup.py
Exercises in this section are intended to be executed from an interactive Python interpreter.
iPython has been installed as part of the requirements.txt installation and is one option. You can start an iPython window by simply typing ipython
. For each step in the list below, type the specified command (or commands) and then press enter until the iPython prompt goes to the next step, and/or shows the expected output (ie. print or pprint commands).
Other options could be just python
or idle
.
-
From the root of the
python_networking
repository, change into the exercise directory.cd data_manipulation/xml
-
Start an interactive Python interpreter. Example below:
# ipython Python 3.6.5 (default, Apr 10 2018, 17:08:37) Type 'copyright', 'credits' or 'license' for more information IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. In [1]:
-
Import the xmltodict library
import xmltodict
-
Open the sample xml file and read it into variable
with open("xml_example.xml") as f: xml_example = f.read()
-
Print the raw XML data
print(xml_example)
-
Parse the XML into a Python (Ordered) dictionary
xml_dict = xmltodict.parse(xml_example)
-
Pretty Print the Python Dictionary Object
from pprint import pprint pprint(xml_dict)
-
Save the interface name into a variable using XML nodes as keys
int_name = xml_dict["interface"]["name"]
-
Print the interface name
print(int_name)
-
Change the IP address of the interface
xml_dict["interface"]["ipv4"]["address"]["ip"] = "192.168.0.2"
-
Check that the IP address has been changed in the dictionary
pprint(xml_dict)
-
Revert to the XML string version of the dictionary
print(xmltodict.unparse(xml_dict))
-
After you've completed exploring, exit the interpreter.
exit()
-
From the root of the
python_networking
repository, change into the exercise directory.cd data_manipulation/json
-
Start an interactive Python interpreter. Example below:
# ipython Python 3.6.5 (default, Apr 10 2018, 17:08:37) Type 'copyright', 'credits' or 'license' for more information IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. In [1]:
-
Import the jsontodict library
import json
-
Open the sample json file and read it into variable
with open("json_example.json") as f: json_example = f.read()
-
Print the raw json data
print(json_example)
-
Parse the json into a Python dictionary
json_dict = json.loads(json_example)
-
Pretty Print the Python Dictionary Object
from pprint import pprint pprint(json_dict)
-
Save the interface name into a variable
int_name = json_dict["interface"]["name"]
-
Print the interface name
print(int_name)
-
Change the IP address of the interface
json_dict["interface"]["ipv4"]["address"][0]["ip"] = "192.168.0.2"
-
Check that the IP address has been changed in the dictionary
pprint(json_dict)
-
Revert to the json string version of the dictionary
print(json.dumps(json_dict))
-
After you've completed exploring, exit the interpreter.
exit()
-
From the root of the
python_networking
repository, change into the exercise directory.cd data_manipulation/yaml
-
Start an interactive Python interpreter. Example below:
# ipython Python 3.6.5 (default, Apr 10 2018, 17:08:37) Type 'copyright', 'credits' or 'license' for more information IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. In [1]:
-
Import the yamltodict library
import yaml
-
Open the sample yaml file and read it into variable
with open("yaml_example.yaml") as f: yaml_example = f.read()
-
Print the raw yaml data
print(yaml_example)
-
Parse the yaml into a Python dictionary
yaml_dict = yaml.load(yaml_example)
-
Pretty Print the Python Dictionary Object
from pprint import pprint pprint(yaml_dict)
-
Save the interface name into a variable
int_name = yaml_dict["interface"]["name"]
-
Print the interface name
print(int_name)
-
Change the IP address of the interface
yaml_dict["interface"]["ipv4"]["address"][0]["ip"] = "192.168.0.2"
-
Check that the IP address has been changed in the dictionary
pprint(yaml_dict)
-
Revert to the yaml string version of the dictionary
print(yaml.dump(yaml_dict, default_flow_style=False))
-
After you've completed exploring, exit the interpreter.
exit()
-
From the root of the
python_networking
repository, change into the exercise directory.cd data_manipulation/csv
-
Start an interactive Python interpreter. Example below:
# ipython Python 3.6.5 (default, Apr 10 2018, 17:08:37) Type 'copyright', 'credits' or 'license' for more information IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. In [1]:
-
Import the csv library
import csv
-
Open the sample csv file and print it to screen
with open("csv_example.csv") as f: print(f.read())
-
Open the sample csv file, and create a csv.reader object
with open("csv_example.csv") as f: csv_python = csv.reader(f) # Loop over each row in csv and leverage the data in code for row in csv_python: print("{device} is in {location} " \ "and has IP {ip}.".format( device = row[0], location = row[2], ip = row[1] ) )
-
Create a new tuple for additional router.
router4 = ("router4", "10.4.0.1", "Chicago")
-
Add new router to CSV file.
with open("csv_example.csv", "a") as f: csv_writer = csv.writer(f) csv_writer.writerow(router4)
-
Re-read and print out the CSV content.
with open("csv_example.csv") as f: print(f.read())
-
After you've completed exploring, exit the interpreter.
exit()
-
From the root of the
python_networking
repository, change into the exercise directory.cd data_manipulation/yang
-
Print the YANG module in a simple text tree
pyang -f tree ietf-interfaces.yang
-
Print only part of the tree
pyang -f tree --tree-path=/interfaces/interface \ ietf-interfaces.yang
-
Print an example XML skeleton (NETCONF)
pyang -f sample-xml-skeleton ietf-interfaces.yang
-
Create an HTTP/JS view of the YANG Model (no output expected in the CLI)
pyang -f jstree -o ietf-interfaces.html \ ietf-interfaces.yang
-
Optional: Open
ietf-interfaces.html
in a web browser. Will need to RDP into the Devbox to do this step. -
Control the "nested depth" in trees
pyang -f tree --tree-depth=2 ietf-ip.yang
-
Display a full module.
pyang -f tree \ ietf-ip.yang
-
Include deviation models in the processing
pyang -f tree \ --deviation-module=cisco-xe-ietf-ip-deviation.yang \ ietf-ip.yang
Exercises in this section are intended to be executed from an interactive Python interpreter.
iPython has been installed as part of the requirements.txt installation and is one option. You can start an iPython window by simply typing ipython
.
Other options could be just python
or idle
.
Each exercise also includes a Python script file that can be executed directly.
-
From the root of the
python_networking
repository, change into the exercise directory.cd device_apis/rest
-
Start an interactive Python interpreter. Example below:
# ipython Python 3.6.5 (default, Apr 10 2018, 17:08:37) Type 'copyright', 'credits' or 'license' for more information IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. In [1]:
-
Import libraries
import requests, urllib3 import sys
-
Add parent directory to path to allow importing common vars
sys.path.append("..") from device_info import vagrant_iosxe as device
-
Disable Self-Signed Cert warning for demo
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
Setup base variable for request
restconf_headers = {"Accept": "application/yang-data+json"} restconf_base = "https://{ip}:{port}/restconf/data" interface_url = restconf_base + "/ietf-interfaces:interfaces/interface={int_name}"
-
Create URL GigE2 Config
url = interface_url.format(ip = device["address"], port = device["restconf_port"], int_name = "GigabitEthernet2" )
-
Check the complete URL you just composed
print(url)
-
Send RESTCONF request to core1 for GigE2 Config
r = requests.get(url, headers = restconf_headers, auth=(device["username"], device["password"]), verify=False)
-
Print returned data
print(r.text)
-
If REST call was successful, report interesting details.
if r.status_code == 200: # Process JSON data into Python Dictionary and use interface = r.json()["ietf-interfaces:interface"] print("The interface {name} has ip address {ip}/{mask}".format( name = interface["name"], ip = interface["ietf-ip:ipv4"]["address"][0]["ip"], mask = interface["ietf-ip:ipv4"]["address"][0]["netmask"], ) ) else: print("No interface {} found.".format("GigabitEthernet2"))
-
Continuing from previous exercise. If starting from new interpreter, execute these steps.
import requests, urllib3, sys sys.path.append("..") from device_info import vagrant_iosxe as device urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) restconf_headers = {"Accept": "application/yang-data+json"} restconf_base = "https://{ip}:{port}/restconf/data" interface_url = restconf_base + "/ietf-interfaces:interfaces/interface={int_name}"
-
Add additional
Content-Type
header.restconf_headers["Content-Type"] = "application/yang-data+json"
-
Create dictionary with details on a new loopback interface.
loopback = {"name": "Loopback101", "description": "Demo interface by RESTCONF", "ip": "192.168.101.1", "netmask": "255.255.255.0"}
-
Setup data body to create new loopback interface
data = { "ietf-interfaces:interface": { "name": loopback["name"], "description": loopback["description"], "type": "iana-if-type:softwareLoopback", "enabled": True, "ietf-ip:ipv4": { "address": [ { "ip": loopback["ip"], "netmask": loopback["netmask"] } ] } } }
-
Create URL
url = interface_url.format(ip = device["address"], port = device["restconf_port"], int_name = loopback["name"] )
-
Check the complete URL you just composed
print(url)
-
Send RESTCONF request to device
r = requests.put(url, headers = restconf_headers, auth=(device["username"], device["password"]), json = data, verify=False)
-
Check Status Code (expected
201
)print("Request Status Code: {}".format(r.status_code))
-
Query for details on the new interface you just created
# Create URL and send RESTCONF request to core1 for GigE2 Config url = interface_url.format(ip = device["address"], port = device["restconf_port"], int_name = "Loopback101" ) r = requests.get(url, headers = restconf_headers, auth=(device["username"], device["password"]), verify=False) # Print returned data print(r.text)
-
Continuing from previous exercise. If starting from new interpreter, execute these steps.
import requests, urllib3, sys sys.path.append("..") from device_info import vagrant_iosxe as device urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) restconf_headers = {"Accept": "application/yang-data+json"} restconf_base = "https://{ip}:{port}/restconf/data" interface_url = restconf_base + "/ietf-interfaces:interfaces/interface={int_name}" url = interface_url.format(ip = device["address"], port = device["restconf_port"], int_name = "Loopback101" )
-
Send DELETE request to remove the Loopback.
r = requests.delete(url, headers = restconf_headers, auth=(device["username"], device["password"]), verify=False)
-
Check Status Code (expected
204
)print("Request Status Code: {}".format(r.status_code))
-
Query for details on the new interface (no output expected, as you just deleted it)
r = requests.get(url, headers = restconf_headers, auth=(device["username"], device["password"]), verify=False)
-
Check status code (expected
404
)print(r.status_code)
-
From the root of the
python_networking
repository, change into the exercise directory.cd device_apis/netconf
-
Start an interactive Python interpreter. Example below:
# ipython Python 3.6.5 (default, Apr 10 2018, 17:08:37) Type 'copyright', 'credits' or 'license' for more information IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. In [1]:
-
Import libraries
from ncclient import manager from xml.dom import minidom import xmltodict import sys
-
Add parent directory to path to allow importing common vars
sys.path.append("..") from device_info import vagrant_iosxe as device
-
Create filter template for an interface
interface_filter = """ <filter> <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"> <interface> <name>{int_name}</name> </interface> </interfaces> </filter> """
-
Open NETCONF connection to device
- Note: Normally you'd use a
with
block to open connection to device. This avoids needing to manuallym.close_session()
at the end of a script, but for interactive use, this format is chosen.
m = manager.connect(host = device["address"], port = device["netconf_port"], username = device["username"], password = device["password"], hostkey_verify = False)
- Note: Normally you'd use a
-
Verify NETCONF connection is active (expected output
true
)m.connected
-
Create desired NETCONF filter for a particular interface
filter = interface_filter.format(int_name = "GigabitEthernet2")
-
Execute a NETCONF using the filter
r = m.get_config("running", filter)
-
Pretty print raw xml to screen
xml_doc = minidom.parseString(r.xml) print(xml_doc.toprettyxml(indent = " "))
-
Process the XML data into Python Dictionary and use
interface = xmltodict.parse(r.xml)
-
Pretty Print the full Python (Ordered) Dictionary.
from pprint import pprint pprint(interface)
-
If RPC returned data, print out the interesting pieces.
if not interface["rpc-reply"]["data"] is None: # Create Python variable for interface details interface = interface["rpc-reply"]["data"]["interfaces"]["interface"] print("The interface {name} has ip address {ip}/{mask}".format( name = interface["name"]["#text"], ip = interface["ipv4"]["address"]["ip"], mask = interface["ipv4"]["address"]["netmask"], ) ) else: print("No interface {} found".format("GigabitEthernet2"))
-
Continuing from previous exercise. If starting from new interpreter, execute these steps.
from ncclient import manager from xml.dom import minidom import xmltodict import sys sys.path.append("..") from device_info import vagrant_iosxe as device interface_filter = """ <filter> <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"> <interface> <name>{int_name}</name> </interface> </interfaces> </filter> """ m = manager.connect(host = device["address"], port = device["netconf_port"], username = device["username"], password = device["password"], hostkey_verify = False)
-
Verify NETCONF connection is active
m.connected
-
Create Python dictionary with new Loopback Details
loopback = {"int_name": "Loopback102", "description": "Demo interface by NETCONF", "ip": "192.168.102.1", "netmask": "255.255.255.0"}
-
Create NETCONF template for an interface
config_data = """ <config> <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"> <interface> <name>{int_name}</name> <description>{description}</description> <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type"> ianaift:softwareLoopback </type> <enabled>true</enabled> <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"> <address> <ip>{ip}</ip> <netmask>{netmask}</netmask> </address> </ipv4> </interface> </interfaces> </config> """
-
Create desired NETCONF config payload
config = config_data.format(**loopback)
-
Send operation
r = m.edit_config(target = "running", config = config)
-
Print OK status (expected output
true
)print("NETCONF RPC OK: {}".format(r.ok))
-
Create a new NETCONF to check on new loopback interface
filter = interface_filter.format(int_name = "Loopback102")
-
Execute a NETCONF using this filter
r = m.get_config("running", filter)
-
Pretty print the raw XML to screen
xml_doc = minidom.parseString(r.xml) print(xml_doc.toprettyxml(indent = " "))
-
Continuing from previous exercise. If starting from new interpreter, execute these steps.
from ncclient import manager from xml.dom import minidom import xmltodict import sys sys.path.append("..") from device_info import vagrant_iosxe as device interface_filter = """ <filter> <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"> <interface> <name>{int_name}</name> </interface> </interfaces> </filter> """ loopback = {"int_name": "Loopback102", "description": "Demo interface by NETCONF", "ip": "192.168.102.1", "netmask": "255.255.255.0"} m = manager.connect(host = device["address"], port = device["netconf_port"], username = device["username"], password = device["password"], hostkey_verify = False)
-
Verify NETCONF connection is active
m.connected
-
Create new config template to delete an interface
config_data = """ <config> <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"> <interface operation="delete"> <name>{int_name}</name> </interface> </interfaces> </config> """
-
Create desired NETCONF config payload and execute to delete the interface
config = config_data.format(**loopback) r = m.edit_config(target = "running", config = config)
-
Print OK status (expected output
true
)print("NETCONF RPC OK: {}".format(r.ok))
-
Create a new NETCONF to check on new loopback interface
filter = interface_filter.format(int_name = "Loopback102")
-
Execute a NETCONF using this filter
r = m.get_config("running", filter)
-
Pretty print the raw XML to screen (expected output will not include the loopback interface, as you just deleted it)
xml_doc = minidom.parseString(r.xml) print(xml_doc.toprettyxml(indent = " "))
-
Send a RPC request to disconnect the connection.
m.close_session() m.connected
-
End the Python interpreter.
exit()
-
From the root of the
python_networking
repository, change into the exercise directory.cd device_apis/cli
-
Start an interactive Python interpreter. Example below:
# ipython Python 3.6.5 (default, Apr 10 2018, 17:08:37) Type 'copyright', 'credits' or 'license' for more information IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. In [1]:
-
Import libraries
from netmiko import ConnectHandler import re import sys
-
Add parent directory to path to allow importing common vars
sys.path.append("..") from device_info import vagrant_iosxe as device
-
Set device_type for netmiko
device["device_type"] = "cisco_ios"
-
Create a CLI command template
show_interface_config_temp = "show running-config interface {}"
-
Open CLI connection to device.
- Note: Normally you'd use a with block to open connection to device. This avoids needing to manually
m.close_session()
at the end of a script, but for interactive use, this format is chosen.
ch = ConnectHandler(ip = device["address"], port = device["ssh_port"], username = device["username"], password = device["password"], device_type = device["device_type"])
- Note: Normally you'd use a with block to open connection to device. This avoids needing to manually
-
Create desired CLI command
command = show_interface_config_temp.format("GigabitEthernet2")
-
Verify the command has been created correctly
print(command)
-
Send command to device
interface = ch.send_command(command)
-
Print the raw command output to the screen
print(interface)
-
Create regular expression searches to parse the output for desired interface details
name = re.search(r'interface (.*)', interface).group(1) description = re.search(r'description (.*)', interface).group(1)
-
Pull out the ip and mask for the interface
ip_info = re.search(r'ip address (.*) (.*)', interface) ip = ip_info.group(1) netmask = ip_info.group(2)
-
Print the desired info to the screen
print("The interface {name} has ip address {ip}/{mask}".format( name = name, ip = ip, mask = netmask, ) )
-
Continuing from previous exercise. If starting from new interpreter, execute these steps.
from netmiko import ConnectHandler import re, sys sys.path.append("..") from device_info import vagrant_iosxe as device device["device_type"] = "cisco_ios" show_interface_config_temp = "show running-config interface {}" ch = ConnectHandler(ip = device["address"], port = device["ssh_port"], username = device["username"], password = device["password"], device_type = device["device_type"])
-
Create Python dictionary with new Loopback Details
loopback = {"int_name": "Loopback103", "description": "Demo interface by CLI and netmiko", "ip": "192.168.103.1", "netmask": "255.255.255.0"}
-
Create a CLI configuration
interface_config = [ "interface {}".format(loopback["int_name"]), "description {}".format(loopback["description"]), "ip address {} {}".format(loopback["ip"], loopback["netmask"]), "no shut" ]
-
Send configuration to device
output = ch.send_config_set(interface_config)
-
Print the raw command output to the screen
print("The following configuration was sent: ") print(output)
-
Create a CLI command to retrieve the new configuration.
command = show_interface_config_temp.format("Loopback103") interface = ch.send_command(command) print(interface)
-
Continuing from previous exercise. If starting from new interpreter, execute these steps.
from netmiko import ConnectHandler import re, sys sys.path.append("..") from device_info import vagrant_iosxe as device device["device_type"] = "cisco_ios" show_interface_config_temp = "show running-config interface {}" ch = ConnectHandler(ip = device["address"], port = device["ssh_port"], username = device["username"], password = device["password"], device_type = device["device_type"])
-
Create a new CLI configuration to delete the interface.
interface_config = [ "no interface {}".format(loopback["int_name"]) ]
-
Send configuration to device
output = ch.send_config_set(interface_config)
-
Print the raw command output to the screen
print("The following configuration was sent: ") print(output)
-
Create a CLI command to verify configuration removed.
command = show_interface_config_temp.format("Loopback103") interface = ch.send_command(command) print(interface)
Note: attempting to view the configuration of a non-existing interface will generate a CLI error. This output is expected, and one of the reasons APIs like NETCONF or RESTCONF are better suited to programmatic interactions.
-
Disconnect from the device.
ch.disconnect()
-
End the Python interpreter.
exit()
pyATS is a network testing tool developed by Cisco and made available for free, with significant elements of the underlying code open source.
pyATS offers network developers the ability to profile the network state of hardware, interfaces, protocols, etc... before, during and after changes, to ensure the network is operating as designed, and identify problems before the dreaded phone call. To enable this level of robust testing, pyATS offers a standard way to communicate with network elements and standardize the data returned into native Python objects. This core functionality opens up a lot of flexibility on how pyATS can be used by network developers.
In the following exercises, you will get a brief introduction to pyATS to connect and learn about device details.
-
From the root of the
python_networking
repository, change into the exercise directory.cd network_testing/pyats
-
Start an interactive Python interpreter. Example below:
# ipython Python 3.6.5 (default, Apr 10 2018, 17:08:37) Type 'copyright', 'credits' or 'license' for more information IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. In [1]:
-
Import in pyATS libraries and tools
from genie.conf import Genie from ats.topology import loader from genie.abstract import Lookup from genie.libs import ops # noqa
-
Read and process the testbed (inventory) file
genie_testbed = Genie.init("./default_testbed.yaml")
-
Create a pyATS device object from testbed
vagrant_iosxe1 = genie_testbed.devices["vagrant-iosxe1"]
-
Connect to the device
vagrant_iosxe1.connect()
- pyATS establishes a connection to the device
-
Create an abstract device to standardize Python API and code for platform
vagrant_iosxe1_abstract = Lookup.from_device(vagrant_iosxe1)
-
Using the abstract device, learn about the Interfaces on the end device
vagrant_iosxe1_interfaces = vagrant_iosxe1_abstract.ops.interface.interface.Interface(vagrant_iosxe1) vagrant_iosxe1_interfaces.learn()
-
Print out the interface details that were learned
vagrant_iosxe1_interfaces.info
-
Display a single interface from the device
vagrant_iosxe1_interfaces.info["GigabitEthernet1"]
-
Print the mac address for the interface
vagrant_iosxe1_interfaces.info["GigabitEthernet1"]["mac_address"]
-
Notice that there was no parsing of command line output needed to access this data
-
Execute a command on the device and print the output
print(vagrant_iosxe1.execute("show version"))
-
Or store the output into a variable
version = vagrant_iosxe1.execute("show version")
-
Send a configuration command to the device
vagrant_iosxe1.configure("ntp server 10.10.10.10")
-
Create a configuration command list and send to the device
config_loopback = [ "interface Loopback201", "description Configured by pyATS", "ip address 172.16.201.1 255.255.255.0", "no shut" ] vagrant_iosxe1.configure(config_loopback)
-
Re-learn the interfaces
vagrant_iosxe1_interfaces = vagrant_iosxe1_abstract.ops.interface.interface.Interface(vagrant_iosxe1) vagrant_iosxe1_interfaces.learn()
-
Get details about new interface
vagrant_iosxe1_interfaces.info["Loopback201"]
-
Disconnect from the devices
vagrant_iosxe1.disconnect()