Parsing¶
We are going to see how to parse configuration by example. To do that we are going to use a modified/simplified version of the openconfig-interfaces
and openconfig-vlan
models. Note they have been slightly modified for simplicity and brevity.
Let’s start by looking at the ASCII tree representation of the model:
+--rw openconfig-interfaces:interfaces
| +--rw interface* [name]
| +--rw config
| | +--rw description? <string>
| | +--rw enabled? <boolean>
| | +--rw loopback-mode? <boolean>
| | +--rw mtu? <uint16>
| | +--rw name? <string>
| | +--rw type <identityref>
| +--rw name <leafref>
| +--rw subinterfaces
| +--rw subinterface* [index]
| +--rw config
| | +--rw description? <string>
| | +--rw enabled? <boolean>
| | +--rw index? <uint32>
| +--rw index <leafref>
+--rw openconfig-vlan:vlans
+--rw vlan* [vlan-id]
+--rw config
| +--rw name? <string>
| +--rw status? <enumeration>
| +--rw vlan-id? <vlan-id(uint16)>
+--rw vlan-id <leafref>
To write a parser you are going to need to write a class following the rules below:
- A grouping (either a list or a container) is represented by a class that inherits from
yangify.parsers.Parser
. - Classes that implement a part of the tree are nested in the parent object and are named as the grouping it implements.
- Each class may have a nested class named
Yangify
that inherits fromyangify.parsers.ParserData
. This class may implement code to help with the processing. See the API documentation for details. - Finally, leaves are processed by a function named after the leaf and will have to return an object of the correct type.
Using the openconfig-vlans:vlans
as an example, a parser may look like this:
class Vlans(Parser):
class vlan(Parser):
class Yangify(ParserData):
def extract_elements(self):
# code to extra each element from the config goes here
class config(Parser):
def vlan_id(self):
# code to parse the vlan id goes here
def name(self):
# code to parse the name goes here
def status(self):
# code to parse the vlan status goes here
def vlan_id(self):
# code to parse the vlan id goes here
This format may be a bit cumbersome in deeply nested models, hence, an alternative maybe to use classes in the global namespace instead, for instance:
class VlanConfig(Parser):
def vlan_id(self):
# code to parse the vlan id goes here
def name(self):
# code to parse the name goes here
def status(self):
# code to parse the vlan status goes here
class Vlan(Parser):
class Yangify(ParserData):
def extract_elements(self):
# code to extra each element from the config goes here
config = VlanConfig
def vlan_id(self):
# code to parse the vlan id goes here
class Vlans(Parser):
vlan = Vlan
Both are equivalent and both have their advantages and disadvantages when it comes to readibility. We are going to use the second method for this demo.
The openconfig-interfaces
parser¶
To explain how this works we are going to write a parser that reads a configuration belonging to an IOS device and maps it into the model. Let’s look at the configuration first:
[2]:
%cat data/ios/config.txt
interface FastEthernet1
description This is Fa1
shutdown
exit
!
interface FastEthernet1.1
description This is Fa1.1
exit
!
interface FastEthernet1.2
description This is Fa1.2
exit
!
interface FastEthernet3
description This is Fa3
no shutdown
exit
!
interface FastEthernet4
shutdown
exit
!
vlan 10
name prod
no shutdown
exit
!
vlan 20
name dev
shutdown
exit
!
Nothing very complex, just a few interfaces and subinterfaces and a couple of vlans. The code for this tutorial is in tutorial_parser.py
, so let’s start by importing it:
[3]:
import tutorial_parser
Now that we have imported the code let’s start looking at the code piece by piece. The starting point is going to be the class Interfaces
which will be used to parse openconfig-interfaces:interfaces
:
[4]:
show_code(tutorial_parser.Interfaces)
class Interfaces(Parser):
"""
Implements openconfig-interfaces:interfaces
"""
interface = Interface
Not much here. The container openconfig-interfaces:interfaces
only has a YANG list in the interface
node. As interface
is a grouping a different Parser
class is used. Let’s look at it:
[5]:
show_code(tutorial_parser.Interface)
class Interface(Parser):
"""
Implements openconfig-interfaces:interfaces/interface
"""
class Yangify(ParserData):
def extract_elements(self) -> Iterator[Tuple[str, Dict[str, Any]]]:
"""
IOS interfaces are in the root of the configuration. However,
subinterfaces are also in the root of the configuration. For
that reason we will have to make sure that we filter the
subinterfaces as we iterate over all the interfaces in the root
of the configuration. That's should be as easy as checking that
the interface name has not dot in it.
"""
for k, v in self.native["interface"].items():
# k == "#text" is due to a harmless artifact in the
# parse_indented_config function that needs to be addressed
if k == "#text" or "." in k:
continue
yield k, v
config = InterfaceConfig
subinterfaces = Subinterfaces
def name(self) -> str:
return self.yy.key
This one is more interesting. It has a few things:
- A
Yangify
class that implementsextract_elements
. This is mandatory asopenconfig-interfaces:interfaces/interface
is a YANG list. - It has a
config
attribute that corresponds to the containeropenconfig-interfaces:interfaces/interface/config
and points to theParser
classInterfaceConfig
- It also has a
subinterfaces
attribute that correspononds to the containeropenconfig-interfaces:interfaces/interface/subinterfaces
- Finally, it has a function to extract the leaf corresponding to
openconfig-interfaces:interfaces/interface/name
.
Two things to note here:
- As explained in the API section linked in the note block early in this document,
Yangify.extract_elements
is used to extract each element returning its key and relevant configuration data. - The key is automatically added to the dictionary inside
self.yy.keys
using the path to the node as the key and made easily available via the shortcut propertyself.yy.key
. This can be used, for instance, when returning the name of the interface, which we already know as it’s the same as key.
Now let’s look at the InterfaceConfig
class that implements openconfig-interfaces:interfaces/interface/config
:
[6]:
show_code(tutorial_parser.InterfaceConfig)
class InterfaceConfig(Parser):
"""
Implements openconfig-interfaces:interfaces/interface/config
"""
def description(self) -> Optional[str]:
return cast(Optional[str], self.yy.native.get("description", {}).get("#text"))
def enabled(self) -> bool:
shutdown = self.yy.native.get("shutdown", {}).get("#standalone")
if shutdown:
return False
else:
return True
def name(self) -> str:
return self.yy.key
def type(self) -> str:
if "Ethernet" in self.yy.key:
return "iana-if-type:ethernetCsmacd"
elif "Loopback" in self.yy.key:
return "iana-if-type:softwareLoopback"
else:
raise ValueError(f"don't know type for interface {self.yy.key}")
This container only has leaves so we only have functions named after those leaves that will return the extracted data.
Now let’s circle back to openconfig-interfaces:interfaces/interface
and head down to the subinterfaces
container, which was parsed with the Subinterfaces
class:
[7]:
show_code(tutorial_parser.Subinterfaces)
class Subinterfaces(Parser):
"""
Implements openconfig-interfaces:interfaces/interface/subinterfaces
"""
subinterface = Subinterface
Like the Interface
class, not much to look at here, let’s head down to the Subinterface
class:
[8]:
show_code(tutorial_parser.Subinterface)
class Subinterface(Parser):
"""
Implements openconfig-interfaces:interfaces/interface/subinterfaces/subinterface
"""
class Yangify(ParserData):
def extract_elements(self) -> Iterator[Tuple[str, Dict[str, Any]]]:
"""
IOS subinterfaces are in the root of the configuration and named following
the format ``$parent_interface.$index``. The model specifies the key is
the $index, which is just a number. These means we will need to:
1. Iterate over all the interfaces
2. Filter the ones that don't start by `$parent_interface.`
3. Extract the $index and return it
"""
# self.keys keeps a record of all the keys found so far in the current
# object. To access them you can use the full YANG path
parent_key = self.keys["/openconfig-interfaces:interfaces/interface"]
for k, v in self.root_native["interface"].items():
if k.startswith(f"{parent_key}."):
yield k, v
config = SubinterfaceConfig
def index(self) -> int:
return int(self.yy.key.split(".")[-1])
This almost looks identical to the Interface
class. Extracting the elements is a bit tricker as it’s explained in the code but it’s still quite similar. The index
function that extracts is also similar to the name
function in the Interface
class, however, the index is just what’s after the .
in the interface name, so we need to extract that from the key and convert it into an integer as that’s what the mode expects.
Finally, let’s look at the SubinterfaceConfig
class:
[9]:
show_code(tutorial_parser.SubinterfaceConfig)
class SubinterfaceConfig(Parser):
"""
Implements openconfig-interfaces:interfaces/interface/subinterfaces/subinterface/config
"""
def description(self) -> Optional[str]:
return cast(Optional[str], self.yy.native.get("description", {}).get("#text"))
def index(self) -> int:
return int(self.yy.key.split(".")[-1])
At this point I am sure there isn’t much to add here :)
Now that we have the parser classes we need to create the root parser. The root parser has the following functions:
- Allow the user pick and choose which parsers to use
- Initialize the configuration, if needed.
- Perform some post operations, if needed.
Using the parser¶
Our root class is going to load the Interfaces
parser we explored before and use parse_indented_config
to prepare the configuration:
[10]:
from yangify import parser
from yangify.parser.text_tree import parse_indented_config
class IOSParser(parser.RootParser):
class Yangify(parser.ParserData):
def init(self) -> None:
self.root_native = parse_indented_config(self.native.splitlines())
self.native = self.root_native
interfaces = tutorial_parser.Interfaces
Now we need to load the configuration file:
[11]:
with open("data/ios/config.txt", "r") as f:
config = f.read()
Now we will create the datamodel as we will need it later on:
[12]:
from yangson.datamodel import DataModel
dm = DataModel.from_file("yang/yang-library-data.json", ["yang/yang-modules/ietf", "yang/yang-modules/openconfig"])
Finally, we are going to instantiate the IOSParser
and call the process
method:
[13]:
p = IOSParser(dm, native=config)
result = p.process()
Now that we got the processed object, let’s see the result:
[14]:
import json
print(json.dumps(result.raw_value(), indent=4))
{
"openconfig-interfaces:interfaces": {
"interface": [
{
"name": "FastEthernet1",
"config": {
"name": "FastEthernet1",
"type": "iana-if-type:ethernetCsmacd",
"description": "This is Fa1",
"enabled": false
},
"subinterfaces": {
"subinterface": [
{
"index": 1,
"config": {
"index": 1,
"description": "This is Fa1.1"
}
},
{
"index": 2,
"config": {
"index": 2,
"description": "This is Fa1.2"
}
}
]
}
},
{
"name": "FastEthernet3",
"config": {
"name": "FastEthernet3",
"type": "iana-if-type:ethernetCsmacd",
"description": "This is Fa3",
"enabled": true
}
},
{
"name": "FastEthernet4",
"config": {
"name": "FastEthernet4",
"type": "iana-if-type:ethernetCsmacd",
"enabled": false
}
}
]
}
}
Adding a second parser¶
In the previous example we created a parser that only parses the openconfig-interfaces
model, however, our tutorial_parser.py
contains code to also parse the openconfig-vlan
model, let’s create a second RootParser
class that can parse both models:
[15]:
from yangify import parser
from yangify.parser.text_tree import parse_indented_config
class IOSParser2(parser.RootParser):
class Yangify(parser.ParserData):
def init(self) -> None:
self.root_native = parse_indented_config(self.native.splitlines())
self.native = self.root_native
interfaces = tutorial_parser.Interfaces
vlans = tutorial_parser.Vlans
[16]:
p = IOSParser2(dm, native=config)
result = p.process()
[17]:
import json
print(json.dumps(result.raw_value(), indent=4))
{
"openconfig-interfaces:interfaces": {
"interface": [
{
"name": "FastEthernet1",
"config": {
"name": "FastEthernet1",
"type": "iana-if-type:ethernetCsmacd",
"description": "This is Fa1",
"enabled": false
},
"subinterfaces": {
"subinterface": [
{
"index": 1,
"config": {
"index": 1,
"description": "This is Fa1.1"
}
},
{
"index": 2,
"config": {
"index": 2,
"description": "This is Fa1.2"
}
}
]
}
},
{
"name": "FastEthernet3",
"config": {
"name": "FastEthernet3",
"type": "iana-if-type:ethernetCsmacd",
"description": "This is Fa3",
"enabled": true
}
},
{
"name": "FastEthernet4",
"config": {
"name": "FastEthernet4",
"type": "iana-if-type:ethernetCsmacd",
"enabled": false
}
}
]
},
"openconfig-vlan:vlans": {
"vlan": [
{
"vlan-id": 10,
"config": {
"vlan-id": 10,
"name": "prod",
"status": "ACTIVE"
}
},
{
"vlan-id": 20,
"config": {
"vlan-id": 20,
"name": "dev",
"status": "SUSPENDED"
}
}
]
}
}