This Python library helps navigating and querying textual (CLI-style) configs of network devices. It may be useful for solving such problems like:
- listing a device interfaces
- extracting IP address or VLAN configurations of a network interface
- checking if a particular option is properly set in all the relevant blocks
It does not support modifying and comparing of configs, as the CiscoConfigParse does, but provides a nice and simple query API.
Netcop works with both Python 2.7 and Python 3.
To install it as a package, use this command:
python3 -m pip install netcop
Netcop works by parsing hierarchical text configs that use newline-separated statements, whitespace indentation of blocks and keywords prefixes as a config path. Thus, it is not limited to a particular vendor's syntax.
In particular, these types of configs are supported by Netcop:
- Cisco IOS, NX-OS, IOS-XR
- Huawei VRP
- Juniper JunOS
- Quagga / FRR
There should be many more of them, I have not checked others yet.
However, Netcop does not have any idea of the config semantics — it can't guess the type of data relying by a given config path whether it is a list, an int, a string or an IP address. It's always a user who knows the semantics and treats a given path to be of a particular type.
Let's say we have this simple config to parse:
c = netcop.Conf('''
interface Port-channel1
no ip address
!
interface Port-channel2
ip address 10.0.0.2 255.255.0.0
ip address 10.1.0.2 255.255.0.0 secondary
!
interface Loopback0
ip address 1.1.1.1 255.255.255.255
!
''')Below are some examples of processing this config.
The result of parsing looks very much like a Python dict and Netcop tries hard to keep its API similar to what you can expect from a dict.
The key operation you can do with a Conf object is to get a sub-object by a string key with the [] operator.
Then we just use any of the following expressions to get a part (slice) of the config as another Conf object that has the same API for subsequent queries:
c['interface']c['interface Port-channel1']c['interface Port-channel2 ip address']
To illustrate the way a config tree is organized, let's take a look at these three queries that return the same result:
c['interface Port-channel2 ip address']c['interface']['Port-channel2 ip']['address']c['interface']['Port-channel2']['ip address']
Unlike the dict's [], this operator never causes the KeyError exception, it returns an empty Conf object instead.
To obtain a sequence of interface names, you just need to use .keys() method:
[i for i in c['interface'].keys()]Output:
['Port-channel1', 'Port-channel2', 'Loopback0']
Just as it is with a dict, you can get the same result by iterating over an object:
[i for i in c['interface']]Likewise, to get IP addresses assigned to all the interfaces, use this snippet:
for ifname in c['interface']:
for ip in c['interface'][ifname]['ip address']:
print(ifname, ip)Output:
Port-channel2 10.0.0.2
Port-channel2 10.1.0.2
Loopback0 1.1.1.1
Just as it is with a dict, you can use .items() to avoid redundant key lookups (resulting output is the same):
for ifname, iface_c in c['interface'].items():
for ip in iface_c['ip address']:
print(ifname, ip)There're 3 ways of traversing lines of the config, or a sub-part of it:
Conf.tails: returns the list of matched lines tails, excluding matched prefix. Does not return lines from the nested blocks. Lines are trimmed from whitespace.
cfg = Conf("""
snmp host A
snmp host B
""")
print(cfg["snmp"].tails)Output:
["host A", "host B"]
Conf.lines(): Iterates over raw matched lines, like.dump()does. Lines may be trimmed, if you specified prefix that covers the line partially, and may not be trimmed if the prefix is not specified.Conf.orig_lines(): Like.lines(), but lines are always in the same form as in the original config (full and untrimmed), no matter which prefix is specified.
In a bool context a Conf object returns if it's empty, or in other words, if a specified config path exists.
# __bool__ operator works:
bool(c) == True
bool(c['interface Loopback0']) == True
bool(c['interface Blah']) == False
bool(c['interface Port-channel1 no ip address']) == True
bool(c['interface Port-channel2 no ip address']) == False
# same for __contains__ operator:
('interface Loopback0' in c) == True
('interface Blah' in c) == False
('interface Port-channel1 no ip address' in c) == True
('interface Port-channel2 no ip address' in c) == FalseSo far, we have seen how to iterate over multiple values by a given path. What if we're sure that there is only one value for a path? Then you can use any of the scalar properties of a Conf object:
.word- a single string keyword.int- an integer value.ip,.cidr(since Python 3.3) - aIPAddressorIPNetworkobject from theipaddressstandard library.tail- all the tailing keywords as a string
You should note that in contrast to indexing and iterating operations that can never fail, the scalar getters can raise KeyError or TypeError if there are no values for a given path, or if there are multiple ones.
Here is an example:
c['interface Loopback0 ip address'].word == '1.1.1.1'
c['interface Loopback0 ip address'].ip == IPv4Address('1.1.1.1')
c['interface Loopback0 ip address'].tail == '1.1.1.1 255.55.255.255'
c['interface Port-channel2 ip address'].ip # KeyError raisedThere are also list properties .ints, .ips and .cidrs, which are just type-converting iterators and shorthands for expressions like [int(x) for x in c].
Netcop does not use regular expressions to make index lookups, it requires exact keywords in the config path. However, there are cases when it is useful to specify a config path with a pattern:
for ifname, ip in c.expand('interface po* ip address *'):
print(ifname, ip)Resulting output:
Port-channel2 10.0.0.2
Port-channel2 10.1.0.2
The .expand() method iterates over all possible paths in a config by a given selector with wildcards using glob syntax. It returns tuples with the length equal to the number of wildcard placeholders in a given key.
There's a special trailing glob pattern supported ~. It means capture the rest of the line and can only occur at the end of the expand query string, separated by space.
Example:
>>> list(c.expand("interface * ip address ~"))
[
("Port-channel2", "10.0.0.2 255.255.0.0"),
("Port-channel2", "10.1.0.2 255.255.0.0 secondary"),
("Loopback0", "1.1.1.1 255.255.255.255"),
]The number of elements in the returned tuple always equals to the number of caputuring globs in the query string.
If the optional return_conf=True kwarg is passed, there's the extra trailing element in the resulting tuples with the subsequent Conf object for the matched prefix.