Abstraction

In the previous sections, we’ve learn how to create a new Feature, though the subject of abstraction hasn’t been approached yet.

First make sure you have read pyats abstract, especially the section on Lookup Decorator as it is the core of abstraction in Genie conf.

Strategy

The strategy is as follow:

  1. Create a base class which holds the structure of the Feature

  2. Create another version of this class for a or some tokens (could be OS, cli/yang/…) and implement the specific build_config for these tokens.

This allows to have the same structure and variable for all implementations, while supporting different implementation for each token, or a series of token.

Let’s give an example, which is really close to the actual implementation in Genie.

from genie.abstract import lookup
class Base(object):
    @lookup('os')
    def build_config(self, *args, **kwargs):
        '''Abstract method to build_config'''
        raise NotImplementedError
    @lookup('os')
    def build_unconfig(self, *args, **kwargs):
        '''Abstract method to build_unconfig'''
        raise NotImplementedError

from genie.conf.base import ConfigurableBase
from genie.decorator import managedattribute
from genie.conf.base.config import CliConfig
from genie.conf.base.attributes import DeviceSubAttributes,\
                                       SubAttributesDict,\
                                       AttributesHelper
class MyFeature(ConfigurableBase):
# Inheriting from Base (As all feature inherits from atleast one the
  Features classes)
    name = managedattribute(
               name='name',
               read_only=True,
               doc='Name of the Feature')
    description = managedattribute(
                      name='description',
                      type=managedattribute.test_istype(str),
                      doc='Description of the Feature')
    class DeviceAttributes(DeviceSubAttributes):
        # And any other structure wanted
        pass
    def __init__(self, name, **kwargs):
        self._name = name
        self.device_attr = SubAttributesDict(self.DeviceAttributes,
                                             parent=self)
        super().__init__(*kwargs)
    def build_config(self, devices=None, apply=True, attributes=None):
         cfgs = {}
         attributes = AttributesHelper(self, attributes)
         if devices is None:
             devices = self.devices
         devices = set(devices)
         # Loop over all the items of 'self.device_attr', sort them,
         # and only care about the keys which are in keys.
         for key, sub, attributes2 in attributes.mapping_items(
                 'device_attr', keys=devices, sort=True):
             # For each, call their build_config with attributes as an argument.
             # attributes2 is only the attributes related to this particular
             # device, and its parent attributes. (To allow parent default
             # values)
             cfgs[key] = sub.build_config(apply=False,
                                          attributes=attributes2)
         if apply:
             for device_name, cfg in sorted(cfgs.items()):
                 if cfg:
                     device = self.testbed.devices[device_name]
                     device.configure(cfg)
         else:
             return cfgs
    def build_unconfig(self, devices=None, apply=True, attributes=None):
         cfgs = {}
         attributes = AttributesHelper(self, attributes)
         if devices is None:
             devices = self.devices
         devices = set(devices)
         # Loop over all the items of 'self.device_attr', sort them,
         # and only care about the keys which are in keys.
         for key, sub, attributes2 in attributes.mapping_items(
                 'device_attr', keys=devices, sort=True):
             # For each, call their build_config with attributes as an argument.
             # attributes2 is only the attributes related to this particular
             # device, and its parent attributes. (To allow parent default
             # values)
             cfgs[key] = sub.build_unconfig(apply=False,
                                          attributes=attributes2)
         if apply:
             for device_name, cfg in sorted(cfgs.items()):
                 if cfg:
                     device = self.testbed.devices[device_name]
                     device.configure(cfg)
         else:
             return cfgs

So far, we have only created the structure of the Feature. It has DeviceAttributes, it also contains 2 managedattributes. However no configuration is part of it.

The configuration is inside a different file, inside a diferent directory, with the token as directory name. For Genie, the first token is OS name, so nxos, iosxr, `iosxe and so on.

Let’s now implement the configuration section of this Feature.

from genie.conf.base.config import CliConfig
from genie.conf.base.cli import CliConfigBuilder
from genie.conf.base.attributes import AttributesHelper
class myFeature(object):
    class DeviceAttributes(object):
        def build_config(self, attributes=None, unconfig=False):
            attributes=AttributesHelper(self, attributes)
            configurations = CliConfigBuilder(unconfig=unconfig)
            with configurations.submode_context(
                                attributes.format('feature {name}',
                                                   force=True)):
                if unconfig and attributes.iswildcard:
                    configurations.submode_unconfig()
                configurations.append_line(attributes.format('description {description}'))
            return CliConfig(device=self.device, unconfig=unconfig,
                             cli_config=configurations)
        def build_unconfig(self, apply=True, attributes=None, **kwargs):
            return self.build_config(apply=apply, attributes=attributes,
                                     unconfig=True, **kwargs)

Hint

This code is not executable, please see next section. This section only explains the concepts and the strategy.

This represents the configuration part of the code, which is in a different files than the structure itself.

Let’s revisit the structure of genie_libs now that we have this new understanding.

genie_libs
   `-- conf
       |-- __init__.py              <-- Package declaration
       `-- vrf                      <-- Feature Directory
           |-- __init__.py
           |-- vrf.py               <-- Structure file
           |-- iosxe                <-- Token
           |   |-- __init__.py      <-- Token declaration
           |   `-- vrf.py           <-- Configuration implementation
           |-- nxos                 <-- Token
           |   |-- __init__.py      <-- Token declaration
           |   `-- vrf.py           <-- Configuration implementation
           |-- iosxr                <-- Token
           |   |-- __init__.py      <-- Token declaration
           |   `-- vrf.py           <-- Configuration implementation
           `-- tests
               `-- test_vrf.py