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) == False
So 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) - aIPAddress
orIPNetwork
object from theipaddress
standard 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 raised
There 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.