Putting it all together: OOB network programming, nested jinjas, decorators, slack, Genie

After a few months of configuring my test network using one-off scripts i have now realized that perhaps this was not entirely proper way of doing things.
In one of my previous blog posts i mentioned that configuring the router using that long change.txt wasn’t ”network as code” but rather network as a long messy text file. This was true, although it was a small step forward.
From now on I will try to combine all the things i’ve done before, that is Unicon, jinja templates but using the OOB approach and modifying the network using code, so that I have the object-oriented app logic in one file, change execution in another (importing the logic), jinja templates in another file (.j2) and jinja attributes in another file (.yml).


The endgoal will be to have the following workflow:
1. prepare the new config,

2. get current network configuration and state,

3. implement the config,

4. get new network configuration and state

5. get change notification with config delta in Slack.

6. Optional (start the workflow from Slack, the ”chatops” way)

First let’s create some basic logic.

class Router:

   def __init__(self, sship, sshuser, sshpass):
    self.config = []
    self.sship = sship
    self.sshuser = sshuser
    self.sshpass = sshpass

   def setIPaddress(self, intname, IP, mask):
    self.intname = intname
    self.ip = IP
    self.mask = mask
    commandlist = []
    commandlist.append(self.intname)
    commandlist.append('ip address ' + str(self.ip) + ' ' + str(self.mask))
    self.config.append(commandlist)
   
  

    def configure(self):
    	#todo

router = Router()
router.setIPaddress("int fa0/0","192.168.0.1","255.255.255.0")
print(router.config)

Now we have the OOB basic, I will take advantage of all the cool bits and pieces from the previous blog posts to create this full script including Unicon, jinja etc.

from jinja2 import Environment, FileSystemLoader
import yaml
from pyats.topology import loader
import sys
import shutil
import diffios
import os
import time
import logging
from gtts import gTTS
import re

class Router:

   def __init__(self):
    self.config = '' 

   def setIPaddress(self, intname, IP, mask):
    self.intname = intname
    self.ip = IP
    self.mask = mask
    commandlist = []
    commandlist.append("interface " + str(self.intname))
    commandlist.append('ip address ' + str(self.ip) + ' ' + str(self.mask))
    self.config += commandlist

   def prepareACLconfig(self, datafile, template):
       self.datafile = datafile
       self.template = template
       accesslists = yaml.load(open(self.datafile), Loader=yaml.SafeLoader)
       env = Environment(loader = FileSystemLoader('.'), trim_blocks=True, autoescape=True)
       template = env.get_template(self.template)
       acl_config = template.render(data=accesslists)
       self.config += acl_config

   def configure(self):
    testbed = loader.load('testbed.yml')
    c = testbed.devices['R1']
    c.connect()
    output = c.configure(router.config)
    c.destroy()
    #todo
#and now let's test an actual config change with acl data in the .yml object and template in the .j2 object, with the GNS3 routers described in the testbed.yml object.
      
router = Router()
router.prepareACLconfig('vpnacl.yml', 'acl.j2')
router.configure()


while all the data files look like this:

tode@ubuntu:~/perfectscripts/generator$ cat vpnacl.yml
 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  


 tode@ubuntu:~/perfectscripts/generator$ cat acl.j2
 {% for accesslist in data.accesslists %}
 ip access-list extended {{accesslist.aclname}}
 {{accesslist.aclcommand}} {{accesslist.protocol}} {{accesslist.source}} {{accesslist.sourcewildcard}} {{accesslist.destination}} {{accesslist.destwildcard}}
 {% endfor %}


 tode@ubuntu:~/perfectscripts/generator$ cat testbed.yml
 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

Decorators

Let’s add a simple function here that will do ‚show run’ before and after the change. To do that, i’ve implemented a simple decorator.

def diffdecorator(func):
    def wrapper(self):
        testbed = loader.load('testbed.yml')
        c = testbed.devices['R1']
        c.connect(log_stdout=False)
        output = c.execute('show run')
        f = open('R1.bak', "w")
        f.write(output)
        f.close()
        c.destroy()
        func()
        c = testbed.devices['R1']
        c.connect(log_stdout=False)
        output = c.execute('show run')
        f = open('R1.txt', "w")
        f.write(output)
        f.close()
        c.destroy()
        diff = diffios.Compare('R1.bak', 'R1.txt')
        deltaoutput = diff.delta()
        filename = 'R1_change_delta.bak'
        f = open(filename, "w")
        f.write(deltaoutput)
        f.close()
    return wrapper

   @diffdecorator
   def configure():
    testbed = loader.load('testbed.yml')
    c = testbed.devices['R1']
    c.connect()
    output = c.configure(router.config)
    c.destroy()
    

Nested Jinja template

Going further, i created an ospf.j2 file and a corresponding ospf.yml file. Now because the ospf can have multiple network statements, a nested jinja template was needed.

tode@ubuntu:~/perfectscripts/generator$ cat ospf.yml
ospf:
   - processnumber: 100
     routerid: 1.1.1.1
     network: 
       - address: 192.168.122.0 0.0.0.255 area 0
       - address: 1.1.1.1 0.0.0.0 area 0
tode@ubuntu:~/perfectscripts/generator$ cat ospf.j2
{% for element in data.ospf%}
  router ospf {{element.processnumber}}
  router-id {{element.routerid}}
  {% for entry in element.network%}
    network {{entry.address}}
  {% endfor %}
{% endfor %}


This has produced the following config:

tode@ubuntu:~/perfectscripts/generator$ python3 ng_oob.py 
   router ospf 100
   router-id 1.1.1.1
       network 192.168.122.0 0.0.0.255 area 0
       network 1.1.1.1 0.0.0.0 area 0

Next steps:

  • using pyats to check network state before change (another decorator)
  • decision about folder structure with .ymls, templates and script change history
  • how to implement idempotency (file-based, or code-based)
  • using Slack to trigger changes (reverse bot)

Adding Slack notification

Let’s rewrite the decorator again:

def diffdecorator(func):
    def wrapper(self):
        testbed = loader.load('testbed.yml')
        c = testbed.devices['R1']
        c.connect(log_stdout=False)
        output = c.execute('show run')
        f = open('R1.bak', "w")
        f.write(output)
        f.close()
        c.destroy()
        func()
        c = testbed.devices['R1']
        c.connect(log_stdout=False)
        output = c.execute('show run')
        f = open('R1.txt', "w")
        f.write(output)
        f.close()
        c.destroy()
        diff = diffios.Compare('R1.bak', 'R1.txt')

        deltaoutput = diff.delta()
        p = get_notifier('slack')
        p.notify(webhook_url='https://hooks.slack.com/services/xxxxx/yyyyy/zzzzzz', message='config on this router has been changed!')
        p.notify(webhook_url='https://hooks.slack.com/services/xxxxx/yyyyy/zzzzzz', message=diff.delta())
        filename = 'R1_change_delta.bak'
        f = open(filename, "w")
        f.write(deltaoutput)
        f.close()
    return wrapper


Adding Genie magic

A while ago I was tasked with creating a report based on the uptime taken from output of ‚show version’ command plus software version. Preparing the regex was funny, and i was well proud of the way i did the ”screenscrubbing”, until I saw how you can do this easier.

The regex way for uptime doesn’t look that bad but what if Cisco changed the output of the show version command?

endresult = re.findall(r'control processor .*', result)

And here comes Genie. Initialising the testbed looks very similar to the unicon way ( Genie.init instead of loader.load )

     testbed = Genie.init('testbed.yml')
     r1 = testbed.devices['R1']
     r1.connect()
     shver = r1.parse("show version")
     
     print(shver['version']['version_short'])
     print(shver['version']['uptime'])

The output of this is:

12.4
 5 hours, 8 minutes

Now another thing we can do with Genie is compare the state of the network. This is different than a configuration diff. A Genie diff can for example show you which OSPF LSAs you received in between running two ”Genie learn commands”. Let me show you how it’s done:

genie learn ospf --testbed testbed.yml --device R1 --output learn
earning '['ospf']' on devices '['R1']'
 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:03<00:00,  3.04s/it]
 +==============================================================================+
 | Genie Learn Summary for device R1                                            |
 +==============================================================================+
 |  Connected to R1                                                             |
 -   Log: learn/connection_R1.txt 
 Learnt feature 'ospf' 
 -  Ops structure:  learn/ospf_ios_R1_ops.txt 
 -  Device Console: learn/ospf_ios_R1_console.txt 
 ============================================================================== 

Then I run my python script which generates and implements the ospf config:

tode@ubuntu:~/perfectscripts/generator$ python3 ng_oob.py 
   router ospf 100
   router-id 1.1.1.1
       network 192.168.122.0 0.0.0.255 area 0
       network 1.1.1.1 0.0.0.0 area 0

(...) here i didn't include the unicon's screen output

Now i run the Genie learn again, this time i redirect the output to another folder:

tode@ubuntu:~/perfectscripts/generator$ genie diff learn learn2
 1it [00:00, 127.13it/s]
 +==============================================================================+
 | Genie Diff Summary between directories learn/ and learn2/                    |
 +==============================================================================+
 |  File: ospf_ios_R1_ops.txt                                                   |
 - Diff can be found at ./diff_ospf_ios_R1_ops.txt 
 

Inside the diff file is the following:

tode@ubuntu:~/perfectscripts/generator$ cat diff_ospf_ios_R1_ops.txt 
 --- learn/ospf_ios_R1_ops.txt
 +++ learn2/ospf_ios_R1_ops.txt
 +info: 
 + vrf: 
 + default: 
 + address_family: 
 ipv4: 
 instance: 
 100: 
 areas: 
 0.0.0.0: 
 area_id: 0.0.0.0
 area_type: normal
 database: 
 lsa_types: 
 1: 
 lsa_type: 1
 lsas: 
 1.1.1.1 1.1.1.1: 
 adv_router: 1.1.1.1
 lsa_id: 1.1.1.1
 ospfv2: 
 body: 
 router: 
 links: 
 1.1.1.1: 
 link_data: 255.255.255.255
 link_id: 1.1.1.1
 topologies: 
 0: 
 metric: 1
 mt_id: 0
 type: stub network
 192.168.122.0: 
 link_data: 255.255.255.0
 link_id: 192.168.122.0
 topologies: 
 0: 
 metric: 1
 mt_id: 0
 type: stub network
 num_of_links: 2
 header: 
 adv_router: 1.1.1.1
 age: 42
 checksum: 0xE54B
 length: 48
 lsa_id: 1.1.1.1
 option: None
 seq_num: 80000001
 type: 1
 interfaces: 
 FastEthernet0/0: 
 bfd: 
 enable: False
 cost: 1
 dead_interval: 40
 demand_circuit: False
 dr_ip_addr: 192.168.122.100
 dr_router_id: 1.1.1.1
 enable: True
 hello_interval: 10
 hello_timer: 00:00:03
 interface_type: broadcast
 lls: True
 name: FastEthernet0/0
 passive: False
 priority: 1
 retransmit_interval: 5
 state: dr
 transmit_delay: 1
 Loopback1: 
 bfd: 
 enable: False
 cost: 1
 demand_circuit: False
 enable: True
 interface_type: loopback
 name: Loopback1
 mpls: 
 te: 
 enable: False
 statistics: 
 area_scope_lsa_cksum_sum: 0x00E54B
 area_scope_lsa_count: 1
 spf_runs_count: 1
 bfd: 
 enable: False
 graceful_restart: 
 cisco: 
 enable: False
 type: cisco
 ietf: 
 enable: False
 type: ietf
 mpls: 
 ldp: 
 autoconfig: False
 autoconfig_area_id: 0.0.0.0
 nsr: 
 enable: False
 preference: 
 single_value: 
 all: 110
 router_id: 1.1.1.1
 spf_control: 
 paths: 4
 throttle: 
 spf: 
 hold: 10000
 maximum: 10000
 start: 5000
 stub_router: 
 always: 
 always: False
 external_lsa: False
 include_stub: False
 summary_lsa: False 

What i’m going to do is include this learn process before and after each change, on top of the diffios change, so that we can have more proof that our change has been significant.

However, we can use this Genie diff also if no change has taken place, or if the change has been external to our administrative domain. Something breaks down even if we didn’t change anything, so how about running a Genie diff to check if we now have fewer ospf routes than before? or fewer bgp routes? It’s never been easier to troubleshoot a network problem.

…to be continued…

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj /  Zmień )

Zdjęcie na Google

Komentujesz korzystając z konta Google. Wyloguj /  Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj /  Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj /  Zmień )

Połączenie z %s