Hello
Over the next few weeks I’d like to test a few automation libraries to see which ones are easier to use than others. I hope this can be a useful introduction for those without much prior experience with automation or coding in general.
I don’t have much experience with python as I only did some programming in java a long time ago so this entry has a lot of code borrowed from all over the internet 🙂 We are all learning here so if you see something particularly noobish, feel free to laugh out loud.
I first launched GNS3 on ubuntu 20.04 and prepared a simple topology
I connected ubuntu to gns3 using the nat cloud. R1 is 192.168.122.100.
NETMIKO
My goal was to implement a simple ACL on R1. To do that, i’ve prepared an acl.j2 template file:
{% for accesslist in data.accesslists %} ip access-list extended {{accesslist.aclname}} {{accesslist.aclcommand}} {{accesslist.protocol}} {{accesslist.source}} {{accesslist.sourcewildcard}} {{accesslist.destination}} {{accesslist.destwildcard}} {% endfor %}
Then I prepared the acl data in the acl.yml file:
accesslists: - aclname: permit_www aclcommand: permit protocol: ip source: 192.168.122.100 sourcewildcard: 0.0.0.0 destination: 8.8.8.8 destwildcard: 0.0.0.0
Finally, the python script.
import sys from netmiko import ConnectHandler import time from netmiko import redispatch from netmiko import Netmiko from jinja2 import Environment, FileSystemLoader import yaml def connect(hostip): net_connect = ConnectHandler(device_type='cisco_ios', host=hostip, username='cisco', password='cisco', secret='cisco') net_connect.find_prompt() net_connect.enable() accesslists = yaml.load(open('acl.yml'), Loader=yaml.SafeLoader) env = Environment(loader = FileSystemLoader) template = env.get_template('acl.j2') acl_config = template.render(data=accesslists) print(acl_config) print(f"Logged into {hostip} successfully") output = net_connect.send_config_set(acl_config.split("\n")) #split method returns a list, that can be used in the send_config_set if __name__ == "__main__": ip = sys.argv[1] connect(ip)
I then ran the script:
tode@ubuntu:~/nmikoymlproj$ python3 firstscript.py 192.168.122.100 ip access-list extended permit_www permit ip 192.168.122.100 0.0.0.0 8.8.8.8 0.0.0.0 Logged into 192.168.122.100 successfully
UNICON
from unicon import Connection import time from jinja2 import Environment, FileSystemLoader import yaml #this prepares the config accesslists = yaml.load(open('acl2.yml'), Loader=yaml.SafeLoader) env = Environment(loader = FileSystemLoader('.'), trim_blocks=True, autoescape=True) template = env.get_template('acl.j2') acl2_config = template.render(data=accesslists) preoutput = acl2_config.split("\n") #this prepares the connection c = Connection(hostname='R1', start=['telnet ' + router1], os='ios', credentials={'default': {'username': 'cisco', 'password': 'cisco'}, 'secret': 'cisco'},) c.connect() #This checks how many commands the config set has. This is because unicon's configure method adds ''end'' after each command. So in case of nested commands we need to push the whole set of commands (e.g. interface loop0,ip addr 1.1.1.1 255.255.255.255). So command is a list, to which I push all elements of the config, then the whole list is pushed to the router. x = len(preoutput) y=0 command = [] while y<x: command.append(preoutput[x-x+y]) y = y+1 #this actually configures the router print(command)output = c.configure(command)
and the output…
tode@ubuntu:~/nmikoymlproj$ python3 uniconscript.py 2020-08-03 04:12:14,672: %UNICON-INFO: +++ R1 logfile /tmp/R1-20200803T041214670.log +++ 2020-08-03 04:12:14,672: %UNICON-INFO: +++ Unicon plugin ios +++ ip access-list extended permit_www permit ip 192.168.122.220 0.0.0.0 9.9.9.9 0.0.0.0 Logged into 192.168.122.100 successfully Trying 192.168.122.100... 2020-08-03 04:12:14,706: %UNICON-INFO: +++ connection to spawn: telnet 192.168.122.100, id: 140510945062576 +++ 2020-08-03 04:12:14,707: %UNICON-INFO: connection to R1 Connected to 192.168.122.100. Escape character is '^]'. User Access Verification Username: cisco Password: R1# 2020-08-03 04:12:15,391: %UNICON-INFO: +++ initializing handle +++ 2020-08-03 04:12:15,391: %UNICON-INFO: +++ R1: executing command 'term length 0' +++ term length 0 R1# 2020-08-03 04:12:15,510: %UNICON-INFO: +++ R1: executing command 'term width 0' +++ term width 0 R1# 2020-08-03 04:12:15,670: %UNICON-INFO: +++ R1: executing command 'show version' +++ show version Cisco IOS Software, 7200 Software (C7200-ADVENTERPRISEK9-M), Version 12.4(24)T5, RELEASE SOFTWARE (fc3) Technical Support: http://www.cisco.com/techsupport (...) Configuration register is 0x2102 R1# 2020-08-03 04:12:15,820: %UNICON-INFO: +++ R1: config +++ config term Enter configuration commands, one per line. End with CNTL/Z. R1(config)#no logging console R1(config)#line console 0 R1(config-line)#exec-timeout 0 R1(config-line)#end R1# ['ip access-list extended permit_www', 'permit ip 192.168.122.220 0.0.0.0 9.9.9.9 0.0.0.0', ''] 2020-08-03 04:12:16,007: %UNICON-INFO: +++ R1: config +++ config term Enter configuration commands, one per line. End with CNTL/Z. R1(config)#ip access-list extended permit_www R1(config-ext-nacl)#permit ip 192.168.122.220 0.0.0.0 9.9.9.9 0.0.0.0 R1(config-ext-nacl)#end R1#
Now let’s take the hosts into an inventory yml file:
from unicon import Connection from jinja2 import Environment, FileSystemLoader import yaml from pyats.topology import loader #this prepares the config and command sets accesslists = yaml.load(open('acl2.yml'), Loader=yaml.SafeLoader) env = Environment(loader = FileSystemLoader('.'), trim_blocks=True, autoescape=True) template = env.get_template('acl.j2') acl2_config = template.render(data=accesslists) preoutput = acl2_config.split("\n") x = len(preoutput) y=0 command = [] while y<x: command.append(preoutput[x-x+y]) y = y+1 #this loads the testbed topology and connects testbed = loader.load("testbed.yml") c = testbed.devices['R1'] c.connect() #actual configuration output = c.configure(command)
where the inventory file looks as follows:
devices: R1: connections: cli: ip: 192.168.122.100 port: 23 protocol: telnet credentials: default: password: cisco username: cisco enable: password: cisco os: ios type: ios
Now this code is still not ideal because too many things are hardcoded:
- acl.j2 template
- acl.yml file with the config
- name of router from the testbed
So let’s do move them out into sys.args + a bit of refactoring to have everything neat
def prepareconfig(template,configdata): accesslists = yaml.load(open(configdata), Loader=yaml.SafeLoader) env = Environment(loader = FileSystemLoader('.'), trim_blocks=True, autoescape=True) template = env.get_template(template) realconfig = template.render(data=accesslists) preoutput = realconfig.split("\n") x = len(preoutput) y=0 command = [] while y<x: command.append(preoutput[x-x+y]) y = y+1 return command def connect(testbedfile,devicename): testbed = loader.load(testbedfile) c = testbed.devices[devicename] c.connect() return c def configure(command,connection): thiscommand = command c = connection output = c.configure(thiscommand) ##USAGE python3 script.py acl.j2 acl.yml R1 testbed.yml if __name__ == "__main__": template = sys.argv[1] configdata = sys.argv[2] devicename = sys.argv[3] testbedfile = sys.argv[4] configure(prepareconfig(template,configdata),connect(testbedfile,devicename))
This seems quite easy, but for every feature it is necessary to come up with a .j2 template and prepare data according to the template.
ANSIBLE
- name: add_entry_to_acl hosts: testrouter tasks: - name: add_new_entry ios_config: lines: - permit ip 10.10.10.10 0.0.0.0 11.11.11.11 0.0.0.0 parents: ip access-list extended permit_www before: ip access-list extended permit_www match: exact authorize: yes
well ain’t that faster than typing up all that python code myself…
Obviously there are things hardcoded in this so let’s do some more work here.
I’ve added an acl20 var to my testrouter.yml under host_vars and changed the playbook in the following way:
- name: modify_acl_entries hosts: testrouter tasks: - name: add_new_entry ios_config: lines: - "{{ acl20 }}" parents: ip access-list extended permit_www before: ip access-list extended permit_www save_when: modified
and the output is…
tode@ubuntu:~/ansiblefolder$ ansible-playbook aclplaybook.yml PLAY [modify_acl_entries] ******************************************************** TASK [add_new_entry] *********************************************************** [DEPRECATION WARNING]: Distribution Ubuntu 20.04 on host testrouter should use /usr/bin/python3, but is using /usr/bin/python for backward compatibility with prior Ansible releases. A future Ansible release will default to using the discovered platform python for this host. See https://docs.ansible.com/ansible/ 2.9/reference_appendices/interpreter_discovery.html for more information. This feature will be removed in version 2.12. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg. changed: [testrouter] PLAY RECAP ********************************************************************* testrouter : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Napalm
I’ve had no previous experience with this one so let’s start from scratch: First, the installation:
pip3 install napalm
Now, on the router in GNS3 you need to have a working file system, on my 7200 router I had to increase the size of disk0 under router template, then issue: format disk0: and you’re good to go.
I created an acltest.cfg file with a simple acl:
ip access-list extended napalm_acl permit host 1.2.3.4 host 7.8.9.10
Then I created a simple python napalm script:
from napalm.base import get_network_driver driver = get_network_driver('ios') dev = driver('192.168.122.100', 'cisco', 'cisco',) dev.open() dev.load_merge_candidate(filename='acltest.cfg') dev.commit_config() dev.close()
ok, quite easy, but there are so many things hardcoded here. Let’s create an inventory file first:
{ "r1": { "IP": "192.168.122.100", "type": "ios", "user": "cisco", "password": "cisco" }, "r2": { "IP": "192.168.122.101", "type": "ios", "user": "cisco", "password": "cisco" } }
and now let’s rework the script a bit
from napalm.base import get_network_driver import sys import json hostname = sys.argv[1] acl_file = sys.argv[2] with open("inventory.json", "r") as f: dev_db = json.load(f) dev_param = dev_db[hostname.lower()] driver = get_network_driver(dev_param['type']) with driver(dev_param['IP'], dev_param['user'], dev_param['password']) as device: device.open() device.load_merge_candidate(acl_file) device.commit_config() device.close()
and now let’s run the script
tode@ubuntu:~/napalmproj$ python3 napalm_script.py R1 acltest.cfg
Alternatively, I could use here j2 templates, process the template and data, output a config file and use it instead of having a ”ready config” (useful if the operator is not familiar with ios). Obviously now it is possible to mix and match all the approaches that i’ve gone thru so far.
Ansible+Napalm
Update@17.08.2020
This one was surprisingly difficult to set up but i really liked the result. I created a config file that i wanted to merge with the running config on the target device.
ip access-list ext ansnap permit ip 16.17.18.19 0.0.0.0 20.21.22.23 0.0.0.0
I also prepared my inventory.yml file
[gnsrouter] testrouter
In group_vars, i created gnsrouter.yml with the following settings:
ansible_connection : network_cli ansible_network_os : ios ansible_python_interpreter : "/usr/bin/python3"
I then created the playbook:
- name: merge_config hosts: testrouter gather_facts: no tasks: - name: whatever napalm_install_config: hostname: '192.168.122.100' username: 'cisco' password: 'cisco' dev_os: "ios" optional_args: port: 22 config_file: 'config_file' commit_changes: True replace_config: False get_diffs: True diff_file: 'R1diff.txt'
Now i run the playbook:
tode@ubuntu:~/ansiblefolder$ ansible-playbook ansnap.yml PLAY [merge_config] ************************************************************ TASK [whatever] **************************************************************** changed: [testrouter] PLAY RECAP ********************************************************************* testrouter : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
And the result is the diff file:
tode@ubuntu:~/ansiblefolder$ cat R1diff.txt +ip access-list ext ansnap + permit ip 16.17.18.19 0.0.0.0 20.21.22.23 0.0.0.0
I’ll try to see what more I can do with this…
Nornir + Netmiko
To create this script, i got my .j2 and .yml acl files from the Netmiko script. I prepared the config.yaml file in the following fashion:
tode@ubuntu:~/nornirfolder$ cat config.yaml --- core: num_workers: 20 inventory: plugin: nornir.plugins.inventory.simple.SimpleInventory options: host_file: "inventory/hosts.yaml" group_file: "inventory/groups.yaml" defaults_file: "inventory/defaults.yaml"
The files look as follows:
tode@ubuntu:~/nornirfolder/inventory$ cat hosts.yaml --- testrouter: hostname: 192.168.122.100 port: 22 groups: - cisco_ios - testroutergroup tode@ubuntu:~/nornirfolder/inventory$ cat groups.yaml --- testroutergroup: groups: - cisco_ios cisco_ios: platform: ios tode@ubuntu:~/nornirfolder/inventory$ cat defaults.yaml --- username: cisco password: cisco
Then it was quite easy once I worked out how to use the run method.
from nornir.plugins.tasks.networking import napalm_get from nornir.plugins.functions.text import print_result import json from nornir.plugins.tasks import networking import sys from netmiko import ConnectHandler import time from netmiko import Netmiko from jinja2 import Environment, FileSystemLoader import yaml #this creates the acl_config string env = Environment(loader = FileSystemLoader('.'), trim_blocks=True, autoescape=True) template = env.get_template('aclnr.j2') accesslists = yaml.load(open('aclnr.yml'), Loader=yaml.SafeLoader) acl_config = template.render(data=accesslists) print(acl_config) nr = InitNornir(config_file="./config.yaml") task_result = nr.run(task=networking.netmiko_send_config, config_commands=acl_config.splitlines()) print_result(task_result)
Nornir + Napalm
A similar example, but with a file config and a manual connection def (i might need this later to do a manual jump through my proxy)
from nornir import InitNornir from nornir.plugins.tasks.networking import napalm_get from nornir.plugins.functions.text import print_result import json from nornir.plugins.tasks.networking import napalm_configure from nornir.plugins.tasks import networking import sys from netmiko import ConnectHandler import time from netmiko import redispatch from netmiko import Netmiko from jinja2 import Environment, FileSystemLoader import yaml def task_manages_connection_manually(task): task.host.open_connection("napalm", configuration=task.nornir.config) r = task.run( task=napalm_configure, filename='aclconfig', ) task.host.close_connection("napalm") nr = InitNornir(config_file="./config.yaml") task_result = nr.run(task=task_manages_connection_manually,) print_result(task_result)
tode@ubuntu:~/nornirfolder$ python3 manualscript.py task_manages_connection_manually************************************************ * testrouter ** changed : True ************************************************* vvvv task_manages_connection_manually ** changed : False vvvvvvvvvvvvvvvvvvvvvvv INFO ---- napalm_configure ** changed : True ---------------------------------------- INFO +ip access-list ext napalmconfg +permit ip 4.7.9.11 0.0.0.0 5.3.1.2 0.0.0.0 ^^^^ END task_manages_connection_manually ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
We can see that the lines have been added (+)
Achieving idempotency here can be done by writing the config file in exactly the same way as it will appear on the cisco router. I corrected the aclconfig file like this and ran the script once again. This time the status of napalm_configure task is: changed: False
ip access-list extended napalmconfg permit ip host 4.7.9.11 host 5.3.1.2 tode@ubuntu:~/nornirfolder$ python3 manualscript.py task_manages_connection_manually************************************************ * testrouter ** changed : False ************************************************ vvvv task_manages_connection_manually ** changed : False vvvvvvvvvvvvvvvvvvvvvvv INFO ---- napalm_configure ** changed : False --------------------------------------- INFO ^^^^ END task_manages_connection_manually ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ tode@ubuntu:~/nornirfolder$