In [1]:
from napalm import get_network_driver
import napalm_yang
import json
def use_mock_devices():
junos_configuration = {
'hostname': '127.0.0.1',
'username': 'vagrant',
'password': '',
'optional_args': {'path': "./junos_mock/", 'profile': ['junos'],
'increase_count_on_error': False}
}
eos_configuration = {
'hostname': '127.0.0.1',
'username': 'vagrant',
'password': 'vagrant',
'optional_args': {'path': "./eos_mock", 'profile': ['eos'],
'increase_count_on_error': False}
}
junos = get_network_driver("mock")
junos_device = junos(**junos_configuration)
eos = get_network_driver("mock")
eos_device = eos(**eos_configuration)
return junos_device, eos_device
def use_real_devices():
junos_configuration = {
'hostname': '127.0.0.1',
'username': 'vagrant',
'password': '',
'optional_args': {'port': 12203, 'config_lock': False}
}
eos_configuration = {
'hostname': '127.0.0.1',
'username': 'vagrant',
'password': 'vagrant',
'optional_args': {'port': 12443}
}
junos = get_network_driver("junos")
junos_device = junos(**junos_configuration)
junos_device.open()
eos = get_network_driver("eos")
eos_device = eos(**eos_configuration)
eos_device.open()
return junos_device, eos_device
def pretty_print(dictionary):
print(json.dumps(dictionary, sort_keys=True, indent=4))
# Use real devices on your lab, tweak config
# junos_device, eos_device = use_real_devices()
# Use mocked devices intended for this test
junos_device, eos_device = use_mock_devices()
In [2]:
config = napalm_yang.base.Root()
# Adding models to the object
config.add_model(napalm_yang.models.openconfig_interfaces())
config.add_model(napalm_yang.models.openconfig_vlan())
At this point, you can use the "util" model_to_dict()
to visualize the binding and the attached models:
In [3]:
# Printing the model in a human readable format
pretty_print(napalm_yang.utils.model_to_dict(config))
You can populate the model programmatically by navigating the model following its specifications. Some notes:
iter
to iterate over elements in a key, value pair fashion.keys
to get list of elements.add
to create and add a new element.delete
to delete an element._new_item
to create an element detached from the list.append
to add an existing element to a list._unset_$attribute
. For example. config._unset_mtu()
Note that models are compiled with pyangbind
so refer to its documentation for more details: http://pynms.io/pyangbind/
In [4]:
# We create an interface and set the description and the mtu
et1 = config.interfaces.interface.add("et1")
et1.config.description = "My description"
et1.config.mtu = 1500
print(et1.config.description)
print(et1.config.mtu)
In [5]:
# Let's create a second interface, this time accessing it from the root
config.interfaces.interface.add("et2")
config.interfaces.interface["et2"].config.description = "Another description"
config.interfaces.interface["et2"].config.mtu = 9000
print(config.interfaces.interface["et2"].config.description)
print(config.interfaces.interface["et2"].config.mtu)
In [6]:
# You can also get the contents as a dict with the ``get`` method.
# ``filter`` let's you decide whether you want to show empty fields or not.
pretty_print(config.get(filter=True))
In [7]:
# If the value is not valid things will break
try:
et1.config.mtu = -1
except ValueError as e:
print(e)
Let's work through the interface list:
In [8]:
# Iterating
for iface, data in config.interfaces.interface.items():
print(iface, data.config.description)
In [9]:
# We can also delete interfaces
print(config.interfaces.interface.keys())
config.interfaces.interface.delete("et1")
print(config.interfaces.interface.keys())
In [10]:
vlans_dict = {
"vlans": { "vlan": { 100: {
"config": {
"vlan_id": 100, "name": "production"}},
200: {
"config": {
"vlan_id": 200, "name": "dev"}}}}}
config.load_dict(vlans_dict)
print(config.vlans.vlan.keys())
print(100, config.vlans.vlan[100].config.name)
print(200, config.vlans.vlan[200].config.name)
In [11]:
with eos_device as d:
running_config = napalm_yang.base.Root()
running_config.add_model(napalm_yang.models.openconfig_interfaces)
running_config.parse_config(device=d)
pretty_print(running_config.get(filter=True))
In [12]:
with open("junos.config", "r") as f:
config = f.read()
running_config = napalm_yang.base.Root()
running_config.add_model(napalm_yang.models.openconfig_interfaces)
running_config.parse_config(native=[config], profile=["junos"])
pretty_print(running_config.get(filter=True))
In [13]:
# Let's create a candidate configuration
candidate = napalm_yang.base.Root()
candidate.add_model(napalm_yang.models.openconfig_interfaces())
def create_iface(candidate, name, description, mtu, prefix, prefix_length):
interface = candidate.interfaces.interface.add(name)
interface.config.description = description
interface.config.mtu = mtu
ip = interface.routed_vlan.ipv4.addresses.address.add(prefix)
ip.config.ip = prefix
ip.config.prefix_length = prefix_length
create_iface(candidate, "et1", "Uplink1", 9000, "192.168.1.1", 24)
create_iface(candidate, "et2", "Uplink2", 9000, "192.168.2.1", 24)
pretty_print(candidate.get(filter=True))
In [14]:
# Now let's translate the object to JunOS
print(candidate.translate_config(profile=junos_device.profile))
In [15]:
# And now to EOS
print(candidate.translate_config(eos_device.profile))
But this is just the begining, the fun part is yet to come : )
Generating configuration is cool but sometimes is not enough. Let's now see how we can use OpenConfig to make some changes to an existing configuration and generate a "replacement" of the configuration or a "merge".
In [16]:
with junos_device as device:
# first let's create a candidate config by retrieving the current state of the device
candidate = napalm_yang.base.Root()
candidate.add_model(napalm_yang.models.openconfig_interfaces)
candidate.parse_config(device=junos_device)
# now let's do a few changes, let's remove lo0.0 and create lo0.1
candidate.interfaces.interface["lo0"].subinterfaces.subinterface.delete("0")
lo1 = candidate.interfaces.interface["lo0"].subinterfaces.subinterface.add("1")
lo1.config.description = "new loopback"
# Let's also default the mtu of ge-0/0/0 which is set to 1400
candidate.interfaces.interface["ge-0/0/0"].config._unset_mtu()
# We will also need a running configuration to compare against
running = napalm_yang.base.Root()
running.add_model(napalm_yang.models.openconfig_interfaces)
running.parse_config(device=junos_device)
In [17]:
# Now let's see how the merge configuration would be
config = candidate.translate_config(profile=junos_device.profile, merge=running)
print(config)
Note the "delete" tags. Let's actually load the configuration in the device and see which changes are reported.
In [18]:
with junos_device as d:
d.load_merge_candidate(config=config)
print(d.compare_config())
d.discard_config()
You can see that the device is reporting the changes we expected. Let's try now a replace instead.
In [19]:
config = candidate.translate_config(profile=junos_device.profile, replace=running)
print(config)
Note that instead of "delete", now we have a replace in one of the top containers, indicating to the device we want to replace everything underneath. Let's merge and see what happens:
In [20]:
with junos_device as d:
d.load_merge_candidate(config=config)
print(d.compare_config())
d.discard_config()
Interestingly, there is an extra change. That is due to the fact that the dhcp
parameter is outside our model's control.
In [21]:
with eos_device as device:
# first let's create a candidate config by retrieving the current state of the device
candidate = napalm_yang.base.Root()
candidate.add_model(napalm_yang.models.openconfig_interfaces)
candidate.parse_config(device=device)
# now let's do a few changes, let's remove lo1 and create lo0
candidate.interfaces.interface.delete("Loopback1")
lo0 = candidate.interfaces.interface.add("Loopback0")
lo0.config.description = "new loopback"
# Let's also default the mtu of ge-0/0/0 which is set to 1400
candidate.interfaces.interface["Port-Channel1"].config._unset_mtu()
# We will also need a running configuration to compare against
running = napalm_yang.base.Root()
running.add_model(napalm_yang.models.openconfig_interfaces)
running.parse_config(device=device)
In [22]:
# Now let's see how the merge configuration would be
config = candidate.translate_config(profile=eos_device.profile, merge=running)
print(config)
In [23]:
with eos_device as d:
d.load_merge_candidate(config=config)
print(d.compare_config())
d.discard_config()
As in the previous example, we got exactly the same changes we were expecting.
In [24]:
config = candidate.translate_config(profile=eos_device.profile, replace=running)
print(config)
In [25]:
with eos_device as d:
d.load_merge_candidate(config=config)
print(d.compare_config())
d.discard_config()
With the replace instead, we got some extra changes as some things are outside our model's control.
Which of the three methods to choose is very subjective and it will depend on your operations:
In [26]:
state = napalm_yang.base.Root()
state.add_model(napalm_yang.models.openconfig_interfaces)
with junos_device as d:
state.parse_state(device=d)
pretty_print(state.get(filter=True))
Note that parse_state
accepts the same parameters as parse_config
which means you can override profiles or even parse from files.
Right now we have seen we can rely on the on-box diff to see the changes to the device. However, you might want to diff the objects directly in certain cases. You can do that with the diff
method. Note that the method will tell you only which changes are to be performed for the models that are known to your binding.
In [27]:
diff = napalm_yang.utils.diff(candidate, running)
pretty_print(diff)
Diff'ing models with state is also supported.
In [28]:
data = {
"interfaces": {
"interface":{
"Et1": {
"config": {
"mtu": 9000
},
},
"Et2": {
"config": {
"mtu": 1500
}
}
}
}
}
# We load a dict for convenience, any source will do
config = napalm_yang.base.Root()
config.add_model(napalm_yang.models.openconfig_interfaces())
config.load_dict(data)
Now we can load the validation file. Here is the content for reference:
---
- to_dict:
_kwargs:
filter: true
interfaces:
interface:
Et1:
config:
mtu: 9000
Et2:
config:
mtu: 9000
_mode: strict
Note that there is a major difference between using the compliance_report
method on getters and on YANG
objects. With the former you have to specify how to get the data, with the later you have to get the data yourself by any means and then specify you want to convert the data into a dict
with the to_dict
method.
In [29]:
report = config.compliance_report("validate.yaml")
pretty_print(report)
We can see it's complaining that the value of Et2
's MTU is 1500. Let's fix it and try again:
In [30]:
config.interfaces.interface["Et2"].config.mtu = 9000
report = config.compliance_report("validate.yaml")
pretty_print(report)
Now we can see in the first complies
element of the report that we are complying. This works for state as the rest of the features too.
We have also included new modules to be able to use napalm-yang
with ansible:
napalm_parse_yang
- Parses configuration from a device or configuration file and returns a dictionary that represents a YANG object.napalm_diff_yang
- Allows you to diff two YANG objects. Useful to see the difference between two states (for example one gathered before a maintenance and another one post-maintenance) or two configurations (for example, a candidate and a running for those systems without on-box diff or in the situation where you want to have a structured diff that is consistent across platforms).napalm_translate_yang
- Translates a YANG object into native configuration. Useful to deploy configuration in combination with napalm_install_config
.There are two examples included:
In the following playbook you can notice the following:
debug
action a diff generated by napalm-yang
debug
is showing us the needed native configuration to merge the configuration we want into the current running
configuration.napalm-yang
.# ansible-playbook playbook_configure.yaml
# Let's gather config of interfaces from device ***************************************
* eos - changed=False -- ------------------------------------
* junos - changed=False -- ------------------------------------
# Let's diff our candidate and running ************************************************
* junos - changed=False -- ------------------------------------
* eos - changed=False -- ------------------------------------
# debug *******************************************************************************
* junos - changed=False ----------------------------------------
{
"changed": false,
"yang_diff": {
"interfaces": {
"interface": {
"both": {
"ae0": {
"subinterfaces": {
"subinterface": {
"both": {
"0": {
"config": {
"description": {
"first": "A new description",
"second": "ASDASDASD"
}
}
}
}
}
}
}
}
}
}
}
}
* eos - changed=False ----------------------------------------
{
"changed": false,
"yang_diff": {
"interfaces": {
"interface": {
"both": {
"Ethernet2": {
"subinterfaces": {
"subinterface": {
"both": {
"1": {
"ipv4": {
"addresses": {
"address": {
"first_only": [
"172.20.1.1"
],
"second_only": [
"172.20.0.1"
]
}
}
}
}
}
}
}
}
}
}
}
}
}
# Let's translate the YANG object to native config ************************************
* eos - changed=False -- ------------------------------------
* junos - changed=False -- ------------------------------------
# debug *******************************************************************************
* junos - changed=False ----------------------------------------
<configuration>
<interfaces>
<interface>
<name>ae0</name>
<unit>
<name>1</name>
<vlan-id>1</vlan-id>
<family>
<inet>
<address>
<name>192.168.101.1/24</name>
</address>
</inet>
</family>
<disable/>
<description>ae0.1</description>
</unit>
<vlan-tagging/>
<unit>
<name>0</name>
<vlan-id>100</vlan-id>
<family>
<inet>
<address>
<name>192.168.100.1/24</name>
</address>
<address>
<name>172.20.100.1/24</name>
</address>
</inet>
</family>
<description>A new description</description>
</unit>
<vlan-tagging/>
<unit>
<name>2</name>
<vlan-id>2</vlan-id>
<family>
<inet>
<address>
<name>192.168.102.1/24</name>
</address>
</inet>
</family>
<description>ae0.2</description>
</unit>
<vlan-tagging/>
</interface>
<interface>
<name>lo0</name>
<unit>
<name>0</name>
<description>lo0.0</description>
</unit>
<description>lo0</description>
</interface>
<interface>
<name>ge-0/0/1</name>
<disable/>
<description>ge-0/0/1</description>
</interface>
<interface>
<name>ge-0/0/0</name>
<unit>
<name>0</name>
<family>
<inet/>
</family>
<description>ge-0/0/0.0</description>
</unit>
<description>management interface</description>
<mtu>1400</mtu>
</interface>
</interfaces>
</configuration>
* eos - changed=False ----------------------------------------
interface Ethernet2
interface Ethernet2.1
ip address 172.20.1.1/24 secondary
default ip address 172.20.0.1/24 secondary
# Install Config and save diff ********************************************************
* eos - changed=True -----------------------------------------
@@ -39,7 +39,7 @@
description another subiface
encapsulation dot1q vlan 1
ip address 192.168.1.1/24
- ip address 172.20.0.1/24 secondary
+ ip address 172.20.1.1/24 secondary
!
interface Ethernet2.2
description asdasdasd
* junos - changed=True -----------------------------------------
[edit interfaces ae0 unit 0]
- description ASDASDASD;
+ description "A new description";
# STATS *******************************************************************************
eos : ok=6 changed=1 failed=0 unreachable=0
junos : ok=6 changed=1 failed=0 unreachable=0
ge-0/0/0
has mtu 1400 and ge-0/0/1
is down.# ansible-playbook playbook_validate_state.yaml
# Let's gather state of interfaces ***************************************************
* junos - changed=False -- -----------------------------------
# Check all interfaces are up **************************************************************************************
* junos - changed=False -- -----------------------------------
# Let's verify the report complies **************************************************************************************
* junos - FAILED!!! -------------------------------------------
{
"complies": false,
"skipped": [],
"to_dict": {
"complies": false,
"extra": [],
"missing": [],
"present": {
"interfaces": {
"complies": false,
"diff": {
"complies": false,
"extra": [],
"missing": [],
"present": {
"interface": {
"complies": false,
"diff": {
"complies": false,
"extra": [],
"missing": [],
"present": {
"ge-0/0/0": {
"complies": false,
"diff": {
"complies": false,
"extra": [],
"missing": [],
"present": {
"state": {
"complies": false,
"diff": {
"complies": false,
"extra": [],
"missing": [],
"present": {
"mtu": {
"actual_value": 1400,
"complies": false,
"nested": false
},
"oper_status": {
"complies": true,
"nested": false
}
}
},
"nested": true
}
}
},
"nested": true
},
"ge-0/0/1": {
"complies": false,
"diff": {
"complies": false,
"extra": [],
"missing": [],
"present": {
"state": {
"complies": false,
"diff": {
"complies": false,
"extra": [],
"missing": [],
"present": {
"mtu": {
"complies": true,
"nested": false
},
"oper_status": {
"actual_value": "DOWN",
"complies": false,
"nested": false
}
}
},
"nested": true
}
}
},
"nested": true
},
"ge-0/0/2": {
"complies": true,
"nested": true
}
}
},
"nested": true
}
}
},
"nested": true
}
}
}
}
# STATS *****************************************************************************
junos : ok=2 changed=0 failed=1 unreachable=0
In [ ]:
For complete documentation and usages examples, please check:
They can be used like any other Salt native module, as long as the credentials have been declared in the pillar (either file or external service). See the proxy documentation for more details.