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!")
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.
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"]