Control network devices through code — Vendor agnostic way

Gopi Narayanaswamy
11 min readDec 6, 2019

Network Automation

Software Defined Network one of the buzz word in the industry, automating network tasks was tedious task earlier, now there are plenty of solution available for controlling and orchestrating the Data, Voice and Security devices

How to control network devices through code

Network devices are controlled through many programming languages. Python scripts are trending in network automation and have a look at what we can do with that language in network automation and why

Python programming language can be used to automate manual tasks by writing simple scripts. It is a convenient tool for the server and networking devices for managing tasks and configurations. Python is also used for interacting with SDN (Software Defined Networking), managing multiple networking devices and utilizing APIs.

The Python interpreter helps the network engineer to make his own scripts to manage the routine tasks and easily configure networking assets within an organization.

Why python?

Other programming languages also can do the same stuff what python can do logically but Python has some really good and advance libraries that make network programming easier and make a script for multiple networking devices

These libraries make easier to access and control vendor agnostic environments by simple scripts

Benefits of Python for Network automation:

Extensive Support Libraries: It provides a large range of standard python libraries that covers different types of domain. Most of the commonly used programming tasks are already scripted into it that limits the length of the codes to be written in Python.

Integration Feature: Python integrates the different types of Enterprise Application Integration that makes it easy to develop different services for different applications. It has powerful control capabilities as it calls directly through C, C++ or Java and other programming languages as well via Python. Python also use to configure XML and other markup languages as it can run on all modern operating systems through same byte code.

Improved Programmer’s Productivity: The language has clean object-oriented designs and extensive support libraries that increase programmer’s productivity.

Productivity: With its strong process with integration features, enhanced control capabilities and unit testing framework contribute the role in increasing speed for most applications and productivity of applications. It is a great option for building scalable, reliable and multi-protocol network applications.

Python modules for network automation: Netmiko and Paramiko

At its core, network programmability and automation have the main goal of simplifying the tasks involved in configuring, managing and operating network equipment, network topologies, network services and network connectivity.

Both Netmiko and Paramiko modules are using SSH connection to get the control of devices. SSH (Secure Shell) is a cryptographic (encryption and decryption) network protocol work on 22 port number for operating network services securely over an unsecured network.

Netmiko Example

In this example, netmiko library called through python import syntax. Then the device details stored in dictionary, ConnectHandler is a class from netmiko and establish a connection using the dictionary

By default send_command() will try to find the current prompt using the find_prompt() method. In other words, Netmiko will attempt to find the existing prompt before the command is sent.

Netmiko will then use this current prompt as the default indicator that the command is done (i.e. when it sees that same prompt string again in the output, then it knows it is done).

Other useful Libraries used for Network Automation

NAPALM

NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor support) is a Python library that implements a set of functions to interact with different network device Operating Systems using a unified API.

NAPALM supports several methods to connect to the devices, to manipulate configurations or to retrieve data.

Supported Network Operating Systems:

· Arista EOS

· Cisco IOS

· Cisco IOS-XR

· Cisco NX-OS

· Juniper JunOS

Load Device Configuration

This script loads the device configuration and prints it.

It’s a basic example, how to use the NAPALM library:

With get_network_driver(device_type) you load the driver suitable for the device type. Then you create a device handle by driver(hostname_or_IP, user, password) and open it, all in one with statement. Within that with block you execute your task.

#!/usr/bin/python3
#
# load device configuration
#
import sys
import json
from napalm import get_network_driver

def die(*msg_list):
“”” abort program with error message “””
error_msg = ‘ ‘.join(str(x) for x in msg_list)
sys.exit(error_msg.rstrip(“\n\r”))

# get command line parameter
if len(sys.argv) != 2:
die(“Usage: get_conf hostname”)
hostname = sys.argv[1]

# load device parameter database
try:
with open(“devices”, “r”) as f:
dev_db = json.load(f)
except (ValueError, IOError, OSError) as err:
die(“Could not read the ‘devices’ file:”, err)

# get device parameter
try:
dev_param = dev_db[hostname.lower()]
except KeyError:
die(“Unknown device ‘{}’”.format(hostname))

# load device driver
driver = get_network_driver(dev_param[‘type’])

# load config
with driver(dev_param[‘IP’], dev_param[‘user’], dev_param[‘password’]) as device:
conf = device.get_config()
print(conf[‘running’])

Textfsm

Textfsm is a text parsing library written in python to turn plain text into structured data. Originally created by Google.

Extract interesting data from the output of the show interface command on a Cisco ASA using TextFSM

# textfsm template
Value Required INTERFACE (\S+)
Value INTERFACE_ZONE (.+?)
Value LINK_STATUS (\w+)
Value PROTOCOL_STATUS (.*)
Value HARDWARE_TYPE ([\w ]+)
Value BANDWIDTH (\d+\s+\w+)
Value DELAY (\d+\s+\w+)
Value DUPLEX (\w+\-\w+)
Value SPEED (\d+\w+\s\w+)
Value DESCRIPTION (.*)
Value ADDRESS ([a-zA-Z0-9]+.[a-zA-Z0-9]+.[a-zA-Z0-9]+)
Value MTU (\d+)
Value IP_ADDRESS (\d+\.\d+\.\d+\.\d+)
Value NET_MASK (\d+\.\d+\.\d+\.\d+)
Value ONEMIN_IN_PPS (\d+)
Value ONEMIN_IN_RATE (\d+)
Value ONEMIN_OUT_PPS (\d+)
Value ONEMIN_OUT_RATE (\d+)
Value ONEMIN_DROP_RATE (\d+)
Value FIVEMIN_IN_PPS (\d+)
Value FIVEMIN_IN_RATE (\d+)
Value FIVEMIN_OUT_PPS (\d+)
Value FIVEMIN_OUT_RATE (\d+)
Value FIVEMIN_DROP_RATE (\d+)
Start
^.*Interface ${INTERFACE} "${INTERFACE_ZONE}", is ${LINK_STATUS}.*protocol is ${PROTOCOL_STATUS}
^\s+Hardware is ${HARDWARE_TYPE} -> Continue
^.*BW ${BANDWIDTH}.*DLY ${DELAY}
^.*\(${DUPLEX}.*Auto-Speed\(${SPEED}\)
^.*Description: ${DESCRIPTION}
^.*MAC address ${ADDRESS}.*MTU ${MTU}
^.*IP address ${IP_ADDRESS}, .*subnet mask ${NET_MASK}
^.*1 minute input rate ${ONEMIN_IN_PPS} pkts/sec,\s+${ONEMIN_IN_RATE} bytes/sec
^.*1 minute output rate ${ONEMIN_OUT_PPS} pkts/sec,\s+${ONEMIN_OUT_RATE} bytes/sec
^.*1 minute drop rate, ${ONEMIN_DROP_RATE}
^.*5 minute input rate ${FIVEMIN_IN_PPS} pkts/sec,\s+${FIVEMIN_IN_RATE} bytes/sec
^.*5 minute output rate ${FIVEMIN_OUT_PPS} pkts/sec,\s+${FIVEMIN_OUT_RATE} bytes/sec
^.*5 minute drop rate, ${FIVEMIN_DROP_RATE} -> Record

Using python interpreter, create an interfaces variable with the output of the show interface command from a Cisco ASA.

# output of show interface
interfaces = ‘’’
Interface GigabitEthernet0/0 “inside”, is up, line protocol is up
Hardware is i82540EM rev02, BW 1000 Mbps, DLY 10 usec
Auto-Duplex(Full-duplex), Auto-Speed(1000 Mbps)
Input flow control is unsupported, output flow control is off
MAC address 0800.2735.03c6, MTU 1500
IP address 169.254.1.11, subnet mask 255.255.255.0
0 packets input, 0 bytes, 0 no buffer
Received 0 broadcasts, 0 runts, 0 giants
0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored, 0 abort
0 pause input, 0 resume input
0 L2 decode drops
1 packets output, 60 bytes, 0 underruns
0 pause output, 0 resume output
0 output errors, 0 collisions, 1 interface resets
0 late collisions, 0 deferred
0 input reset drops, 0 output reset drops
input queue (blocks free curr/low): hardware (511/511)
output queue (blocks free curr/low): hardware (511/510)
Traffic Statistics for “inside”:
0 packets input, 0 bytes
1 packets output, 28 bytes
0 packets dropped
1 minute input rate 0 pkts/sec, 0 bytes/sec
1 minute output rate 0 pkts/sec, 0 bytes/sec
1 minute drop rate, 0 pkts/sec
5 minute input rate 0 pkts/sec, 0 bytes/sec
5 minute output rate 0 pkts/sec, 0 bytes/sec
5 minute drop rate, 0 pkts/sec
Interface Management0/0 “management”, is up, line protocol is up
Hardware is i82540EM rev02, BW 1000 Mbps, DLY 10 usec
Auto-Duplex(Full-duplex), Auto-Speed(1000 Mbps)
Input flow control is unsupported, output flow control is off
MAC address 0800.277d.ea42, MTU 1500
IP address 10.0.2.15, subnet mask 255.255.255.0
1086 packets input, 83965 bytes, 0 no buffer
Received 2 broadcasts, 0 runts, 0 giants
0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored, 0 abort
0 pause input, 0 resume input
0 L2 decode drops
1071 packets output, 130571 bytes, 0 underruns
0 pause output, 0 resume output
0 output errors, 0 collisions, 1 interface resets
0 late collisions, 0 deferred
0 input reset drops, 0 output reset drops
input queue (blocks free curr/low): hardware (497/458)
output queue (blocks free curr/low): hardware (510/506)
Traffic Statistics for “management”:
1085 packets input, 62837 bytes
1071 packets output, 114891 bytes
2 packets dropped
1 minute input rate 16 pkts/sec, 905 bytes/sec
1 minute output rate 16 pkts/sec, 1762 bytes/sec
1 minute drop rate, 0 pkts/sec
5 minute input rate 0 pkts/sec, 0 bytes/sec
5 minute output rate 0 pkts/sec, 0 bytes/sec
5 minute drop rate, 0 pkts/sec
‘’’
Below program extract the required information

# import library
import textfsm

# open the template file
with open(‘cisco_asa_show_interface.template’, ‘r’) as f:
template = textfsm.TextFSM(f)

# run the interface data through the template parser
template.ParseText(interfaces)

# output
[[‘GigabitEthernet0/0’,
‘inside’,
‘up’,
‘up’,
‘i82540EM rev02’,
‘1000 Mbps’,
’10 usec’,
‘Full-duplex’,
‘1000 Mbps’,
‘’,
‘0800.2735.03c6’,
‘1500’,
‘169.254.1.11’,
‘255.255.255.0’,
‘0’,
‘0’,
‘0’,
‘0’,
‘0’,
‘0’,
‘0’,
‘0’,
‘0’,
‘0’],
[‘Management0/0’,
‘management’,
‘up’,
‘up’,
‘i82540EM rev02’,
‘1000 Mbps’,
’10 usec’,
‘Full-duplex’,
‘1000 Mbps’,
‘’,
‘0800.277d.ea42’,
‘1500’,
‘10.0.2.15’,
‘255.255.255.0’,
‘16’,
‘905’,
‘16’,
‘1762’,
‘0’,
‘0’,
‘0’,
‘0’,
‘0’,
‘0’]]

Using Ansible for Network Automation

Ansible

Ansible is a radically simple IT automation engine that automates cloud provisioning, configuration management, application deployment, intra-service orchestration, and many other IT needs.

Designed for multi-tier deployments since day one, Ansible models your IT infrastructure by describing how all of your systems inter-relate, rather than just managing one system at a time.

It uses no agents and no additional custom security infrastructure, so it’s easy to deploy — and most importantly, it uses a very simple language (YAML, in the form of Ansible Playbooks) that allow you to describe your automation jobs in a way that approaches plain English.

MANAGE YOUR INVENTORY IN SIMPLE TEXT FILES

By default, Ansible represents what machines it manages using a very simple INI file that puts all of your managed machines in groups of your own choosing.

Here’s what a plain text inventory file looks like:

[webservers]
www1.example.com
www2.example.com
[dbservers]
db0.example.com
db1.example.com

PLAYBOOKS: A SIMPLE+POWERFUL AUTOMATION LANGUAGE

Playbooks are Ansible’s configuration, deployment, and orchestration language. They can describe a policy you want your remote systems to enforce, or a set of steps in a general IT process.

Playbooks are designed to be human-readable and are developed in a basic text language. There are multiple ways to organize playbooks and the files they include, and we’ll offer up some suggestions on that and making the most out of Ansible.

Playbooks are expressed in YAML format and have a minimum of syntax, which intentionally tries to not be a programming language or script, but rather a model of a configuration or a process.

Powerful Network Modules in Ansible

network_cli

This connection plugin provides a connection to remote devices over the SSH and implements a CLI shell. This connection plugin is typically used by network devices for sending and receiving CLi commands to network devices.

---
- hosts: cisco
connection: network_cli
tasks:
- nxos_config:
backup: yes

cli_command and cli_config

Agnostic modules is to simplify Ansible Playbooks for network engineers that deal with a variety of network platforms

Below example shows the importance of cli_command

- name: RUN ARISTA COMMAND
eos_command:
commands: show ip int br
when: ansible_network_os == 'eos'
- name: RUN CISCO NXOS COMMAND
nxos_command:
commands: show ip int br
when: ansible_network_os == 'nxos'
- name: RUN JUNOS COMMAND
junos_command:
commands: show interface terse
when: ansible_network_os == 'junos'

With the cli_command let’s look at how we can make this agnostic playbook for Cisco, Juniper and Arista extremely simple:

---
- name: RUN COMMAND AND PRINT TO TERMINAL WINDOW
hosts: routers
gather_facts: false
tasks:
- name: RUN SHOW COMMAND
cli_command:
command: "{{show_interfaces}}"
register: command_output
- name: PRINT TO TERMINAL WINDOW
debug:
msg: "{{command_output.stdout}}"

Three *os_command tasks are reduced to one task. The show_interfaces variable is stored as a group variable on a per-platform basis

Other Network modules

net_get and net_put

• net_get — copy a file from a network device to Ansible Controller
• net_put — copy a file from Ansible Controller to a network device
• netconf_get — fetch configuration/state data from NETCONF enabled network devices
• netconf_rpc — execute operations on NETCONF enabled network devices
• netconf_config — netconf device configuration, module allows the user to send a configuration XML file to a netconf device, and detects if there was a configuration change.
• cli_config — push text based configuration to network devices over network_cli

netconf_get, netconf_rpc and netconf_config

• netconf_get — fetch configuration/state data from NETCONF enabled network devices
• netconf_rpc — execute operations on NETCONF enabled network devices
• netconf_config — netconf device configuration, module allows the user to send a configuration XML file to a netconf device, and detects if there was a configuration change.

Play book examples

Collecting facts and creating backup files with a playbook

Ansible facts modules gather system information ‘facts’ that are available to the rest of your playbook.

Ansible Networking ships with a number of network-specific facts modules. In this example, we use the _facts modules eos_facts, ios_facts and vyos_facts to connect to the remote networking device. As the credentials are not explicitly passed via module arguments, Ansible uses the username and password from the inventory file.

Ansible’s “Network Fact modules” gather information from the system and store the results in facts prefixed with ansible_net_. The data collected by these modules is documented in the Return Values section of the module docs, in this case eos_facts and vyos_facts. We can use the facts, such as ansible_net_version late on in the “Display some facts” task.

To ensure we call the correct mode (*_facts) the task is conditionally run based on the group defined in the inventory file, for more information on the use of conditionals in Ansible Playbooks see The When Statement.

- name: "Demonstrate connecting to switches"
hosts: switches
gather_facts: no
tasks:
###
# Collect data
#
- name: Gather facts (eos)
eos_facts:
when: ansible_network_os == 'eos'
- name: Gather facts (ops)
ios_facts:
when: ansible_network_os == 'ios'
- name: Gather facts (vyos)
vyos_facts:
when: ansible_network_os == 'vyos'
###
# Demonstrate variables
#
- name: Display some facts
debug:
msg: "The hostname is {{ ansible_net_hostname }} and the OS is {{ ansible_net_version }}"
- name: Facts from a specific host
debug:
var: hostvars['vyos01.example.net']
- name: Write facts to disk using a template
copy:
content: |
#jinja2: lstrip_blocks: True
EOS device info:
{% for host in groups['eos'] %}
Hostname: {{ hostvars[host].ansible_net_hostname }}
Version: {{ hostvars[host].ansible_net_version }}
Model: {{ hostvars[host].ansible_net_model }}
Serial: {{ hostvars[host].ansible_net_serialnum }}
{% endfor %}
IOS device info:
{% for host in groups['ios'] %}
Hostname: {{ hostvars[host].ansible_net_hostname }}
Version: {{ hostvars[host].ansible_net_version }}
Model: {{ hostvars[host].ansible_net_model }}
Serial: {{ hostvars[host].ansible_net_serialnum }}
{% endfor %}
VyOS device info:
{% for host in groups['vyos'] %}
Hostname: {{ hostvars[host].ansible_net_hostname }}
Version: {{ hostvars[host].ansible_net_version }}
Model: {{ hostvars[host].ansible_net_model }}
Serial: {{ hostvars[host].ansible_net_serialnum }}
{% endfor %}
dest: /tmp/switch-facts
run_once: yes
###
# Get running configuration
#
- name: Backup switch (eos)
eos_config:
backup: yes
register: backup_eos_location
when: ansible_network_os == 'eos'
- name: backup switch (vyos)
vyos_config:
backup: yes
register: backup_vyos_location
when: ansible_network_os == 'vyos'
- name: Create backup dir
file:
path: "/tmp/backups/{{ inventory_hostname }}"
state: directory
recurse: yes
- name: Copy backup files into /tmp/backups/ (eos)
copy:
src: "{{ backup_eos_location.backup_path }}"
dest: "/tmp/backups/{{ inventory_hostname }}/{{ inventory_hostname }}.bck"
when: ansible_network_os == 'eos'
- name: Copy backup files into /tmp/backups/ (vyos)
copy:
src: "{{ backup_vyos_location.backup_path }}"
dest: "/tmp/backups/{{ inventory_hostname }}/{{ inventory_hostname }}.bck"
when: ansible_network_os == 'vyos'

Save the file with facts-demo.yml and inventory file as inventory

Running the playbook

To run the playbook, run the following from a console prompt:

ansible-playbook -i inventory facts-demo.yml

This should return output similar to the following:

PLAY RECAP
eos01.example.net : ok=7 changed=2 unreachable=0 failed=0
ios01.example.net : ok=7 changed=2 unreachable=0 failed=0
vyos01.example.net : ok=6 changed=2 unreachable=0 failed=0

Examining the playbook results

Next, look at the contents of the file we created containing the switch facts:

cat /tmp/switch-facts

You can also look at the backup files:

The backup files will be in /tmp/backups

--

--