Update Sep 16, 2017: As NAPALM officially supports Python 3, I have changed my NAPALM container (ehlers/napalm) and the scripts on this page to Python 3.

NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor support) is a python library that implements a set of functions to interact with different router vendor devices using an unified API. For further information see the documentation at https://napalm.readthedocs.io/. Unfortunately the docs are quite sparse, for the details you may need to look into the sources at https://github.com/napalm-automation.

To test NAPALM within GNS3 I’ve created a docker container (ehlers/napalm). I haven’t tested, but the “Network Automation” appliance in GNS3 Marketplace / Appliances should work as well. I’ve created a small GNS3 project using NAPALM, two Cisco IOS router (3725 and IOSv) and a JunOS Olive router.

GNS3 project

Cisco IOS

The router needs a usable disk, so NAPALM can store data on it. This can be checked by running “dir” on the IOS router. If you get an error, format the disk, you might have to erase it before.

IOS router need an initial configuration, that allows NAPALM to access the router by SSH. In the “SSH to Cisco and Juniper router” post, you find a more detailed description, how to enable SSH access.

hostname <host>
ip domain name <domain>
crypto key generate rsa
 How many bits in the modulus [512]: 1024/2048
ip ssh version 2
!
aaa new-model
aaa authentication login default local
aaa authorization exec default local
!
username <config_user> privilege 15 secret <password>
!
line vty 0 4
 transport input ssh
!
! for testing only: no authentication on console
aaa authentication login no_auth none
line con 0
 privilege level 15
 login authentication no_auth

Furthermore the archive feature and SCP has to be enabled.

archive
 path flash:$h
!
ip scp server enable

Juniper JunOS

To configure JunOS devices NAPALM also needs SSH access.

set system host-name <host>
set system login user <config_user> class super-user
set system login user <config_user> authentication plain-text-password
set system services ssh

Device Database

Any automation tool needs a device database with some parameters like device type, IP address and configuration username / password. On operational networks typically a SQL database is used, but that’s an overkill for our small test.

Instead I use a JSON formatted “devices” file:

{
    "r1": {
        "IP": "10.1.1.1",
	"type": "ios",
        "user": "config",
        "password": "Config"
    },
    "iosv-1": {
        "IP": "10.1.1.2",
	"type": "ios",
        "user": "config",
        "password": "Config"
    },
    "junos-1": {
        "IP": "10.1.1.3",
	"type": "junos",
        "user": "config",
        "password": "Config"
    }
}

NAPALM

Jason Edelman created a nice tutorial how to use NAPALM (Python library) to Manage IOS Devices. Based on that I created small python scripts for two typical use cases.

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

# abort program with error message
def die(error_string):
    sys.stderr.write(error_string)
    sys.exit(1)


# get command line parameter
if len(sys.argv) != 2:
    die("Usage: get_conf hostname\n")
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: {}\n".format(err))

# get device parameter
try:
    dev_param = dev_db[hostname.lower()]
except KeyError:
    die("Unknown device '{}'\n".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'])

Changing Device Configuration

Now lets modify the device configuration.

First we need files with the config changes, in this example I’m adding a loopback device.

Here the IOS version:

interface Loopback0
 ip address 1.1.1.1 255.255.255.255
end

and here the JunOS way:

set interfaces lo0 unit 0 family inet
delete interfaces lo0 unit 0 family inet
set interfaces lo0 unit 0 family inet address 1.1.1.1/32

The following script (merge_conf <hostname> <conf_file>) applies the configuration changes to a device. It sends the changes to the devices, displays the changes and (after confirmation) commits them.

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

# abort program with error message
def die(error_string):
    sys.stderr.write(error_string)
    sys.exit(1)


# get command line parameter
if len(sys.argv) != 3:
    die("Usage: merge_conf hostname conf_file\n")
hostname = sys.argv[1]
conf_file = sys.argv[2]

# 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: {}\n".format(err))

# get device parameter
try:
    dev_param = dev_db[hostname.lower()]
except KeyError:
    die("Unknown device '{}'\n".format(hostname))

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

# merge config
with driver(dev_param['IP'], dev_param['user'], dev_param['password']) as device:
    device.load_merge_candidate(filename=conf_file)
    diffs = device.compare_config()
    print(diffs)
    yesno = input('\nApply changes? [yes,no] (no) ').lower()
    if yesno == 'y' or yesno == 'yes':
        print("Applying changes...")
        device.commit_config()
    else:
        print("Discarding changes...")
        device.discard_config()

Here a log of a configuration update on JunOS:

root@napalm-1:~# ./merge_conf junos-1 conf_lo0.junos
[edit interfaces]
+   lo0 {
+       unit 0 {
+           family inet {
+               address 1.1.1.1/32;
+           }
+       }
+   }

Apply changes? [y/N] y
Applying changes...