.. _tutorial: =============== NetworkParse Tutorial =============== `networkparse` is designed to make navigating around a hierarchical network configuration file as simple as possible. Example Configuration -------------------------------- All the examples below are based off this configuration: .. testcode:: running_config_contents = """ ! version 12.4 service nagle no service pad service tcp-keepalives-in ! hostname Foo ! boot-start-marker boot-end-marker ! security authentication failure rate 4 log security passwords min-length 6 ! interface FastEthernet0/0 ip address 172.16.2.1 255.255.255.0 ip access-group ETH0_0_IN in ip access-group BLACKHOLE out no ip unreachables no ip proxy-arp ip nat inside ip virtual-reassembly ip tcp adjust-mss 1452 load-interval 30 speed 100 full-duplex no keepalive no cdp log mismatch duplex hold-queue 100 in hold-queue 100 out ! interface FastEthernet0/1 ip address 172.16.3.1 255.255.255.0 no ip unreachables ! interface FastEthernet1/0 ip address 172.16.4.1 255.255.255.0 no ip unreachables shutdown ! """.strip() .. note:: We call `strip()` on the configuration. This isn't necessary, it just removes the beginning and ending blank lines. For Cisco-style network devices, the config text is expected to be the exact output of `show running-config`, `show running-config all`, or `show startup-config`. The exact supported commands are documented for each parser in their respective classes. See :ref:`Parsing` for more information. Step 1: Import the configuration -------------------------------- The first step in using `networkparse` will always be to import the network configuration: .. testcode:: from networkparse import parse config = parse.ConfigIOS(running_config_contents) print(config.tree_display(line_number=True, child_count=True)) .. testoutput:: :options: +NORMALIZE_WHITESPACE 1: ! (0 children) 2: version 12.4 (0 children) 3: service nagle (0 children) 4: no service pad (0 children) 5: service tcp-keepalives-in (0 children) 6: ! (0 children) 7: hostname Foo (0 children) 8: ! (0 children) 9: boot-start-marker (0 children) 10: boot-end-marker (0 children) 11: ! (0 children) 12: security authentication failure rate 4 log (0 children) 13: security passwords min-length 6 (0 children) 14: ! (0 children) 15: interface FastEthernet0/0 (15 children) 16: ip address 172.16.2.1 255.255.255.0 (0 children) 17: ip access-group ETH0_0_IN in (0 children) 18: ip access-group BLACKHOLE out (0 children) 19: no ip unreachables (0 children) 20: no ip proxy-arp (0 children) 21: ip nat inside (0 children) 22: ip virtual-reassembly (0 children) 23: ip tcp adjust-mss 1452 (0 children) 24: load-interval 30 (0 children) 25: speed 100 (0 children) 26: full-duplex (0 children) 27: no keepalive (0 children) 28: no cdp log mismatch duplex (0 children) 29: hold-queue 100 in (0 children) 30: hold-queue 100 out (0 children) 31: ! (0 children) 32: interface FastEthernet0/1 (2 children) 33: ip address 172.16.3.1 255.255.255.0 (0 children) 34: no ip unreachables (0 children) 35: ! (0 children) 36: interface FastEthernet1/0 (3 children) 37: ip address 172.16.4.1 255.255.255.0 (0 children) 38: no ip unreachables (0 children) 39: shutdown (0 children) 40: ! (0 children) :func:`~networkparse.core.ConfigLineList.tree_display` is a convenience function for displaying the contents of a config or list of configuration lines. When building out a series of searches to check a configuration, use :func:`~networkparse.core.ConfigLineList.tree_display` to help with debugging or show the final lines of interest. .. note:: We called `tree_display()` with `line_number=True` here. For the remainder of the examples we won't do this. Step 2: Simple Searches -------------------------------- Exact Matches ++++++++++++++++++++++++++++++++ Let's say we want to ensure this device is running the firmware version we expect. To do this, we'll use :func:`~networkparse.core.ConfigLineList.filter` to get a list of all matching configuration lines: .. testcode:: lines = config.filter("version 12.4") print(lines.tree_display(child_count=True)) if lines: print("Version found") else: print("Version not found") .. testoutput:: :options: -ELLIPSIS, +NORMALIZE_WHITESPACE version 12.4 (0 children) Version found Great! We found the matching line. If we were expecting a newer version of firmware: .. testcode:: lines = config.filter("version 15.0") print(lines.tree_display(child_count=True)) if lines: print("Version found") else: print("Version not found") .. testoutput:: :options: -ELLIPSIS, +NORMALIZE_WHITESPACE (empty line list) Version not found Regular Expressions ++++++++++++++++++++++++++++++++ In the previous example, we used a string and searched for an exact match. Now we just want to explore which services are enabled or disabled on the device. There are two approaches here, both of which will produce the same result: .. testcode:: # Allow the string to match any where in the line, rather that requiring # it to match the entire line print("full_match=False:") lines = config.filter("service", full_match=False) print(lines.tree_display(child_count=True)) # Give a regular expression which allows "anything before this or anything after this" print("\nRegular expression:") lines = config.filter(".*service.*") print(lines.tree_display(child_count=True)) .. testoutput:: :options: -ELLIPSIS, +NORMALIZE_WHITESPACE full_match=False: service nagle (0 children) no service pad (0 children) service tcp-keepalives-in (0 children) Regular expression: service nagle (0 children) no service pad (0 children) service tcp-keepalives-in (0 children) `networkparse` is using the `Python 3 re library`_ under the hood, so any supported regular expression there may be used with :func:`~networkparse.core.ConfigLineList.filter`. .. _`Python 3 re library`: https://docs.python.org/3/library/re.html Step 3: Navigating Results -------------------------------- Let's say we wanted to check the IP address of each of our interfaces. There are several approaches to this question, each of which is explored below. Accessing Children ++++++++++++++++++++++++++++++++ Our first attempt at this will be find each interface, then get the `ip address` call within it. .. testcode:: interfaces = config.filter("interface .+") for interface in interfaces: # You can get the exact content of a line by teating it like a string print(interface) # You can access the children of a configuration line using .children addr = interface.children.filter("ip address .*").one() print(addr) print() .. testoutput:: :options: -ELLIPSIS, +NORMALIZE_WHITESPACE interface FastEthernet0/0 ip address 172.16.2.1 255.255.255.0 interface FastEthernet0/1 ip address 172.16.3.1 255.255.255.0 interface FastEthernet1/0 ip address 172.16.4.1 255.255.255.0 In this example, we first get all the interfaces using :func:`~networkparse.core.ConfigLineList.filter`, which returns a list of configuration lines (a :class:`~networkparse.core.ConfigLinelist`). We then loop through that list, using :attr:`~networkparse.core.ConfigLine.children` to access the configuration lines under each interface. :attr:`~networkparse.core.ConfigLine.children` is a :class:`~networkparse.core.ConfigLinelist` just like our base configuration object, so :func:`~networkparse.core.ConfigLineList.filter` can be used again. .. note:: On the line :code:`addr_line = interface.children.filter("ip address .*").one()`, we called :func:`~networkparse.core.ConfigLineList.one` at the end. `filter()` returns a :class:`~networkparse.core.ConfigLineList`, which may be any number of configuration lines. Calling `one()` on a list when you expect only a single item will return just the single result, along with doing some error checking to make sure the item actually exists. Accessing Parents ++++++++++++++++++++++++++++++++ Approach number two would be to find all the `ip address` calls and find the associated interface from that. .. testcode:: # Using "depth=None", filter will find both direct children of line list OR # lines under any of the children addr_lines = config.filter("ip address .+", depth=None) for addr in addr_lines: interface = addr.parent print(interface) print(addr) print() .. testoutput:: :hide: :options: -ELLIPSIS, +NORMALIZE_WHITESPACE interface FastEthernet0/0 ip address 172.16.2.1 255.255.255.0 interface FastEthernet0/1 ip address 172.16.3.1 255.255.255.0 interface FastEthernet1/0 ip address 172.16.4.1 255.255.255.0 Output will be the same as :ref:`Accessing Children`. Step 5: Filtering by Children -------------------------------- Often when looking at interfaces, VLANs, or ACLs you'll need to find only those items that are configured a certain way. You could do this the manual way, using a pattern similar to what was shown in :ref:`Accessing Parents`, but `networkparse` also offers :func:`~networkparse.core.ConfigLineList.filter_with_child`. If we wanted to find any interfaces that are shutdown: .. testcode:: interfaces = config.filter("interface .+").filter_with_child("shutdown") print(interfaces.tree_display(child_count=True)) .. testoutput:: :options: -ELLIPSIS, +NORMALIZE_WHITESPACE interface FastEthernet1/0 (3 children) ip address 172.16.4.1 255.255.255.0 (0 children) no ip unreachables (0 children) shutdown (0 children) In a single line, we do two steps: 1. Find all interfaces 2. From that list of config lines, remove any that don't have "shutdown" as a child .. note:: Because :func:`~networkparse.core.ConfigLineList.filter`'s (and :func:`~networkparse.core.ConfigLineList.filter_with_child`'s) default to requiring a full line match, our search won't accidentally match "no shutdown" lines as well. Step 6: Parsing Lines -------------------------------- In many auditing situations you need to check configuration parameters against an "acceptable" value. For example, let's say we need to verify the authentication failure rate is less than 5. For this, `networkparse` usage relies on `standard Python string functions`_ like `split()`. In more advanced cases, using the `re`_ module with `match groups`_. .. _`standard Python string functions`: https://docs.python.org/3/library/string.html .. _`re`: https://docs.python.org/3/library/re.html .. _`match groups`: https://regexone.com/lesson/capturing_groups First, a verbose version: .. testcode:: # Find our line auth_line = config.filter(r"security authentication failure rate .*").one() print(f"Line: {auth_line}") # Break the line up at each space parts = auth_line.split() print(parts) # Get the number as text and convert it to a number so we can comparse rate = int(parts[4]) print(f"Rate: {rate}") if rate < 5: print("Rate is correct") else: print("Rate is too high") .. testoutput:: :options: -ELLIPSIS, +NORMALIZE_WHITESPACE Line: security authentication failure rate 4 log ['security', 'authentication', 'failure', 'rate', '4', 'log'] Rate: 4 Rate is correct In the real world, you'll likely be more succinct: .. testcode:: auth_line = config.filter(r"security authentication failure rate .*").one() rate = int(auth_line.split()[4]) if rate < 5: print("Rate is correct") else: print("Rate is too high") .. testoutput:: :options: -ELLIPSIS, +NORMALIZE_WHITESPACE Rate is correct Next Steps -------------------------------- The API documentation displays all the functionality available in `networkparse`, including methods not covered here and more arguments available on :func:`~networkparse.core.ConfigLineList.filter`.