Merging and Replacing configurations¶
When translating a configuration you can provide both the “candidate” data (the configuration you want to) and, optionally, you can provide also provide the “running” data (the configuration you start with). These concepts are the same as in RFC 6241. When providing the “running” data yangify is going to tell the Translator to:
- In merge mode:
- Remove leaves that are no longer needed. This is done by calling
{ leaf_name }
with valueNone
. Note that if you are not merging data, empty leaves are just ignored by yangify. - Remove elements of lists that are not present in the candidate data but where in the running data.
- In replace mode:
- Remove elements of lists that are not present in the candidate data but where in the running data.
- Process the running config with
Yangigy.replace
set toTrue
.
As with the previous tutorials, let’s see this in action by example. We are going to use the same code as in the previous tutorial so let’s start by importing it and creating the RootTranslator
object:
[2]:
import tutorial_translator
from yangify import translator
from yangify.translator.config_tree import ConfigTree
class IOSTranslator(translator.RootTranslator):
class Yangify(translator.TranslatorData):
def init(self) -> None:
self.root_result = ConfigTree()
self.result = self.root_result
def post(self) -> None:
self.root_result = self.root_result.to_string()
interfaces = tutorial_translator.Interfaces
vlans = tutorial_translator.Vlans
Now let’s load some data that we will use throughout this tutorial. We will start by setting this initial data into a “running” variable and then we will keep copying this running data into a “candidate” variable and modify it before passing it through the translator.
[3]:
%cat data/ios/data.json
{
"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"
}
}
]
}
}
[4]:
import json
with open("data/ios/data.json", "r") as f:
running = json.load(f)
Now we will create the datamodel as we will need it later on:
[5]:
from yangson.datamodel import DataModel
dm = DataModel.from_file("yang/yang-library-data.json", ["yang/yang-modules/ietf", "yang/yang-modules/openconfig"])
Changing leaves¶
Let’s start by looking at what happens when we remove or change leaves:
[6]:
candidate = copy.deepcopy(running)
# Removing "description" and "enabled" from Fa1
candidate["openconfig-interfaces:interfaces"]["interface"][0]["config"].pop("description")
candidate["openconfig-interfaces:interfaces"]["interface"][0]["config"].pop("enabled")
# Setting a new description for Fa3
candidate["openconfig-interfaces:interfaces"]["interface"][1]["config"]["description"] = "A new description"
Merge¶
Now let’s see the effect that’s going to have in the “merge” operation (replace=False
)
[7]:
p = IOSTranslator(
dm,
candidate=candidate,
running=running,
replace=False,
)
print(p.process())
interface FastEthernet1
no description
shutdown
exit
!
interface FastEthernet3
description A new description
exit
!
Ok, intuitevely that’s probably what we expected, let’s see the code and see what happened and why:
[8]:
show_code(tutorial_translator.InterfaceConfig)
class InterfaceConfig(Translator):
"""
Implements openconfig-interfaces:interfaces/interface/config
"""
name = unneeded
type = unneeded
def description(self, value: Optional[str]) -> None:
if value:
self.yy.result.add_command(f" description {value}")
else:
self.yy.result.add_command(f" no description")
def enabled(self, value: Optional[bool]) -> None:
if value:
self.yy.result.add_command(f" no shutdown")
else:
self.yy.result.add_command(f" shutdown")
Let’s see what happened: 1. The Fa3 example is quite straightforward, description
was called with a new value so description {value}
was added to the list of commands to run. 2. In the case of Fa1, both description
and enabled
were called with value = None
because the leaves existed in the running but not in the candidate.
It’s important to note that Yangify
ignores the default values specified by the YANG model. You can read the default value though by calling self.yy.schema.get_child("{leaf_name}").default
. For instance:
def enabled(self, value: Optional[bool]) -> None:
if value is None:
value = self.yy.schema.get_child("description").default
if value:
self.yy.result.append(f" no shutdown")
else:
self.yy.result.append(f" shutdown")
If this is something that people wants to do we may evalute adding a use_defaults
option to the Translator
class to do that automatically.
Replace¶
Now let’s do the same but setting replace=True
in our translator class:
[9]:
p = IOSTranslator(
dm,
candidate=candidate,
running=running,
replace=True,
)
print(p.process())
default interface FastEthernet1
no interface FastEthernet1.1
no interface FastEthernet1.2
interface FastEthernet1.1
description This is Fa1.1
exit
!
interface FastEthernet1.2
description This is Fa1.2
exit
!
default interface FastEthernet3
interface FastEthernet3
description A new description
no shutdown
exit
!
default interface FastEthernet4
interface FastEthernet4
shutdown
exit
!
no vlan 10
vlan 10
name prod
no shutdown
exit
!
no vlan 20
vlan 20
name dev
shutdown
exit
!
As you can see all the blocks where first defaulted or removed only to be reapplied again. In this case there is no need to actively remove in Fa1
as the default interface FastEthernet1
already took care of that.
Adding element lists¶
Now let’s add a new vlan and see what happens:
[10]:
candidate = copy.deepcopy(running)
vlan_30 = {
'vlan-id': 30,
'config': {
'vlan-id': 30,
'name': 'sales',
'status': 'ACTIVE'
}
}
candidate["openconfig-vlan:vlans"]["vlan"].append(vlan_30)
Merge¶
The merge operation should just add this new vlan:
[11]:
p = IOSTranslator(
dm,
candidate=candidate,
running=running,
replace=False,
)
print(p.process())
vlan 30
name sales
no shutdown
exit
!
Replace¶
The replace operation doesn’t like leaving things to change so it will default and reapply all the blocks regardless:
[12]:
p = IOSTranslator(
dm,
candidate=candidate,
running=running,
replace=True,
)
print(p.process())
default interface FastEthernet1
no interface FastEthernet1.1
no interface FastEthernet1.2
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
!
default interface FastEthernet3
interface FastEthernet3
description This is Fa3
no shutdown
exit
!
default interface FastEthernet4
interface FastEthernet4
shutdown
exit
!
no vlan 10
vlan 10
name prod
no shutdown
exit
!
no vlan 20
vlan 20
name dev
shutdown
exit
!
no vlan 30
vlan 30
name sales
no shutdown
exit
!
Removing element lists¶
Now let’s remove an element of list, by removing a vlan:
[13]:
candidate = copy.deepcopy(running)
# remove vlan10
candidate["openconfig-vlan:vlans"]["vlan"].pop(0)
[13]:
{'vlan-id': 10, 'config': {'vlan-id': 10, 'name': 'prod', 'status': 'ACTIVE'}}
Merge¶
The merge operation should just remove the vlan:
[14]:
p = IOSTranslator(
dm,
candidate=candidate,
running=running,
replace=False,
)
print(p.process())
no vlan 10
Replace¶
While the replace, again, leaves no things to chance:
[15]:
p = IOSTranslator(
dm,
candidate=candidate,
running=running,
replace=True,
)
print(p.process())
default interface FastEthernet1
no interface FastEthernet1.1
no interface FastEthernet1.2
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
!
default interface FastEthernet3
interface FastEthernet3
description This is Fa3
no shutdown
exit
!
default interface FastEthernet4
interface FastEthernet4
shutdown
exit
!
no vlan 10
no vlan 20
vlan 20
name dev
shutdown
exit
!
No changes¶
Let’s see what happens when there are no changes.
Merge¶
This one should detect there are no changes and report an empty result:
[16]:
p = IOSTranslator(
dm,
candidate=running,
running=running,
replace=False,
)
print(p.process())
Replace¶
The replace should reapply everything though:
[17]:
p = IOSTranslator(
dm,
candidate=running,
running=running,
replace=True,
)
print(p.process())
default interface FastEthernet1
no interface FastEthernet1.1
no interface FastEthernet1.2
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
!
default interface FastEthernet3
interface FastEthernet3
description This is Fa3
no shutdown
exit
!
default interface FastEthernet4
interface FastEthernet4
shutdown
exit
!
no vlan 10
vlan 10
name prod
no shutdown
exit
!
no vlan 20
vlan 20
name dev
shutdown
exit
!