YANG MODELS FROM A OOP DESIGN PERSPECTIVE.

YANG is a data modeling language originally built for the NETCONF protocol, RFC 6020, with the intent to describe the configuration and the state of network devices. In version 1.1, RFC 7950, the standard scope was expanded, no longer directed only for NETCONF, adding support to other protocols such as RESTCONF, and with other forms of encoding in addition to XML, such as JSON.

A YANG model defines four main types of data nodes, Leaf, Leaf-list, Container, and List, described below:

Leaf

A leaf defines a simple attribute of the model and doesn’t have any children, for example, an Interface Description or the Interface IP Address.

YANG Example:

leaf description {
      description
        "Interface specific description";
      type string {
        pattern '.*';
        length "0..240";
      }
    }

XML Representation

<description>Network Interface</description>

Configuration snippet:

Interface GigabitEthernet1
    description Network Interface

Leaf-list

Defines a sequence of values with the same contextual meaning, for example, the prefix-list entries within a match clause on a route-map.

YANG Example:

leaf-list prefix-list {
      description
        "Match entries of prefix-lists";
      type string;
    }

XML Representation

<prefix-list>list1</prefix-list>
<prefix-list>list2</prefix-list>

Configuration snippet:

route-map Teste permit 10
    match ip address prefix-list list1 list2

Container

A Container is a data node used to group data nodes that have relations between them, for example enabling password encryption.

YANG Example:

container service {
    leaf password-encryption {
        description
            "Encrypt system passwords";
        type empty;
    }
}

XML Representation

<service>
    < password-encryption/>
</service>

Configuration snippet:

service password-encryption

List

A list is similar to a sequence of containers but with a key attribute to uniquely identify the containers, as an example, the neighbor definition inside the BGP global configuration.

YANG Example:

list neighbor {
    description
      "Specify a neighbor router";
    must "count(*) > 1" {
    }
    key "id";
    leaf id {
      type inet:ip-address;
      must
      "(/ios:native/ios:router/ios-bgp:bgp/ios-bgp:neighbor[ios-bgp:id=current()]/ios-bgp:ebgp-multihop/ios-bgp:max-hop and ../ios-bgp:next-hop-unchanged) or not(../next-hop-unchanged)" {
          error-message "ebgp multihop must be created before next-hop-unchanged and deleted after next-hop-unchanged";
      }
    }
    uses ipv4-unicast-neighbor;
      }

XML Representation

<neighbor>
    <id>10.0.0.1</id>
    <remote-as>100</remote-as>
    <description>Test neighbor</description>
</neighbor>
<neighbor>
    <id>10.0.0.2</id>
    <remote-as>200</remote-as>
    <description>Test neighbor</description>
</neighbor>

Configuration snippet:

 neighbor 10.0.0.1 remote-as 100
 neighbor 10.0.0.1 description Test neighbor
 neighbor 10.0.0.2 remote-as 200
 neighbor 10.0.0.1 description Test neighbor

Building the abstraction

First, the project goal is the simplification of payload creation, which can be an XML or any other kind of encoding, so the class implementation must allow multiple encoding and data validation across all components, but looking at the Single Responsibility Principle (SRP), which states that:

Gather together the things that change for the same reasons. Separate things that change for different reasons.

Analysing from the possible changes perspective, the project implementation may change when the device firmware changes it's version, or when the component is augmented by other YANG components, or with the implementation of a new encoding.

So it is possible to distinguish two main reasons for changes: a change in the data model and the change in encoding format. Leading to the implementation of two classes, one for the Components and the other for encoding serialization.

For the Components Class, it should be able to check if the input data is valid and implement a hierarchical architecture just like YANG models, with that it is possible to decouple the data validation from the higher-level module, allowing extension without modifying the already implemented classes.

For implementing hierarchical architecture, each component must implement a method that adds children and a method that, for each child and itself, calls the execution of the serializer, which can be implemented with the following code:

Abstract Component Class

from abc import ABC, abstractmethod
from pytanga.visitors import AbstractVisitor


class AbstractComponent(ABC):

    @abstractmethod
    def add(self, component) -> None:
        """ This should method add a subComponent.    
        """
        pass

    @abstractmethod
    def parse(self, serializer: AbstractVisitor):
        """ This method should call the parse method for all children passing the serializer.    
        """
        pass

    @abstractmethod
    def getXMLNS(self) :
        pass

In the serialization process, it is required that the top-level component object be able to process all it's children and return the desired string, that could be an XML, an JSON, or any other data format.

To achieve those requirements and keep the serialization logic decoupled from the component data, the serialization class should be injected as a dependency of the component class and receive the concrete object as a parameter of the serializer.

With the component concrete object, the serialization object has access to the component attributes, so each component must store it's children results in a variable which, in the current implementation, is self.childrenData, the XML namespace, stored in self._xmlns, and the component tag, stored in self.tag.

Concrete Component Class:

from pytanga.components import AbstractComponent


class prefixSyntaxError(Exception):
    pass


class prefixComponent(AbstractComponent):

    def __init__(self,
                 seq,
                 action,
                 network,
                 ge=None,
                 le=None):
        self._xmlns = {}
        self.attributes = self.setAttributes(seq,
                                             action,
                                             network,
                                             ge,
                                             le)
        self.parent_xmlns = {}
        self._children: List[AbstractComponent] = []
        self.childrenData = []
        self.tag = 'seq'

    @property
    def xmlns(self):
        return self._xmlns

    @xmlns.setter
    def xmlns(self, xmlns):
        self._xmlns = xmlns

    def setAttributes(self,
                      seq,
                      action,
                      network,
                      ge,
                      le):
        attributes = {}

        attributes['no'] = str(seq)
        if(action == 'permit' or action == 'deny'):
            attributes['action'] = action
        else:
            raise prefixSyntaxError("Incorrect action")
        attributes['ip'] = network
        if(ge):
            attributes['ge'] = str(ge)
        if(le):
            attributes['le'] = str(le)

        return attributes

    def add(self, component) -> None:
        self._children.append(component)

    def getXMLNS(self):
        childrenData = []
        for child in self._children:
            self.parent_xmlns.update(child.getXMLNS())
        return self.parent_xmlns

    def parse(self, serializer):
        self.childrenData = []
        self.getXMLNS()
        for child in self._children:
            self.childrenData.append(child.parse(serializer))
        return serializer.parse(self)

Concrete Serializer Class:

from . import AbstractVisitor
import re
import xml.etree.ElementTree as ET


class VisitorError(Exception):
    pass


class NETCONFVisitor(AbstractVisitor):

    def parse(self, leaf):
        output = self.parseComponentData(leaf.tag, leaf.attributes, leaf.xmlns)
        for child in leaf.childrenData:
            output.append(child)
        return output

    def print(self, output):
        return ET.tostring(output, encoding='utf-8').decode()

    def parseComponentData(self, tag, data, xmlns):
        output = ET.Element(tag, **xmlns)
        for key in data:
            if((not isinstance(data[key], dict)) and
               (not isinstance(data[key], list))):
                child = ET.Element(key)
                child.text = data[key]
                output.append(child)
            elif (isinstance(data[key], list)):
                for value in data[key]:
                    child = ET.Element(key)
                    child.text = value
                    output.append(child)
            elif (isinstance(data[key], dict)):
                if('keys' in data[key]):
                    keys = data[key]['keys']
                    value = data[key]['value']
                    child = ET.Element(key, **keys)
                    child.text = value
                    if(key == 'identifier'):
                        output.insert(0, child)
                    else:
                        output.append(child)
                else:
                    child = ET.Element(key)
                    interDict = data[key]
                    for item in interDict:
                        if((not isinstance(interDict[item], dict)) and
                           (not isinstance(interDict[item], list))):
                            subChild = ET.Element(item)
                            subChild.text = interDict[item]
                            child.append(subChild)
                        elif (isinstance(interDict[item], list)):
                            for vaule in interDict[item]:
                                subChild = ET.Element(item)
                                subChild.text = value
                                child.append(subChild)
                        elif (isinstance(interDict[item], dict)):
                            if('keys' in interDict[item]):
                                extra = {}
                                extra = interDict[item]['keys']
                                subChild = ET.Element(item, **extra)
                                subChild.text = interDict[item]['value']
                                child.append(subChild)
                            else:
                                subChild = ET.Element(item)
                                t_interDict = interDict[item]
                                for t_item in t_interDict:
                                    if((not isinstance(t_interDict[t_item], dict)) and
                                       (not isinstance(t_interDict[t_item], list))):
                                        t_subChild = ET.Element(t_item)
                                        t_subChild.text = t_interDict[t_item]
                                        subChild.append(t_subChild)
                                    elif (isinstance(t_interDict[t_item], list)):
                                        for value in t_interDict[t_item]:
                                            t_subChild = ET.Element(t_item)
                                            t_subChild.text = value
                                            subChild.append(t_subChild)
                                    elif (isinstance(t_interDict[t_item], dict)):
                                        if('keys' in t_interDict[t_item]):
                                            extra = t_interDict[t_item]['keys']
                                            t_subChild = ET.Element(t_item, **extra)
                                            t_subChild.text = t_interDict[t_item]['value']
                                            subChild.append(t_subChild)
                                        else:
                                            raise VisitorError(
                                                "Too many nested levels, build a new module")
                                child.append(subChild)

                    output.append(child)
        return output

Summarizing the implementation, the sequence diagram illustrates the iteration between the classes presented and the class diagram presents their relationship.

Sequence diagram

process.png

Class Diagram

UML.png

Conclusion

With the exposed architecture, the serialization and the components classes are decoupled, and the creation of new components or parsers doesn’t require changes in the code, fulfilling the OpenClosed Principle:

A Module should be open for extension but closed for modification.

Under a design pattern perspective, the architecture represents a mix of two well know patterns the Composite Pattern, used for the YANG modules, and the Visitor Pattern, used to decouple the serialization logic from the components.

Finally, this article presented the architecture implemented in Pytanga, a python module that aims to simplify the NETCONF payload creation, the code is available on this GitHub repository and the documentation available in this link: Pytanga Documentation.

Did you find this article valuable?

Support Renato Almeida de Oliveira by becoming a sponsor. Any amount is appreciated!