diff --git a/napalm_base/__init__.py b/napalm_base/__init__.py index bce3d114..18814a4f 100644 --- a/napalm_base/__init__.py +++ b/napalm_base/__init__.py @@ -37,6 +37,7 @@ from napalm_base.exceptions import ModuleImportError from napalm_base.mock import MockDriver from napalm_base.utils import py23_compat +from napalm_base import yang try: __version__ = pkg_resources.get_distribution('napalm-base').version @@ -46,7 +47,8 @@ __all__ = [ 'get_network_driver', # export the function - 'NetworkDriver' # also export the base class + 'NetworkDriver', # also export the base class + 'yang', ] diff --git a/napalm_base/base.py b/napalm_base/base.py index 1912f005..c0e8cd6a 100644 --- a/napalm_base/base.py +++ b/napalm_base/base.py @@ -20,6 +20,12 @@ import napalm_base.exceptions import napalm_base.helpers + +try: + import napalm_yang +except ImportError: + napalm_yang = None + import napalm_base.constants as c from napalm_base import validate @@ -70,6 +76,15 @@ def __del__(self): except Exception: pass + @property + def yang(self): + if not napalm_yang: + raise ImportError("No module named napalm_yang. Please install `napalm-yang`") + + if not hasattr(self, "_yang"): + self._yang = napalm_base.yang.Yang(self) + return self._yang + def open(self): """ Opens a connection to the device. diff --git a/napalm_base/yang.py b/napalm_base/yang.py new file mode 100644 index 00000000..8f1c198c --- /dev/null +++ b/napalm_base/yang.py @@ -0,0 +1,113 @@ +# 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, device): + self.device = device + self.device.running = napalm_yang.base.Root() + 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, data="config", candidate=False, filter=True): + # 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(data)) + + # 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 candidate: + instance = module() + self.device.candidate.add_model(instance) + import pdb + pdb.set_trace() + parsefunc = getattr(self.device.candidate, "parse_{}".format(data)) + 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 + return {a._yang_name: a.get(filter=filter) 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, data="config"): + root = napalm_yang.base.Root() + root.add_model(module) + return napalm_yang.utils.model_to_dict(root, data) + + return method diff --git a/requirements-dev.txt b/requirements-dev.txt index 5663f897..ab248ce4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,4 +17,5 @@ napalm-panos napalm-pluribus napalm-ros napalm-vyos +napalm-yang -r requirements.txt diff --git a/test.py b/test.py new file mode 100644 index 00000000..824baf0b --- /dev/null +++ b/test.py @@ -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.get_openconfig_interfaces(candidate=True)) +print(eos_device.yang.get_openconfig_network_instance()) + +print("# Raw translation") +print(eos_device.yang.translate()) +print("-------------") + +print("# Merge without changes, should be empty") +print(eos_device.yang.translate(merge=True)) +print("-------------") + +print("# Replace without changes") +print(eos_device.yang.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.yang.diff()) +print("-------------") + +print("# Merge change") +merge_config = eos_device.yang.translate(merge=True) +print(merge_config) +print("-------------") + +print("# Replace change") +replace_config = eos_device.yang.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.model_openconfig_vlan()) +pretty_print(eos_device.yang.model_openconfig_vlan(data="state")) diff --git a/test/yang/test_yang.py b/test/yang/test_yang.py new file mode 100644 index 00000000..e69de29b