There are a lot of very good articles on the Internet about how Network Engineers can use Ansible to create standardized network device configurations or use Ansible with existing network vendor API’s to make changes to network devices. Some of my favorites can be found on the Python for Network Engineers and Jason Edelman’s sites.

However, what if you have [older, legacy] network devices or are running on software revisions that don’t support the newer vendor API’s? What if you need to push a common configuration among a multi-vendor or multi-platform of devices quickly? Pushing configurations quickly is easy with my PyMultiChange tool, but one of its biggest limitations is a multi-[vendor, platform] support – where common configurations may have differing syntax, by [vendor, platform] to accomplish the same task. I have yet to find any blogs on Google that share ideas of this category.

For a while, this led me to believe that it it just wasn’t possible, unless you invested the time in developing the appropriate Ansible modules.  However, I had an idea the other day, which proved that it is possible to push configurations to this category of network devices.

Here is my example playbook:

---
- hosts: netdevices
  connection: local
  gather_facts: no
  
  tasks:
  - name: Create SNMP Configuration
    template:
      src=templates/snmp-contact.j2
      dest=input/{{ hostname }}.conf
    delegate_to: 127.0.0.1

  - name: Configure SNMP Contact on network device
    command: scripts/netsible.py {{ hostname }} input/{{ hostname }}.conf 
    delegate_to: 127.0.0.1

In this playbook, you can see that I call a group of devices call ‘netdevices’. The first play is to generate the configuration. In this case, I am modifying the snmp-server contact information. It calls a source template called snmp-contact.j2. Here is what the template looks like:

config t
snmp-server contact {{ contact_name }}
end
copy run start

The template calls the ‘contact_name’ variable and destination of the template is in the input directory and named using the ‘hostname’ variable.

The hostname variable is called from host_vars. Here is the host_var for a test device, called core1a:

hostname: core1a

The contact_name variable is called from group_vars/all. Here is what my group_var/all looks like:

contact_name: netdude@packetgeek.net

The result is a file in the input directory called core1a.conf, with the following configuration:

config t
snmp-server contact netdude@packetgeek.net
end
copy run start

Once the configuration file has been created, the next play is called. This play is responsible for pushing the configuration to each device. It runs a local script called netsible.py. The script takes two arguments. The first is the hostname of the device to access. The second is the location of the configuration file that was created.

In the background, the script connects to the network device, via SSH, accesses enable mode, reads the configuration file, then executes each command on the router. The script utilizes my netlib library, to make this process simple. Here is the code for the netsible.py script:

#!/usr/bin/env python

from netlib.netlib.user_creds import simple_yaml
from netlib.netlib.conn_type import SSH

from os.path import expanduser
import sys

creds = simple_yaml()
base_dir = expanduser("~/net-ansible")
hostname = sys.argv[1]
command_file = sys.argv[2]
ssh = SSH(hostname, creds['username'], creds['password'])

ssh.connect()
ssh.set_enable(creds['enable'])

with open(base_dir + "/" + command_file) as f:
    for line in f.readlines():
        line = line.strip()
        ssh.command(line)
f.close()

ssh.close()

If your device is running on a version of code that doesn’t support ssh, it would be easy, with the netlib library, to utilize telnet. All you would have to do is import the Telnet library via:

from netlib.netlib.conn_type import Telnet

Then replace the ssh variable with the Telnet Library.

ssh = Telnet(hostname, creds['username'], creds['password'])

In the playbook, the ‘delegate_to’ call tells Ansible to run the command locally on the Ansible master, rather than Ansible connecting to the remote devices directly.

Here is what it looks like when I run the playbook:

net-ansible]$ ansible-playbook -i hosts push.yml 

PLAY [netdevices] ************************************************************* 

TASK: [Create SNMP Configuration] ********************************************* 
ok: [darkstar -> 127.0.0.1]
ok: [core1a -> 127.0.0.1]

TASK: [Configure SNMP Contact on network device] ****************************** 
changed: [darkstar -> 127.0.0.1]
changed: [core1a -> 127.0.0.1]

PLAY RECAP ******************************************************************** 
core1a                     : ok=2    changed=1    unreachable=0    failed=0   
darkstar                   : ok=2    changed=1    unreachable=0    failed=0   

net-ansible]$

This obviously works, but it does have a couple limitations, currently the playbook is not multi-[vendor, platform] ready. To do this, I would need to specify host_vars that define each device by vendor or platoform.

For example, I could define a variable called ‘network_platform’ in the host_vars and define each host by platform. I could use the values of IOS, NX-OS, IOS-XR, EOS, or JUN-OS defined as the ‘network_platform’ in the host_vars. Then when I called my playbooks, it could look like:

---
- hosts: netdevices
  connection: local
  gather_facts: no
  
  tasks:
  - name: IOS | Create SNMP Configuration
    template:
      src=templates/ios/snmp-contact.j2
      dest=input/{{ hostname }}.conf
    delegate_to: 127.0.0.1
    when: network_platform == IOS

  - name: NX-OS | Create SNMP Configuration
    template:
      src=templates/nx-os/snmp-contact.j2
      dest=input/{{ hostname }}.conf
    delegate_to: 127.0.0.1
    when: network_platform == NX-OS

  - name: IOS-XR | Create SNMP Configuration
    template:
      src=templates/ios-xr/snmp-contact.j2
      dest=input/{{ hostname }}.conf
    delegate_to: 127.0.0.1
    when: network_platform == IOS-XR

  - name: EOS | Create SNMP Configuration
    template:
      src=templates/eos/snmp-contact.j2
      dest=input/{{ hostname }}.conf
    delegate_to: 127.0.0.1
    when: network_platform == EOS

  - name: JUN-OS | Create SNMP Configuration
    template:
      src=templates/jun-os/snmp-contact.j2
      dest=input/{{ hostname }}.conf
    delegate_to: 127.0.0.1
    when: network_platform == JUN-OS

  - name: Configure SNMP Contact on network device
    command: scripts/netsible.py {{ hostname }} input/{{ hostname }}.conf 
    delegate_to: 127.0.0.1

The other limitation that is that the script writes the configuration to the network devices every time that the playbook is ran, regardless of whether it’s needed or not. For creating an snmp contact, this isn’t a huge deal, with the exception of taking extra CPU cycles. However, what if you ran a playbook that was entirely roll based, and it called a role to define BGP route reflectors. Obviously, this would bounce BGP neighbors every time that you ran the playbook. Basically, it boils down to needing a method of checking whether the configuration is actually needed before the script applies it. This is something that I hope to be able to work on. In the mean time, I hope that you’ve enjoyed this. If you have any ideas, please feel free to share them with me!

I have a generic Github repository that I’ve been using to play with Ansible Network Engineering functionality. Feel free to play with it and contribute to it! Note that ‘netlib‘ is called as a submodule. :) Enjoy!

Share on FacebookTweet about this on TwitterShare on LinkedInShare on RedditEmail this to someone

I re-wrote ‘pyMultiChange‘ around my new library for connecting and managing devices. Before I was using ‘pyRouterLib’, but now I’ve deprecated that library with the creation of my new library ‘netlib‘.

Netlib is super easy to use and is more complete than pyRouterLib was. Utilizing Netlib, allowed me to cut the code in pyMultiChange by almost half and netlib is more flexible and user friendly.

To use netlib, first clone the git repo and install the necessary python libraries:

git clone https://github.com/jtdub/netlib.git
cd netlib
sudo pip install -r requirements.txt
sudo python setup.py install

After that, you’re ready to go. Accessing network devices via telnet and ssh are currently supported. Both have a very similar API syntax, that is layed out in the README on github.

Here is an example of how to use the ssh module:

$ python
>>> from netlib.conn_type import SSH
>>> conn = SSH('core1a', username='******', password='******')
>>> conn.connect()
'\r\ncore1a.sat>'
>>> conn.set_enable(enable_password='******')
'\r\ncore1a.sat#'
>>> conn.disable_paging()
>>> print(conn.command('show version'))
show version
Cisco IOS Software, C3750 Software (C3750-IPSERVICESK9-M), Version 12.2(55)SE9, RELEASE SOFTWARE (fc1)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2014 by Cisco Systems, Inc.
Compiled Mon 03-Mar-14 22:45 by prod_rel_team
Image text-base: 0x01000000, data-base: 0x02F00000

ROM: Bootstrap program is C3750 boot loader
BOOTLDR: C3750 Boot Loader (C3750-HBOOT-M) Version 12.2(44)SE5, RELEASE SOFTWARE (fc1)

core1a.sat uptime is 9 weeks, 5 days, 2 hours, 17 minutes
System returned to ROM by power-on
System restarted at 20:25:45 CST Fri Jun 19 2015
System image file is "flash:c3750-ipservicesk9-mz.122-55.SE9.bin"


This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.

A summary of U.S. laws governing Cisco cryptographic products may be found at:
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html

If you require further assistance please contact us by sending email to
export@cisco.com.

cisco WS-C3750-24TS (PowerPC405) processor (revision L0) with 131072K bytes of memory.
Processor board ID CAT1042ZGKL
Last reset from power-on
6 Virtual Ethernet interfaces
24 FastEthernet interfaces
2 Gigabit Ethernet interfaces
The password-recovery mechanism is enabled.

512K bytes of flash-simulated non-volatile configuration memory.
Base ethernet MAC Address       : 00:19:E7:5F:8F:80
Motherboard assembly number     : 73-9677-10
Power supply part number        : 341-0034-01
Motherboard serial number       : CAT10415NLR
Power supply serial number      : DTH1037117A
Model revision number           : L0
Motherboard revision number     : A0
Model number                    : WS-C3750-24TS-S
System serial number            : CAT1042ZGKL
Top Assembly Part Number        : 800-25857-02
Top Assembly Revision Number    : D0
Version ID                      : V05
CLEI Code Number                : CNMV100CRE
Hardware Board Revision Number  : 0x01


Switch Ports Model              SW Version            SW Image                 
------ ----- -----              ----------            ----------               
*    1 26    WS-C3750-24TS      12.2(55)SE9           C3750-IPSERVICESK9-M     


Configuration register is 0xF

core1a.sat#
>>> conn.close()

How easy is that? I’m stoked about netlib. It should make rapidly creating code for interact with network devices pretty trivial. I’m experimenting with SNMP functionality, though it’s not ready for prime time.

I also have a method for storing and reading user credentials, so that they don’t have to be stored in the script, called every time a script is run, or entered manually for every device that is accessed.

Let me know what you think, add feature requests, or do a pull request. :)

Share on FacebookTweet about this on TwitterShare on LinkedInShare on RedditEmail this to someone