Blog Detail
Parsing network device configurations can be a difficult task. Automatically determining a remediation plan by comparing a running configuration to a generated template configuration can be even more difficult. In this blog, I’ll discuss Hierarchical Configuration, hier_config for short, and how it can be leveraged to streamline a business’s configuration compliance needs.
What Is Hierarchical Configuration?
Hier_config is a Python library that is able to consume a running configuration of a network device, compare it against its intended templated configuration, and build the remediation steps necessary to bring a device into spec with its intended configuration. Hier_config has been used extensively on Cisco IOS, NX-OS, IOS XR, IOS XE, and Arista EOS devices. However, any network operating system that utilizes a CLI syntax that is structured in a similar fashion to IOS should work mostly out of the box.
The biggest advantage that I’ve seen of hier_config vs other attempts at configuration compliance is that hier_config has a built-in understanding of parent / child relationships within a configuration.
For example:
router bgp 65000
neighbor 192.0.2.1 remote-as 65001
address-family ipv4 unicast
neighbor neighbor 192.0.2.1 activate
router bgp 65000 is the parent object for the above configuration snippet. neighbor 192.0.2.1 remote-as 65001 and address-family ipv4 unicast are children of router bgp 65000. Finally neighbor neighbor 192.0.2.1 activate is a child of address-family ipv4 unicast.
Hier_config understands those relationships within the configuration, whereas most products do not. This makes hier_config a very powerful configuration compliance tool when it comes to defining remediation steps. Instead of rewriting the entire parent / child hierarchy of objects, hier_config can be defined to only remediate the specific child steps as necessary.
Another advantage is the ability for hier_config to understand what commands are idempotent, require negation, and when to negate a command. Take updating a description of an interface. You don’t need to negate an existing interface description to apply another description. You just update the description on the interface and move on. Another example could be updating logging or snmp servers on a configuration. You very likely want to continue sending logging and snmp updates to the old servers until the new servers have been applied. Therefore, the correct order to apply the commands would be to add the new servers and then remove the old servers. Being able to define these nuances will be covered in the Hierarchical Configuration Options
section.
How Can Hierarchical Configuration Be Installed?
Hier_config requires a minimum Python version of 3.8.
There are two methods of installing hier_config. The first method is to install it from PyPI via pip.
pip install hier_config
The second method is to install it from GitHub.
- Install Poetry
- Clone the repository:
git clone https://github.com/netdevops/hier_config.git
- Install hier_config:
cd hier_config && poetry install
Hierarchical Configuration Basics
Many examples have references to files. Those files are publicly available in the ./tests/fixtures folder of the GitHub repository.
The very basic usage of hier_config is very easy. Let’s break down the below sample script.
#!/usr/bin/env python3
# Import the hier_config Host library
from hier_config import Host
# Create a hier_config Host object
host = Host(hostname="aggr-example.rtr", os="ios")
# Load a running configuration from a file
host.load_running_config_from_file("./tests/fixtures/running_config.conf")
# Load an intended configuration from a file
host.load_generated_config_from_file("./tests/fixtures/generated_config.conf")
# Create the remediation steps
host.remediation_config()
# Display the remediation steps
print(host.remediation_config_filtered_text(include_tags={}, exclude_tags={}))
With six lines of code, you can automatically create the remediation steps to bring a device into spec with its intended or generated configuration. Those steps are:
- Import the hier_config Host object.
- Create a Host object.
- Load the running configuration into the Host object.
- Load the generated configuration into the Host object.
- Initialize the remediation.
- Display the remediation steps.
The output of the remediation steps for the above example is:
vlan 3
name switch_mgmt_10.0.3.0/24
vlan 4
name switch_mgmt_10.0.4.0/24
interface Vlan2
mtu 9000
ip access-group TEST in
no shutdown
interface Vlan3
description switch_mgmt_10.0.3.0/24
ip address 10.0.3.1 255.255.0.0
interface Vlan4
mtu 9000
description switch_mgmt_10.0.4.0/24
ip address 10.0.4.1 255.255.0.0
ip access-group TEST in
no shutdown
The actual differences between the two configurations are:
% diff ./tests/fixtures/running_config.conf ./tests/fixtures/generated_config.conf
9a10,12
> name switch_mgmt_10.0.3.0/24
> !
> vlan 4
12a16
> mtu 9000
15c19,20
< shutdown
---
> ip access-group TEST in
> no shutdown
18a24,30
> description switch_mgmt_10.0.3.0/24
> ip address 10.0.3.1 255.255.0.0
> ip access-group TEST in
> no shutdown
> !
> interface Vlan4
> mtu 9000
At its core, that is the most very basic example of hier_config.
Lineage Rules Explained
Lineage rules are rules that are written in YAML. They allow users to seek out very specific sections of configurations or even seek out very generalized lines within a configuration. For example, suppose you just wanted to seek out interface descriptions. Your lineage rule would look like:
- lineage:
- startswith: interface
- startswith: description
In the above example, a start of a lineage is defined with the – lineage: syntax. From there the interface is defined with the – startswith: interface syntax under the – lineage: umbrella. This tells hier_config to search for any configuration that starts with the string interface as the parent of a configuration line. When it finds an interface parent, it then looks at any child configuration line of the interface that starts with the string description.
With lineage rules, you can get as deep into the children or as shallow as you need. Suppose you want to inspect the existence or absence of http, ssh, snmp, and logging within a configuration. This can be done with a single lineage rule, like so:
- lineage:
- startswith:
- ip ssh
- no ip ssh
- ip http
- no ip http
- snmp-server
- no snmp-server
- logging
- no logging
Or suppose, you want to inspect whether BGP IPv4 AFIs are activated. You can do this with the following:
- lineage:
- startswith: router bgp
- startswith: address-family ipv4
- endswith: activate
In the above example, I utilized a different keyword to look for activated BGP neighbors. The keywords that can be utilized within lineage rules are:
- startswith
- endswith
- contains
- equals
- re_search
You can also put all of the above examples together in the same set of lineage rules like so:
- lineage:
- startswith: interface
- startswith: description
- lineage:
- startswith:
- ip ssh
- no ip ssh
- ip http
- no ip http
- snmp-server
- no snmp-server
- logging
- no logging
- lineage:
- startswith: router bgp
- startswith: address-family ipv4
- endswith: activate
When hier_config consumes the lineage rules, it consumes them as a list of lineage rules and processes them individually.
Tagging Configuration Sections
With a firm understanding of lineage rules, more complex use cases become available within hier_config. A powerful use case is the ability to tag specific sections of configuration and only display remediations based on those tags. This becomes very handy when you’re attempting to execute a maintenance that only targets low risk configuration changes or isolate the more risky configuration changes to scrutinize their execution during a maintenance.
Tagging expands on the use of the lineage rules by creating an add_tags keyword to a lineage rule.
Suppose you had a running configuration that had an ntp configuration that looked like:
ntp server 192.0.2.1 prefer version 2
However, your intended configuration utilized a publicly available NTP server on the internet:
ip name-server 1.1.1.1
ip name-server 8.8.8.8
ntp server time.nist.gov
You could create a lineage rule that targeted that specific remediation like this:
- lineage:
- startswith:
- ip name-server
- no ip name-server
- ntp
- no ntp
add_tags: ntp
Now we can modify the script above to load the tags and create a remediation of the said tags:
#!/usr/bin/env python3
# Import the hier_config Host library
from hier_config import Host
# Create a hier_config Host object
host = Host(hostname="aggr-example.rtr", os="ios")
# Load the tagged lineage rules
host.load_tags_from_file("./tests/fixtures/tags_ios.yml")
# Load a running configuration from a file
host.load_running_config_from_file("./tests/fixtures/running_config.conf")
# Load an intended configuration from a file
host.load_generated_config_from_file("./tests/fixtures/generated_config.conf")
# Create the remediation steps
host.remediation_config()
# Display the remediation steps for only the "ntp" tags
print(host.remediation_config_filtered_text(include_tags={"ntp"}, exclude_tags={}))
In the script, we made two changes. The first change is to load the tagged lineage rules: host.load_tags_from_file("./tests/fixtures/tags_ios.yml")
. And the second is to filter the remediation steps by only including steps that are tagged with ntp via the include_tags argument.
The remediation looks like:
no ntp server 192.0.2.1 prefer version 2
ip name-server 1.1.1.1
ip name-server 8.8.8.8
ntp server time.nist.gov
Hierarchical Configuration Options
There are a number of options that can be loaded into hier_config to make it better conform to the nuances of your network device. By default, hier_config loads a set of sane defaults for Cisco IOS, IOS XE, IOS XR, NX-OS, and Arista EOS.
Below are the configuration options available for manipulation.
base_options: dict = {
"style": None,
"sectional_overwrite": [],
"sectional_overwrite_no_negate": [],
"ordering": [],
"indent_adjust": [],
"parent_allows_duplicate_child": [],
"sectional_exiting": [],
"full_text_sub": [],
"per_line_sub": [],
"idempotent_commands_blacklist": [],
"idempotent_commands": [],
"negation_default_when": [],
"negation_negate_with": [],
}
The default options can be completely overwritten and loaded from a yaml file, or individual components of the options can be manipulated to provide the functionality that is desired.
Here is an example of manipulating the built-in options.
# Import the hier_config Host library
from hier_config import Host
# Create a hier_config Host object
host = Host(hostname="aggr-example.rtr", os="ios")
# Create an NTP negation ordered lineage rule
ordered_negate_ntp = {"lineage": [{"startswith": ["no ntp"], "order": 700}]}
# Update the hier_config options "ordering" key.
host.hconfig_options["ordering"].append(ordered_negate_ntp)
Here is an example of completely overwriting the default options and loading in your own.
# import YAML
import yaml
# Import the hier_config Host library
from hier_config import Host
# Load the hier_config options into memory
with open("./tests/fixtures/options_ios.yml") as f:
options = yaml.load(f.read(), Loader=yaml.SafeLoader)
# Create a hier_config Host object
host = Host(hostame="aggr-example.rtr", os="ios", hconfig_options=options)
In the following sections, I’ll cover the most common options.
style
The style defines the os family. Such as ios, iosxr, etc.
Example:
style: ios
ordering
Ordering is one of the most useful hier_config options. This allows you to use lineage rules to define the order in which remediation steps are presented to the user. For the ntp example above, the ntp server was negated (no ntp server 192.0.2.1
) before the new ntp server was added. In most cases, this wouldn’t be advantageous. Thus, ordering can be used to define the proper order to execute commands.
All commands are assigned a default order weight of 500, with a usable order weight of 1 – 999. The smaller the weight value, the higher on the list of steps a command is to be executed. The larger the weight value, the lower on the list of steps a command is to be executed. To create an order in which new ntp servers are added before old ntp servers are removed, you can create an order lineage that weights the negation to the bottom.
Example:
ordering:
- lineage:
- startswith: no ntp
order: 700
With the above order lineage applied, the output of the above ntp example would look like:
ip name-server 1.1.1.1
ip name-server 8.8.8.8
ntp server time.nist.gov
no ntp server 192.0.2.1 prefer version 2
sectional_exiting
Sectional exiting features configuration sections that have a configuration syntax that defines the end of a configuration section. Examples of this are RPL (route policy language) configurations in IOS XR or peer policy and peer session configurations in IOS BGP sections. The sectional exiting configuration allows you to define the configuration syntax so that hier_config can render a remediation that properly exits those configurations.
An example of sectional exiting is:
sectional_exiting:
- lineage:
- startswith: router bgp
- startswith: template peer-policy
exit_text: exit-peer-policy
- lineage:
- startswith: router bgp
- startswith: template peer-session
exit_text: exit-peer-session
sectional_overwrite_no_negate
The sectional overwrite with no negate hier_config option will completely overwrite sections of configuration without negating them. This option is often used with the RPL sections of IOS XR devices that require that the entire RPL be re-created when making modifications to them, rather than editing individual lines within the RPL.
An example of sectional overwrite with no negate is:
sectional_overwrite_no_negate:
- lineage:
- startswith: as-path-set
- lineage:
- startswith: prefix-set
- lineage:
- startswith: route-policy
- lineage:
- startswith: extcommunity-set
- lineage:
- startswith: community-set
sectional_overwrite
Sectional overwrite is just like sectional overwrite with no negate, except that hier_config will negate a section of configuration and then completely re-create it.
full_text_sub
Full text sub allows for substitutions of a multi-line string. Regular expressions are commonly used and allowed in this section. An example of this would be:
full_text_sub:
- search: "banner\s(exec|motd)\s(\S)\n(.*\n){1,}(\ 2)"
replace: ""
This example simply searches for a banner message in the configuration and replaces it with an empty string.
per_line_sub
Per line sub allows for substitutions of individual lines. This is commonly used to remove artifacts from a running configuration that don’t provide any value when creating remediation steps.
An example is removing lines such as:
Building configuration...
Current configuration : 3781 bytes
Per line sub can be used to remove those lines:
per_line_sub:
- search: "Building configuration.*"
replace: ""
- search: "Current configuration.*"
replace: ""
idempotent_commands
Idempotent commands are commands that can just be overwritten and don’t need negation. Lineage rules can be created to define those commands that are idempotent.
An example of idempotent commands are:
idempotent_commands:
- lineage:
- startswith: vlan
- startswith: name
- lineage:
- startswith: interface
- startswith: description
The lineage rules above specify that defining a vlan name and updating an interface description are both idempotent commands.
Conclusion
Hierarchical Configuration is a very powerful configuration compliance tool for building and executing upon rendered remediation configurations. There is also an Ansible Collection that can greatly simplify remediation work. If you have any questions, comments, or issues, please feel free to reach out on the Network to Code Slack on #hier_config
or open a GitHub issue.
-James
Contact Us to Learn More
Share details about yourself & someone from our team will reach out to you ASAP!