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 4 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
103 changes: 103 additions & 0 deletions napalm_base/yang.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 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 this should come from napalm-yang
SUPPORTED_MODELS = [
"openconfig-interfaces",
"openconfig-network-instance",
]

# 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 SUPPORTED_MODELS:
# We are going to dynamically attach a getter for each
# supported YANG model.
model = model.replace("-", "_")
funcname = "get_{}".format(model)
setattr(Yang, funcname, yang_get_wrapper(model))

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(model):
"""
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.
"""
def method(self, **kwargs):
# This is the class for the model
modelobj = getattr(napalm_yang.models, model)()

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

# We extract the attribute assigned to the running object
# for example, device.running.interfaces
modelattr = getattr(self.device.running, modelobj.elements().keys()[0])

# 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
parsefunc(device=self.device, attrs=[modelattr])

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

# In addition to populate the running object, we return a dict with the contents
# of the parsed model
f = kwargs.get("filter", True)
return modelattr.get(filter=f)

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
57 changes: 57 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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, should be empty")
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")
print(eos_device.yang.config.translate(merge=True))
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("-------------")

eos_device.close()