Implementing idempotency in Unicon a.k.a do the desired changes already exist on the router? Part 1 – getting inputs

Hello

Unfortunately, making changes in Unicon is not idempotent. Unicon doesn’t realize natively if a change in the config is needed or not, so it will try to add config irrespectively of whether the config you want to add already exists on the target device or not. Having this feature can also help avoid mistakes such as overriding valid config by mistake.

I was thinking along these lines: I should take the existing running config and the implementation change config set, and analyze this change to see if a particular config change will actually modify the config or not. I believe that in order to do this, the running config needs to be analyzed in terms of keys and values, e.g: interface name is a key, ip address is a value.

int loop1 KEY
ip addr 1.1.1.1 255.255.255.0 VALUE

So the first condition is that if i want to implement a change on fa0/0, i should look for the key interface fa0/0, if the key doesn’t exist, it’s a real change and it should be implemented.

The second condition is that if i want to implement a change on a key that already exists, then I need to compare the values: if the new value is different, it’s a real change again and it should be implemented.
Fortunately, a library exists that already understands this key-value relationship: CiscoConfParse

Additionally, I’ve been thinking that to make any progress with my Python skills, i should try to use some cool features i didn’t know before. This time it is the lambda feature; it is basically an unnamed def. Here it helps me split the string into two parts, and the map function takes the first resulting string and maps it to the key, the second string becomes the value.

from ciscoconfparse import CiscoConfParse

#this is my config
parse = CiscoConfParse([ '!', 'interface FastEthernet0/0', ' ip address 192.168.0.1 255.255.255.0', '!', 'interface FastEthernet0/1', ' ip address 172.16.0.1 255.255.255.0'])

#Looking for condition 1: does a key(parent) exist? i'm parsing all objects to find words beginning with 'interf'

all_intfs = parse.find_objects(r"^interf")

#Looking for condition 2: if a key (parent) exists, does the same value (child) exists for this parent?

answer = parse.find_objects_w_child(parentspec=r"^interface FastEthernet0/0", childspec=r"192.168.0.1")
#this returns a list, so i need to see if the length of this list is >0

#the logic should be like this: if change has a key and value, look for the same key in existing config, if len>0, look for value for this key, if len>0, change not needed.
#let's say we have an upcoming parent-child config change, already parsed to the following string

mychangeinconfig = 'int fa0/0:ip address 192.168.0.1 255.255.255.0'
mylist = []
mylist.append(mychangeinconfig)

#we can try to make this change into a key: value dictionary

def list_to_dict(somelist):
return dict(map(lambda s: s.split(':'), somelist))
mydict = list_to_dict(mylist)
for k, v in mydict.items():
  myparent = k
  mychild = v

#or we can present this change config in the form of two separate strings, this doesn't matter too much.

mychangeparent = 'interface FastEthernet0/0'
mychangechild = 'ip address 192.168.0.1 255.255.255.0'
#now the key is int fa0/0 and the value is ip address 192.168.0.1 255.255.255.0
#we could now put this in the search algorithm
#answer = parse.find_objects_w_child(parentspec=myparent, childspec=mychild)

#LOGIC: if answer found in the form of list, the change is not needed.
if len(answer)>0:
  #change not needed
else:
  #implement change

#DONE Challenge1: to prepare a text file with following config changes:
R1
int fa0/0
 ip addr 172.16.0.1 255.255.255.0
int fa0/1
 ip addr 169.254.0.1 255.255.255.0
R2
logging buffered informational
R3
int vlan 100
 shut
@ and parse it such that the final form is the following:
{R1: {int fa0/0:ip addr 172.16.0.1 255.255.255.0, int fa0/1:ip addr 169.254.0.1 255.255.255.0}, R2: {logging buffered informational}, R3: {int vlan 100: shut}}


#Challenge2 TODO: to find out if the following changes should be implemented or if they already exist
change = {R1: {int fa0/0:ip addr 172.16.0.1 255.255.255.0}, R2: {logging buffered notifications}, R3: {int vlan 100: shut}} 

This should be fun with my low-end programming skills 😀

Challenge 1 – preparing the input with the new change

The input with the change needs to be prepared in a way that will be possible to parse.

#i cheated in preparing the change config set, but only a little. The thing is, i need to have clearly separated input to be able to parse it. Here i use commas to separate router names from keys, semicolons to separate keys from values and question marks to separate keys:values from new keys:values. Finally /n begins another router change config. 

text = 'R1,int fa0/0;ip address 192.168.0.1 255.255.255.0?int fa0/1;ip address 192.168.1.1 255.255.255.0\nR2,int fa0/0;ip address 192.168.0.2 255.255.255.0\nR3,int fa0/0;ip address 192.168.0.3 255.255.255.0'

print('this is raw text input for my change' + text)

def text_2_dic(atext):
  stext = atext.split('\n')
  mydict = dict(map(lambda s: s.split(','), stext))
  Dict = { }
  for k, v in mydict.items():
    v = dict(x.split(";") for x in v.split("?"))
    for a,b in v.items():
      Dict[k] = {}
      Dict[k][a] = b
  return Dict
print('and this is final dictionary with the parsed change input: \n ' + str(text_2_dic(text))
C:\Users\A666773\Desktop>python listtest.py
this is raw text input for my change: R1,int fa0/0;ip address 192.168.0.1 255.255.255.0?int fa0/1;ip address 192.168.1.1 255.255.255.0
R2,int fa0/0;ip address 192.168.0.2 255.255.255.0
R3,int fa0/0;ip address 192.168.0.3 255.255.255.0
and this is final dictionary with the parsed change input:
{'R1': {'int fa0/1': 'ip address 192.168.1.1 255.255.255.0'}, 'R2': {'int fa0/0': 'ip address 192.168.0.2 255.255.255.0'}, 'R3': {'int fa0/0': 'ip address 192.168.0.3 255.255.255.0'}}

This was actually wrong, because the final dictionary didn’t have the int fa0/0 change. The problem was in initialising the dictionary inside the loop rather than outside:

text = 'R1,int fa0/0;ip address 192.168.0.1 255.255.255.0?int fa0/1;ip address 192.168.1.1 255.255.255.0\nR2,int fa0/0;ip address 192.168.0.2 255.255.255.0\nR3,int fa0/0;ip address 192.168.0.3 255.255.255.0'
  def text_2_dic(atext):
    stext = atext.split('\n')
    mydict = dict(map(lambda s: s.split(','), stext))

  print('+++++++++++++++++++++++++++++++++++++++++++\n\n')
  print('this is the predictionary \n' + str(mydict))
  print('+++++++++++++++++++++++++++++++++++++++++++\n\n')
  Dict = { }
  for k, v in mydict.items():
    v = dict(x.split(";") for x in v.split("?"))
    Dict[k] = {}
    for a,b in v.items():
      Dict[k][a] = b 
  return Dict
print('and this is final dictionary: \n ' + str(text_2_dic(text)))

OUTPUT:

Another problem appeared: what about configs which are one-line only? Not all configs have a parent and a child, like logging buffered in this case below. For now, i’ll use a trick with a semicolon and a empty space value. We’ll see what happens later. Maybe this will have to be reworked.

INPUT

text = 'R1,int fa0/0;ip address 192.168.0.1 255.255.255.0?int fa0/1;ip address 192.168.1.1 255.255.255.0?logging buffered; \nR2,int fa0/0;ip address 192.168.0.2 255.255.255.0\nR3,int fa0/0;ip address 192.168.0.3 255.255.255.0'

OUTPUT

and this is final dictionary:
{'R1': {'int fa0/0': 'ip address 192.168.0.1 255.255.255.0', 'int fa0/1': 'ip address 192.168.1.1 255.255.255.0', 'logging buffered': ' '}, 'R2': {'int fa0/0': 'ip address 192.168.0.2 255.255.255.0'}, 'R3': {'int fa0/0': 'ip address 192.168.0.3 255.255.255.0'}}

This was still wrong though, because if i inserted into the input multiple changes for a single subkey (e.g. interface), these changes were not found in the final dictionary.

I need to find if dictionary v contains multiple items. If it does, i loop through these items creating tuplets (eachtuplet), and then i update the dictionary from those tuplets.

def text_2_dic(atext): 
  stext = atext.split('\n') 
  mydict = dict(map(lambda s: s.split(','), stext)) 
  print('+++++++++++++++++++++++++++++++++++++++++++\n\n') 
  print('this is the predictionary \n' + str(mydict)) 
  print('+++++++++++++++++++++++++++++++++++++++++++\n\n') 
  Dict = { } 
  for k, v in mydict.items(): 
    v = dict(x.split(";") for x in v.split("?")) 
    Dict[k] = {} 
    x = 0 
    for x in range(x,len(v)): 
      eachtuplet = list(v.items())[x] 
      Dict[k][eachtuplet[0]] = eachtuplet[1] 
return Dict

INPUT:

text = 'R1,int fa0/0;ip address 192.168.0.1 255.255.255.0?int fa0/0;desc LAN?int fa0/0;shut?int fa0/1;ip address 192.168.1.1 255.255.255.0?logging buffered; \nR2,int fa0/0;ip address 192.168.0.2 255.255.255.0\nR3,int fa0/0;ip address 192.168.0.3 255.255.255.0'

OUTPUT

And this is the final dictionary:
{'R1': {'int fa0/0': 'shut', 'int fa0/1': 'ip address 192.168.1.1 255.255.255.0', 'logging buffered': ' '}, 'R2': {'int fa0/0': 'ip address 192.168.0.2 255.255.255.0'}, 'R3': {'int fa0/0': 'ip address 192.168.0.3 255.255.255.0'}}

Adding pretty print…

import pprint
(...)
pp = pprint.PrettyPrinter(indent=4)
pp.pprint(text_2_dic(text))

Obviously there are more scenarios to be accounted for (deleting with ‚no’ or nested commands like qos policies) but for now that’s enough. I can improve that later.

Challenge 2 – loading and parsing the configs

Now that we have this nice dictionary with the parsed config changes, we need to load the existing configs.

For now, i will be using mock (abbreviated device configs) and a mock (custom) change text file.

This is the beginning of the larger def that will later compare the inputs and return True or False.

def comparechangewithconfigof(routerfilelist):
  realdevicelist = routerfilelist.split('%')
  x = 0
#looping through list of configs and parsing the config
  for x in range(x,len(realdevicelist)):
    with open(realdevicelist[x], 'r') as reader:
      config = reader.read()
      my_config_as_list = config.split("\n")
      parsedconfig = CiscoConfParse(my_config_as_list)
#opening the change file and transforming into Dict
    with open('change.txt', 'r') as reader:
      configchangetext = reader.read()
      stext = configchangetext.split('\n')
      mydict = dict(map(lambda s: s.split(','), stext))
      Dict = { }
      for k, v in mydict.items():
        v = dict(z.split(";") for z in v.split("?"))
        Dict[k] = {}
        y = 0
        for y in range(y,len(v)):
          eachitem = list(v.items())[y]
          Dict[k][eachitem[0]] = eachitem[1]
print(Dict)


comparechangewithconfigof('R1%R2%R3')

Inputs are now ready. Now it’s time to compare the change input with the existing configs and ask if the change makes sense (worth implementing?). To be continued next week in part 2.

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