Parse Cisco IOS configurations using ciscoconfparse

In my last post, I took a look on how to parse information from a Cisco IOS configuration using regular expressions. This post focuses on the same use case as the last one, but this time I use the ciscoconfparse library. The use of the library doesn’t mean that you can ignore regular expressions at all. You need at least a basic understanding of it. Before continuing, I highly recommend to read my last post about Parse Cisco IOS configurations using RegEx. I will reuse some of the RegEx and skip the detailed explanation in this post.

Within the examples, I like to extract the following information from the Cisco IOS configuration:

  • check if OSPFv2 is used as routing protocol
  • extract the interface name and description
  • extract the current IPv4 address (if any) from the interface

You can find the entire example code on my python script examples repository on GitHub.

Before we start

Ciscoconfparse helps when working with Cisco IOS configurations by building a relationship between the command hierarchies. This simplifies the search within the structure of the config (e.g. “give me all interfaces that are configured with X”).

Before start to gather information from a configuration, we need to create a new instance of the CiscoConfParse class. To create an new object, we have two choices:

  • use a text file that is stored on the hard disk or
  • use a python list that contains every command lines

Within the example code, we use the file based approach. The following snippet creates a new instance based on the example_config.txt file:

from ciscoconfparse import CiscoConfParse

if __name__ == "__main__":
    # create CiscoConfParse object using a configuration file that is stored in the 
    # same directory as the script
    confparse = CiscoConfParse("example_config.txt")

    # ... work with the confparse object

Now we are able to work with the configuration.

find commands with ciscoconfparse

First, we like to check if OSPFv2 is used within the configuration. We do this by searching the router ospf command within the configuration. The code is quite similar compared to the regular expression example, but I think it is more readable and easier to understand.

To check that a commands is part of the configuration, we will use the has_line_with() method. The following example shows how to check for the expected configuration command:

# check if OSPF is used as the routing protocol
# the following regex_pattern matches only the "router ospf <process-id>" command (no VRFs)
ospf_regex_pattern = r"^router ospf \d+$"

# in this case, we will simply check that the ospf router command is part of the config
is_ospf_in_use = confparse.has_line_with(ospf_regex_pattern)

if is_ospf_in_use:
    print("==> OSPF is used in this configuration")
    result["features"].append("ospf")
else:
    print("==> OSPF is not used in this configuration")

If you read the regular expression post, you will see that there is not a big difference in the examples at this point. The only exception is the has_line_with() method call, which avoids the use of the cumbersome conditional expression that was part of the RegEx example.

parse the interface names and description

The ciscoconfparse library is also helpful to extract parameters that are stored within different configuration levels, e.g. the interface configuration. Within the regular expression post, this was a quite complex expression that we’ve created (dealing with whitespace, indentation etc.).

The regular expressions for the line statements are the same, but we don’t care about the order and structure of the file. The CiscoConfParse class provides a way to find all commands that match a certain regular expression: find_objects(). This method returns a list of IOSCfgLine objects that references the matching configuration commands. These objects also allow the search within all child elements using the re_search_children() method. After this point, we just need to modify the strings according our requirements: strip the interface part to get the interface name and the description to get the description of the interface.

The following snippet shows the relevant python code from the example:

# extract the interface name and description
# first, we get all interface commands from the configuration
interface_cmds = confparse.find_objects(r"^interface ")

# iterate over the resulting IOSCfgLine objects
for interface_cmd in interface_cmds:
    # get the interface name (remove the interface command from the configuration line)
    intf_name = interface_cmd.text[len("interface "):]
    result["interfaces"][intf_name] = {}

    # search for the description command, if not set use "not set" as value
    result["interfaces"][intf_name]["description"] = "not set"
    for cmd in interface_cmd.re_search_children(r"^ description "):
        result["interfaces"][intf_name]["description"] = cmd.text.strip()[len("description "):]

work with strings

Within the last example, you may wonder about the following expression:

intf_name = interface_cmd.text[len("interface "):]

It looks very complex and why not use the strip() function? In the past I had some bad experience with it, because I got some unexpected results. This is the reason, why I usually use an alternative approach that I like to explain now in more detail.

The text attribute from the IOSCfgLine object returns the associated configuration command as a string. The expression within the square brackets is used for slicing. It returns a string starting after the “interface ” part. The following statement has the same result as the example above:

intf_name = interface_cmd.text[10:]

We get the rest of the string, starting at the 11th character (a string is zero-based indexed, therefore 10 equals the 11th character). To dig deeper into this topic, please take a look at the list chapter in the official python introduction that describes this topic in more detail.

Now you may ask, why use the first expression? From my perspective, it is more explicit and readable. I recognize fast, what happens and what should be stripped from the string. One drawback of this approach is that it doesn’t check the content before. The string can contain almost anything and the result can be something unexpected. In the example, ciscoconfparse ensures that the command starts with “interface“.

parse the IPv4 address of an interface

Now we got some basic information about the interface. I think this was a lot easier than the approach using regular expressions. If we need to extract the IPv4 address from an interface, ciscoconfparse can also help in this case. It is possible to change the result type of the IOSCfgLine.re_match_iter_types() method. In our case, we need use the IPv4Obj as result type, which is also part of the ciscoconfparse library. It is more or less just a wrapper around the ipaddress module that is included with python3. I already wrote about the ipaddress module in python in the past.

Everything starts again with the re_search_children() method. This time we need all interfaces, that defines an ip address following an IPv4 address (to avoid a match on the no ip address command). In a second step, we can extract the IPv4Obj using the re_match_iter_typed() method on the resulting IOSCfgLine object. The following example shows, how we can extract the IPv4 address:

# extract the interface name and description
# first, we get all interface commands from the configuration
interface_cmds = confparse.find_objects(r"^interface ")

# iterate over the resulting IOSCfgLine objects
for interface_cmd in interface_cmds:

    # ... read interface name and description
    # extract IP addresses if defined
    IPv4_REGEX = r"ip\saddress\s(\S+\s+\S+)"
    for cmd in interface_cmd.re_search_children(IPv4_REGEX):
        # ciscoconfparse provides a helper function for this task
        ipv4_addr = interface_cmd.re_match_iter_typed(IPv4_REGEX, result_type=IPv4Obj)

        result["interfaces"][intf_name].update({
            "ipv4": {
                "address": ipv4_addr.ip.exploded,
                "netmask": ipv4_addr.netmask.exploded
            }
        })

Okay, that’s it. Now we can do whatever we want with the information from the configuration.

Conclusion

Compared to the regular expression post, we just need 61 lines of code compared to 79 lines. Furthermore, the code is (at least for me) more readable. It is also more robust and easier to understand. One drawback is that the use of this library adds an additional dependency to your project. If you try to extract more parameters, the regular expression approach is quite difficult to extend and maintain. If you have the choice, i recommend the use of the ciscoconfparse library whenever possible.

At this point, I highly recommend the official documentation about it at pennington.net, where you can find a lot of additional examples. I can highly recommend the very nice configuration diff example.

That’s it for today, thank you for reading.


Links within this post