Skip to content
This repository has been archived by the owner on Sep 17, 2019. It is now read-only.

Added integration with napalm-yang #264

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions napalm_base/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# local modules
import napalm_base.exceptions
import napalm_base.helpers
import napalm_base.yang
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to avoid napalm-yang as a dependency for napalm-base, we can check if it is installed, i.e.:

try:
    import napalm_yang
    HAS_NY = True
except ImportError:
    HAS_NY = False


import napalm_base.constants as c

Expand Down Expand Up @@ -93,6 +94,13 @@ def __raise_clean_exception(exc_type, exc_value, exc_traceback):
# Traceback should already be attached to exception; no need to re-attach
raise exc_value

@property
def yang(self):
if not hasattr(self, "config"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just before this, we can:

if not HAS_NY:
    raise SomeException('Please install napalm-yang for this fancy stuff')

self.config = napalm_base.yang.Yang("config", self)
self.state = napalm_base.yang.Yang("state", self)
return self

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks strange...you are accessing an attribute (i.e. using it as a getter), but then causing it to behave as a setter? So you are basically creating a method that looks like an attribute and returns the object.

Can you provide some more details on why here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so that's certainly a hack. Ideally we would have something like:

class Foo(object):
    def __init__(self):
        self.config = napalm_base.yang.Yang("config", self)
        self.state = napalm_base.yang.Yang("state", self)

class NetworkDriver(object):

        def __init__(self):
             self.yang = Foo()

But right now we can't do that because __init__ throws a NotImplementedException and no driver calls super for that reason. So to make that happen we would have to change all drivers. With this hack I managed to provide same behavior without having to change any driver.

If you have a better solution let me know, this is the best I could come up with so far. We could probably use a wrapper or a metaclass but figured this was simpler/easier to read (if we go with this hack I will add details to the docstrings so people reading this knows why the hack).

def open(self):
"""
Opens a connection to the device.
Expand Down
115 changes: 115 additions & 0 deletions napalm_base/yang.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright 2017 Dravetech AB. All rights reserved.
#
# The contents of this file are licensed under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.

# Python3 support
from __future__ import print_function
from __future__ import unicode_literals

import napalm_yang


# TODO we probably need to adapt the validate framework as well


class Yang(object):

def __init__(self, mode, device):
self._mode = mode
self.device = device
self.device.running = napalm_yang.base.Root()

if mode == "config":
self.device.candidate = napalm_yang.base.Root()

for model in napalm_yang.SUPPORTED_MODELS:
# We are going to dynamically attach a getter for each
# supported YANG model.
module_name = model[0].replace("-", "_")
funcname = "get_{}".format(module_name)
setattr(Yang, funcname, yang_get_wrapper(module_name))
funcname = "model_{}".format(module_name)
setattr(Yang, funcname, yang_model_wrapper(module_name))

def translate(self, merge=False, replace=False, profile=None):
if profile is None:
profile = self.device.profile

if merge:
return self.device.candidate.translate_config(profile=profile,
merge=self.device.running)
elif replace:
return self.device.candidate.translate_config(profile=profile,
replace=self.device.running)
else:
return self.device.candidate.translate_config(profile=profile)

def diff(self):
return napalm_yang.utils.diff(self.device.candidate, self.device.running)


def yang_get_wrapper(module):
"""
This method basically implements the getter for YANG models.

The method abstracts loading the model into the root objects (candidate
and running) and calls the parsers.
"""
module = getattr(napalm_yang.models, module)

def method(self, **kwargs):
# This is the class for the model
instance = module()

# We attach it to the running object
self.device.running.add_model(instance)

# We get the correct method (parse_config or parse_state)
parsefunc = getattr(self.device.running, "parse_{}".format(self._mode))

# We parse *only* the model that corresponds to this call
running_attrs = [getattr(self.device.running, a) for a in instance.elements().keys()]
parsefunc(device=self.device, attrs=running_attrs)

# If we are in configuration mode and the user requests it
# we create a candidate as well
if kwargs.pop("candidate"):
instance = module()
self.device.candidate.add_model(instance)
parsefunc = getattr(self.device.candidate, "parse_{}".format(self._mode))
attrs = [getattr(self.device.candidate, a) for a in instance.elements().keys()]
parsefunc(device=self.device, attrs=attrs)

# In addition to populate the running object, we return a dict with the contents
# of the parsed model
f = kwargs.get("filter", True)
return {a._yang_name: a.get(filter=f) for a in running_attrs}

return method


def yang_model_wrapper(module):
"""
This method basically implements the getter for YANG models.

The method abstracts loading the model into the root objects (candidate
and running) and calls the parsers.
"""
module = getattr(napalm_yang.models, module)

def method(self, **kwargs):
root = napalm_yang.base.Root()
root.add_model(module)
return napalm_yang.utils.model_to_dict(root, self._mode)

return method
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ jtextfsm
jinja2
netaddr
pyYAML
napalm-yang
68 changes: 68 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from napalm_base import get_network_driver

import json


def pretty_print(dictionary):
print(json.dumps(dictionary, sort_keys=True, indent=4))


eos_configuration = {
'hostname': '127.0.0.1',
'username': 'vagrant',
'password': 'vagrant',
'optional_args': {'port': 12443}
}

eos = get_network_driver("eos")
eos_device = eos(**eos_configuration)

eos_device.open()
pretty_print(eos_device.yang.config.get_openconfig_interfaces(candidate=True))
print(eos_device.yang.config.get_openconfig_network_instance())

print("# Raw translation")
print(eos_device.yang.config.translate())
print("-------------")

print("# Merge without changes, should be empty")
print(eos_device.yang.config.translate(merge=True))
print("-------------")

print("# Replace without changes")
print(eos_device.yang.config.translate(replace=True))
print("-------------")


print("# Change a description")
eos_device.candidate.interfaces.interface["Ethernet1"].config.description = "This is a new description" # noqa
pretty_print(eos_device.config.diff())
print("-------------")

print("# Merge change")
merge_config = eos_device.yang.config.translate(merge=True)
print(merge_config)
print("-------------")

print("# Replace change")
replace_config = eos_device.yang.config.translate(replace=True)
print(replace_config)
print("-------------")

print("# Let's replace the current interfaces configuration from the device")
eos_device.load_merge_candidate(config=replace_config)
print(eos_device.compare_config())
eos_device.discard_config()
print("-------------")

print("# Let's merge the current interfaces configuration from the device")
eos_device.load_merge_candidate(config=merge_config)
print(eos_device.compare_config())
eos_device.discard_config()
print("-------------")

eos_device.close()

print("# For reference, you can also print the model for both the config and the state parts of the model") # noqa
pretty_print(eos_device.yang.config.model_openconfig_vlan())
pretty_print(eos_device.yang.state.model_openconfig_vlan())