Merging and Replacing configurations

This tutorial continues from the Translating tutorial, so make sure you read it first.

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:

  1. In merge mode:
  2. Remove leaves that are no longer needed. This is done by calling { leaf_name } with value None. Note that if you are not merging data, empty leaves are just ignored by yangify.
  3. Remove elements of lists that are not present in the candidate data but where in the running data.
  4. In replace mode:
  5. Remove elements of lists that are not present in the candidate data but where in the running data.
  6. Process the running config with Yangigy.replace set to True.

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
!