Reconfigure static ARP entries using ciscoconfparse

Today I’d like to show you another use case using the ciscoconfparse python module. During the migration from a Cisco VSS to a Cisco Nexus vPC pair, I need to translate a lot of static ARP entries (>2000) from an existing configuration. To do this translation manually is not very effective, because the configuration is expressed differently in NX-OS compared to Cisco IOS. From my perspective, this is an ideal case for ciscoconfparse and python.

Within this post, I’ll also explain in more detail how I usually create such a configuration parser/generator. You can find the entire example code in my python script example repository on GitHub.

Migrate a configuration from Cisco IOS to NX-OS

From time to time I need to rewrite configurations for different platforms for migration purpose. I had one case several times in the past: the migration from a Cisco 6500 VSS (Virtual Switching System) to a Cisco Nexus vPC (virtual port-channel) pair.

Both systems have a common data plane for traffic forwarding, but only the VSS system has also a common control plane. The following figure illustrates this (fundamental) difference when comparing these two architectures.

The concept of dual control planes introduces some challenges when working with Layer 3 protocols (e.g. OSPF, HSRP etc.). The entire system now requires two configuration files and Cisco NX-OS configuration is also expressed differently compared to a Cisco IOS configuration. One major difference is the configuration of static ARP entries. On Cisco IOS, they are defined in global configuration mode whereas Cisco NX-OS defines those on a per SVI (switched virtual interface) basis.

In most cases, this is not a very big issue, but If you have a network with a large amount of Microsoft Server systems that uses the Microsoft Network Load Balancing (multicast and/or IGMP mode), you have very fast the situation that the configuration contains more than 1000 static ARP entries… I found that this is a very good scenario to automate the migration of the configuration.

Within this scenario we will use the ciscoconfparse library (again), which is created an maintained by 

David Michael Pennington. I wrote some weeks ago an article about the generation of HSRP configuration using this library. If you want to know more about it, please have a look at the earlier post.

What the script should do

Given the scenario, the script must do the following tasks:

  1. parse the existing static ARP entries from the given Cisco VSS configuration
  2. parse the existing IP interfaces from the Cisco VSS configuration
  3. create an internal model using a python dictionary, which associates the static ARP addresses to the correct IP interface
  4. create the configuration for Cisco NX-OS

From my perspective, it is better to create a generic model prior generating the configuration when working with existing configurations. There are several benefits associated with this procedure, including the functional separation of the tasks. Furthermore, the code can be used again in the future, e.g. when generating static ARP entries tomorrow from an Excel sheet, not very likely but I think you know what I mean.

Using ciscoconfparse to generate an internal model

I updated my python script example repository on GitHub today and added a new directory called migrate-static-arp-entries. I’ve taken some code from my early example with the HSRP configuration, because we need the parsing of the existing IP interfaces again. For testing purpose, I added a sample configuration to the repository, which looks similar to the following snippet:

!
interface Vlan100
 ip address 10.0.100.1 255.255.255.0
!
interface Vlan101
 ip address 10.0.101.254 255.255.255.0
!
interface Vlan102
 ip address 10.0.102.1 255.255.255.128
!
interface Vlan103
 ip address 10.0.103.1 255.255.255.128
!
arp 10.0.100.115 0100.5e7f.9271 ARPA
arp 10.0.100.105 0100.5e7f.8a64 ARPA
arp 10.0.100.109 0100.5e7f.8ef0 ARPA
arp 10.0.100.110 0100.5e7f.8964 ARPA
arp 10.0.101.124 0100.5e7f.907b ARPA
arp 10.0.101.122 0100.5e7f.9b72 ARPA
arp 10.0.102.104 0100.5e7f.835a ARPA
arp 10.0.102.116 0100.5e7f.9b79 ARPA
arp 10.0.102.110 0100.5e7f.8e65 ARPA
arp 10.0.103.105 0100.5e7f.8162 ARPA
arp 10.0.103.106 0100.5e7f.826b ARPA
arp 10.0.103.123 0100.5e7f.987e ARPA
arp 10.0.103.123 0100.5e7f.9a7c ARPA
!

parse SVI interfaces

As mentioned at the beginning, we will reuse some code from the first ciscoconfparse example with HSRP. The first function within the script parses the VLAN SVI information and stores the relevant part into a python dictionary. The function looks similar to the following code example:

12def get_vlan_svi_records_from_existing_configuration(cisco_conf_parse_obj):
13    """
14    parse VLAN SVI interfaces with IP address
15    :param cisco_conf_parse_obj: Instance of CiscoConfParse
16    :return: list with dictionaries that contain the VLAN ID, IP address and subnet mask
17    """
18    vlan_interfaces = cisco_conf_parse_obj.find_objects_w_child("^interface Vlan", r"^ ip address.*")
19    vlan_svi_records = list()
20
21    for vlan_interface in vlan_interfaces:
22        vlan_svi_record = dict()
23
24        ipv4_addr = vlan_interface.re_match_iter_typed(r"ip\saddress\s(\S+\s+\S+)", result_type=IPv4Obj)
25
26        vlan_interface_string = vlan_interface.text.lstrip("interface ")
27        vlan_id = vlan_interface_string.lstrip("Vlan")
28
29        vlan_svi_record['ipv4_addr'] = str(ipv4_addr.ip)
30        vlan_svi_record['ipv4_netmask'] = str(ipv4_addr.netmask)
31        vlan_svi_record['vlan_id'] = vlan_id
32        vlan_svi_records.append(vlan_svi_record)
33
34    print("DONE")
35    return vlan_svi_records

In line 18 we parse all interface Vlan blocks from the configuration using the method find_objects_w_child() from the given CiscoConfParse object. The results of this function are examined using the for-loop from line 22 to 32. This loop also populates our vlan_svi_records list with the information from the VLAN SVI interfaces. After this steps, we have the first part of our internal model, which looks similar to the following JSON file:

[
    {
        "ipv4_addr": "10.0.100.1",
        "ipv4_netmask": "255.255.255.0",
        "vlan_id": "100"
    }
]

parse the static ARP entries

So far, so good. Now we can use the vlan_svi_records dictionary and associate the static ARP entries to it. The following code example will parse these entries from the configuration and add the values to the corresponding IP interface within the model.

58static_arp_entries = parsed_config.find_objects("^arp\s(\S+\s+\S+)")
59
60for static_arp_entry in static_arp_entries:
61    arp_record = dict()
62
63    arr_obj = static_arp_entry.text.split()
64    ipv4 = arr_obj[1]
65    mac = arr_obj[2]
66
67    arp_ipv4_addr = IPv4Address(ipv4)
68
69    for vlan_svi in vlan_svis:
70        svi_ipv4_network = IPv4Network(vlan_svi['ipv4_addr'] + "/" + vlan_svi['ipv4_netmask'], strict=False)
71        if arp_ipv4_addr in svi_ipv4_network.hosts():
72            if "static_arps" not in vlan_svi.keys():
73                vlan_svi['static_arps'] = list()
74
75            record = {
76                'ipv4_host': ipv4,
77                'mac': mac
78            }
79            vlan_svi['static_arps'].append(record)
80
81            break

As you can see in line 58, we parse every command within the global configuration that starts with arp following an IP address. We create an IPv4Address object from the ARP entry within the lines 63 to 67. Then we go through our VLAN SVI list to check, if the current ARP entry is part of the subnet from the SVI. This check is performed in line 72. If the ARP entry is part of the subnet, we add the entry to the static_arps list within the vlan_svi object. You might note, that this code is not aware of static ARP definitions for VRF instances.

After this step, we have our final configuration model based on a python dictionary. It looks similar to the following JSON structure:

[
    {
        "ipv4_addr": "10.0.100.1",
        "ipv4_netmask": "255.255.255.0",
        "static_arps": [
            {
                "ipv4_host": "10.0.100.80",
                "mac": "0100.5e7f.9271"
            }
        ],
        "vlan_id": "100"
    }
]

That’s it. Now we have our own little data model and we can start to translate it in almost any configuration that we need.

create the configuration

As mentioned in the scenario at the beginning of the post, we need a configuration for Cisco NX-OS. Using the generated model, we can do this by using the following lines of code:

print("Write results to file...")
cisco_nxos_template = CiscoConfParse(['!'])

for vlan_svi in vlan_svis:
    cisco_nxos_template.append_line("interface Vlan%s" % vlan_svi['vlan_id'])
    for static_arp in vlan_svi['static_arps']:
        cisco_nxos_template.append_line(" ip arp %s %s" % (static_arp['ipv4_host'], static_arp['mac']))
    cisco_nxos_template.append_line('!')

cisco_nxos_template.save_as(os.path.join(output_dir, "cisco_nxos_config.txt"))

We also had almost the same code in the HSRP example and I think it is quite easy to read. At the end, we walk through our model and translate it into Cisco NX-OS configuration commands.

Conclusion

I hope this post helps to understand how easy the work with existing configuration is, if you use the ciscoconfparse module. The most scripts that I’ve created for this type of use case work in a similar fashion: extract a common data model and reflect it to whatever configuration or format you need. This model driven approach is very common and the idea is similar to the SMI (for SNMP) or YANG (for NETCONF) data format. Because I don’t need any interaction with other systems, I can simply create my own one :simple_smile:

If you have any questions, feedback or even additional or alternative use cases please use the comments section below. Thanks for reading.


Links within this post