<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Basics
Let's start with some basics, we will use the following objects as the base for the rest of the tutorial.
Step1:  Creating a Binding
To work with YANG models you first create a root object with  napalm_yang.base.Root() and then you add as many models as you want with the add_model method
Step2:  At this point, you can use the "util" model_to_dict() to visualize the binding and the attached models
Step3:  Populating models
Now that you have the models loaded, there are different ways to populate them.
Populating the model programatically
You can populate the model programmatically by navigating the model following its specifications. Some notes
Step4:  Let's work through the interface list
Step5:  Populating the model from a dict
You can load a dictionary into the object.
Step6:  Populating the model from a device
You can also load the native configuration of a device into a model.
Step7:  Populating from a file
Or from a configuration file stored on disk. The only catch is that you will have to tell parse_config which profile to use to parse it.
Step8:  Translating models
Now we know how to populate models, let's translate them into native configuration.
Step9:  But this is just the begining, the fun part is yet to come 
Step10:  Note the "delete" tags. Let's actually load the configuration in the device and see which changes are reported.
Step11:  You can see that the device is reporting the changes we expected. Let's try now a replace instead.
Step12:  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
Step13:  Interestingly, there is an extra change. That is due to the fact that the dhcp parameter is outside our model's control.
Not so friendly platforms
This also works with not so friendly platforms. Let's do the same we did in the previous section with an EOS device.
Step14:  As in the previous example, we got exactly the same changes we were expecting.
Step15:  With the replace instead, we got some extra changes as some things are outside our model's control.
Generate config, merge or replace
Which of the three methods to choose is very subjective and it will depend on your operations
Step16:  Note that parse_state accepts the same parameters as parse_config which means you can override profiles or even parse from files.
Diffing objects
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.
Step17:  Diff'ing models with state is also supported.
Compliance Report
This feature also works with YANG models. Let's assume we want to verify we have set the MTU of all of our interfaces to 9000.
Step18:  Now we can load the validation file. Here is the content for reference
Step19:  We can see it's complaining that the value of Et2's MTU is 1500. Let's fix it and try again
<ASSISTANT_TASK:>
Python Code:
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()
Explanation: Basics
Let's start with some basics, we will use the following objects as the base for the rest of the tutorial.
End of explanation
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())
Explanation: Creating a Binding
To work with YANG models you first create a root object with  napalm_yang.base.Root() and then you add as many models as you want with the add_model method:
End of explanation
# Printing the model in a human readable format
pretty_print(napalm_yang.utils.model_to_dict(config))
Explanation: At this point, you can use the "util" model_to_dict() to visualize the binding and the attached models:
End of explanation
# 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)
# 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)
# 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))
# If the value is not valid things will break
try:
    et1.config.mtu = -1
except ValueError as e:
    print(e)
Explanation: Populating models
Now that you have the models loaded, there are different ways to populate them.
Populating the model programatically
You can populate the model programmatically by navigating the model following its specifications. Some notes:
Containers and leafs are attributes, which means you access them with a '.'. For example, interface.config.description.
YANG lists have the following list of methods:
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.
Values can be defaulted by using a special method _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/
End of explanation
# Iterating
for iface, data in config.interfaces.interface.items():
    print(iface, data.config.description)
# We can also delete interfaces
print(config.interfaces.interface.keys())
config.interfaces.interface.delete("et1")
print(config.interfaces.interface.keys())
Explanation: Let's work through the interface list:
End of explanation
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)
Explanation: Populating the model from a dict
You can load a dictionary into the object.
End of explanation
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))
Explanation: Populating the model from a device
You can also load the native configuration of a device into a model.
End of explanation
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))
Explanation: Populating from a file
Or from a configuration file stored on disk. The only catch is that you will have to tell parse_config which profile to use to parse it.
End of explanation
# 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))
# Now let's translate the object to JunOS
print(candidate.translate_config(profile=junos_device.profile))
# And now to EOS
print(candidate.translate_config(eos_device.profile))
Explanation: Translating models
Now we know how to populate models, let's translate them into native configuration.
End of explanation
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)
# Now let's see how the merge configuration would be
config = candidate.translate_config(profile=junos_device.profile, merge=running)
print(config)
Explanation: But this is just the begining, the fun part is yet to come : )
Advanced manipulation of the configuration
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".
A configuration replacement will replace the entire section. For example, if you are manipulating interfaces, all interfaces' configuration will be what's in the model you have. If some configuration parameter is not there, it will be wiped out.
A configuration merge is a bit different. Lists of elements are synchronized, which means that if you have one or more interfaces in the running configuration that don't exist in the candidate, they will be removed. This will happen to any list of elements. However, elements that exist in both the candidate and the running configuration will be merged, which means that if some attribute is not set in the candidate but is set in the running, it will not be modified.
End of explanation
with junos_device as d:
    d.load_merge_candidate(config=config)
    print(d.compare_config())
    d.discard_config()
Explanation: Note the "delete" tags. Let's actually load the configuration in the device and see which changes are reported.
End of explanation
config = candidate.translate_config(profile=junos_device.profile, replace=running)
print(config)
Explanation: You can see that the device is reporting the changes we expected. Let's try now a replace instead.
End of explanation
with junos_device as d:
    d.load_merge_candidate(config=config)
    print(d.compare_config())
    d.discard_config()
Explanation: 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:
End of explanation
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)
# Now let's see how the merge configuration would be
config = candidate.translate_config(profile=eos_device.profile, merge=running)
print(config)
with eos_device as d:
    d.load_merge_candidate(config=config)
    print(d.compare_config())
    d.discard_config()
Explanation: Interestingly, there is an extra change. That is due to the fact that the dhcp parameter is outside our model's control.
Not so friendly platforms
This also works with not so friendly platforms. Let's do the same we did in the previous section with an EOS device.
End of explanation
config = candidate.translate_config(profile=eos_device.profile, replace=running)
print(config)
with eos_device as d:
    d.load_merge_candidate(config=config)
    print(d.compare_config())
    d.discard_config()
Explanation: As in the previous example, we got exactly the same changes we were expecting.
End of explanation
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))
Explanation: With the replace instead, we got some extra changes as some things are outside our model's control.
Generate config, merge or replace
Which of the three methods to choose is very subjective and it will depend on your operations:
Generating configuration. The drawback of this one is that configuration is only applied, never removed so it's good for places where you don't know or control most of your configuration.
"Merge" configuration. This one is good to keep some configuration in a known state while leaving other outside it.
"Replace" configuration. This one allows you to fully control the configuration. If you can use this, it means you dictate the fate of your network and not the other way around.
Getting state
Getting state from a device works pretty much like getting configuration.
End of explanation
diff = napalm_yang.utils.diff(candidate, running)
pretty_print(diff)
Explanation: Note that parse_state accepts the same parameters as parse_config which means you can override profiles or even parse from files.
Diffing objects
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.
End of explanation
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)
Explanation: Diff'ing models with state is also supported.
Compliance Report
This feature also works with YANG models. Let's assume we want to verify we have set the MTU of all of our interfaces to 9000.
End of explanation
report = config.compliance_report("validate.yaml")
pretty_print(report)
Explanation: 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.
End of explanation
config.interfaces.interface["Et2"].config.mtu = 9000
report = config.compliance_report("validate.yaml")
pretty_print(report)
Explanation: We can see it's complaining that the value of Et2's MTU is 1500. Let's fix it and try again:
End of explanation
<END_TASK>