Skip to content

IETF Interfaces

Warning

Pydantify is still in an alpha state and many parts can hopefully be improved in future versions.

YANG Model

Using pyang the model can be validated and displayed as a tree.

pyang -f tree ietf-interfaces.yang
module: ietf-interfaces
  +--rw interfaces
  |  +--rw interface* [name]
  |     +--rw name                        string
  |     +--rw description?                string
  |     +--rw type                        identityref
  |     +--rw enabled?                    boolean
  |     +--rw link-up-down-trap-enable?   enumeration {if-mib}?
  +--ro interfaces-state
     +--ro interface* [name]
        +--ro name               string
        +--ro type               identityref
        +--ro admin-status       enumeration {if-mib}?
        +--ro oper-status        enumeration
        +--ro last-change?       yang:date-and-time
        +--ro if-index           int32 {if-mib}?
        +--ro phys-address?      yang:phys-address
        +--ro higher-layer-if*   interface-state-ref
        +--ro lower-layer-if*    interface-state-ref
        +--ro speed?             yang:gauge64
        +--ro statistics
           +--ro discontinuity-time    yang:date-and-time
           +--ro in-octets?            yang:counter64
           +--ro in-unicast-pkts?      yang:counter64
           +--ro in-broadcast-pkts?    yang:counter64
           +--ro in-multicast-pkts?    yang:counter64
           +--ro in-discards?          yang:counter32
           +--ro in-errors?            yang:counter32
           +--ro in-unknown-protos?    yang:counter32
           +--ro out-octets?           yang:counter64
           +--ro out-unicast-pkts?     yang:counter64
           +--ro out-broadcast-pkts?   yang:counter64
           +--ro out-multicast-pkts?   yang:counter64
           +--ro out-discards?         yang:counter32
           +--ro out-errors?           yang:counter32

Note

Only the model ietf-interfaces is used without any models like ietf-ip, which augment the ietf-interfaces model.

Create pydantic model

To focus only on the configuration part of the model, the model path can be trimmed to the tree branch ietf-interfaces:interfaces/.

$ pydantify -t=ietf-interfaces/interfaces ietf-interfaces.yang
[INFO] /workspaces/pydantify/pydantify/plugins/pydantic_plugin.py:41 (emit): Output model generated in 0.063s.

The generated module will be in the file out/out.py. We can move and rename it to ietf_interfaces.py.

ietf_interfaces.py
ietf_interfaces.py
from __future__ import annotations

from enum import Enum
from typing import Any, List, Optional

from pydantic import BaseModel, ConfigDict, Field, RootModel
from typing_extensions import Annotated


class EnumerationEnum(Enum):
    enabled = 'enabled'
    disabled = 'disabled'


class InterfaceListEntry(BaseModel):
    """
    The list of configured interfaces on the device.

    The operational state of an interface is available in the
    /interfaces-state/interface list.  If the configuration of a
    system-controlled interface cannot be used by the system
    (e.g., the interface hardware present does not match the
    interface type), then the configuration is not applied to
    the system-controlled interface shown in the
    /interfaces-state/interface list.  If the configuration
    of a user-controlled interface cannot be used by the system,
    the configured interface is not instantiated in the
    /interfaces-state/interface list.
    """

    model_config = ConfigDict(
        populate_by_name=True,
        regex_engine="python-re",
    )
    name: Annotated[
        Optional[str], Field(alias='ietf-interfaces:name', title='NameLeaf')
    ] = None
    """
    The name of the interface.

    A device MAY restrict the allowed values for this leaf,
    possibly depending on the type of the interface.
    For system-controlled interfaces, this leaf is the
    device-specific name of the interface.  The 'config false'
    list /interfaces-state/interface contains the currently
    existing interfaces on the device.

    If a client tries to create configuration for a
    system-controlled interface that is not present in the
    /interfaces-state/interface list, the server MAY reject
    the request if the implementation does not support
    pre-provisioning of interfaces or if the name refers to
    an interface that can never exist in the system.  A
    NETCONF server MUST reply with an rpc-error with the
    error-tag 'invalid-value' in this case.

    If the device supports pre-provisioning of interface
    configuration, the 'pre-provisioning' feature is
    advertised.

    If the device allows arbitrarily named user-controlled
    interfaces, the 'arbitrary-names' feature is advertised.

    When a configured user-controlled interface is created by
    the system, it is instantiated with the same name in the
    /interface-state/interface list.
    """
    description: Annotated[
        Optional[str],
        Field(alias='ietf-interfaces:description', title='DescriptionLeaf'),
    ] = None
    """
    A textual description of the interface.

    A server implementation MAY map this leaf to the ifAlias
    MIB object.  Such an implementation needs to use some
    mechanism to handle the differences in size and characters
    allowed between this leaf and ifAlias.  The definition of
    such a mechanism is outside the scope of this document.

    Since ifAlias is defined to be stored in non-volatile
    storage, the MIB implementation MUST map ifAlias to the
    value of 'description' in the persistently stored
    datastore.

    Specifically, if the device supports ':startup', when
    ifAlias is read the device MUST return the value of
    'description' in the 'startup' datastore, and when it is
    written, it MUST be written to the 'running' and 'startup'
    datastores.  Note that it is up to the implementation to

    decide whether to modify this single leaf in 'startup' or
    perform an implicit copy-config from 'running' to
    'startup'.

    If the device does not support ':startup', ifAlias MUST
    be mapped to the 'description' leaf in the 'running'
    datastore.
    """
    type: Annotated[Any, Field(alias='ietf-interfaces:type', title='TypeLeaf')]
    """
    The type of the interface.

    When an interface entry is created, a server MAY
    initialize the type leaf with a valid value, e.g., if it
    is possible to derive the type from the name of the
    interface.

    If a client tries to set the type of an interface to a
    value that can never be used by the system, e.g., if the
    type is not supported or if the type does not match the
    name of the interface, the server MUST reject the request.
    A NETCONF server MUST reply with an rpc-error with the
    error-tag 'invalid-value' in this case.
    """
    enabled: Annotated[
        Optional[bool], Field(alias='ietf-interfaces:enabled', title='EnabledLeaf')
    ] = True
    """
    This leaf contains the configured, desired state of the
    interface.

    Systems that implement the IF-MIB use the value of this
    leaf in the 'running' datastore to set
    IF-MIB.ifAdminStatus to 'up' or 'down' after an ifEntry
    has been initialized, as described in RFC 2863.



    Changes in this leaf in the 'running' datastore are
    reflected in ifAdminStatus, but if ifAdminStatus is
    changed over SNMP, this leaf is not affected.
    """
    link_up_down_trap_enable: Annotated[
        Optional[EnumerationEnum],
        Field(
            alias='ietf-interfaces:link-up-down-trap-enable',
            title='Link-up-down-trap-enableLeaf',
        ),
    ] = None
    """
    Controls whether linkUp/linkDown SNMP notifications
    should be generated for this interface.

    If this node is not configured, the value 'enabled' is
    operationally used by the server for interfaces that do
    not operate on top of any other interface (i.e., there are
    no 'lower-layer-if' entries), and 'disabled' otherwise.
    """


class InterfacesContainer(BaseModel):
    """
    Interface configuration parameters.
    """

    model_config = ConfigDict(
        populate_by_name=True,
        regex_engine="python-re",
    )
    interface: Annotated[
        Optional[List[InterfaceListEntry]], Field(alias='ietf-interfaces:interface')
    ] = None


class Model(BaseModel):
    """
    Initialize an instance of this class and serialize it to JSON; this results in a RESTCONF payload.

    ## Tips
    Initialization:
    - all values have to be set via keyword arguments
    - if a class contains only a `root` field, it can be initialized as follows:
        - `member=MyNode(root=<value>)`
        - `member=<value>`

    Serialziation:
    - `exclude_defaults=True` omits fields set to their default value (recommended)
    - `by_alias=True` ensures qualified names are used (necessary)
    """

    model_config = ConfigDict(
        populate_by_name=True,
        regex_engine="python-re",
    )
    interfaces: Annotated[
        Optional[InterfacesContainer], Field(alias='ietf-interfaces:interfaces')
    ] = None


if __name__ == "__main__":
    model = Model(
        # <Initialize model here>
    )

    restconf_payload = model.model_dump_json(
        exclude_defaults=True, by_alias=True, indent=2
    )

    print(f"Generated output: {restconf_payload}")

    # Send config to network device:
    # from pydantify.utility import restconf_patch_request
    # restconf_patch_request(url='...', user_pw_auth=('usr', 'pw'), data=restconf_payload)

Parse JSON data into the model

Using requests, or any other HTTP library, the data can be retrieved in JSON format.

restconf.py
response = session.get(f"https://{host}/restconf/data/ietf-interfaces:interfaces")
response.raise_for_status()

The output depends on the network device. This example uses a Cisco CSR1k with three interfaces.

restconf data
{
  "ietf-interfaces:interfaces": {
    "interface": [
      {
        "name": "GigabitEthernet1",
        "description": "MANAGEMENT INTERFACE - DON'T TOUCH ME",
        "type": "iana-if-type:ethernetCsmacd",
        "enabled": true,
        "ietf-ip:ipv4": {
          "address": [
            {
              "ip": "10.10.20.48",
              "netmask": "255.255.255.0"
            }
          ]
        },
        "ietf-ip:ipv6": {}
      },
      {
        "name": "GigabitEthernet2",
        "description": "https://pydantify.github.io/pydantify/",
        "type": "iana-if-type:ethernetCsmacd",
        "enabled": true,
        "ietf-ip:ipv4": {},
        "ietf-ip:ipv6": {}
      },
      {
        "name": "GigabitEthernet3",
        "description": "Configured and Merged by Ansible Network",
        "type": "iana-if-type:ethernetCsmacd",
        "enabled": false,
        "ietf-ip:ipv4": {},
        "ietf-ip:ipv6": {}
      }
    ]
  }
}

The received response from the device includes data from the ietf-ip model, which augment the ietf-interfaces model. Because the build model does not have these fields, the model configuration must be set to ignore extra fields (default). Pydantic Configuration extra

restconf.py
from ietf_interfaces import Model

model = Model.model_validate(response.json())

print(model.model_dump_json(exclude_defaults=True, by_alias=True, indent=2))

Now the model is filled with the received data. By using the option by_alias=True, all keys contain the model prefix in the output.

model output
{
  "ietf-interfaces:interfaces": {
    "ietf-interfaces:interface": [
      {
        "ietf-interfaces:name": "GigabitEthernet1",
        "ietf-interfaces:description": "MANAGEMENT INTERFACE - DON'T TOUCH ME",
        "ietf-interfaces:type": "iana-if-type:ethernetCsmacd",
        "ietf-interfaces:enabled": true
      },
      {
        "ietf-interfaces:name": "GigabitEthernet2",
        "ietf-interfaces:description": "Configured and Merged by Ansible Network",
        "ietf-interfaces:type": "iana-if-type:ethernetCsmacd",
        "ietf-interfaces:enabled": false
      },
      {
        "ietf-interfaces:name": "GigabitEthernet3",
        "ietf-interfaces:description": "Configured and Merged by Ansible Network",
        "ietf-interfaces:type": "iana-if-type:ethernetCsmacd",
        "ietf-interfaces:enabled": false
      }
    ]
  }
}

As with all Python objects, you can access them and make evaluations.

restconf.py
for interface in model.interfaces.interface:
    print(
        "Interface {name} is {status}".format(
            name=interface.name,
            status="enabled" if interface.enabled else "disabled",
        )
    )

The first interface is the management interface and is enabled; the other two are disabled.

Interface status
Interface GigabitEthernet1 is enabled
Interface GigabitEthernet2 is disabled
Interface GigabitEthernet3 is disabled

Update/change model

This example takes the second interface, changes the interface to enable, and updates the description.

restconf.py
from ietf_interfaces import DescriptionLeaf, EnabledLeaf, InterfacesContainer

interface = model.interfaces.interface[1]
interface.enabled = True
interface.description = "https://pydantify.github.io/pydantify/"

Taking a look at only this level of the tree, the generated output contains the changes.

updated interface
{
  "ietf-interfaces:name": "GigabitEthernet2",
  "ietf-interfaces:description": "https://pydantify.github.io/pydantify/",
  "ietf-interfaces:type": "iana-if-type:ethernetCsmacd",
  "ietf-interfaces:enabled": true
}

For an easy configuration update, a new model can be created only containing the changed interface.

restconf.py
new_model = Model(interfaces=InterfacesContainer(interface=[interface]))

Now the output contains all layers of the YANG tree starting at the root interfaces field.

new model output
{
  "ietf-interfaces:interfaces": {
    "ietf-interfaces:interface": [
      {
        "ietf-interfaces:name": "GigabitEthernet2",
        "ietf-interfaces:description": "https://pydantify.github.io/pydantify/",
        "ietf-interfaces:type": "iana-if-type:ethernetCsmacd",
        "ietf-interfaces:enabled": true
      }
    ]
  }
}

Update device configuration

Using the newly created model from the top, a PATCH request can be sent to the device using the root URL of the model (same URL was used to get the data).

exclude_defaults=True

It is recommended to use the option exlude_defaults=True not to send unnecessary data.

restconf.py
response = session.patch(
    f"https://{host}/restconf/data/ietf-interfaces:interfaces/",
    data=new_model.model_dump_json(exclude_defaults=True, by_alias=True),
)

To address the interface directly using the URL, to not only update but also be able to replace the configuration, the data structure needs not a map containing a list of interfaces but a map containing a map looking like this:

JSON paylod to address interface direclty
{
  "ietf-interfaces:interface": {
    "ietf-interfaces:name": "GigabitEthernet2",
    "ietf-interfaces:description": "https://pydantify.github.io/pydantify/",
    "ietf-interfaces:type": "iana-if-type:ethernetCsmacd",
    "ietf-interfaces:enabled": true,
  }
}

Now the URL must include the interface like ietf-interfaces:interfaces/interface=GigabitEthernet2.

restconf.py
response = session.patch(
    f"https://{host}/restconf/data/ietf-interfaces:interfaces/interface=GigabitEthernet2",
    json={
        "ietf-interfaces:interface": interface.model_dump(
            exclude_defaults=True, by_alias=True
        )
    },
)