Arista cEOS-lab, Python et NETCONF/YANG

En parcourant la littérature Arista sur le sujet YANG, il apparait une forte orientation gNMI/gRPC. Ceci étant, NETCONF est également supporté et si l’objectif n’est pas expressément de faire de la télémétrie, il peut parfaitement convenir d’utiliser le protocole NETCONF, avec des avantages uniques, pour interagir avec les équipements EOS.

Les tests vont s’appliquer sur un switch virtuel cEOS-lab pour des questions de légèreté d’empreinte CPU et mémoire. De même, plutôt que de partir d’une feuille blanche en Python avec la librairie ncclient et devoir gérer des fichiers de configuration YAML et des itérations sur un inventory, je propose d’utiliser mon framework d’automatisation Python préféré : nornir et son plugin de connexion NETCONF.

Après avoir téléchargé la dernière version de cEOS-lab en 64 bits (4.24.2.1F), il convient de l’importer dans Docker et de la lancer.

docker import cEOS64-lab-4.24.2.1F.tar ceosimage:4.24.2.1F

docker create --name=ceos1 --privileged -e INTFTYPE=eth -e ETBA=1 -e SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT=1 -e CEOS=1 -e EOS_PLATFORM=ceoslab -e container=docker -p 2000:22/tcp -i -t ceosimage:4.24.2.1F /sbin/init systemd.setenv=INTFTYPE=eth systemd.setenv=ETBA=1 systemd.setenv=SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT=1 systemd.setenv=CEOS=1 systemd.setenv=EOS_PLATFORM=ceoslab systemd.setenv=container=docker

docker network create net1
docker network create net2
docker network connect net1 ceos1
docker network connect net2 ceos1

docker start ceos1

Une configuration minimum de l’équipement virtuel, permettra d’effectuer les tests NETCONF.

docker exec -it ceos1 Cli

enable

configure
username admin secret arista
management api netconf
transport ssh def
end

write memory

Comme à l’accoutumée, une configuration basique du framework nornir permet de démarrer rapidement.

---
core:
    num_workers: 100
    raise_on_error: True
inventory:
    plugin: nornir.plugins.inventory.simple.SimpleInventory
    options:
        host_file: "inventory/hosts.yaml"
        group_file: "inventory/groups.yaml"
        defaults_file: "inventory/defaults.yaml"
---
ceos1:
    hostname: 127.0.0.1
    port: 2000
    groups:
        - ceos
    data:
        filter: >
            <filter>
                <interfaces>
                    <interface>
                        <name>Ethernet1</name>
                    </interface>
                </interfaces>
            </filter>
---
ceos:
    platform: 'eos'
    connection_options:
      netconf:
        extras:
          allow_agent: False
          hostkey_verify: False
---
username: admin
password: arista

Le client NETCONF intégré au framework nornir est utilisé en mode subtree avec un filtre XML sélectionnant la configuration de l’interface Ethernet1. Je pars du principe que les concepts autour de YANG sont connus et maîtrisés. Je laisse à chacun la possibilité d’analyser les modules YANG (Schéma) supportés par Arista et de comprendre comment le filtre a été construit à partir des modules interfaces OpenConfig supportés et augmentés par Arista.

<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="urn:uuid:c5dd25be-4269-4ad3-bcae-a824746d4c0d">
   <data>
      <interfaces xmlns="http://openconfig.net/yang/interfaces">
         <interface>
            <name>Ethernet1</name>
            <config>
               <description />
               <enabled>true</enabled>
               <load-interval xmlns="http://arista.com/yang/openconfig/interfaces/augments">300</load-interval>
               <loopback-mode>false</loopback-mode>
               <mtu>0</mtu>
               <name>Ethernet1</name>
               <tpid xmlns="http://openconfig.net/yang/vlan" xmlns:oc-vlan-types="http://openconfig.net/yang/vlan-types">oc-vlan-types:TPID_0X8100</tpid>
               <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:ethernetCsmacd</type>
            </config>
            <ethernet xmlns="http://openconfig.net/yang/interfaces/ethernet">
               <config>
                  <fec-encoding xmlns="http://arista.com/yang/openconfig/interfaces/augments">
                     <disabled>false</disabled>
                     <fire-code>false</fire-code>
                     <reed-solomon>false</reed-solomon>
                     <reed-solomon544>false</reed-solomon544>
                  </fec-encoding>
                  <forwarding-viable xmlns="http://openconfig.net/yang/hercules/interfaces">true</forwarding-viable>
                  <mac-address>00:00:00:00:00:00</mac-address>
                  <port-speed>SPEED_UNKNOWN</port-speed>
                  <sfp-1000base-t xmlns="http://arista.com/yang/openconfig/interfaces/augments">false</sfp-1000base-t>
               </config>
               <pfc xmlns="http://arista.com/yang/openconfig/interfaces/augments">
                  <priorities>
                     <priority>
                        <index>0</index>
                        <state>
                           <in-frames>0</in-frames>
                           <index>0</index>
                           <out-frames>0</out-frames>
                        </state>
                     </priority>
                     <priority>
                        <index>1</index>
                        <state>
                           <in-frames>0</in-frames>
                           <index>1</index>
                           <out-frames>0</out-frames>
                        </state>
                     </priority>
                     <priority>
                        <index>2</index>
                        <state>
                           <in-frames>0</in-frames>
                           <index>2</index>
                           <out-frames>0</out-frames>
                        </state>
                     </priority>
                     <priority>
                        <index>3</index>
                        <state>
                           <in-frames>0</in-frames>
                           <index>3</index>
                           <out-frames>0</out-frames>
                        </state>
                     </priority>
                     <priority>
                        <index>4</index>
                        <state>
                           <in-frames>0</in-frames>
                           <index>4</index>
                           <out-frames>0</out-frames>
                        </state>
                     </priority>
                     <priority>
                        <index>5</index>
                        <state>
                           <in-frames>0</in-frames>
                           <index>5</index>
                           <out-frames>0</out-frames>
                        </state>
                     </priority>
                     <priority>
                        <index>6</index>
                        <state>
                           <in-frames>0</in-frames>
                           <index>6</index>
                           <out-frames>0</out-frames>
                        </state>
                     </priority>
                     <priority>
                        <index>7</index>
                        <state>
                           <in-frames>0</in-frames>
                           <index>7</index>
                           <out-frames>0</out-frames>
                        </state>
                     </priority>
                  </priorities>
               </pfc>
               <state>
                  <auto-negotiate>false</auto-negotiate>
                  <counters>
                     <in-crc-errors>0</in-crc-errors>
                     <in-fragment-frames>0</in-fragment-frames>
                     <in-jabber-frames>0</in-jabber-frames>
                     <in-mac-control-frames>0</in-mac-control-frames>
                     <in-mac-pause-frames>0</in-mac-pause-frames>
                     <in-oversize-frames>0</in-oversize-frames>
                     <out-mac-control-frames>0</out-mac-control-frames>
                     <out-mac-pause-frames>0</out-mac-pause-frames>
                  </counters>
                  <duplex-mode>FULL</duplex-mode>
                  <enable-flow-control>false</enable-flow-control>
                  <forwarding-viable xmlns="http://openconfig.net/yang/hercules/interfaces">true</forwarding-viable>
                  <hw-mac-address>02:42:ac:12:00:02</hw-mac-address>
                  <mac-address>02:42:ac:12:00:02</mac-address>
                  <negotiated-port-speed>SPEED_UNKNOWN</negotiated-port-speed>
                  <port-speed>SPEED_UNKNOWN</port-speed>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_50GB_1LANE</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_400GB</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_200GB_8LANE</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_10GB</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_100MB</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_1GB</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_10MB</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_200GB_4LANE</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_100GB</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_40GB</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_2500MB</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_5GB</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_100GB_2LANE</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_25GB</supported-speeds>
                  <supported-speeds xmlns="http://arista.com/yang/openconfig/interfaces/augments">SPEED_50GB</supported-speeds>
               </state>
            </ethernet>
            <hold-time>
               <config>
                  <down>0</down>
                  <up>0</up>
               </config>
               <state>
                  <down>0</down>
                  <up>0</up>
               </state>
            </hold-time>
            <state>
               <admin-status>UP</admin-status>
               <counters>
                  <in-broadcast-pkts>0</in-broadcast-pkts>
                  <in-discards>0</in-discards>
                  <in-errors>0</in-errors>
                  <in-fcs-errors>0</in-fcs-errors>
                  <in-multicast-pkts>7</in-multicast-pkts>
                  <in-octets>518</in-octets>
                  <in-unicast-pkts>0</in-unicast-pkts>
                  <out-broadcast-pkts>0</out-broadcast-pkts>
                  <out-discards>0</out-discards>
                  <out-errors>0</out-errors>
                  <out-multicast-pkts>6</out-multicast-pkts>
                  <out-octets>444</out-octets>
                  <out-unicast-pkts>0</out-unicast-pkts>
               </counters>
               <description />
               <enabled>true</enabled>
               <hardware-port xmlns="http://openconfig.net/yang/platform/port">Port1</hardware-port>
               <ifindex>1</ifindex>
               <inactive xmlns="http://arista.com/yang/openconfig/interfaces/augments">false</inactive>
               <last-change>1598691161162488320</last-change>
               <loopback-mode>false</loopback-mode>
               <mtu>0</mtu>
               <name>Ethernet1</name>
               <oper-status>UP</oper-status>
               <tpid xmlns="http://openconfig.net/yang/vlan" xmlns:oc-vlan-types="http://openconfig.net/yang/vlan-types">oc-vlan-types:TPID_0X8100</tpid>
               <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:ethernetCsmacd</type>
            </state>
            <subinterfaces>
               <subinterface>
                  <index>0</index>
                  <config>
                     <description />
                     <enabled>true</enabled>
                     <index>0</index>
                  </config>
                  <ipv4 xmlns="http://openconfig.net/yang/interfaces/ip">
                     <config>
                        <dhcp-client>false</dhcp-client>
                        <enabled>false</enabled>
                        <mtu>1500</mtu>
                     </config>
                     <state>
                        <dhcp-client>false</dhcp-client>
                        <enabled>false</enabled>
                        <mtu>1500</mtu>
                     </state>
                     <unnumbered>
                        <config>
                           <enabled>false</enabled>
                        </config>
                        <state>
                           <enabled>false</enabled>
                        </state>
                     </unnumbered>
                  </ipv4>
                  <ipv6 xmlns="http://openconfig.net/yang/interfaces/ip">
                     <config>
                        <dhcp-client>false</dhcp-client>
                        <enabled>false</enabled>
                        <mtu>1500</mtu>
                     </config>
                     <state>
                        <dhcp-client>false</dhcp-client>
                        <enabled>false</enabled>
                        <mtu>1500</mtu>
                     </state>
                  </ipv6>
                  <state>
                     <counters>
                        <in-broadcast-pkts>0</in-broadcast-pkts>
                        <in-discards>0</in-discards>
                        <in-errors>0</in-errors>
                        <in-fcs-errors>0</in-fcs-errors>
                        <in-multicast-pkts>7</in-multicast-pkts>
                        <in-octets>518</in-octets>
                        <in-unicast-pkts>0</in-unicast-pkts>
                        <out-broadcast-pkts>0</out-broadcast-pkts>
                        <out-discards>0</out-discards>
                        <out-errors>0</out-errors>
                        <out-multicast-pkts>6</out-multicast-pkts>
                        <out-octets>444</out-octets>
                        <out-unicast-pkts>0</out-unicast-pkts>
                     </counters>
                     <description />
                     <enabled>true</enabled>
                     <index>0</index>
                  </state>
               </subinterface>
            </subinterfaces>
         </interface>
      </interfaces>
   </data>
</rpc-reply>

Pour les curieux, il est possible d’analyser le fichier OpenConfig de base et l’augmentation Arista à l’aide de l’outil pyang.

Voici quelques exemples de scripts Python avec leurs résultats respectifs.

#!/usr/bin/env python
from nornir import InitNornir
from nornir.core.task import Result, Task
from nornir.core.exceptions import NornirExecutionError


nr = InitNornir(config_file='config.yaml')


def netconf_get(task: Task) -> Result:
    manager = task.host.get_connection('netconf', task.nornir.config)
    for name, host in nr.inventory.hosts.items():
        # get server capabilities
        for capability in manager.server_capabilities:
            print(capability)
    return Result(result='ok', host=task.host)


if __name__ == "__main__":
    try:
        nr.run(task=netconf_get)
    except NornirExecutionError:
        print("Please look at the error in log file!")
#!/usr/bin/env python
from nornir import InitNornir
from nornir.core.task import Result, Task
from nornir.core.exceptions import NornirExecutionError


nr = InitNornir(config_file='config.yaml')


def netconf_get(task: Task) -> Result:
    manager = task.host.get_connection('netconf', task.nornir.config)
    for name, host in nr.inventory.hosts.items():
        # get result from filter
        print(manager.get(host['filter']))
    return Result(result='ok', host=task.host)


if __name__ == "__main__":
    try:
        nr.run(task=netconf_get)
    except NornirExecutionError:
        print("Please look at the error in log file!")
#!/usr/bin/env python
from nornir import InitNornir
from nornir.core.task import Result, Task
from nornir.core.exceptions import NornirExecutionError
import xml.dom.minidom


nr = InitNornir(config_file='config.yaml')


def netconf_get(task: Task) -> Result:
    manager = task.host.get_connection('netconf', task.nornir.config)
    for name, host in nr.inventory.hosts.items():
        # get specific XML node from filter
        response = xml.dom.minidom.parseString(manager.get(host['filter']).xml)
        print('Port Speed: {}'.format(response.getElementsByTagName('port-speed')[0].firstChild.nodeValue))
    return Result(result='ok', host=task.host)


if __name__ == "__main__":
    try:
        nr.run(task=netconf_get)
    except NornirExecutionError:
        print("Please look at the error in log file!")
#!/usr/bin/env python
from nornir import InitNornir
from nornir.core.task import Result, Task
from nornir.core.exceptions import NornirExecutionError
import xml.dom.minidom


nr = InitNornir(config_file='config.yaml')


def netconf_get(task: Task) -> Result:
    manager = task.host.get_connection('netconf', task.nornir.config)
    for name, host in nr.inventory.hosts.items():
        # get list of XML nodes from filter
        response = xml.dom.minidom.parseString(manager.get(host['filter']).xml)
        available_speeds = response.getElementsByTagName('supported-speeds')
        for available_speed in available_speeds:
            print('Speed could be: {}'.format(available_speed.firstChild.nodeValue))
    return Result(result='ok', host=task.host)


if __name__ == "__main__":
    try:
        nr.run(task=netconf_get)
    except NornirExecutionError:
        print("Please look at the error in log file!")
Python get-server-capabilities
get-server-capabilities, on notera la forte orientation OpenConfig
Python get-filter
get-filter
Python get-node
get-node
Python get-nodes
get-nodes

Suite à la parution de cet article sur LinkedIn, deux questions m’ont été posées en message privé. Je vais retranscrire les réponses en français ici.

  • Pourquoi ne pas utiliser XPATH à la place d’un filtre XML ?
  • Pourquoi ne pas utiliser Ansible à la place Python ?

Lorsqu’on lit le RFC 6241 (Network Configuration Protocol), le « subtree filter » doit être présent avec le protocole NETCONF, mais XPath est une « capabilities » dont l’implémentation n’est pas obligatoire et il revient au développeur de vérifier sa présence pour pouvoir l’utiliser sans erreur. Or cette capacité n’est pas implémentée et ne peut donc pas être utilisée dans le cas qui nous concerne. Ceux qui veulent rapidement s’en convaincre peuvent simplement faire une connexion sur l’équipement virtuel pour recevoir le « hello » NETCONF et constater que la capacité XPath n’est pas présente : ssh -s admin@127.0.0.1 -p 2000 netconf

Pour ce qui est d’Ansible je vais commencer par une note d’humour : Ansible est écrit en Python… Ceci étant, il est vrai que la face visible du produit semble plus accessible avec ses playbooks écrits en YAML. Faire un choix entre un produit d’automatisation comme Ansible ou un langage natif comme Python n’est pas toujours qu’une question de goût et de niveau en développement. Il faut également tenir compte de la stratégie du groupe qui fait le produit et du constructeur d’équipements. Dans notre cas, un coup d’oeil rapide aux options des plateformes dans Ansible permet de voir que le type de connexion NETCONF n’est pas officiellement supporté par la team Ansible pour Arista. Il n’est par ailleurs pas supporté pour beaucoup de plateformes.

Ansible NETCONF Connection
Support des type de connexions Ansible en 2.9

Ceci étant, ça ne veut pas dire qu’il est impossible d’utiliser Ansible pour effectuer une connexion NETCONF sur un switch virtuel Arista. On peut en faire l’expérience en utilisant le mode « default » qui est disponible pour le paramètre ansible_network_os lorsqu’on ne fait pas appel au mode « auto » afin d’éviter le message d’alerte à chaque tâche du playbook.

[defaults]
inventory = ./hosts
roles_path = roles.galaxy:roles
host_key_checking = False
[ceos]
ceos1 ansible_host=127.0.0.1 ansible_port=2000

[all:vars]
ansible_connection=netconf
ansible_network_os=default
ansible_user=admin
ansible_password=arista
---
- name: Netconf get with a filter
  hosts: all
  vars:
    ansible_python_interpreter: "python"
  connection: local
  gather_facts: no
  tasks:
    - name: Get interface Ethernet1 informations with filter
      netconf_get:
        source: running
        display: json
        filter: <interfaces><interface><name>Ethernet1</name></interface></interfaces>
      register: reply
    - name: Print NETCONF reply
      debug:
        var: reply.output
    - name: Show attribute value
      debug:
        var: reply.output.data.interfaces.interface.ethernet.config["port-speed"]
Ansible playbook tache 1
name: Print NETCONF reply
Ansible playbook tache 2
name: Show attribute value