Getting Started with Python Network Libraries for Network Engineers – Part 4

Blog Detail

This post is the fourth in a series exploring Python libraries that are commonly used for interacting with network devices. The previous entries in this series covered libraries for directly interacting with network devices. Libraries such as netmikoNAPALM, and Scrapli greatly ease the process of connecting to, and configuring, a network device. This blog will examine Nornir, an automation framework for orchestrating tasks across an entire inventory of devices.

What Is Nornir, and How Does It Compare to Other Automation Frameworks?

Nornir is an automation framework built to address the same use cases as other popular automation frameworks, such as Ansible, Salt, or Puppet. It exists to allow the user to interact with many devices, at-scale, in a programmatic fashion.

  • Nornir’s workflow revolves around the definition of tasks and identifying which hosts in your inventory a task should be executed against.
  • Nornir is not an opinionated framework. It makes no assumptions around what kind of tasks you want to automate, how to connect to a device, configuration data-models, or any of the myriad implementation details that comprise a comprehensive network automation solution. There are many existing libraries to solve these problems, and these libraries can be called by Nornir during task execution. Nornir remains focused on the issue of task management and is meant to be a single pillar in the construction of a personalized system. Nornir does the scaling; what processes you actually run in your environment are for you to define.
  • Nornir has no domain-specific language. It is written, configured, and executed as pure Python code. While a DSL like Ansible’s may be simpler to learn, initially, DSLs can become cumbersome and complex when dealing with complicated logic or when attempting to extend functionality. Without a non-Python abstraction layer you have access to all of its data structures, flow control, and third-party libraries.

If you are familiar with Python web frameworks, you may notice the different philosophies of Ansible and Nornir are similar to the differences between Django and Flask, with very similar pros and cons.

Installation

You can install Nornir via pip:

pip install nornir

Or, if you are using Poetry for dependency management:

poetry add nornir

Nornir is supported by the community in the form of plugins. Plugins provide inventory integrations, predefined tasks, and other tools to simplify development. This demo will be using the nornir-scrapli plugin in order to utilize the popular Scrapli library for connecting to network devices, and Scrapli’s support for Genie to parse device output into structured data.

pip install nornir-scrapli
pip install 'pyats[library]'
poetry add nornir-scrapli
poetry add 'pyats[library]'

Note: This demo will be using the Cisco DevNet Sandbox as target for the example code. Visit Cisco DevNet to sign up and follow along for free.

Configuration and Inventory

Before Nornir can connect to any devices, it must be configured and an inventory must be defined. The “Simple Inventory” plugin is perfect for this demo due to the small size of our test inventory, and it comes built into Nornir. It uses .yaml files to define the host, group, and default configurations. More specific configuration settings override more generic ones, with default configurations being overwritten first.

# /config.yaml

---
inventory:
  plugin: SimpleInventory
  options:
    host_file: "inventory/hosts.yaml"
    group_file: "inventory/groups.yaml"
    defaults_file: "inventory/defaults.yaml"

runner:
  plugin: threaded
  options:
    num_workers: 3

Strict SSH key checking must be disabled since the public sandbox devices of the DevNet lab have unknown SSH keys. This specific bit of config is unique to Scrapli, and more info about its options can be found in the scrapli-nornir documentation:

# /inventory/defaults.yaml

connection_options:
  scrapli:
    extras:
      auth_strict_key: False

Three groups have been configured below, one for each different network OS in the sandbox lab. Each device has a unique set of credentials, although those have been redacted here. The credentials are provided when connecting to the DevNet lab and are subject to change at any time. The naming convention of the groups is arbitrary. You can create any number of groups to allow hosts to inherit any kind of data. Hosts can belong to any number of groups.

The platform key is a configuration option that Nornir uses to pass OS information to transport plugins for driver and parser selection. This can be defined at any point in the inventory configuration or defined programmatically:

# /inventory/groups.yaml

nxos_devices:
  username: #REDACTED
  password: #REDACTED
  platform: cisco_nxos

iosxr_devices:
  username: #REDACTED
  password: #REDACTED
  platform: cisco_iosxr

iosxe_devices:
  username: #REDACTED
  password: #REDACTED
  platform: cisco_iosxe

Please note that it is bad practice to place credentials directly into code. It is one of the most common causes of accidentally exposed secrets.

Finally, the individual hosts must be created in the inventory. The top-level key is an arbitrary name used to uniquely identify a single host, and the hostname key is the network address Nornir will pass to any tasks that require connectivity. The groups key is a list of group names of which each host is a member. The information under the data key is special and can be any arbitrary structured data. It allows for per-host custom fields for any kind of configuration, identification, or contextual data that may be helpful in targeting or orchestration.

# /inventory/hosts.yaml

switch1:
  hostname: sandbox-nxos-1.cisco.com
  groups:
    - nxos_devices
  data:
    site: "cloud"
    role: "butcher"

router1:
  hostname: sandbox-iosxr-1.cisco.com
  groups:
    - iosxr_devices
  data:
    site: "sandbox"
    role: "baker"

router2:
  hostname: sandbox-iosxe-latest-1.cisco.com
  groups:
    - iosxe_devices
  data:
    site: "sandbox"
    role: "butcher"

Initializing Nornir

Let’s create a new Python script to truly get started. The following code instantiates a new Nornir object with a copy of the configured inventory.

# /demo.py

from nornir import InitNornir
nr = InitNornir("config.yaml")

Note: It is possible to programmatically configure Nornir, instead of using externally imported .yaml files, or to combine both methods. See Nornir’s documentation for more details.

# /demo.py

from nornir import InitNornir
nr = InitNornir(
    runner={
        "plugin": "threaded",
        "options": {
            "num_workers": 3,
        },
    },
    inventory={
        "plugin": "SimpleInventory",
        "options": {
            "host_file": "inventory/hosts.yaml",
            "group_file": "inventory/groups.yaml",
            "defaults_file": "inventory/defaults.yaml"
        },
    },
)

Filtering the Inventory

Once the Nornir object is instantiated, you can access the configured inventory data as object properties.

nr.inventory.hosts and nr.inventory.groups are dictionary-like objects that can be iterated over to see all of the hosts or groups in the inventory.

>>> nr.inventory.hosts.keys()
dict_keys(['switch1', 'router1', 'router2'])

>>> nr.inventory.groups.keys()
dict_keys(['nxos_devices', 'iosxr_devices', 'iosxe_devices'])

Iterating over the inventory can be useful, but Nornir implements a system of filters that allow a subset of hosts to be efficiently targeted. The following filter works to target all hosts with the “butcher” role:

>>> nr.filter(role="butcher").inventory.hosts
{'switch1': Host: switch1, 'router2': Host: router2}

This filter identifies all of the hosts with their ‘site’ set to sandbox:

>>> nr.filter(site="sandbox").inventory.hosts.keys()
dict_keys(['router1', 'router2'])

Filters can be combined to narrow down search results:

>>> filter1 = nr.filter(site="sandbox")
..: filter1.filter(role="butcher").inventory.hosts
{'router2': Host: router2}

Filtering in Nornir is a deep subject. Please check the Nornir documentation for more advanced topics and other methods of filtering the Nornir inventory.

Executing Tasks

Now that Nornir is initialized and its inventory can be filtered, tasks can be executed against those hosts. This first example is using a predefined task provided by the Scrapli plugin to retrieve the results of show interface status brief, and having a built-in utility plugin from nornir_utils display that result in an easy-to-read manner.

# /demo.py

from nornir import InitNornir
from nornir_utils.plugins.functions import print_result
from nornir_scrapli.tasks import send_command

nr = InitNornir("config.yaml")

result = nr.run(task=send_command, command="show ip interface brief")
print_result(result)

Running the script will produce the following output, which has been truncated for brevity:

send_command********************************************************************
* router1 ** changed : False ***************************************************
vvvv send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
Wed Jul 13 18:13:38.817 UTC

Interface                      IP-Address      Status          Protocol Vrf-Name
BVI227                         unassigned      Shutdown        Down     default
BVI511                         10.200.188.33   Down            Down     default
Bundle-Ether10                 unassigned      Down            Down     default
Loopback0                      2.2.2.2         Up              Up       default
^^^^ END send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* router2 ** changed : False ***************************************************
vvvv send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES NVRAM  up                    up
GigabitEthernet2       unassigned      YES NVRAM  administratively down down
Loopback11             unassigned      YES unset  up                    up
Loopback111            10.10.30.1      YES manual up                    up
Tunnel0                10.10.30.1      YES TFTP   up                    up
^^^^ END send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* switch1 ** changed : False ***************************************************
vvvv send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
IP Interface Status for VRF "default"(1)
Interface            IP Address      Interface Status
Vlan10               192.168.10.1    protocol-down/link-down/admin-down
Vlan100              172.16.100.1    protocol-down/link-down/admin-down
Lo1                  1.1.1.1         protocol-down/link-down/admin-up
Lo2                  2.2.2.2         protocol-up/link-up/admin-up
Eth1/3               192.168.1.10    protocol-down/link-down/admin-down
Eth1/5               172.16.1.1      protocol-down/link-down/admin-down
^^^^ END send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

If you prepend the run method with one of the filters from earlier, this command can be run against a subset of hosts. The following modification of the demo.py script will target only hosts with the baker role. (Output is, again, truncated for brevity.)

result = nr.filter(role="baker").run(
    task=send_command, command="show ip interface brief"
)
print_result(result)
send_command********************************************************************
* router1 ** changed : False ***************************************************
vvvv send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
Wed Jul 13 18:44:47.061 UTC

Interface                      IP-Address      Status          Protocol Vrf-Name
BVI227                         unassigned      Shutdown        Down     default
BVI511                         10.200.188.33   Down            Down     default
Bundle-Ether10                 unassigned      Down            Down     default
Loopback0                      2.2.2.2         Up              Up       default
Loopback1                      2.2.2.20        Up              Down     default
tunnel-ip99                    1.1.1.1         Down            Down     default
MgmtEth0/RP0/CPU0/0            10.10.20.175    Up              Up       default
GigabitEthernet0/0/0/0         unassigned      Down            Down     default
GigabitEthernet0/0/0/0.100     10.1.1.1        Down            Down     default
^^^^ END send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Note: These are publicly accessible sandbox devices. Their configuration and output is subject to constant change.

Defining Custom Tasks

task is a reusable chunk of code that implements logic to be run on a single host. Every task is a function that takes a Task object as its first argument and returns a Result object. Community plugins integrating popular network libraries such as Netmiko and Napalm provide many useful prebuilt tasks, and it is simple to create your own, or to extend existing tasks.

# /demo_2.py

from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result

nr = InitNornir("config.yaml")

def hello_world_task(task: Task) -> Result:
    """
    Your first custom Nornir task!
    """
    return Result(host=task.host, result=f"{task.host.name} says hello!")

result = nr.filter(site="sandbox").run(task=hello_world_task)
print_result(result)
hello_world_task****************************************************************
* router1 ** changed : False ***************************************************
vvvv hello_world_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
router1 says hello!
^^^^ END hello_world_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* router2 ** changed : False ***************************************************
vvvv hello_world_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
router2 says hello!
^^^^ END hello_world_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Tasks can call other tasks or manipulate the data in the Nornir object’s inventory. Scrapli can use pyATS Genie to parse device output into a Python dictionary, which can then be associated with the host the data was gathered from. Thanks to the task argument passed to the custom task function, it has access to the inventory data of the targeted hosts.

# /demo_3.py

from pprint import pprint
from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_scrapli.tasks import send_command

nr = InitNornir("config.yaml")

# This task populates the inventory of the Nornir object with
# the host's "show interface status" data.
def populate_host_interface_status(task: Task) -> None:
    """
    Get the live interface status from a device.
    Transform the response into structured data associated with each host.
    """
    result = task.run(task=send_command, command="show ip interface brief")
    task.host["interface_status"] = result.scrapli_response.genie_parse_output()


nr.run(task=populate_host_interface_status)
router2 = nr.inventory.hosts["router2"]
pprint(router2["interface_status"])

Output:

{'interface': {'GigabitEthernet1': {'interface_is_ok': 'YES',
                                    'ip_address': '10.10.20.48',
                                    'method': 'NVRAM',
                                    'protocol': 'up',
                                    'status': 'up'},
               'GigabitEthernet2': {'interface_is_ok': 'YES',
                                    'ip_address': '10.255.255.1',
                                    'method': 'other',
                                    'protocol': 'up',
                                    'status': 'up'},
               'GigabitEthernet3': {'interface_is_ok': 'YES',
                                    'ip_address': 'unassigned',
                                    'method': 'NVRAM',
                                    'protocol': 'down',
                                    'status': 'administratively down'},
               'Loopback100': {'interface_is_ok': 'YES',
                               'ip_address': '172.16.100.1',
                               'method': 'other',
                               'protocol': 'up',
                               'status': 'up'},
               'Loopback11': {'interface_is_ok': 'YES',
                              'ip_address': 'unassigned',
                              'method': 'unset',
                              'protocol': 'up',
                              'status': 'up'},
               'Loopback111': {'interface_is_ok': 'YES',
                               'ip_address': '10.10.30.1',
                               'method': 'manual',
                               'protocol': 'up',
                               'status': 'up'},
               'Loopback21': {'interface_is_ok': 'YES',
                              'ip_address': 'unassigned',
                              'method': 'unset',
                              'protocol': 'up',
                              'status': 'up'},
               'Tunnel0': {'interface_is_ok': 'YES',
                           'ip_address': '10.10.30.1',
                           'method': 'TFTP',
                           'protocol': 'up',
                           'status': 'up'}}}

Conclusion

Nornir is a powerful framework due to its focus and simplicity. It is an excellent option for teams who do not intend to use the breadth of functionality from a more batteries-included framework, teams with a strong foundation in Python programming, or teams looking to integrate an automation framework into an existing Python ecosystem. Make sure to check out the full Nornir documentation and list of Nornir plugins from the community, and don’t forget Network to Code’s Nornir channel on Slack.

– Boabdil

New to Python libraries? NTC’s Training Academy is holding a 3-day course Automating Networks with Python I on September 26-28, 2022 with 50% labs to get you up to speed.
Visit our 2022 public course schedule to see our full list.



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Getting Started with Python Network Libraries for Network Engineers – Part 3

Blog Detail

This blog post is the third in a series covering common Python libraries that can be used to interact with network devices. In this post we will cover the Scrapli Python library by Carl Montanari. Per its documentation, Scrapli is the words “scrape” and “cli” (i.e. screen scrape) squished together. Its goal is to provide a thoroughly tested, well typed, well documented, simple API that supports both synchronous and asynchronous interaction with network devices.

Differentiation from Other Libraries

Scrapli is different from other libraries in the following ways:

  1. Scrapli provides multiple forms of transport. It defaults to using a system’s local SSH binary. This is in contrast to Netmiko, which uses Paramiko for transport. The forms of transport it supports can be found here along with a justification for the decision to allow for multiple transports and default to a system’s local SSH binary.
  2. Scrapli supports the same platforms as NAPALM out of the box. This is a subset of the platforms supported by Netmiko. The scrapli community project provides the ability for the scrapli community to contribute to and use drivers beyond those included in the base project. The platforms supported out of the box are:
    • Cisco IOS-XE
    • Cisco NX-OS
    • Juniper JunOS
    • Cisco IOS-XR
    • Arista EOS
  3. If you’re looking to write unit/integration tests for your code, scrapli has an adjacent library called scrapli-replay that can be used to do so.

Installation

You can install Scrapli via pip install scrapli. Or, if you are using Poetry, you can use poetry add scrapli. Other methods for installing scrapli are documented here.

Getting Connected

Note: We will only cover connecting via SSH in this blog post.

To start, you’ll want to import and instantiate one of the scrapli “core” drivers from scrapli.driver.core.

Scrapli requires a hostname (IP or DNS name) and an authentication method (generally username and password) to get connected. In addition to this, you will also want to specify whether or not to use SSH strict host key checking (we pass in False below).

>>> from scrapli.driver.core import IOSXEDriver
>>>
>>> conn = IOSXEDriver(
...     host="192.0.2.3",
...     auth_username="cisco",
...     auth_password="cisco",
...     auth_strict_key=False,
... )
>>> type(conn)
scrapli.driver.core.cisco_iosxe.sync_driver.IOSXEDriver
>>> conn.open()

Note: If host strict key checking is enabled (the default), an SSH session will be permitted only to hosts that have a key defined inside of your system’s “known_hosts” file.

You can also use the Scrapli class to dynamically select and instantiate a driver (much like ConnectHandler in Netmiko) in the following way:

>>> from scrapli import Scrapli
>>>
>>> conn = Scrapli(
...     host="192.0.2.3",
...     auth_username="cisco",
...     auth_password="cisco",
...     auth_strict_key=False,
...     platform="cisco_iosxe",
...  )
>>> type(conn)
scrapli.driver.core.cisco_iosxe.sync_driver.IOSXEDriver
>>> conn.open()

A list of the basic driver arguments can be found here.

Sending Commands

Once you have instantiated your Driver object, you can send single show commands via the .send_command() method. You will use the same command syntax you would type in if you were directly connected to the device via SSH:

>>> response = conn.send_command("show ip interface brief")
>>> print(response.result)
Interface              IP-Address      OK? Method Status                Protocol
FastEthernet0          unassigned      YES NVRAM  down                  down
GigabitEthernet1/0/1   192.0.2.3       YES unset  up                    up
GigabitEthernet1/0/2   unassigned      YES unset  up                    up
GigabitEthernet1/0/3   unassigned      YES unset  up                    up
>>> print(response.failed)
False

Likewise, you can send multiple commands using the .send_commands() method.

>>> response = conn.send_commands(["show ip interface brief", "show running-config | include hostname"])
>>> print(response.result[0])
Interface              IP-Address      OK? Method Status                Protocol
FastEthernet0          unassigned      YES NVRAM  down                  down
GigabitEthernet1/0/1   192.0.2.3       YES unset  up                    up
GigabitEthernet1/0/2   unassigned      YES unset  up                    up
GigabitEthernet1/0/3   unassigned      YES unset  up                    up
>>> print(response.result[1])
hostname cisco

You can also send commands from a file using the .send_commands_from_file() method.

# commands.txt
show ip int br
show run | i hostname
response = conn.send_commands_from_file("commands.txt")
>>> print(response.result[0])
Interface              IP-Address      OK? Method Status                Protocol
FastEthernet0          unassigned      YES NVRAM  down                  down
GigabitEthernet1/0/1   192.0.2.3       YES unset  up                    up
GigabitEthernet1/0/2   unassigned      YES unset  up                    up
GigabitEthernet1/0/3   unassigned      YES unset  up                    up
>>> print(response.result[1])
hostname cisco

If you want to run a command to edit the configuration, you would use the .send_configs() method instead. This method takes care of entering configuration mode for you, and it requires the commands be in a list or set:

>>> response = conn.send_configs(["interface Gi1/0/3", "no description"])
>>> print(response.result)
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.
cisco(config)#interface Gi1/0/3
cisco(config-if)#no description

Note: The send_configs() method doesn’t exit config mode like Netmiko does. It, instead, relies on the send_command() method to acquire the user exec mode prompt.

As is the case with .send_commands(), you can also send configurations from a file using the send_configs_from_file() method.

# config_changes.txt
interface Gi1/0/3
 no description
>>> response = conn.send_configs_from_file("config_changes.txt")
>>> print(response.result)
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.
cisco(config)#interface Gi1/0/3
cisco(config-if)#no description

Finally, there is a send_config() method that will parse the configuration provided as input and split it into lines, each line being sent to the device. This is useful for copying full configs over to the device.

Command Output Parsing

We’ve covered parsing strategies here on this blog before. Just as with Netmiko, Scrapli supports TextFSMTTP, and Genie. Let’s take a quick look on how to use them with Scrapli.

TextFSM

Scrapli defines TextFSM as an “extra”. As such, the first step to using TextFSM templates with scrapli is to install the TextFSM library. You can use pip or Poetry to do so.

pip install 'scrapli[textfsm]'
poetry add 'scrapli[textfsm]'

Once TextFSM is installed, you can use the .textfsm_parse_output() method on a Scrapli response object to return the output as parsed by the NTC template. This method uses the platform inferred by the driver to set the textfsm-platform. It combines this with the command sent to the device to guess as to which template it should use to parse the returned data.

>>> response = conn.send_command("show interfaces")
>>> print(response.textfsm_parse_output())
[{'abort': '',
  'address': '381c.1ae6.cd81',
  'bandwidth': '100000 Kbit',
  'bia': '381c.1ae6.cd81',
  'crc': '0',
  'delay': '100 usec',
...

You can see the template used for this command here.

Scrapli also supports defining the template that should be used to parse the response manually. You can import and use the textfsm_parse function from scrapli.helper to do so.

>>> from scrapli.helper import textfsm_parse
>>>
>>> response = conn.send_command("show interfaces")
>>> structured_result = textfsm_parse("/path/to/template", response.result)

TTP

Scrapli also supports the TTP parsing library. As with TextFSM, you will need to install it via pip install ttp or poetry add ttp before it can be used. The TTP installation does not currently include any templates, so you will need to find or create your own and provide the path to those templates.

>>> response = conn.send_command("show interfaces")
>>> structured_result = response.ttp_parse_output(template="show_interfaces_template.ttp")
>>> pprint(structured_result)
[[[{'description': 'CAM1',
    'interface': 'GigabitEthernet1/0/1',
    'link_status': 'up',
    'protocol_status': 'up'},
   {'description': 'CAM2',
    'interface': 'GigabitEthernet1/0/2',
    'link_status': 'up',
    'protocol_status': 'up'},
...

Genie

The last parser that Scrapli currently supports is Genie. As with TextFSM and TTP, scrapli does not install Genie nor its required library, pyATS, by default, so you will need to install them separately via pip install 'pyats[library]' or poetry add 'pyats[library]'. Athough Genie parsers exist for non-Cisco platforms, In Scrapli, Genie parsers can be used only for Cisco devices.

>>> response = conn.send_command("show interfaces")
>>> structured_result = response.genie_parse_output()
>>> pprint(structured_result)
{'GigabitEthernet1/0/1': {'arp_timeout': '04:00:00',
                          'arp_type': 'arpa',
                          'bandwidth': 100000,
                          'connected': True,
                          'counters': {'in_broadcast_pkts': 41240,
...

Note: Genie does not support custom templates.

Next Steps

  • Perhaps you’d like to use asyncio for interactions with network devices, or learn more advanced concepts? The documentation is excellent, and a great place to start.
  • If you’d like more examples of common interactions using scrapli, Carl has created a directory with such examples here.
  • Scrapli has an ecosystem including a few adjacent tools that are worth checking out (scrapli_netconf, scrapli_nornir…etc.).

Conclusion

I hope you enjoy Scrapli. It’s an excellent tool and is my personal go-to tool for new projects in which I need to automate interactions with devices for which it has a driver.

-Phillip

New to Python libraries? NTC’s Training Academy is holding a 3-day course Automating Networks with Python I on September 26-28, 2022 with 50% labs to get you up to speed.
Visit our 2022 public course schedule to see our full list.



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!