Capítulo 4. Junos PyEZ

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Este capítulo trata sobre Junos PyEZ, otra herramienta de automatización que permite invocar llamadas a procedimientos remotos en dispositivos Junos. PyEZ es una biblioteca Python que permite la administración y automatización de los dispositivos Junos. Es un proyecto de código abierto mantenido y apoyado por Juniper Networks con contribuciones de la comunidad de usuarios. El proyecto PyEZ de Junos está alojado en GitHub en https://github.com/Juniper/py-junos-eznc.

Las API de PyEZ proporcionan un "mini-marco" que puede utilizarse para resolver tareas de automatización tanto sencillas como complejas. PyEZ puede utilizarse desde el shell interactivo de Python para realizar rápidamente tareas sencillas en uno o varios dispositivos Junos, o incorporarse en scripts completos de Python de complejidad variable para automatizar la gestión y administración de toda una red de dispositivos Junos. Las primeras secciones de este capítulo muestran la introducción de comandos en el intérprete de comandos interactivo de Python indicado por el indicador >>>. "Un ejemplo de PyEZ" mostrará un script Python completo utilizando la biblioteca PyEZ.

PyEZ proporciona una capa de abstracción construida sobre el protocolo NETCONF tratado en el Capítulo 2. No requiere una interacción directa con NETCONF, sino que utiliza la biblioteca , independiente del proveedor ncclient1 para su transporte NETCONF. Dado que la biblioteca PyEZ utiliza NETCONF para sus llamadas a procedimientos remotos, puede utilizarse con todas las versiones de software Junos y plataformas Junos actualmente soportadas.

Al igual que la API RESTful de Junos tratada en el Capítulo 3, la biblioteca PyEZ permite invocar RPCs de Junos individuales y recuperar las respuestas resultantes. Sin embargo, a diferencia del servicio API RESTful de Junos, PyEZ también ofrece funciones opcionales para simplificar aún más las tareas comunes de automatización. Un ejemplo de estas funciones se produce automáticamente en la conexión inicial a un dispositivo Junos con la biblioteca PyEZ. Por defecto, la biblioteca PyEZ recopila información básica sobre el dispositivo y la almacena en un diccionario Python. Se puede acceder fácilmente a este diccionario con el atributo facts, tratado en "Recopilación de datos".

Otras características de abstracción de la biblioteca PyEZ tienen que ver con el tratamiento de las respuestas RPC. En lugar de devolver respuestas RPC como cadenas XML o JSON, PyEZ utiliza la biblioteca lxml para devolver directamente estructuras de datos Python específicas de XML. También puede combinarse con la biblioteca jxmlease, introducida en "Uso de datos estructurados en Python", para simplificar el análisis sintáctico de estas estructuras de datos específicas de XML en una estructura de datos nativa de Python. Las tablas y vistas son otra herramienta para mapear respuestas RPC en estructuras de datos nativas de Python. PyEZ proporciona varias tablas y vistas predefinidas para RPC comunes y también permite a los usuarios definir las suyas propias para extraer información de cualquier RPC de Junos.

Para la configuración, PyEZ admite cambios en formatos de texto, XML o conjuntos. Además, incluye un motor para combinar valores suministrados por el usuario con plantillas para producir dinámicamente cambios de configuración específicos de dispositivos, clientes o características.

Instalación

Ejecutar un script Python que utilice la biblioteca Junos PyEZ requiere que Junos PyEZ esté instalado en el host de automatización que ejecuta el script. Junos PyEZ depende de Python y de varias bibliotecas del sistema cuya instalación es específica del sistema operativo. Estas dependencias también están sujetas a cambios con las nuevas versiones de PyEZ. Por lo tanto, este libro no pretende cubrir el procedimiento de instalación del software de sistema necesario. En su lugar, consulta la sección "Instalación de PyEZ" de la "Guía del desarrollador de Junos PyEZ" específica de la versión, que encontrarás en la página de inicio de Junos PyEZ de Juniper, para obtener información sobre la instalación del software de sistema necesario en los sistemas operativos habituales.

Nota

En el momento de escribir esto, PyEZ no es compatible con Python 3.x. Asegúrate de que Python 2.7 (o una versión 2.x posterior) está instalado en tu sistema, y de que el sistema está configurado correctamente para utilizar Python 2.x para cualquier script que utilice la biblioteca PyEZ de Junos.

Una vez instaladas las bibliotecas del sistema adecuadas en el sistema anfitrión , se puede utilizar PyPI, el Índice de Paquetes Python, para instalar Junos PyEZ y sus bibliotecas Python dependientes. Sólo tienes que ejecutar el comando pip install junos-eznc en el intérprete de comandos root para instalar la última versión estable de Junos PyEZ. También puedes utilizar el comando pip install git+https://github.com/Juniper/py-junos-eznc.git2 para instalar la última versión de desarrollo de Junos PyEZ directamente desde el repositorio de GitHub.

Advertencia

PyPI instala automáticamente los módulos de Python necesarios cuando instalas Junos PyEZ con el comando pip install. Uno de esos módulos Python prerrequisito es lxml. Como parte de su instalación, el módulo lxml insiste en descargar y compilar la biblioteca C libxml2 a partir del código fuente, aunque tu sistema ya tenga instalada una biblioteca libxml2 funcional. Este requisito significa que tu host debe tener instaladas todas las herramientas necesarias para compilar libxml2. Éstas incluyen un compilador C, un programa make y una biblioteca de compresión zlib que incluya archivos de cabecera (normalmente se encuentra en un paquete zlib-dev). Si al host le falta alguna de estas herramientas necesarias, la instalación de Junos PyEZ puede fallar.

Conectividad de dispositivos

Como se explica en la introducción del capítulo , PyEZ utiliza NETCONF para comunicarse con un dispositivo Junos remoto. Por lo tanto, PyEZ requiere que NETCONF esté habilitado en el dispositivo de destino. Todas las versiones actualmente soportadas del software Junos admiten el protocolo NETCONF sobre un transporte SSH, pero este servicio NETCONF-over-SSH no está habilitado por defecto. La configuración mínima necesaria para habilitar el servicio NETCONF sobre SSH es:

set system services netconf ssh

El servicio NETCONF sobre SSH escucha en el puerto TCP 830, por defecto, y puede funcionar tanto en IPv4 como en IPv6. La siguiente salida de la CLI de Junos muestra la configuración del servicio NETCONF-sobre-SSH y la verificación de que el servicio está escuchando en el puerto TCP 830:

user@r0> configure 
Entering configuration mode

[edit]
user@r0# set system services netconf ssh 

[edit]
user@r0# commit and-quit 
commit complete
Exiting configuration mode

user@r0> show system connections inet | match 830 
tcp4       0      0  *.830                    *.*                       LISTEN

user@r0> show system connections inet6 | match 830   
tcp6       0      0  *.830                    *.*                       LISTEN
Nota

Cuando el servicio SSH está activado con la configuración set system services ssh, también es posible llegar al servicio NETCONF-sobre-SSH en el puerto TCP 22. Sin embargo, es preferible la configuración set system services netconf ssh porque la biblioteca PyEZ intenta conectarse al puerto NETCONF-sobre-SSH (puerto TCP 830) por defecto.

Una vez configurado el servicio NETCONF-sobre-SSH, el dispositivo está listo para ser utilizado con Junos PyEZ.

Crear una instancia de dispositivo

La biblioteca PyEZ proporciona una clase jnpr.junos.Device para representar un dispositivo Junos al que se accede mediante la biblioteca PyEZ. El primer paso para utilizar la biblioteca es instanciar una instancia de esta clase con los parámetros específicos del dispositivo Junos:

user@h0$ python 1
Python 2.7.9 (default, Mar  1 2015, 12:57:24) 
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from jnpr.junos import Device 2
>>> r0 = Device(host='r0',user='user',password='user123') 3
1

Primero invoca el intérprete de comandos interactivo Python con el comando python.

Nota

El comando exacto para invocar el shell interactivo de Python es específico del sistema operativo y de la instalación de Python del host de automatización. Utiliza el comando apropiado para tu entorno específico.

2

Antes de poder utilizar la clase jnpr.junos.Device, primero hay que importarla. Esta línea importa el paquete Python jnpr.junos y copia el nombre Device en el espacio de nombres local, lo que te permite simplemente hacer referencia a Device(). Una sintaxis alternativa es import jnpr.junos. De nuevo, importa el paquete Python jnpr.junos, pero no copia el nombre Device en el espacio de nombres local. Si utilizas esta sintaxis, deberás hacer referencia a la clase como atributo de jnpr.junos utilizando la sintaxis jnpr.junos.Device().

3

Al llamar al objeto de clase Device con la sintaxis Device() se crea un nuevo objeto de instancia. Esta instancia representa una sesión específica NETCONF-sobre-SSH a un dispositivo Junos específico. En este caso, el objeto de instancia se asigna a la variable llamada r0. No hay nada especial en el nombre r0, y podría utilizarse cualquier nombre válido de variable Python en su lugar. Los parámetros de la llamada a Device() establecen los valores iniciales de los atributos de la instancia. En este ejemplo, los parámetros host, user, y password se han establecido con los valores adecuados para el dispositivo Junos con nombre de host r0.

Aunque es típico especificar los parámetros host, user y password, el único argumento obligatorio de la llamada Device()es el parámetro host. La información del host también se puede especificar como primer argumento (sin nombre) de la llamada a Device(). La Tabla 4-1 detalla los parámetros Device() y sus valores por defecto.

Tabla 4-1. Parámetros de la clase jnpr.junos.Device
ParámetroDescripciónValor por defecto
hostUn nombre de host, un nombre de dominio o una dirección IPv4 o IPv6 en la que el dispositivo Junos está ejecutando el servicio NETCONF-sobre-SSH. Si se utiliza un nombre de host o de dominio, debe resolverse en una dirección IPv4 o IPv6. Este parámetro también puede especificarse como primer argumento sin nombre de la llamada a Device().None (Debe ser especificado por la persona que llama).
portEl puerto TCP en el que se puede acceder al servicio NETCONF-sobre-SSH . Si set system services ssh está configurado en el dispositivo Junos, puedes acceder al servicio NETCONF especificando el argumento port = 22 a la llamada Device().830
userEl nombre de usuario utilizado para iniciar sesión en el dispositivo Junos. Como se explica en "Autenticación y autorización", la ejecución de RPC está controlada por la configuración de autorización de esta cuenta de usuario en el dispositivo Junos.El valor de la variable de entorno $USERpara la cuenta que ejecuta el script Python en el host de automatización. Normalmente se establece en el nombre de usuario del usuario que ejecuta el script de Python. El comportamiento por defecto puede ser útil si el nombre de usuario en el host de automatización y en el dispositivo Junos es el mismo. En el intérprete de comandos interactivo de Python, puedes confirmar el valor de la variable de entorno $USER con:
>>> import os
>>> print(os.environ['USER'])
user
passwordLa contraseña utilizada para autentificar al usuario en el dispositivo Junos. Si se están utilizando claves SSH, este valor se utiliza como frase de contraseña para desbloquear la clave privada SSH. De lo contrario, este valor se utiliza para autenticar la contraseña.None (No es necesaria una contraseña para una clave SSH con una frase de contraseña vacía).
gather_factsUn booleano que indica si se recopila o no información básica del dispositivo en la conexión inicial con el método de instancia open(). Consulta " Recopilación de información" para más detalles.True (Se reúnen los hechos.)
auto_probeEsta configuración intenta comprobar si el puerto TCP especificado por port es alcanzable intentando primero una conexión TCP simple a ese puerto. Sólo después de que esta conexión de prueba tenga éxito se intenta la conexión real NETCONF-sobre-SSH. El valor auto_probees un número entero que define el número de segundos que se debe intentar realizar esta conexión TCP de prueba a port antes de que se agote el tiempo de espera. Si el valor es 0, el sondeo automático se desactiva y no se intenta ninguna conexión TCP de prueba. (En este caso, la conexión real NETCONF-sobre-SSH se intenta inmediatamente).0 (La autoprobación está desactivada. Este valor se hereda del atributo auto_probe de la clase Device en el momento de la instanciación. El valor auto_probepuede cambiarse para todas las instancias de dispositivo estableciendo Device.auto_probe = value antes de instanciar cualquier instancia de dispositivo).
ssh_configLa ruta, en el host de automatización, a un archivo de configuración de cliente SSH utilizado para la conexión SSH. El archivo de configuración del cliente SSH puede utilizarse para controlar muchos aspectos de la conexión SSH. Para hosts de automatización tipo Unix, utiliza man ssh_config para obtener más detalles sobre los ajustes disponibles.~/.ssh/config (El ~ se expande al directorio personal del usuario. Si no se encuentra ningún archivo de configuración SSH, se utilizan los valores por defecto de todo el sistema).
ssh_private_key_fileLa ruta, en el host de automatización, a un archivo de clave privada SSH utilizado con la autenticación de clave SSH.Ninguna (Si se especifica, se utilizan los archivos de claves SSH configurados en el archivo ssh_config ).
normalizeUn booleano para indicar si las respuestas XML de este dispositivo deben o no tener normalizados los espacios en blanco. Para más información, consulta "Normalización de respuestas".False (Los espacios en blanco son no normalizados.)

Establecer la conexión

La creación de una instancia de la clase Deviceno inicia una conexión NETCONF con la instancia. Aunque la instancia se ha inicializado con toda la información necesaria para establecer una conexión NETCONF, la conexión real sólo se establece cuando se invoca el método de instancia open(). Una conexión NETCONF con la instancia de dispositivo r0 creada en el ejemplo anterior se inicia con:

>>> r0.open()
Device(r0)

El método open() devuelve la instancia del dispositivo, como muestra la salida Device(r0) 3 en nuestro ejemplo. Este valor de retorno no es necesario en este ejemplo, porque apunta al mismo objeto que la variable r0. Sin embargo, devolver la instancia del dispositivo tiene un propósito: permite una sintaxis alternativa que encadena las invocaciones Device() y open() en una sola línea. Aquí tienes esa sintaxis alternativa:

>>> r0 = Device(host='r0',user='user',password='user123').open()
>>>

Independientemente de si la llamada a open() se invoca sobre una variable de instancia existente o se encadena con la instanciación de Device(), la conexión NETCONF se abre utilizando la información almacenada en los atributos de instancia del dispositivo. Estos atributos incluyen los parámetros auto_probe, host, port, user, y password, así como la información de configuración SSH. Todos estos atributos se establecen cuando se instancia la instancia pasando argumentos a la llamada Device(), o utilizando los valores por defecto detallados en la Tabla 4-1. Sin embargo, es posible anular las opciones auto_probe, gather_facts, y normalize especificando parámetros a la llamada open(). Si cualquiera de estos parámetros se suministra como argumento a la llamada open(), anula los atributos de la instancia del dispositivo.

Autenticación y autorización

A diferencia del servicio de API RESTful , PyEZ no requiere que cada llamada a la API sea autenticada. En su lugar, la autenticación sólo se produce cuando la sesión NETCONF-sobre-SSH es iniciada por la llamada a open(). La autenticación del servicio NETCONF-sobre-SSH puede utilizar métodos de autenticación de clave pública SSH o de contraseña.

Los métodos de autenticación SSH utilizados, y el orden en que se intentan, dependen de la configuración SSH del cliente. Una configuración SSH de cliente típica intenta utilizar primero la autenticación de clave pública y, después, la autenticación de contraseña. En cualquier caso, el dispositivo Junos utiliza el sistema de autenticación estándar de Junos especificado en el nivel jerárquico [edit system] de la configuración de Junos para permitir o denegar la autenticación. En otras palabras, las sesiones NETCONF sobre SSH se autentican exactamente igual que las conexiones SSH estándar a la CLI.

Cuando se utiliza el método de autenticación de clave pública, la clave pública SSH debe configurarse en el dispositivo Junos en [edit system login user username authentication] de la jerarquía de configuración. Dependiendo del tipo de clave pública SSH, la clave se especifica mediante la sentencia de configuración ssh-dsa, ssh-ecdsa, ssh-ed25519 o ssh-rsa. Además de tener la clave pública configurada en el dispositivo Junos, debe existir la correspondiente clave privada SSH en el host de automatización. Para que pueda ser utilizada por PyEZ, esta clave privada SSH debe estar en la ubicación por defecto, o en la ruta especificada por el atributo de instancia de dispositivo ssh_private_key_file. Si la clave privada SSH requiere una frase de contraseña, el método open() intenta utilizar el atributo password de la instancia como frase de contraseña. Si la frase de contraseña de la clave privada SSH está vacía, no es necesario establecer el atributo password de la instancia.

Aquí tienes un ejemplo de configuración de Junos con una clave pública configurada para el usuario user:

user@r0> show configuration system login user user 
uid 2001;
class super-user;
authentication {
    ssh-dsa "ssh-dss AAAAB3Nza ...output trimmed... SJCS9boQ== user@h0";
}

La clave SSH privada correspondiente está en la ubicación por defecto, ~/.ssh/id_dsa, en el host de automatización:

user@h0$ cat ~/.ssh/id_dsa
-----BEGIN DSA PRIVATE KEY-----
MIIBugIBAAKBgQCqBuyGycDhwXmEDb3hXcEfSpD5gaomT91ojlcsSPVtoj773KqZ
...ouput trimmed...
PO+bL6L74rIKIi3cfFk=
-----END DSA PRIVATE KEY-----

Esta clave privada tiene una frase de contraseña vacía,4 permitiendo una conexión NETCONF a r0 sin especificar una contraseña:

user@h0$ python
Python 2.7.9 (default, Mar  1 2015, 12:57:24) 
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from jnpr.junos import Device
>>> r0 = Device('r0')
>>> r0.open()
Device(r0)
>>>

En este ejemplo, también se omitió el parámetro useren la llamada a Device(). Esto funciona porque la variable de entorno $USER es user, que coincide con el nombre de usuario remoto en r0.

Cuando se utiliza la autenticación por contraseña, el dispositivo Junos puede utilizar RADIUS, TACACS+ o una base de datos de contraseñas local para verificar la contraseña. El orden exacto de autenticación viene determinado por la jerarquía de configuración de [edit system authentication-order]. De nuevo, esto es exactamente lo mismo que autenticar una conexión SSH a la CLI. El authentication-order configurado puede probar varias bases de datos de contraseñas antes de permitir o denegar finalmente el intento de autenticación.

La autenticación se produce cuando se establece la sesión NETCONF sobre SSH, pero la autorización se produce para cada llamada a procedimiento remoto. La autorización NETCONF utiliza exactamente el mismo mecanismo y configuración que la autorización CLI. La autorización para ejecutar un RPC específico se determina asignando un usuario, o potencialmente un usuario plantilla en el caso de la autenticación RADIUS/TACACS+, a una clase de inicio de sesión. A su vez, la clase de inicio de sesión especifica un conjunto de permisos que determinan si una llamada RPC está permitida o denegada. Si lo necesitas, puedes volver a consultar "Autenticación y autorización" para refrescar la relación entre la configuración de Junos y la autorización RPC .

Excepciones de conexión

La biblioteca PyEZ de Junos define varias excepciones que pueden aparecer cuando falla la llamada a open(). Estas excepciones son subclases de la clase más general jnpr.junos.exception.ConnectError aunque una excepción genérica jnpr.junos.exception.ConnectError La Tabla 4-2 proporciona una lista y descripciones de estas excepciones y de la situación en la que se produce cada excepción.

Tabla 4-2. Posibles excepciones provocadas por el método open()
ExcepciónDescripción
jnpr.junos.exception.ProbeErrorSe activa si auto_probees distinto de cero y la acción de sondeo falla. Esto suele indicar que el dispositivo host no está accesible en el puerto TCP port. Esto podría indicar un problema de resolución de nombres, un problema de alcanzabilidad IP o una mala configuración del dispositivo. Aunque esta excepción no proporciona una indicación muy específica del problema, es una indicación de que la conexión NETCONF no puede tener éxito.
jnpr.junos.exception.ConnectUnknownHostErrorAparece si el nombre de host especificado en el atributo host no puede resolverse en una dirección IP. Esto suele indicar un problema con el proceso de resolución del Sistema de Nombres de Dominio (DNS).
jnpr.junos.exception.ConnectTimeoutErrorAparece si hay un problema de accesibilidad IP en para conectarse a port en host. Esto indica que no se ha recibido ninguna respuesta del dispositivo Junos. Entre las posibles causas se incluyen una dirección IP incorrecta, el filtrado del cortafuegos o problemas generales de enrutamiento entre el host de automatización y el dispositivo Junos.
jnpr.junos.exception.ConnectRefusedErrorSe activa si el dispositivo Junos rechaza la conexión TCP a port. Esto puede indicar que el servicio NETCONF-over-SSH no está configurado en port, o puede indicar que se ha alcanzado el número máximo de conexiones simultáneas.
jnpr.junos.exception.ConnectAuthErrorAparece si user o password son incorrectos y el dispositivo Junos rechaza el intento de autenticación.
jnpr.junos.exception.ConnectNotMasterErrorAparece si la conexión se realiza a un motor de enrutamiento no maestro en un dispositivo multi-RE.
jnpr.junos.exception.ConnectErrorUna excepción genérica de que se lanza si la biblioteca ncclient lanza una excepción no reconocida durante el proceso de conexión.

Para atrapar y manejar estas excepciones con elegancia, envuelve el método open() en un bloque try/except. Aquí tienes un ejemplo sencillo que imprime un mensaje cuando se encuentra una de estas excepciones. En la salida, aparece intencionadamente un jnpr.junos.exception.​ConnectAuthError al especificar una contraseña incorrecta:

>>> from jnpr.junos import Device
>>> import jnpr.junos.exception
>>> r0 = Device(host='r0',user='user',password='badpass')
>>> try:
...     r0.open()
... except jnpr.junos.exception.ConnectError as err:
...     print('Error: ' + repr(err))
... 
Error: ConnectAuthError(r0)
>>>

El módulo jnpr.junos.exception debe ser importado por la sentencia import antes de que se haga referencia a la excepción específica jnpr.junos.exception.ConnectError en la sentencia except.

Nota

Como todas estas excepciones son subclases de la clase jnpr.junos.exception.ConnectError, al especificar la excepción única se capturan todas las posibles excepciones planteadas por open().

Recopilación de datos

Por defecto, la biblioteca PyEZ recopila información básica sobre el dispositivo Junos durante la llamada open(). PyEZ se refiere a esta información básica como hechos, y la información es accesible a través del atributo diccionario factsde la instancia del dispositivo. Este ejemplo utiliza el módulo pprint para "imprimir" el diccionario facts recopilado durante la llamada r0.open():

>>> r0.open()
Device(r0)
>>> from pprint import pprint
>>> pprint(r0.facts)
{'2RE': False,
 'HOME': '/var/home/user',
 'RE0': {'last_reboot_reason': 'Router rebooted after a normal shutdown.',
         'mastership_state': 'master',
         'model': 'RE-VMX',
         'status': 'OK',
         'up_time': '6 days, 7 hours, 36 minutes, 44 seconds'},
 'domain': 'example.com',
 'fqdn': 'r0.example.com',
 'hostname': 'r0',
 'ifd_style': 'CLASSIC',
 'master': 'RE0',
 'model': 'MX960',
 'personality': 'MX',
 'serialnumber': 'VMX5868',
 'switch_style': 'BRIDGE_DOMAIN',
 'vc_capable': False,
 'version': '15.1R1.9',
 'version_RE0': '15.1R1.9',
 'version_info': junos.version_info(major=(15, 1), type=R, minor=1, build=9)}
>>>

Estos hechos pueden probarse fácilmente en un script para implementar una lógica basada en los hechos. Este código muestra un ejemplo sencillo basado en la clave model del diccionario facts. La clave model indica el modelo del dispositivo Junos:

if r0.facts['model'] == 'MX480':
    # Handle the MX480 case
    ... MX480 code ...
else:
    # Handle the case of other models
    ... Other models code ...

El ejemplo sigue una ruta de código si el dispositivo Junos es un MX480 y otra ruta de código para todos los demás modelos de dispositivo Junos.

Aunque la recopilación de hechos está activada por defecto, puede desactivarse estableciendo el parámetro opcional gather_factsen False en las llamadas Device() o open(). Puede que quieras desactivar la recopilación de datos para acelerar la conexión inicial o si se produce un problema inesperado en la recopilación de datos de un dispositivo.

Si la recopilación de datos se desactiva durante la conexión NETCONF inicial, aún puede iniciarse más tarde invocando el método de instancia facts_refresh(). Como su nombre indica, el método facts_refresh() también se puede utilizar para actualizar un diccionario facts existente con la información más reciente del dispositivo Junos.

Cerrar la conexión

El método close() cierra limpiamente la sesión NETCONF iniciada por la ejecución satisfactoria del método open(). El método close() debe invocarse cuando hayas terminado de hacer llamadas RPC a la instancia del dispositivo:

>>> r0.close()

Llamar al método close() no destruye la instancia del dispositivo; sólo cierra su sesión NETCONF. Llamar a una RPC después de que se haya cerrado una instancia provoca que se lance una excepción jnpr.junos.exception.ConnectClosedError , como se muestra en este ejemplo:

>>> r0.close()
>>> version_info = r0.rpc.get_software_information()
Traceback (most recent call last):
...ouput trimmed...
jnpr.junos.exception.ConnectClosedError: ConnectClosedError(r0)
>>> r0.open()
Device(r0)
>>> version_info = r0.rpc.get_software_information()
>>> r0.close()

El ejemplo también muestra cómo se puede volver a abrir una instancia que se ha cerrado previamente invocando de nuevo el método de instancia open(). Se ejecuta la RPC y la instancia se vuelve a cerrar .

Ejecución RPC

Esta sección comienza con una de las capacidades de bajo nivel de PyEZ. PyEZ permite al usuario invocar RPCs XML de Junos utilizando sencillas sentencias Python. Aunque PyEZ ofrece abstracciones de más alto nivel, como tablas y vistas (tratadas en "Tablas y vistas operativas"), simplifica el proceso de invocar RPCs XML de Junos y analizar
las respuestas XML correspondientes. PyEZ no requiere formatear la RPC
como XML, y no requiere interactuar directamente con NETCONF.

RPC a la carta

Una vez que se ha creado una instancia de dispositivo y se ha invocado al método open() para iniciar la sesión NETCONF, se puede utilizar la propiedad rpc de la instancia de dispositivo para ejecutar una RPC. Cada RPC XML de Junos puede invocarse como un método de la propiedad rpc. El formato general de estas llamadas a métodos es

device_instance_variable.rpc.rpc_method_name()

Por ejemplo, el RPC get-route-summary-information se invoca en la instancia de dispositivo r0 ya abierta con:

>>> route_summary_info = r0.rpc.get_route_summary_information()
Nota

El nombre RPC XML es get-route-summary-information, mientras que el nombre del método es get_route_summary_information. El nombre del método se deriva del nombre RPC XML simplemente sustituyendo los guiones por guiones bajos. Esta sustitución es necesaria porque las reglas de nomenclatura de Python no permiten que los nombres de los métodos incluyan guiones.

La respuesta del RPC get-route-summary-information es devuelta por el método r0.rpc.get_route_summary_information() y almacenada en la variable route_summary_info variable. Por ahora, no te preocupes por el contenido de la respuesta. El contenido de la respuesta RPC se tratará en detalle en "Respuestas RPC".

PyEZ denomina a este concepto "RPC bajo demanda" porque la biblioteca PyEZ no contiene un método para cada RPC XML de Junos. Tener un método real para cada RPC XML de Junos requeriría miles de métodos. Además, para evitar que estos métodos estuvieran perpetuamente desincronizados con las capacidades del dispositivo, PyEZ tendría que estar estrechamente acoplado a la plataforma y versión de Junos. Con RPC bajo demanda, no hay acoplamiento estrecho; se añaden nuevas funciones a cada plataforma con cada versión de Junos y la versión existente de PyEZ puede acceder instantáneamente a esas RPC.

En cambio, la RPC bajo demanda se implementa utilizando el concepto de metaprogramación de . Cada método RPC se genera, y ejecuta, dinámicamente en el momento en que se invoca. Los usuarios de PyEZ no están obligados a entender los detalles de cómo se implementa esta metaprogramación, pero es útil comprender el concepto general.

Como estos métodos RPC se generan "bajo demanda", se puede invocar un método RPC para cada RPC XML de Junos en cualquier plataforma Junos. Si un dispositivo Junos admite una RPC XML concreta, esa RPC XML siempre puede invocarse desde PyEZ utilizando RPC bajo demanda.

El aspecto correspondiente de este paradigma de diseño es que la biblioteca PyEZ no puede saber de antemano si una RPC XML es válida. Cualquier nombre de método RPC se convertirá primero en su correspondiente nombre RPC XML sustituyendo los guiones por guiones bajos, luego se envolverá en los elementos XML adecuados y se enviará al dispositivo a través de la sesión NETCONF. Sólo después de recibir la respuesta NETCONF del dispositivo se puede descubrir un error y lanzar una excepción. En "Excepciones RPC" se detalla este caso y otras posibles excepciones de .

Advertencia

PyEZ también proporciona a un método de instancia de dispositivo cli(), pero este método está pensado principalmente para depuración y debe evitarse en los scripts de PyEZ. Por defecto, este método devuelve una cadena de texto que contiene la salida CLI normal:

>>> response = r0.cli('show system uptime')
/usr/local/lib/python2.7/dist-packages/jnpr/junos/devi...
  warnings.warn("CLI command is for debug use only!", ...)
>>> print response

Current time: 2015-07-13 14:02:00 PDT
Time Source:  NTP CLOCK 
System booted: 2015-07-13 07:42:46 PDT (06:19:14 ago)
Protocols started: 2015-07-13 07:42:46 PDT (06:19:14 ago)
Last configured: 2015-07-13 08:07:26 PDT (05:54:34 ago) by root
 2:02PM  up 6:19, 1 users, load averages: 0.59, 0.41, 0.33

>>>

El método cli() es propenso a todas las "trampas" de la automatización de red tradicional de "raspado de pantalla". Ni el comando ni la respuesta tienen un formato de datos estructurado. Ambos están sujetos a errores de análisis o incluso a cambios entre versiones de Junos. Por esta razón, debes evitar utilizar este método en cualquier esfuerzo de automatización de producción.

Parámetros RPC

Como has visto con el servicio API RESTful de Junos en "Añadir parámetros a las RPC", algunas RPC XML de Junos admiten parámetros opcionales que limitan o alteran la respuesta XML. Al describir la RPC en formato XML, estos parámetros aparecen como etiquetas XML anidadas dentro de la etiqueta XML del nombre de la RPC. Como ejemplo, aquí tienes la RPC XML equivalente para el comando CLI show route protocol isis 10.0.15.0/24 active-path:

user@r0> show route protocol isis 10.0.15.0/24 active-path | display xml rpc
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/15.1R1/junos">
    <rpc>
        <get-route-information>
                <destination>10.0.15.0/24</destination>
                <active-path/>
                <protocol>isis</protocol>
        </get-route-information>
    </rpc>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

En la salida anterior, puedes ver que hay tres elementos XML anidados dentro del elemento XML <get-route-information> de la RPC . Las etiquetas <destination> y <protocol> tienen valores en su contenido, mientras que la etiqueta <active-path/> es un elemento XML vacío. El mecanismo RPC de PyEZ permite al usuario especificar estos parámetros sin tener que formatearlos en XML. Los parámetros se especifican simplemente como argumentos de palabra clave a los métodos RPC introducidos en "RPC bajo demanda". Una invocación equivalente al método PyEZ para el comando CLI show route protocol isis 10.0.15.0/24 active-path es:

>>> isis_route = r0.rpc.get_route_information(protocol='isis',
...                                           destination='10.0.15.0/24',
...                                           active_path=True)

Las etiquetas XML se convierten en argumentos de la palabra clave, y cualquier contenido XML se convierte en el valor de la palabra clave. Al igual que en los nombres de los métodos RPC, los guiones bajos sustituyen a los guiones en las etiquetas XML. Así, la etiqueta <active-path/> se convierte en la palabra clave active_path. Si el parámetro RPC no requiere un valor, como es el caso de <active-path/>, el valor de la palabra clave debe ser True.

Tiempo de espera RPC

Por defecto, una llamada a un método RPC se agotará en si no se recibe la respuesta completa del dispositivo Junos en 30 segundos. Aunque este valor predeterminado es razonable para la mayoría de las llamadas RPC, hay ocasiones en las que puedes necesitar anularlo. Un ejemplo de RPC que puede tardar más de 30 segundos en ejecutarse es get-flow-session-information en un cortafuegos de la serie SRX que tiene muchos miles de flujos de seguridad activos. Otro ejemplo es get-route-information en un router que lleva la tabla completa de enrutamiento de Internet (más de 500.000 rutas y creciendo).

Cuando te encuentres con una de estas situaciones, tu primera pregunta debería ser siempre: "¿Realmente necesito toda esta información?". Quizá sólo necesites las rutas de un determinado AS par, o los flujos de seguridad con un puerto TCP de destino 443. En esos casos, haz que la RPC filtre la salida directamente en el dispositivo pasando los parámetros adecuados a la RPC, tal y como se ha comentado en el apartado anterior.

Sin embargo, hay otras ocasiones en las que la situación es inevitable. El mejor ejemplo puede ser la instalación de un paquete de software Junos con el RPC request-package-add. En este caso, necesitas anular el tiempo de espera de 30 segundos con un tiempo de espera más largo. En otras situaciones, puede que quieras acortar el tiempo de espera predeterminado para que tu script detecte más rápidamente un dispositivo que no responde. Independientemente de si necesitas aumentar o reducir el tiempo de espera de 30 segundos, hay dos formas de conseguir este objetivo.

El primer método para cambiar el tiempo de espera de la RPC es simplemente establecer la propiedad timeoutde la instancia del dispositivo. Establecer la propiedad timeoutafecta a todas las RPC invocadas en este manejador de dispositivo. Aquí tienes un ejemplo utilizando la variable de instancia r0 existente:

>>> r0.timeout
30
>>> r0.timeout = 10
>>> r0.timeout
10

El ejemplo anterior confirma el tiempo de espera por defecto de 30 segundos mostrando el valor del atributo r0.timeout. A continuación, el tiempo de espera se establece en 10 segundos, y la línea final confirma que el valor es ahora de 10 segundos.

El segundo método para modificar el tiempo de espera RPC sólo cambia el valor del tiempo de espera para una única RPC. Las futuras RPC seguirán utilizando el valor por defecto, o el valor establecido al asignar el atributo timeout de la instancia del dispositivo. Este segundo método se consigue pasando un parámetro con la palabra clave dev_timeout al método de la RPC. Aquí tienes un ejemplo de este método:

>>> summary_info = r0.rpc.get_route_summary_information()
>>> bgp_routes = r0.rpc.get_route_information(dev_timeout = 180,
...                                           protocol='bgp')
>>> isis_routes = r0.rpc.get_route_information(protocol='isis')

En este ejemplo, el RPC get-route-summary-information se ejecuta con el tiempo de espera predeterminado de 30 segundos. A continuación, se recopilan las rutas BGP con un tiempo de espera específico de 180 segundos. Por último, se recopilan las rutas ISIS con el tiempo de espera predeterminado de 30 segundos .

Excepciones RPC

Además de las excepciones relacionadas con la conexión que se comentan en "Excepciones de conexión", la biblioteca PyEZ de Junos define varias excepciones que pueden surgir cuando falla un método RPC bajo demanda. La Tabla 4-3 proporciona una lista y descripciones de estas excepciones y de la situación en la que se plantea cada excepción.

Tabla 4-3. Posibles excepciones planteadas por los métodos RPC bajo demanda
ExcepciónDescripción
jnpr.junos.exception.ConnectClosedErrorSe produce si la sesión NETCONF subyacente se cerró inesperadamente antes de que se invocara el método RPC. Esto puede ocurrir debido a una conmutación RE en el dispositivo de destino, un problema de alcanzabilidad de red entre el host de automatización y el dispositivo de destino, o un fallo del propio dispositivo de destino, o porque te olvidaste de llamar al método open().
jnpr.junos.exception.RpcTimeoutErrorSe activa si la sesión NETCONF subyacente de está conectada y la RPC se ha enviado correctamente al dispositivo, pero no se recibe una respuesta del dispositivo dentro del periodo de tiempo de espera de la RPC (como se ha explicado en la sección anterior).
jnpr.junos.exception.PermissionErrorAparece si la autorización de , como se explica en "Autenticación y autorización", no permite que se ejecute la RPC.
jnpr.junos.exception.RpcErrorSe lanza si hay elementos <xnm:error> o <xnm:warning> presentes en la respuesta RPC. Esta excepción también se lanza si hay elementos NETCONF <rpc-error>presentes en la respuesta o si la biblioteca ncclient lanza una excepción no reconocida.

El modo exacto en que debe gestionarse cada una de estas excepciones puede ser muy específico para tus requisitos de automatización concretos. Por ejemplo, si la respuesta de una determinada RPC es necesaria para el procesamiento posterior, una excepción jnpr.junos.exception.PermissionErrorindicaría un error que impide el procesamiento posterior (al menos para esa instancia específica del dispositivo). En este caso, imprimir el error y salir del script (o continuar con la siguiente instancia de dispositivo en el bucle) podría ser adecuado. Sin embargo, puede haber otros casos en los que el usuario proporcione una entrada que seleccione la RPC que se va a ejecutar. En este caso, es posible que quieras manejar con elegancia la excepción jnpr.junos.exception.PermissionError imprimiendo el error y pidiendo al usuario que especifique una RPC diferente.

Un requisito común en el manejo de excepciones es intentar reabrir la conexión NETCONF cuando se recibe una excepción jnpr.junos.exception.ConnectClosedError. Dado que esta excepción puede indicar una condición transitoria, puede ser posible recuperarse de la condición con gracia. Sin embargo, esta excepción también puede indicar un problema más persistente. Intentar reabrir la conexión NETCONF sin restricciones podría conducir a un bucle infinito.

El siguiente fragmento de código Python ilustra un algoritmo para intentar reabrir la conexión NETCONF, y reejecutar la RPC fallida, un número limitado de veces. No sólo ilustra un requisito común, sino que proporciona un marco para la gestión adicional de excepciones más específicas. Simplemente añadiendo otro bloque except, se podría añadir una gestión de errores más elegante para otras excepciones:

import jnpr.junos.exception 1
from time import sleep

MAX_ATTEMPTS = 3 2
WAIT_BEFORE_RECONNECT = 10

# Assumes r0 already exists and is a connected device instance

for attempt in range(MAX_ATTEMPTS): 3
    try: 4
        routes = r0.rpc.get_route_information()
    except jnpr.junos.exception.ConnectClosedError: 5
        sleep(WAIT_BEFORE_RECONNECT) 6
        try: r0.open()
        except jnpr.junos.exception.ConnectError: pass
    else: 7
        # Success. No exception was raised.
        # break will skip the for loop's else.
        break
else: 8
    # Max attempts exceeded. All attempts have failed.
    # Re-raise most recent exception from last attempt.
    raise

# ... continue with the rest of script if RPC succeeded ...
1

Es necesario importar el módulo de excepciones de PyEZ para poder capturar excepciones específicas de PyEZ mediante una sentencia except. Importa la función sleep() del módulo time al espacio de nombres local.

2

MAX_ATTEMPTS se utiliza como constante para indicar el número máximo de veces que se debe reintentar una RPC. En este ejemplo, una RPC se reintentará un máximo de tres veces. WAIT_BEFORE_RECONNECT se utiliza como constante para indicar el número de segundos que hay que esperar tras un fallo de RPC antes de intentar reabrir la conexión NETCONF. Esto permite hasta 30 segundos (WAIT_BEFORE_RECONNECT * MAX_ATTEMPTS) para que se resuelva una condición transitoria.

3

El bucle for intentará ejecutar la RPC hasta MAX_ATTEMPTS veces.

4

La sentencia try marca un bloque de sentencias que se ejecutarán hasta que se produzca una excepción. La función RPC bajo demanda ejecuta la RPC get-route-information dentro de este bloque try. El resultado de la RPC se almacena en la variable routes. Esta RPC puede tener éxito sin ninguna excepción, o puede lanzar cualquiera de las excepciones enumeradas en la Tabla 4-3.

5

Si la sentencia del bloque try lanza una excepción, se comprueba si coincide con una excepción de jnpr.junos.exception.ConnectClosedError. Si la excepción coincide, se ejecutan las sentencias de este bloque except. Si la excepción no coincide con este bloque except, el manejador de excepciones por defecto de Python detendrá la ejecución del programa e imprimirá el error. Podrían añadirse sentencias de excepción adicionales para manejar otras posibles excepciones. Cada bloque except se probará en orden hasta que se encuentre una coincidencia o se agote la lista de excepciones.

6

Intentar reconectar inmediatamente una conexión NETCONF cerrada es probable que falle. En su lugar, esperar un cierto periodo de tiempo proporciona una oportunidad para que pase una condición transitoria (conmutación RE, reconvergencia de red, etc.). La sentencia sleep suspende la ejecución del script durante WAIT_BEFORE_RECONNECTsegundos. A continuación, la ejecución pasa a la siguiente línea. El método open() se envuelve en un bloque try para capturar cualquier excepción que se produzca si la llamada falla porque la condición de red subyacente sigue existiendo. La sentencia except: pass atrapará e ignorará todas las excepciones jnpr.junos.exception.ConnectErrorplanteadas por r0.open().

Nota

Todas las excepciones planteadas por r0.open() se enumeran en la Tabla 4-2. Todas estas excepciones son subclases de jnpr.junos.exception.ConnectErrorPor lo tanto, si se captura esta clase de excepción, se capturan todas las excepciones creadas por r0.open().

Si la conexión NETCONF sigue sin estar abierta, esta condición se capturará como otra jnpr.junos.exception.ConnectClosedError excepción durante el siguiente intento de ejecutar la RPC.

7

El bloque else de una sentencia compuesta try/except/else sólo se ejecuta si no se han producido excepciones en el bloque try. En otras palabras, el bloque else sólo se ejecuta si r0.rpc.get_route_information() se ha ejecutado correctamente. La sentencia break saldrá del bucle for cuando la RPC tenga éxito. Al salir de un bucle for con una sentencia break se omite la cláusula else del bucle.

8

La cláusula else de un bucle Python for sólo se ejecuta si el bucle sale normalmente. En este código, "salir normalmente" significa que se han agotado todos los intentos (attempt > MAX_ATTEMPTS) y que la ejecución de la RPC ha fallado (ha lanzado una excepción) en cada intento. Si todos los intentos han fallado, se lanza la excepción más reciente.

Este ejemplo asume que a la variable r0ya se le ha asignado una instancia de dispositivo, como se describe en " Crear una instancia de dispositivo", y que ya tiene abierta una conexión NETCONF, como se describe en "Establecer la conexión". El ejemplo también trata intencionadamente cualquier excepción lanzada por la RPC que no sea jnpr.junos.exception.ConnectClosedError como un error fatal. Por último, si la excepción jnpr.junos.exception.ConnectClosedErrorpersiste durante más de MAX_ATTEMPTS, la excepción más reciente se propagará al mecanismo de gestión de errores por defecto de Python. La excepción más reciente será probablemente una de las excepciones de la Tabla 4-2, lanzada por el último intento de reabrir la conexión .

Respuestas RPC

Ahora que has visto en cómo invocar RPCs XML de Junos utilizando sencillas sentencias Python, esta sección trata de qué hacer con las respuestas resultantes. PyEZ ofrece múltiples formas de analizar una respuesta en estructuras de datos Python, y también ofrece un mecanismo opcional para eliminar los espacios en blanco extraños de la respuesta. Esta sección cubre cada una de las funciones para controlar las respuestas RPC.

Elementos lxml

La respuesta por defecto a una RPC NETCONF XML es una cadena que representa un documento XML. Como has visto, esta respuesta RPC es la misma que la salida mostrada por la CLI cuando se añade el modificador | display xml al comando CLI equivalente. Por ejemplo

user@r0> show system users | display xml
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/15.1R1/junos">
    <system-users-information xmlns="http://xml.juniper.net/junos/15.1R1/junos">
        <uptime-information>
            <date-time junos:seconds="1436915514">4:11PM</date-time>
            <up-time junos:seconds="116940">1 day,  8:29</up-time>
            <active-user-count junos:format="4 users">4</active-user-count>
            <load-average-1>0.56</load-average-1>
            <load-average-5>0.43</load-average-5>
            <load-average-15>0.36</load-average-15>
            <user-table>
                <user-entry>
                    <user>root</user>
                    <tty>u0</tty>
                    <from>-</from>
                    <login-time junos:seconds="1436897214">11:06AM</login-time>
                    <idle-time junos:seconds="60">1</idle-time>
                    <command>cli</command>
                </user-entry>
                <user-entry>
                    <user>foo</user>
                    <tty>pts/0</tty>
                    <from>172.29.104.149</from>
                    <login-time junos:seconds="1436884614">7:36AM</login-time>
                    <idle-time junos:seconds="30900">8:35</idle-time>
                    <command>-cli (cli)</command>
                </user-entry>
                <user-entry>
                    <user>bar</user>
                    <tty>pts/1</tty>
                    <from>172.29.104.149</from>
                    <login-time junos:seconds="1436884614">7:36AM</login-time>
                    <idle-time junos:seconds="30900">8:35</idle-time>
                    <command>-cli (cli)</command>
                </user-entry>
                <user-entry>
                    <user>user</user>
                    <tty>pts/2</tty>
                    <from>172.29.104.149</from>
                    <login-time junos:seconds="1436884614">7:36AM</login-time>
                    <idle-time junos:seconds="0">-</idle-time>
                    <command>-cli (cli)</command>
                </user-entry>
            </user-table>
        </uptime-information>
    </system-users-information>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

En lugar de devolver esta cadena de documento XML directamente al usuario, como viste con el servicio API RESTful en "Formateo de respuestas HTTP", la biblioteca PyEZ utiliza la biblioteca lxml para analizar el documento XML y devolver la respuesta ya analizada. La respuesta es un objeto lxml.etree.Element enraizado en el primer elemento hijo del elemento <rpc-reply>. En el caso de el comando show system users, o el equivalente get-system-users-information RPC, el primer elemento hijo del elemento <rpc-reply> es el elemento <system-users-information>. Esto se demuestra mostrando el atributo tagde la respuesta RPC get-system-users-information:

>>> response = r0.rpc.get_system_users_information(normalize=True)
>>> response.tag
'system-users-information'
Nota

Este ejemplo pasó el argumento normalize=True al método r0.rpc.get_system_users_information(). La normalización de la respuesta se trata en detalle en "Normalización de la respuesta". A efectos de estos ejemplos, simplemente asegúrate de incluir también el argumento normalize. Si no lo haces, algunos de los siguientes ejemplos devolverán resultados diferentes o fallarán por completo.

Cada objeto lxml.etree.Element tiene enlaces a objetos lxml.etree.Element padre, hijo y hermanos, que forman un árbol que representa la respuesta XML analizada. Con fines de depuración, se puede utilizar la función lxml.etree.dump() para volcar el texto XML de la respuesta (aunque sin el bonito formato de la CLI de Junos):

>>> from lxml import etree
>>> etree.dump(response)
<system-users-information>
<uptime-information>
...ouput trimmed...
</uptime-information>
</system-users-information>

>>>

Aunque los objetos lxml.etree.Elementpueden parecer más complicados que una estructura de datos compuesta por listas y dicts nativos de Python, los objetos lxml.etree.Element ofrecen un sólido conjunto de APIs para seleccionar y extraer partes de la respuesta RPC. Muchas de estas APIs utilizan una expresión XPath para hacer coincidir un subárbol, o subárboles, de la respuesta. Ya conociste las expresiones XPath en " Acceso a datos XML con XPath". La siguiente barra lateral amplía la introducción anterior con información específica sobre el subconjunto de expresiones XPath disponibles en lxml. Para un estudio más detallado de XPath en general, consulta el Tutorial de XPath de W3Schools.

Ahora que ya tienes conocimientos básicos de XPath, veamos algunos ejemplos de cómo se pueden utilizar las API lxml para seleccionar información de una respuesta RPC. Estos ejemplos utilizan la misma variable get-system-users-information RPC response de los ejemplos anteriores. Puede ser útil analizar cada una de estas sentencias y sus resultados utilizando la salida show system users | display xmlque aparece al principio de esta sección.5

El contenido del texto del primer elemento XML que coincida con una expresión XPath puede recuperarse con el método findtext():

>>> response.findtext("uptime-information/up-time")
'1 day, 8:29'

El argumento del método findtext()es un XPath relativo al elemento response. Como response representa al elemento <system-users-information>, el XPath uptime-information/up-time coincide con la etiqueta <up-time> de la respuesta.

El elemento <up-time> también contiene un atributo seconds que proporciona el tiempo de actividad del sistema en un número de segundos más fácil de analizar desde que se inició el sistema. Se puede acceder al valor de este atributo encadenando el método find()y el atributo attrib:

>>> response.find("uptime-information/up-time").attrib['seconds']
'116940'

Mientras que el método findtext() devuelve una cadena, el método find() devuelve un objeto lxml.etree.Element. A continuación, se puede acceder a los atributos XML de ese objeto lxml.etree.Element mediante el diccionario attrib. El diccionario attrib tiene como clave el nombre del atributo XML.

En la variable response, hay un elemento XML <user-entry> por cada usuario conectado actualmente al dispositivo. Cada elemento <user-entry> contiene un elemento <user> con el nombre de usuario del usuario. El método findall() devuelve una lista de lxml.etree.Element objetos que coinciden con un XPath. En este ejemplo, se utiliza findall() para seleccionar el elemento <user> dentro de cada elemento <user-entry>:

>>> users = response.findall("uptime-information/user-table/user-entry/user")
>>> for user in users:
...     print user.text
... 
root
foo
bar
user
>>>

El resultado de este ejemplo es una lista de los nombres de usuario de todos los usuarios que han iniciado sesión en el dispositivo Junos.

El siguiente ejemplo combina el método findtext() con una expresión XPath que selecciona el primer elemento XML coincidente que tenga un elemento hijo específico coincidente:

>>> response.findtext("uptime-information/user-table/user-entry[tty='u0']/user")
'root'

El resultado de este ejemplo es el nombre de usuario del usuario actualmente conectado a la consola del dispositivo Junos. (En este dispositivo, la consola tiene un nombre de tty de u0.) El XPath selecciona el elemento <user> del primer elemento <user-entry> que también tiene un elemento hijo <tty> con el valor u0.

El siguiente ejemplo recupera el número de segundos que la sesión del usuario bar'ha estado inactiva. Esto se hace combinando el método find(), un XPath con el predicado [tag='text'] y el diccionario de atributos attrib:

>>> XPATH = "uptime-information/user-table/user-entry[user='bar']/idle-time"
>>> response.find(XPATH).attrib['seconds']
'30900'
Nota

En el ejemplo anterior, la variable XPATH se utiliza como una constante simplemente para evitar el salto de línea en el libro impreso.

Como el ejemplo anterior utilizaba el método find(), sólo devolverá información de la primera sesión CLI del usuario bar. Si el usuario bar tiene varias sesiones CLI abiertas, podrías recuperar una lista de los tiempos de inactividad sustituyendo find() por findall().

Una diferencia significativa entre los objetos lxml.etree.Element, con sus correspondientes métodos, y las estructuras de datos nativas de Python es cómo se gestionan los datos inexistentes. Para demostrarlo, crearemos un diccionario Python multinivel equivalente para almacenar la información sobre el tiempo de actividad:

>>> from pprint import pprint
>>> example_dict = { 'uptime-information' : { 'up-time' : '1 day, 8:29' }}
>>> pprint(example_dict)
{'uptime-information': {'up-time': '1 day, 8:29'}}

Podría decirse que acceder a la información de este diccionario es más fácil que utilizar el método findtext():

>>> example_dict['uptime-information']['up-time']
'1 day, 8:29'

Sin embargo, considera lo que ocurre cuando intentas acceder a datos que no existen en la respuesta. El diccionario nativo de Python lanza una excepción KeyError:

>>> example_dict['foo']['bar']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'foo'

Si existe la posibilidad de que falte algún dato en la respuesta, el acceso al diccionario debe ir envuelto en un bloque try/except que gestione con elegancia la excepción resultante KeyError. Por el contrario, si falta la información solicitada en un objeto lxml.etree.Element, los métodos find() y findtext() simplemente devuelven None en lugar de lanzar una excepción:

>>> print response.find("foo/bar")
None
>>> print response.findtext("foo/bar")
None

El método findall() tiene un comportamiento similar. Devuelve una lista vacía cuando la expresión XPath no coincide con ningún elemento XML:

>>> print response.findall("foo/bar")
[]

También es importante recordar que el método findall() siempre devuelve una lista de objetos lxml.etree.Element. No devuelve directamente un objeto lxml.etree.Element. Este comportamiento sigue siendo cierto incluso cuando sólo hay un objeto en la lista:

>>> user_entries = response.findall("uptime-information/user-table/user-entry") 
>>> type(user_entries)
<type 'list'>
>>> len(user_entries)
4

Cuando hay varios usuarios conectados, esta lista contiene un objeto por cada usuario. Sin embargo, el siguiente ejemplo muestra la respuesta cuando sólo hay un usuario conectado:

>>> new_response = r0.rpc.get_system_users_information(normalize=True)
>>> new_users = new_response.findall("uptime-information/user-table/user-entry") 
>>> type(new_users)
<type 'list'>
>>> len(new_users)
1

En este caso de un solo usuario, observa que la respuesta sigue siendo una lista; es sólo una lista con un único elemento. Este comportamiento permite al programa recorrer la lista de entradas de usuario sin tener que crear rutas de código diferentes para los casos de ningún usuario, un usuario y varios usuarios.

Hay una última situación que es importante comprender. Intentar acceder a un atributo XML inexistente sigue provocando un KeyError. Esto se debe a que un atributo lxml.etree.Elementattrib de un objeto es un diccionario Python cuya clave es el nombre del atributo XML:

>>> response.find("uptime-information/up-time").attrib['foo']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "lxml.etree.pyx", line 2366, in lxml.etree._Attrib.__getitem__ (src/lx...
KeyError: 'foo'

Una situación relacionada pero ligeramente diferente es intentar acceder a un atributo XML de un elemento inexistente. En este ejemplo, el método find() devuelve None porque no hay ningún elemento XML que coincida con la expresión XPath.

Una forma de gestionar el acceso al diccionario attrib es envolver el acceso en un bloque try/except que capture tanto el AttributeError como el KeyError :

>>> try: foo_attrib = response.find("uptime-information/bar").attrib['foo']
... except (AttributeError, KeyError): foo_attrib = None
... 
>>> print foo_attrib
None
>>> try: foo_attrib = response.find("uptime-information/up-time").attrib['foo']
... except (AttributeError, KeyError): foo_attrib = None
... 
>>> print foo_attrib
None

Aunque éste no ha sido un conjunto exhaustivo de ejemplos, ahora has visto lo básico para acceder a las respuestas RPC de PyEZ en el formato de objeto predeterminado lxml.etree.Element. Estos ejemplos han mostrado algunas de las formas más comunes de acceder a la información de respuesta, pero la biblioteca lxml ofrece muchas más herramientas. La documentación completa sobre la API de lxmlestá disponible en el sitio web lxml . La API lxml también es compatible en su mayor parte con la conocida API ElementTree. La documentación sobre la API ElementTree forma parte de la documentación de la Biblioteca Estándar de Python en ElementTree XML API.

La siguiente sección comienza a explorar los otros formatos opcionales que PyEZ puede utilizar para devolver respuestas RPC .

Normalización de la respuesta

La normalización de la respuesta es una función de PyEZ que realmente altera el contenido XML devuelto por un método RPC. Hay algunas RPC de Junos que devuelven datos XML en los que los valores de ciertos elementos XML están envueltos en una nueva línea u otros caracteres de espacio en blanco. Un ejemplo de estos espacios en blanco adicionales puede verse en con la RPC get-system-users-information que utilizamos en el apartado anterior:

>>> response = r0.rpc.get_system_users_information()
>>> response.findtext("uptime-information/up-time")
'\n4 days, 17 mins\n'

Observa que el texto del elemento <up-time> tiene un carácter de nueva línea antes y después de la cadena de valores. La normalización de la respuesta está diseñada para resolver esta situación. Cuando la normalización de la respuesta está activada, se eliminan todos los caracteres de espacio en blanco al principio y al final del valor de cada elemento XML. La normalización de la respuesta está desactivada por defecto (excepto cuando se utilizan tablas y vistas, como se explica en "Tablas y vistas operativas"). Se puede activar añadiendo el argumento normalize=True a un método RPC:

>>> response = r0.rpc.get_system_users_information(normalize=True)
>>> response.findtext("uptime-information/up-time")
'4 days, 17 mins'

Observa que se han eliminado los caracteres de nueva línea al principio y al final del valor, pero se mantienen los espacios en blanco dentro del valor.

Te habrás dado cuenta de que se añadió el argumento normalize=True a la invocación del método r0.rpc.get_system_users_information()en la sección anterior. ¿Por qué se utilizó el argumento en esos ejemplos? Los caracteres de espacio en blanco adicionales presentes en algunas respuestas RPC hacen que algunas expresiones XPath sean más difíciles y menos intuitivas. Como ejemplo, considera la expresión XPath utilizada para encontrar al usuario conectado a la consola del dispositivo Junos (en este dispositivo la consola tiene un nombre de tty de u0). Sin la normalización de la respuesta, el ejemplo XPath anterior no devuelve un elemento XML coincidente:

>>> response = r0.rpc.get_system_users_information()
>>> response.findtext("uptime-information/user-table/user-entry[tty='u0']/user")
>>>

La respuesta vacía se debe a que el valor de la parte [tag='value'] de la expresión XPath debe coincidir exactamente. Mostrar explícitamente el valor de la etiqueta <tty> para la entrada de usuario deseada (que resulta estar en la posición 7 en este ejemplo concreto) revela que el valor tiene caracteres de nueva línea inicial y final:

>>> response.findtext("uptime-information/user-table/user-entry[7]/tty")
'\nu0\n'
>>>

Podrías modificar la expresión XPath para buscar este valor concreto, como se muestra en este ejemplo:

>>> response.findtext(
      ...    "uptime-information/user-table/user-entry[tty='\nu0\n']/user"
      ... )
'\nroot\n'
>>>

Sin embargo, puede ser algo impredecible si hay espacios en blanco adicionales para el valor de cualquier elemento XML en una respuesta RPC determinada. En su lugar, es más fácil utilizar simplemente la normalización de la respuesta para que coincida con el valor esperado sin tener que preocuparse por los espacios en blanco iniciales o finales:

>>> response = r0.rpc.get_system_users_information(normalize=True)
>>> response.findtext("uptime-information/user-table/user-entry[tty='u0']/user")
'root'
>>>

La normalización de la respuesta elimina los espacios en blanco iniciales y finales de los valores de todos los elementos XML de la respuesta. Así, no sólo se simplifica la expresión XPath, sino que se evita la necesidad de realizar un procesamiento adicional para eliminar los espacios en blanco del valor del nombre de usuario al que se accede.

Una nota final sobre la normalización de la respuesta: puede activarse por cada RPC, como se ha mostrado hasta ahora, o puede activarse para todas las RPC de una instancia de dispositivo o de una sesión NETCONF especificando el argumento normalize=True en las llamadas a Device() o open(), respectivamente. Aquí tienes un ejemplo de activación para la instancia del dispositivo:

>>> r0 = Device(host='r0',user='user',password='user123',normalize=True)
>>> r0.open()
Device(r0)
>>> response = r0.rpc.get_system_users_information()
>>> response.findtext("uptime-information/up-time")
'4 days, 2:03'

Si la normalización de la respuesta está activada para la instancia del dispositivo, como se acaba de mostrar, sigue siendo posible anular el comportamiento en cada RPC especificando un argumento normalize=False al invocar el método RPC .

jxmlease

Además de analizar documentos XML de para convertirlos en objetos lxml.etree.Element, también puedes utilizar la biblioteca jxmlease para analizar respuestas RPC y convertirlas en objetos jxmlease. Puede que prefieras jxmlease, descrito en "Uso de datos estructurados en Python", a utilizar XPath y la biblioteca lxml. Este formato ofrece un excelente equilibrio entre funcionalidad y facilidad de uso. Se puede acceder a los valores de los elementos XML utilizando las mismas herramientas que los diccionarios y listas nativos de Python. Has visto un ejemplo de uso de jxmlease en el script de ejemplo del Capítulo 3. En ese ejemplo, jxmlease se utilizó para analizar la cadena XML devuelta por el servicio RESTful API de Junos. Sin embargo, jxmlease también puede utilizarse para analizar directamente un objeto lxml.etree.Element. Este análisis se realiza pasando un objeto lxml.etree.Element a una instancia de la clase jxmlease.EtreeParser. Aquí tienes un ejemplo de uso de esta técnica para devolver la salida del RPC get-system-users-information como un objeto jxmlease.XMLDictNode:

>>> import jxmlease
>>> parser = jxmlease.EtreeParser()
>>> response = parser(r0.rpc.get_system_users_information())
>>> response.prettyprint(depth=3)
{'system-users-information': {'uptime-information': {'active-user-count': u'6',
                                                     'date-time': u'12:39PM',
                                                     'load-average-1': u'0.29',
                                                     'load-average-15': u'0.41',
                                                     'load-average-5': u'0.43',
                                                     'up-time': u'3 days, 4:57',
                                                     'user-table': {...}}}}
>>> type(response)
<class 'jxmlease.XMLDictNode'>

La respuesta es en realidad un objeto jxmlease.XMLDictNode, pero se comporta como un diccionario ordenado.

Puedes acceder a cualquier nivel del objeto jxmlease.XMLDictNode especificando una cadena de claves de diccionario. La etiqueta de cada elemento XML se utiliza como clave del diccionario, y comienza con la etiqueta del elemento raíz de la respuesta RPC. Como ejemplo, esta sentencia accede al tiempo de actividad del sistema:

>>> print response['system-users-information']['uptime-information']['up-time']
3 days, 4:19

Observa que las claves empiezan por ['system-users-information'] y que el equivalente de la normalización de la respuesta se aplica a los objetos jxmlease.XMLDictNode por defecto.

Para los elementos XML que tienen atributos, puedes utilizar el método get_xml_attr() del objeto para recuperar el valor del atributo:

>>> ut = response['system-users-information']['uptime-information']['up-time']
>>> ut.get_xml_attr('seconds')
'274740'

get_xml_attr() también permite devolver un valor por defecto si el nombre del atributo XML no existe:

>>> ut.get_xml_attr('foo',0)
0

El script de ejemplo de PyEZ en "Un ejemplo de PyEZ" demuestra aún más el uso de jxmlease con respuestas PyEZ RPC .

JSON

Junos empezó a soportar el formato de salida JSON en la versión 14.2 o posterior. Cuando se utiliza PyEZ para invocar una RPC en un dispositivo Junos que ejecuta la versión 14.2 o posterior, PyEZ puede solicitar esta salida JSON. Puedes solicitar la salida JSON de una RPC pasando un argumento que sea un diccionariocon una única clave 'format' con un valor de 'json'. Este argumento hace que el dispositivo Junos devuelva la respuesta RPC como una cadena JSON. Sin embargo, la cadena JSON no se devuelve directamente al usuario. En su lugar, PyEZ invoca a json.loads() para que analice la cadena JSON y la convierta en una estructura de datos nativa de Python compuesta por diccionarios y listas. Aquí tienes un ejemplo:

>>> response = r0.rpc.get_system_users_information({'format': 'json'})
>>> type(response)
<type 'dict'>
>>> from pprint import pprint
>>> pprint(response, depth=3)
{u'system-users-information': [{u'attributes': {...},
                                u'uptime-information': [...]}]}

Este comportamiento de analizar automáticamente la respuesta JSON en una estructura de datos Python es análogo al comportamiento con el formato XML por defecto. Cuando se utiliza el formato XML por defecto, el dispositivo devuelve una cadena que contiene un documento XML y PyEZ analiza el documento XML para convertirlo en una estructura de datos Python. Se puede acceder a la respuesta resultante con todas las herramientas normales de Python para manejar diccionarios y listas. Consulta "Datos JSON" para más detalles sobre el formato JSON, incluidas sus limitaciones .

Tablas y vistas operativas

Además de la función "RPC bajo demanda", PyEZ ofrece otro método para invocar una RPC operativa y mapear la respuesta en una estructura de datos Python. Esta función de "tablas y vistas" proporciona un control preciso para mapear partes de la respuesta RPC en una estructura de datos Python. Además, permite almacenar esta asignación para reutilizarla fácilmente. De hecho, PyEZ viene preempaquetado con un conjunto de tablas y vistas de ejemplo que puedes utilizar o modificar para adaptarlas a tus necesidades particulares.

Los datos operativos de un dispositivo Junos son el conjunto de estados que representan las condiciones actuales de funcionamiento del dispositivo. Los datos operativos son información de sólo lectura que está separada de los datos de configuración del dispositivo. Normalmente, puedes ver los datos operativos mediante los comandos CLI show o mediante RPCs XML operativos. Puedes considerar estos datos operativos como similares a una base de datos. Las bases de datos se organizan en una colección de tablas, y de forma similar, PyEZ organiza conceptualmente los datos operativos de un dispositivo Junos en una colección de tablas.

En PyEZ, una "tabla" representa la información que devuelve una determinada RPC XML. Una tabla PyEZ se divide a su vez en una lista de "elementos". Estos elementos son todos los nodos XML de la salida RPC que coinciden con una determinada expresión XPath.

De forma similar a cómo una vista de base de datos selecciona y presenta un subconjunto de campos de una tabla de base de datos, una "vista" PyEZ selecciona y mapea un conjunto de campos (nodos XML) de cada elemento de la tabla PyEZ en una estructura de datos nativa de Python. Cada tabla PyEZ tiene al menos una vista, la vista por defecto, para mapear los campos de un elemento en una estructura de datos nativa de Python. Se pueden definir vistas adicionales, pero sólo es necesaria la vista por defecto.

Las vistas múltiples se utilizan para seleccionar información diferente de los elementos de la tabla. El concepto de vistas múltiples es similar al de los indicadores terse, brief, detail, y extensive de varios comandos de la CLI. Al igual que cada uno de estos indicadores de la CLI muestra información diferente del mismo comando de la CLI, las vistas diferentes presentan información diferente de la misma tabla.

Tablas y vistas operativas preempaquetadas

Empecemos utilizando algunas de las tablas y vistas existentes incluidas en PyEZ. Las tablas y vistas se definen en archivos YAML, que tienen una extensión de nombre de archivo . yml. El contenido de estos archivos YAML se tratará en detalle en la próxima sesión, cuando expliquemos cómo crear tus propias tablas y vistas.

Las tablas y vistas preempaquetadas incluidas con PyEZ se encuentran en el subdirectorio op del directorio de instalación de el módulo jnpr.junos, como muestra este listado de directorios:

user@h0$ pwd
/usr/local/lib/python2.7/dist-packages/jnpr/junos
user@h0$ ls op/*.yml
op/arp.yml                     op/fpc.yml           op/lacp.yml  op/phyport.yml
op/bfd.yml                     op/idpattacks.yml    op/ldp.yml   op/routes.yml
op/ccc.yml                     op/intopticdiag.yml  op/lldp.yml  op/teddb.yml
op/ethernetswitchingtable.yml  op/isis.yml          op/nd.yml    op/vlan.yml
op/ethport.yml                 op/l2circuit.yml     op/ospf.yml  op/xcvr.yml

El subdirectorio op es relativo a la ubicación donde está instalado el módulo jnpr.junos de la biblioteca PyEZ. En el host de automatización de ejemplo, este directorio es /usr/local/lib/python2.7/dist-packages/jnpr/junos, pero la ubicación es específica de la instalación y puede ser diferente en tu máquina. Determina la ubicación del directorio en tu máquina mostrando el directorio del atributo jnpr.junos.__file__. Utiliza esta receta:

>>> import jnpr.junos
>>> import os.path
>>> os.path.dirname(jnpr.junos.__file__)
'/usr/local/lib/python2.7/dist-packages/jnpr/junos'

Para utilizar las tablas y vistas preempaquetadas, necesitarás conocer los nombres de las tablas disponibles. Puedes determinar los nombres de las tablas buscando en los archivos .yml la cadena Table:, como se muestra en este ejemplo:

user@h0$ pwd
/usr/local/lib/python2.7/dist-packages/jnpr/junos
user@h0$ grep Table: op/*.yml
op/arp.yml:ArpTable:
op/bfd.yml:BfdSessionTable:
op/bfd.yml:_BfdSessionClientTable:
op/ccc.yml:CCCTable:
op/ethernetswitchingtable.yml:EthernetSwitchingTable:
op/ethernetswitchingtable.yml:_MacTableEntriesTable:
op/ethernetswitchingtable.yml:_MacTableInterfacesTable:
op/ethport.yml:EthPortTable:
op/fpc.yml:FpcHwTable:
op/fpc.yml:FpcMiReHwTable:
op/fpc.yml:FpcInfoTable:
op/fpc.yml:FpcMiReInfoTable:
op/idpattacks.yml:IDPAttackTable:
op/intopticdiag.yml:PhyPortDiagTable:
op/isis.yml:IsisAdjacencyTable:
op/isis.yml:_IsisAdjacencyLogTable:
op/l2circuit.yml:L2CircuitConnectionTable:
op/lacp.yml:LacpPortTable:
op/lacp.yml:_LacpPortStateTable:
op/lacp.yml:_LacpPortProtoTable:
op/ldp.yml:LdpNeighborTable:
op/ldp.yml:_LdpNeighborHelloFlagsTable:
op/ldp.yml:_LdpNeighborTypesTable:
op/lldp.yml:LLDPNeighborTable:
op/nd.yml:NdTable:
op/ospf.yml:OspfNeighborTable:
op/phyport.yml:PhyPortTable:
op/phyport.yml:PhyPortStatsTable:
op/phyport.yml:PhyPortErrorTable:
op/routes.yml:RouteTable:
op/routes.yml:RouteSummaryTable:
op/routes.yml:_rspTable:
op/teddb.yml:TedTable:
op/teddb.yml:_linkTable:
op/teddb.yml:TedSummaryTable:
op/vlan.yml:VlanTable:
op/xcvr.yml:XcvrTable:

El primer paso para utilizar una de estas tablas es importar la clase Python adecuada. (El nombre de la clase Python es el mismo que el nombre de la tabla definida en el archivo YAML ). Utilicemos como ejemplo ArpTable de la primera línea de la salida anterior. Para importar la clase ArpTable, introduce la siguiente sentencia from:

>>> from jnpr.junos.op.arp import ArpTable

Al igual que los métodos RPC bajo demanda, las tablas y vistas operan sobre una instancia de dispositivo PyEZ con una sesión NETCONF abierta. Como antes, utilizamos las llamadas Device() y open() para crear y abrir la variable de instancia de dispositivo r0:

>>> from jnpr.junos import Device
>>> r0 = Device(host='r0',user='user',password='user123')
>>> r0.open()
Device(r0)
>>>

Crea una instancia de tabla vacía pasando la variable de instancia del dispositivo (r0) como argumento al constructor de la clase (ArpTable()):

>>> arp_table = ArpTable(r0)

La variable de instancia arp_table se puede rellenar ahora con todos los elementos de la tabla invocando el método de instancia get():

>>> arp_table.get()
ArpTable:r0: 9 items

En la salida anterior, el método get() ejecuta una RPC y utiliza los resultados para rellenar la instancia arp_tablecon nueve elementos.

Nota

En el caso de ArpTable, cada elemento representa un nodo <arp-table-entry> de la salida XML del RPC get-arp-table-information.

Una forma alternativa de crear y rellenar la tabla es vincular la tabla como un atributo de la variable de instancia del dispositivo. El método get() se invoca entonces sobre este atributo vinculado:

>>> from jnpr.junos.op.arp import ArpTable
>>> r0.bind(arp_table=ArpTable)
>>> r0.arp_table.get()
ArpTable:r0: 9 items

En la salida anterior, vemos de nuevo que el método get() ejecuta una RPC y rellena el atributo r0.arp_tablecon nueve elementos. Almacenar el arp_table como un atributo de la variable de instancia de dispositivo es una notación conveniente cuando mantienes una tabla para varios dispositivos. Por ejemplo, puedes almacenar y acceder fácilmente a tablas ArpTable separadas para los dispositivos r0, r1, y r2. A continuación, puedes acceder a cada tabla como un atributo de la respectiva variable de instancia de dispositivo.

También es posible recuperar elementos concretos de la tabla pasando argumentos al método get(). Cuando se invoca el método get(), se ejecuta la correspondiente RPC XML definida en el archivo YAML de la tabla. Al método get() se le pueden pasar los mismos parámetros que si estuvieras invocando la RPC utilizando RPC bajo demanda. En el caso de ArpTable, se ejecuta el RPC get-arp-table-information. El RPC get-arp-table-information admite un argumento <interface>para limitar la respuesta a las entradas ARP de una interfaz específica. He aquí un ejemplo para recuperar sólo las entradas ARP de la interfaz lógica ge-0/0/0.0:

>>> arp_table.get(interface='ge-0/0/0.0')
ArpTable:r0: 1 items
>>> pprint(arp_table.items())
[('00:05:86:18:ec:02',
  [('interface_name', 'ge-0/0/0.0'),
   ('ip_address', '10.0.4.2'),
   ('mac_address', '00:05:86:18:ec:02')])]

Invocar el método get() de una tabla siempre actualiza los elementos de la tabla ejecutando un RPC XML contra el dispositivo Junos y recibiendo la respuesta del RPC. Cuando invocas el método get(), la nueva respuesta sobrescribe todos los datos anteriores de la tabla. Recuerda invocar el método get(), con los argumentos adecuados, cada vez que necesites actualizar los datos almacenados en la tabla.

Nota

La normalización de los datos de tablas y vistas está activada por defecto. Consulta "Normalización de respuestas" para obtener más información sobre la normalización. Si necesitas desactivar la normalización, pasa el argumento normalize=False al método get():

>>> arp_table.get(normalize=False)
ArpTable:r0: 9 items

Cuando creas una instancia de una tabla, devuelve un objeto jnpr.junos.factory.OpTable. Cada objeto jnpr.junos.factory.OpTable funciona de forma similar a un objeto Python OrderedDict de vista. El siguiente ejemplo actualiza la instancia arp_tablecon todas las entradas de la tabla ARP y, a continuación, muestra arp_table's type is jnpr.junos.factory.OpTable.ArpTableque es una subclase de jnpr.junos.factory.OpTableAl igual que con un diccionario, se accede a las claves y valores de arp_table utilizando el método items():

>>> arp_table.get()
ArpTable:r0: 9 items
>>> type(arp_table)
<class 'jnpr.junos.factory.OpTable.ArpTable'>
>>> from pprint import pprint
>>> pprint(arp_table.items())
[('00:05:86:48:49:00',
  [('interface_name', 'ge-0/0/4.0'),
   ('ip_address', '10.0.1.2'),
   ('mac_address', '00:05:86:48:49:00')]),
 ('00:05:86:78:2a:02',
  [('interface_name', 'ge-0/0/1.0'),
   ('ip_address', '10.0.2.2'),
   ('mac_address', '00:05:86:78:2a:02')]),
 ('00:05:86:68:0b:02',
  [('interface_name', 'ge-0/0/2.0'),
   ('ip_address', '10.0.3.2'),
   ('mac_address', '00:05:86:68:0b:02')]),
 ('00:05:86:18:ec:02',
  [('interface_name', 'ge-0/0/0.0'),
   ('ip_address', '10.0.4.2'),
   ('mac_address', '00:05:86:18:ec:02')]),
 ('00:05:86:08:cd:00',
  [('interface_name', 'ge-0/0/3.0'),
   ('ip_address', '10.0.5.2'),
   ('mac_address', '00:05:86:08:cd:00')]),
 ('10:0e:7e:b1:f4:00',
  [('interface_name', 'fxp0.0'),
   ('ip_address', '10.102.191.252'),
   ('mac_address', '10:0e:7e:b1:f4:00')]),
 ('10:0e:7e:b1:b0:80',
  [('interface_name', 'fxp0.0'),
   ('ip_address', '10.102.191.253'),
   ('mac_address', '10:0e:7e:b1:b0:80')]),
 ('00:00:5e:00:01:c9',
  [('interface_name', 'fxp0.0'),
   ('ip_address', '10.102.191.254'),
   ('mac_address', '00:00:5e:00:01:c9')]),
 ('56:68:a6:6a:47:b2',
  [('interface_name', 'em1.0'),
   ('ip_address', '128.0.0.16'),
   ('mac_address', '56:68:a6:6a:47:b2')])]
>>>
Nota

En el ejemplo anterior se ha utilizado el primer método para crear y rellenar la tabla, la variable de instancia arp_table. Si la tabla se ha vinculado a la variable de instancia dispositivo, utilizando el segundo método, accederías a ella con r0.arp_table en lugar de arp_table:

>>> type(r0.arp_table)
<class 'jnpr.junos.factory.OpTable.ArpTable'>

Como la tabla funciona de forma similar a un OrderedDict, se puede acceder a los elementos individuales (que son objetos jnpr.junos.factory.View.ArpView) por posición o por clave:

>>> type(arp_table[0])
<class 'jnpr.junos.factory.View.ArpView'>
>>> pprint(arp_table[0].items())
[('interface_name', 'ge-0/0/4.0'),
 ('ip_address', '10.0.1.2'),
 ('mac_address', '00:05:86:48:49:00')]
>>> pprint(arp_table['00:05:86:48:49:00'].items())
[('interface_name', 'ge-0/0/4.0'),
 ('ip_address', '10.0.1.2'),
 ('mac_address', '00:05:86:48:49:00')]
>>>

Se puede acceder a los valores individuales de un elemento de la vista con referencias de dos niveles utilizando un índice o un valor clave para la referencia externa:

>>> arp_table['00:05:86:48:49:00']['ip_address']
'10.0.1.2'
>>> arp_table[0]['ip_address']
'10.0.1.2'
>>> arp_table['00:05:86:48:49:00']['interface_name']
'ge-0/0/4.0'
>>> arp_table[0]['interface_name']
'ge-0/0/4.0'

Si asignas un objeto vista (una subclase de jnpr.junos.factory.View) a una variable, también puedes acceder a cada uno de los campos del objeto vista como propiedades de Python, o como un diccionario :

>>> arp_item = arp_table[0]
>>> arp_item.ip_address
'10.0.4.2'
>>> arp_item.interface_name
'ge-0/0/0.0'
>>> arp_item.mac_address
'00:05:86:18:ec:02'
>>>
>>> arp_item.keys()
['interface_name', 'ip_address', 'mac_address']
>>> arp_item.items()
[('interface_name', 'ge-0/0/0.0'), 
 ('ip_address', '10.0.4.2'), 
 ('mac_address', '00:05:86:18:ec:02')]

Además, cada objeto vista tiene dos propiedades especiales , name y T. La propiedad name proporciona el nombre único, o clave, de la vista dentro de la tabla. La propiedad Tes una referencia a la tabla asociada que contiene la vista:

>>> arp_item.name
'00:05:86:18:ec:02'
>>> arp_item.T
ArpTable:r0: 9 items

En esta sección, has visto cómo instanciar, rellenar y acceder a tablas y vistas preempaquetadas. Ahora vamos a ver cómo definir una tabla y una vista personalizadas en lugar de limitarnos a las tablas y vistas preempaquetadas que se incluyen en con PyEZ.

Crear nuevas tablas y vistas operativas

Las tablas PyEZ y las vistas se definen en archivos .yml utilizando el formato YAML. YAML utiliza una sintaxis sencilla e intuitiva legible por humanos para definir estructuras de datos jerárquicas formadas por escalares, listas y matrices asociativas (similares a los diccionarios de Python). Para más información sobre YAML, consulta la siguiente barra lateral, "YAML de un vistazo", y para información completa sobre YAML consulta la especificación YAML.

Las definiciones de tablas y vistas de PyEZ utilizan un subconjunto de la sintaxis YAML completa. Utilizan principalmente matrices asociativas con múltiples niveles de jerarquía. Los valores suelen ser tipos de datos de cadena. He aquí un ejemplo utilizando el archivo arp.yml, que define las clases ArpTable y ArpView utilizadas en la sección anterior:

---
ArpTable: 1
  rpc: get-arp-table-information 2
  item: arp-table-entry 3
  key: mac-address 4
  view: ArpView 5

ArpView: 6
  fields: 7
    mac_address: mac-address 8
    ip_address: ip-address 9
    interface_name: interface-name 10
1

El cargador YAML de PyEZ asume que todo lo que aparece en la primera columna es una definición de tabla o de vista. Las definiciones de vista se distinguen de las definiciones de tabla por sus claves. Esta línea inicia una definición de tabla. El nombre de la tabla se convierte en el nombre de la clase Python correspondiente. La única restricción sobre el nombre de la tabla es que debe ser un nombre válido para una clase Python.

2

El RPC XML de Junos(get-arp-table-information) que se invoca para recuperar los datos de los elementos de la tabla.

3

Una expresión XPath utilizada para seleccionar cada elemento de la tabla en la respuesta RPC.

4

El nombre de un elemento XML dentro de cada elemento de la tabla. El valor de este elemento XML se convierte en la clave utilizada para acceder a cada elemento dentro de la estructura de datos nativa de Python.

5

El nombre de la vista por defecto utilizada para mapear un elemento de la tabla en una estructura de datos nativa de Python. Este valor debe coincidir exactamente con un nombre de vista definido en el mismo documento YAML. En este documento, la única vista es ArpView, definida por la clave de matriz asociada de nivel superior.

6

El cargador YAML de PyEZ asume que todo lo que aparece en la primera columna es una definición de tabla o de vista. Las definiciones de vista se distinguen de las definiciones de tabla por sus claves. Esta línea inicia una definición de vista.

7

El valor de la clave fields es una matriz asociativa que asigna expresiones XPath a nombres. Los nombres se utilizan como claves en la estructura de datos nativa de Python (el objeto vista). Las expresiones XPath se utilizan para obtener valores del objeto XML de cada elemento.

8

La expresión XPath mac-address se utiliza para establecer el valor de la clave mac_address en el objeto vista nativo de Python. Dado que mac_address se utiliza como clave en una estructura de datos de Python, debe ajustarse a los requisitos de denominación de variables de Python (debe utilizar guiones bajos, no guiones).

9

De nuevo, ip-address es una expresión XPath y ip_addresses el nombre de la clave en el objeto vista de Python.

10

La clave final de cada objeto vista de Python es interface_name. El valor de la clave interface_name viene determinado por la expresión XPath interface-name.

Aunque el archivo arp.yml muestra la estructura necesaria y el contenido de de las definiciones de tablas y vistas PyEZ, no incluye todos los pares clave/valor posibles. La Tabla 4-5 proporciona una descripción de cada par clave/valor disponible en que puede utilizarse en la definición de una tabla.

Tabla 4-5. Claves para definir una tabla PyEZ
Nombre claveObligatorio u opcionalDescripción
rpcNecesarioEl nombre de la RPC XML de Junos que se invoca para recuperar los datos de los elementos de la tabla. Este valor debe utilizar el nombre real de la etiqueta RPC XML (con guiones) en lugar del nombre del método PyEZ RPC on Demand (con guiones bajos).
argsOpcionalUna matriz asociativa cuyos elementos se pasan como argumentos por defecto a rpc. El parámetro args sólo debe especificarse si rpc debe llamarse siempre con argumentos args. Las claves de la matriz asociativa son los argumentos pasados al método PyEZ RPC on Demand (con guiones bajos, no guiones). Si un argumento RPC es una bandera, establece el valor de la matriz asociativa en True.
args_keyOpcional

El nombre de un primer argumento opcional sin nombre del método get(). Por ejemplo, la definición preempaquetada RouteTableincluye:

RouteTable:
  rpc: get-route-information
  args_key: destination
Esta definición permite al usuario llamar a:
>>> route_table.get('10.0.0.0/8')

Lo que provoca que se envíe la siguiente RPC al dispositivo Junos:

<get-route-information>
    <destination>10.0.0.0/8</destination>
</get-route-information>
itemNecesarioUna expresión XPath XML de que selecciona cada elemento de la tabla (registro de datos) de la respuesta RPC. Cada elemento XML de la respuesta RPC que coincida con la expresión XPath se convierte en un elemento de la tabla. La expresión XPath es relativa al primer elemento de la respuesta después de la etiqueta <rpc-reply>. Éste es el mismo comportamiento que observamos en "Elementos lxml " al utilizar métodos lxml con respuestas RPC.
keyOpcional, pero recomendado

Un XPath de para seleccionar un elemento XML dentro de cada elemento de la tabla. El valor del elemento XML se convierte en la clave utilizada para acceder a cada elemento dentro de la estructura de datos nativa de Python.

Si no se especifica key, se utiliza como clave el XPath name. Si se necesitan varios elementos XML para identificar unívocamente un elemento de la tabla, el valor de esta clave es una lista YAML que contiene un XPath para cada elemento XML que forma la clave. Se recomienda especificar explícitamente key aunque su valor sea el predeterminado name.

viewNecesarioaEl nombre de la vista por defecto utilizada para mapear un elemento de la tabla en una estructura de datos nativa de Python. Este valor debe coincidir exactamente con el nombre de una vista definida en el mismo documento YAML.

a Si la clave view no está presente, cada elemento de la tabla se devuelve como un objeto lxml.etree.Element. Esto anula gran parte de las ventajas de las tablas y las vistas, por lo que la clave view es efectivamente necesaria.

Pongamos en práctica la información de la Tabla 4-5 creando una nueva definición de tabla para el comando CLI show system usersque hemos utilizado anteriormente en este capítulo. En primer lugar, el comando CLI show system users asigna a el RPC XML get-system-users-information. Nuestra definición de tabla incluirá el argumento no-resolve RPC para evitar búsquedas DNS en los elementos <from> de la respuesta RPC:

user@r0> show system users no-resolve | display xml rpc 
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/15.1R2/junos">
    <rpc>
        <get-system-users-information>
                <no-resolve/>
        </get-system-users-information>
    </rpc>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

Es útil hacer referencia a la estructura de la respuesta RPC esperada al crear la nueva definición de tabla y vista. Aquí se incluye una versión abreviada de la respuesta get-system-users-information para que te sirva de referencia. Esta respuesta RPC incluye tanto información de todo el sistema (<up-time>, <active-user-count>, <load-average-1>, etc.) como información específica de cada inicio de sesión (los elementos <user-entry> ):

user@r0> show system users no-resolve | display xml        
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/15.1R2/junos">
    <system-users-information xmlns="http://xml.juniper.net/junos/15.1R2/junos">
        <uptime-information>
            <date-time junos:seconds="1437696318">5:05PM</date-time>
            <up-time junos:seconds="192060">2 days,  5:21</up-time>
            <active-user-count junos:format="8 users">8</active-user-count>
            <load-average-1>0.46</load-average-1>
            <load-average-5>0.50</load-average-5>
            <load-average-15>0.44</load-average-15>
            <user-table>
                <user-entry>
                    <user>root</user>
                    <tty>u0</tty>
                    <from>-</from>
                    <login-time junos:seconds="0">Wed08PM</login-time>
                    <idle-time junos:seconds="16860">4:41</idle-time>
                    <command>cli</command>
                </user-entry>
                <user-entry>
                    <user>user</user>
                    <tty>pts/2</tty>
                    <from>172.29.98.24</from>
                    <login-time junos:seconds="1437694518">4:35PM</login-time>
                    <idle-time junos:seconds="0">-</idle-time>
                    <command>-cli (cli)</command>
                </user-entry>
                <user-entry>
                    <user>user</user>
                    <tty>pts/3</tty>
                    <from>172.29.104.116</from>
                    <login-time junos:seconds="1437667698">9:08AM</login-time>
                    <idle-time junos:seconds="6420">1:47</idle-time>
                    <command>-cli (cli)</command>
                </user-entry>
                ... additional user-entry elements omitted ...
            </user-table>
        </uptime-information>
    </system-users-information>
    <cli>
        <banner></banner>               
    </cli>
</rpc-reply>

La definición de la tabla se centrará en la información específica del inicio de sesión, extrayendo los elementos de <user-entry>en elementos de la tabla. Aquí tienes la definición de la tabla y las explicaciones de cada campo. Sigue el proceso escribiendo este contenido en un archivo users.yml. Recuerda utilizar espacios, no tabuladores, para la indentación:

---
### ------------------------------------------------------
### show system users no-resolve
### ------------------------------------------------------

UserTable:
    rpc: get-system-users-information
    args:
        no_resolve: True
    item: uptime-information/user-table/user-entry
    key:
        - user
        - tty
    view: UserView

Este archivo YAML define una tabla llamada UserTable. La tabla se rellena ejecutando el RPC get-system-users-information con el argumento <no-resolve/>. La tabla contendrá un elemento por cada entrada XML que coincida con la expresión XPath uptime-information/user-table/user-entry. Y, por defecto, se aplicará la vista UserView a cada elemento de la tabla. Este ejemplo muestra una clave multielemento. Cada parte de la clave (tty y user) es una expresión XPath relativa a un elemento de la tabla (un elemento <user-entry>). La clave de cada elemento de la tabla será una tupla formada a partir de los valores user y tty.

Nota

En la mayoría de los casos, hay un único elemento XML que identifica unívocamente a cada elemento. En este caso, el elemento <tty> identifica de forma única a cada <user-entry> y podría haberse utilizado como clave simple. Sin embargo, a efectos de demostración, hemos optado por mostrar una clave multielemento utilizando la combinación de los elementos <user> y <tty>.

La clave item de la definición de tabla anterior es un XPath relativamente sencillo que especifica una jerarquía de tres niveles, uptime-information/user-table/user-entry, para seleccionar todos los elementos de <user-entry>. Aunque las expresiones XPath avanzadas quedan fuera del alcance de este libro, puede ser útil ver cómo se utiliza una expresión XPath más complicada para controlar la selección de elementos de la tabla. Por ejemplo, sustituyendo esta expresión XPath sólo se seleccionarán los inicios de sesión de usuario que hayan estado inactivos durante más de 1 hora (3600 segundos):

item: "uptime-information/user-table/user-entry[idle-time[@seconds>3600]]"

Cada elemento de la tabla sigue representando un elemento <user-entry> , pero sólo se seleccionan determinados elementos <user-entry>. En concreto, se seleccionan los elementos <user-entry> que tienen un elemento hijo <idle-time> , y donde el elemento <idle-time> tiene un atributo seconds con un valor mayor que 3600.

Nota

La expresión XPath precedente debe ir entre comillas dobles. Contiene corchetes, que de otro modo YAML interpretaría como una lista en línea.

Ahora que hemos visto cómo definir una tabla más compleja, centremos nuestra atención en la definición de la vista correspondiente. El único propósito de una definición de vista es asignar valores a claves en un objeto de vista Python. El valor de una clave determinada procede de una expresión XPath correspondiente. La expresión XPath es relativa a cada elemento de la tabla, y normalmente selecciona un único elemento del elemento de la tabla. El nodo de texto del elemento seleccionado se convierte en el valor de la clave en el objeto de vista Python.

Añade esta definición de vista al mismo archivo users.yml que contiene la definición de UserTable:

UserView:
    fields:
        from: from
        login_time: login-time
        idle_time: idle-time
        command: command

No pongas un separador de documentos YAML (---) entre las definiciones de tabla y vista. Ambas forman parte del mismo documento YAML.

Cada definición de vista comienza con el nombre de la vista en la primera columna. De nuevo, las definiciones de vista se distinguen de las definiciones de tabla en función de sus claves. Si la clave o rpc está presente, PyEZ asume que es una definición de tabla. Si no está presente rpc, PyEZ asume que es una definición de vista.

En el ejemplo, el nombre de la vista es UserView. El nombre de la vista se convierte en el nombre de la clase Python correspondiente, por lo que el nombre debe seguir las convenciones de Python para nombres de clases. También debe haber un nombre de vista que coincida exactamente con la propiedad viewde la definición de la tabla. Ésta es la vista por defecto de la tabla. Se pueden definir vistas adicionales, pero sólo es necesaria la vista por defecto.

Fíjate en los dos puntos después de UserView. Estos dos puntos indican que UserView es una clave de una matriz asociativa. El valor de la clave UserView es otra matriz asociativa, que puede tener cuatro tipos de claves : fields extends , groups y fields_groupnameEl ejemplo utiliza la más sencilla de estas claves, fields. El valor de la clave fields es otra matriz asociativa.

La matriz asociativa fields define un conjunto de nombres y sus correspondientes expresiones XPath (que son relativas al elemento de la tabla). Dado que los nombres se convierten en atributos del objeto de vista Python correspondiente, deben ajustarse a las convenciones de nomenclatura de Python para variables. En el ejemplo, from, login_time, idle_time, y command son los nombres de los campos.

Advertencia

Los atributos de instancia de vista (nombres de propiedades o métodos) no pueden utilizarse como nombres de campos. Actualmente, los atributos de instancia de vista incluyen: asview, items, key, keys, name, refresh, to_json, updater, values, xml, D, FIELDS, GROUPS, ITEM_NAME_XPATH, y T. No utilices estos nombres como nombres de campo. También debes evitar los nombres de campo que empiecen por guión bajo.

Las expresiones XPath son from, login-time, idle-time, y command. Cada una de estas expresiones XPath selecciona un único elemento hijo coincidente de un elemento de la tabla. Vuelve a la salida XML y observa cómo cada elemento <user-entry> contiene <from>, <login-time>, <idle-time>, y <command> elementos hijos.

También es posible utilizar expresiones XPath más complicadas para la selección de campos. Toma este ejemplo de definición de campo del preempaquetado RouteTableView en el archivo routes.yml:

    via: "nh/via | nh/nh-local-interface"

El nombre del campo es via. La expresión XPath es nh/via | nh/nh-local-interface. Esta expresión XPath selecciona todos los elementos <via> y<nh-local-interface> bajo los elementos hijos <nh> del elemento. Si sólo hay un elemento coincidente, el campo via será el valor de la cadena del elemento coincidente. Sin embargo, si la expresión XPath selecciona más de un elemento, el valor del campo via será una lista de valores de cadena. Los valores de cadena se toman de cada elemento coincidente.

Por defecto, los valores de los campos son cadenas Python. Sin embargo, hay ocasiones en las que los elementos de respuesta XML contienen valores numéricos. En estos casos, el valor del campo puede definirse como un elemento Python int o float. Como ejemplo, considera los elementos <login-time> y <idle-time> de cada <user-entry>:

<login-time junos:seconds="0">Wed08PM</login-time>
<idle-time junos:seconds="16860">4:41</idle-time>

El valor de cada elemento es una cadena de fecha/hora, no un valor numérico. Estas cadenas son los valores de los campos login_time y idle_time de la definición anterior UserView. Sin embargo, estos elementos XML también contienen un atributo seconds6 con un valor numérico. Definamos un nuevo UserExtView que incluya valores enteros para el tiempo de inicio de sesión y el tiempo de inactividad. Añade esta nueva definición de vista al mismo archivo users.yml:

UserExtView:
    extends: UserView
    fields:
        login_seconds: {"login-time/@seconds": int}
        idle_seconds: {"idle-time/@seconds": int}
Advertencia

La especificación YAML reserva el carácter @ para un uso futuro. Actualmente, YAML aceptará un @ que aparezca dentro de una cadena sin comillas. Sin embargo, producirá un error si la cadena comienza con un carácter @, como @seconds. Encerrar estas cadenas entre comillas evita posibles errores con futuras versiones de YAML.

La nueva definición de la vista UserExtView demuestra varios puntos. En primer lugar, observa los valores de los campos login_seconds y idle_seconds. En lugar de especificar una expresión XPath, se utiliza una matriz asociativa en línea. Esta matriz asociativa incluye una expresión XPath y el tipo de datos Python que se utilizará para el valor resultante (int, en este caso). Las definiciones de campo también utilizan expresiones XPath más complejas que seleccionan el valor de el atributo seconds dentro de cada elemento.

Nota

El tipo de un campo de vista puede definirse como int, float o flag. El tipo flag establece un valor booleano que es True si el elemento está presente y False si el elemento no está presente. Aquí tienes un ejemplo del preempaquetado EthPortView en el archivo ethport.yml:

    running: { ifdf-running: flag }
    present: { ifdf-present: flag }

Ahora, fíjate en la clave extends en la definición UserExtView. La clave extends es simplemente una forma de crear una nueva vista que es un superconjunto de otra vista. La línea extends: UserView hace que todos los campos de la definición UserView se incluyan en UserExtView. Además de los campos de UserView, UserExtView también incluirá los nuevos campos login_seconds y idle_seconds definidos bajo la clave fields.

Además de contener elementos <user-entry> para cada inicio de sesión, la respuesta RPC get-system-users-information incluye información de todo el sistema tal como los elementos <active-user-count> y <load-average-1>. Aunque es un poco inusual, es posible incluir esta información de todo el sistema en cada objeto UserExtView. Esto se hace añadiendo dos campos adicionales a la definición UserExtView. La definición completa de UserExtView es ahora:

UserExtView:
    extends: UserView
    fields:
        login_seconds: {"login-time/@seconds": int}
        idle_seconds: {"idle-time/@seconds": int}
        num_users: {../../active-user-count: int}
        load_avg:  {../../load-average-1: float}

Como todas las definiciones de campo, los nuevos campos num_users y load_avg especifican cada uno una expresión XPath relativa a cada elemento de la tabla. Resulta que estas expresiones XPath utilizan la notación de elemento padre (..) para recorrer la jerarquía XML de la respuesta y seleccionar los nodos que no están contenidos en el elemento <user-entry>. Como cada elemento <user-entry> comparte un elemento padre común <user-table> , el resultado es que cada objeto UserExtView contendrá los campos num_users y load_avg con exactamente los mismos valores. Los demás campos -from, login_time, idle_time, commands, login_seconds, y idle_seconds- siguen teniendo valores específicos de inicio de sesión porque esos campos se seleccionan del elemento <user-entry> por elemento. Fíjate en que el campo load_avg está definido como un campo Python float.

Hay una última herramienta disponible para definir vistas. Esa herramienta es groups. Los grupos son completamente opcionales. Son simplemente una forma de agrupar un conjunto de campos que comparten expresiones XPath con un prefijo común. En la definición de UserExtView, los campos num_users y load_avg comparten el prefijo ../... Aquí tienes una definición alternativa de UserExtView utilizando la herramienta groups:

UserExtView:
    groups:
        common: ../..
    fields_common:
        num_users: {active-user-count: int}
        load_avg:  {load-average-1: float}
    fields:
        from: from
        login_time: login-time
        idle_time: idle-time
        command: command
        login_seconds: {"login-time/@seconds": int}
        idle_seconds: {"idle-time/@seconds": int}

Cada grupo tiene un nombre y un prefijo XPath. En este ejemplo, el nombre es common y ../.. es el prefijo XPath. La clave fields_groupname para definir los campos que comparten el prefijo XPath. En este caso, la clave fields_common define los campos num_users y load_avg. Las expresiones XPath completas para estos campos serán ../../active-user-count y ../../load-average-1, respectivamente.

Nota

Te habrás dado cuenta de que el anterior UserExtView no incluía la clave extends. En su lugar, cada uno de los campos de UserView se copia en los campos de UserExtView. En este momento, las claves groups y extends son mutuamente excluyentes. No puedes utilizar ambas claves en la definición de una vista.

Antes de continuar, vamos a combinar las definiciones de tabla y vista en el archivo completo users.yml. Este archivo no utiliza groups en la definición UserExtView:

---
### ------------------------------------------------------
### show system users no-resolve
### ------------------------------------------------------

UserTable:
    rpc: get-system-users-information
    args:
        no_resolve: True
    item: uptime-information/user-table/user-entry
    key:
        - user
        - tty
    view: UserView

UserView:
    fields:
        from: from
        login_time: login-time
        idle_time: idle-time
        command: command

UserExtView:
    extends: UserView
    fields:
        login_seconds: {"login-time/@seconds": int}
        idle_seconds: {"idle-time/@seconds": int}
        num_users: {../../active-user-count: int}
        load_avg:  {../../load-average-1: float}

Utilizar la Nueva Tabla y Vista Operativas

Ahora que hemos creado el archivo users.ymlcompleto, vamos a utilizar esas definiciones de tabla y vista. Empieza por crear y abrir una instancia de dispositivo:

>>> from jnpr.junos import Device
>>> r0 = Device(host='r0',user='user',password='user123')
>>> r0.open()
Device(r0)

Las definiciones de tablas y vistas se cargan utilizando la función loadyaml() de el módulo jnpr.junos.factory. Esta función crea las clases Python y devuelve un diccionario que asigna los nombres de tabla y vista a sus correspondientes funciones de clase:

>>> from jnpr.junos.factory import loadyaml
>>> user_defs = loadyaml('users.yml')
>>> from pprint import pprint
>>> pprint(user_defs)
{'UserExtView': <class 'jnpr.junos.factory.View.UserExtView'>,
 'UserTable': <class 'jnpr.junos.factory.OpTable.UserTable'>,
 'UserView': <class 'jnpr.junos.factory.View.UserView'>}

La función loadyaml() toma una ruta al archivo YAML que contiene las definiciones de tabla y vista. Puede ser una ruta absoluta o relativa. Las rutas relativas son relativas al directorio de trabajo actual.

Una vez creadas las definiciones de tabla y vista, se crea una instancia de la clase tabla. Se accede a la función de clase indexando el diccionario users_def con el nombre de la tabla. La función de clase toma como único argumento una instancia de dispositivo:

>>> user_table = user_defs['UserTable'](r0)

Alternativamente, copia los nombres de las clases en el espacio de nombres global, e invoca la función de la clase utilizando el nombre de la tabla:

>>> globals().update(user_defs)
>>> user_table = UserTable(r0)

Cualquiera de estos métodos da como resultado una instancia vacía de UserTable. La instancia se asigna a la variable user_table. Al igual que con una tabla preempaquetada, el método get()rellena la tabla invocando al RPC especificado:

>>> user_table.get()
UserTable:r0: 8 items
>>> pprint(user_table.items())
[(('root', 'u0'),
  [('command', 'cli'),
   ('idle_time', '4:41'),
   ('from', '-'),
   ('login_time', 'Wed08PM')]),
 (('foo', 'pts/0'),
  [('command', '-cli (cli)'),
   ('idle_time', '7:56'),
   ('from', '172.29.104.116'),
   ('login_time', '9:08AM')]),
 (('bar', 'pts/1'),
  [('command', '-cli (cli)'),
   ('idle_time', '7:56'),
   ('from', '172.29.104.116'),
   ('login_time', '9:08AM')]),
 (('user', 'pts/2'),
  [('command', '-cli (cli)'),
   ('idle_time', '-'),
   ('from', '172.29.98.24'),
   ('login_time', '4:35PM')]),
 (('user', 'pts/3'),
  [('command', '-cli (cli)'),
   ('idle_time', '1:47'),
   ('from', '172.29.104.116'),
   ('login_time', '9:08AM')]),
 (('user', 'pts/4'),
  [('command', '-cli (cli)'),
   ('idle_time', '29'),
   ('from', '172.29.98.24'),
   ('login_time', '4:35PM')]),
 (('foo', 'pts/5'),
  [('command', '-cli (cli)'),
   ('idle_time', '29'),
   ('from', '172.29.98.24'),
   ('login_time', '4:35PM')]),
 (('bar', 'pts/6'),
  [('command', '-cli (cli)'),
   ('idle_time', '29'),
   ('from', '172.29.98.24'),
   ('login_time', '4:35PM')])]
>>>

Tómate tu tiempo para comparar la salida de pprint(user_table.items()) con las definiciones de UserTable y UserView del archivo users.yml. Presta atención a los campos y valores de la vista. Fíjate también en la clave de cada elemento de la tabla. Es una tupla formada a partir de los valores de user y tty. Puedes acceder a una propiedad de una vista concreta especificando la tupla como clave de la tabla:

>>> user_table[('foo','pts/5')]['from']
'172.29.98.24'

Otro método para cargar una tabla operativa y una vista definidas por el usuario consiste en crear el correspondiente archivo .py que ejecute la función loadyaml() y actualice el espacio de nombres global. Guarda el siguiente contenido en un archivo users.py en el mismo directorio que users.yml:

"""
Pythonifier for UserTable and UserView
"""
from jnpr.junos.factory import loadyaml
from os.path import splitext
_YAML_ = splitext(__file__)[0] + '.yml'
globals().update(loadyaml(_YAML_))

Este código determina el archivo YAML que hay que cargar sustituyendo la extensión . py por .yml. A continuación, utiliza las mismas funciones loadyaml() y globals().update() demostradas anteriormente. He aquí un ejemplo de uso del archivo users.py para crear y rellenar una UserTable instancia:

>>> from users import UserTable
>>> user_table = UserTable(r0)
>>> user_table.get()
UserTable:r0: 8 items
>>>

Con la adición del archivo <nombre>.py, el procedimiento para crear una nueva instancia de tabla es el mismo independientemente de si el archivo de definición YAML está preempaquetado con PyEZ o definido por el usuario.

Nota

Animamos a los usuarios de PyEZ a que envíen sus propias definiciones de tablas y vistas de al proyecto PyEZ a través de una solicitud de extracción de GitHub. A medida que crece la biblioteca de tablas y vistas, es más probable que encuentres una tabla y una vista existentes que puedas reutilizar o modificar para adaptarlas a tus necesidades.

Aplicar una visión diferente

¿Recuerdas el UserExtViewdefinido en el archivo users.yml? El UserExtView define campos adicionales para cada elemento de la tabla. ¿Cómo aplicas esa vista a la instancia de la tabla user_table? Simplemente importa la clase de vista y establece la propiedad view de user_tablea la nueva clase UserExtView:

>>> from users import UserExtView
>>> user_table.view = UserExtView
>>> pprint(user_table.items())
[(('root', 'u0'),
  [('idle_seconds', 16860),
   ('from', '-'),
   ('idle_time', '4:41'),
   ('login_seconds', 0),
   ('num_users', 8),
   ('command', 'cli'),
   ('load_avg', 0.51),
   ('login_time', 'Wed08PM')]),
 (('foo', 'pts/0'),
  [('idle_seconds', 28560),
   ('from', '172.29.104.116'),
   ('idle_time', '7:56'),
   ('login_seconds', 1437667701),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '9:08AM')]),
 (('bar', 'pts/1'),
  [('idle_seconds', 28560),
   ('from', '172.29.104.116'),
   ('idle_time', '7:56'),
   ('login_seconds', 1437667701),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '9:08AM')]),
 (('user', 'pts/2'),
  [('idle_seconds', 0),
   ('from', '172.29.98.24'),
   ('idle_time', '-'),
   ('login_seconds', 1437694521),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '4:35PM')]),
 (('user', 'pts/3'),
  [('idle_seconds', 6420),
   ('from', '172.29.104.116'),
   ('idle_time', '1:47'),
   ('login_seconds', 1437667701),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '9:08AM')]),
 (('user', 'pts/4'),
  [('idle_seconds', 1740),
   ('from', '172.29.98.24'),
   ('idle_time', '29'),
   ('login_seconds', 1437694521),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '4:35PM')]),
 (('foo', 'pts/5'),
  [('idle_seconds', 1740),
   ('from', '172.29.98.24'),
   ('idle_time', '29'),
   ('login_seconds', 1437694521),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '4:35PM')]),
 (('bar', 'pts/6'),
  [('idle_seconds', 1740),
   ('from', '172.29.98.24'),
   ('idle_time', '29'),
   ('login_seconds', 1437694521),
   ('num_users', 8),
   ('command', '-cli (cli)'),
   ('load_avg', 0.51),
   ('login_time', '4:35PM')])]
>>>

También puedes aplicar el método asview() a una única instancia de vista (que representa un único elemento de la tabla), como se muestra en el siguiente ejemplo. En primer lugar, la vista de la tabla se restablece a UserView:

>>> from users import UserView
>>> user_table.view = UserView

A continuación, el primer elemento de la tabla se asigna a one_view:

>>> one_view = user_table[0]

Los elementos dentro de one_view se imprimen utilizando el valor predeterminado UserView, y luego se imprimen de nuevo utilizando el valor UserExtView:

>>> pprint(one_view.items())
[('command', 'cli'),
 ('idle_time', '4:41'),
 ('from', '-'),
 ('login_time', 'Wed08PM')]
>>> pprint(one_view.asview(UserExtView).items())
[('idle_seconds', 16860),
 ('from', '-'),
 ('idle_time', '4:41'),
 ('login_seconds', 0),
 ('num_users', 8),
 ('command', 'cli'),
 ('load_avg', 0.51),
 ('login_time', 'Wed08PM')]
>>>

Guardar y cargar archivos XML desde tablas

Una nota final sobre las tablas y las vistas antes de pasar a configurar los dispositivos Junos con PyEZ es que las tablas PyEZ se rellenan normalmente ejecutando una RPC XML a través de una conexión NETCONF abierta. Sin embargo, también es posible guardar, y reutilizar posteriormente, la respuesta XML RPC. Guardar la respuesta XML RPC se consigue invocando el método savexml() en la instancia de la tabla. El argumento path es obligatorio y especifica dónde (una ruta de archivo absoluta o relativa) almacenar la salida XML. Los indicadores opcionales hostname y timestamp pueden utilizarse para guardar varias tablas en archivos XML únicos:

>>> user_table.savexml(path='/tmp/user_table.xml',hostname=True,timestamp=True)
>>> quit()
user@h0$ ls /tmp/*.xml
/tmp/user_table_r0_20150723151807.xml

Posteriormente, se puede utilizar un archivo XML para rellenar una tabla. Cargar una tabla desde un archivo XML no requiere una conexión NETCONF con el dispositivo que produjo originalmente la respuesta RPC. Para cargar la tabla desde un archivo XML, proporciona el argumento patha la función de clase de la tabla:

>>> user_table = UserTable(path='/tmp/user_table_r0_20150723151807.xml')
>>> user_table.get()
UserTable:/tmp/user_table_r0_20150723151807.xml: 8 items
>>>
Nota

Todavía se debe llamar al método get() para rellenar la tabla.

Ahora que hemos visto varias formas de invocar RPC operativos y analizar las respuestas resultantes, vamos a centrar nuestra atención en la configuración del dispositivo .

Configuración

PyEZ proporciona a una clase Config que simplifica el proceso de cargar y confirmar cambios de configuración en un dispositivo Junos. Además, PyEZ se integra con el motor de plantillas Jinja2 para simplificar el proceso de creación del propio fragmento de configuración. Por último, PyEZ ofrece utilidades para comparar configuraciones, revertir cambios de configuración y bloquear o desbloquear la base de datos de configuración.

El primer paso para utilizar la clase Config es crear una variable de instancia. He aquí un ejemplo de creación de una variable de instancia de configuración utilizando la variable de instancia de dispositivo r0 ya abierta:

>>> from jnpr.junos.utils.config import Config
>>> r0_cu = Config(r0)
>>> r0_cu
jnpr.junos.utils.Config(r0)

Alternativamente, la instancia de configuración puede vincularse como una propiedad de la instancia del dispositivo:

>>> from jnpr.junos.utils.config import Config
>>> r0.bind(cu=Config)
>>> r0.cu
jnpr.junos.utils.Config(r0)

Para el resto de esta sección, utilizaremos esta sintaxis de propiedad de dispositivo r0.cu. Sin embargo, el nombre de la propiedad de dispositivo no tiene nada de especial. En nuestros ejemplos, hemos elegido el nombre cu, pero podría utilizarse en su lugar cualquier nombre válido de variable Python (que no sea ya una propiedad de la instancia del dispositivo). Empecemos cargando fragmentos de configuración en la configuración candidata del dispositivo.

Carga de los cambios de configuración

El método load() puede utilizarse para cargar un fragmento de configuración, o la configuración completa, en la configuración candidata del dispositivo. La configuración se puede especificar en texto (también conocido como "llave rizada"), conjunto o sintaxis XML. Alternativamente, la configuración puede especificarse como un objeto lxml.etree.Element.

Los fragmentos de configuración en sintaxis de texto, conjunto o XML se pueden cargar desde un archivo en el host de automatización o desde un objeto de cadena de Python. Los archivos se especifican utilizando el argumento path del método load(). Las cadenas se pasan como primer argumento sin nombre al método load(). El método load() intenta determinar automáticamente el formato del contenido de la configuración. El formato de una cadena de configuración viene determinado por el contenido de la cadena. El formato de un archivo de configuración viene determinado por la extensión del nombre de archivo de la ruta, según la Tabla 4-6. En cualquier caso, el formato automático se puede anular estableciendo el argumento format en text, set o xml.

Tabla 4-6. Asignación de extensiones de nombre de archivo de ruta al formato de configuración
Extensión del nombre de archivo de la rutaFormato de configuración
.conf, .text o .txtTexto en formato de configuración "llave rizada".
.setTexto en formato de configuración del juego
.xmlTexto en formato de configuración XML

Los indicadores booleanos overwrite y merge del método load() controlan cómo afecta la nueva configuración a la configuración actual. El comportamiento por defecto es equivalente a el comando del modo de configuración CLI load replace. Si la bandera overwrite está activada, el comportamiento es equivalente a load override, y si la bandera merge está activada, el comportamiento es equivalente al comando load merge. Las banderas overwrite y merge se excluyen mutuamente. No puedes activar ambas a la vez. Además, no puedes activar la bandera overwrite cuando la configuración está en formato establecido.

Aquí tienes un ejemplo sencillo para cambiar el nombre de host del dispositivo pasando una cadena de configuración en formato set:

>>> from lxml import etree
>>> new_hostname = "set system host-name r0.new" 
>>> result = r0.cu.load(new_hostname)
>>> etree.dump(result)
<load-configuration-results>
<ok/>
</load-configuration-results>

>>>

El método load() devuelve un objeto lxml.etree.Element que indica el resultado.

Aquí tienes otros ejemplos de utilización del método load():

# load merge set
result = r0.cu.load(path='hostname.set', merge=True)

# load override
result = r0.cu.load(path='new_config.txt', overwrite=True)

# load merge
result = r0.cu.load(path='hostname.conf', merge=True)

# load replace xml
result = r0.cu.load(path='hostname.xml')

Si se produce un error, se lanza una excepción jnpr.junos.exception.ConfigLoadError :

>>> r0.cu.load('bad config syntax', format='set')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/utils/config.py", l...
    return try_load(rpc_contents, rpc_xattrs)
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/utils/config.py", l...
    raise ConfigLoadError(cmd=err.cmd, rsp=err.rsp, errs=err.errs)
jnpr.junos.exception.ConfigLoadError: ConfigLoadError(severity: error, bad_el...
>>>

Por defecto, el método load() modifica la base de datos de configuración compartida. El equivalente al comando CLI configure exclusive se puede conseguir llamando primero al método lock(). El método lock() devuelve True si la configuración se bloquea con éxito, y lanza una excepción jnpr.junos.exception.LockError si el bloqueo de la base de datos de configuración falla porque la configuración ya está bloqueada:

>>> r0.cu.lock()
True
>>> result = r0.cu.load(path='hostname.xml')
>>> r0.cu.pdiff()

[edit system]
-  host-name r0;
+  host-name r0.new;

>>> r0.cu.unlock()
True
>>> r0.cu.pdiff()
None
>>>

Observa que el método unlock() puede utilizarse para desbloquear la base de datos de configuración y descartar los cambios de configuración candidatos. El método pdiff()muestra la diferencia de configuración y se utiliza para depurar. Se explica más adelante en "Visualización de las diferencias de configuración".

Nota

El equivalente a el comando CLI configure privatese puede conseguir llamando a los RPCs abrir-configuración y cerrar-configuración. Un ejemplo se muestra en "Crear, aplicar y confirmar la configuración candidata".

Plantillas de configuración

Las plantillas permiten generar grandes bloques de configuración con el mínimo esfuerzo. PyEZ utiliza el motor de plantillas Jinja2 para generar archivos de configuración a partir de plantillas. Jinja2 es una popular biblioteca Python de código abierto del equipo Pocoo. Para obtener documentación más detallada sobre Jinja2, puedes visitar el sitio web de Jinja2.

Las plantillas Jinja2 combinan un formulario de texto reutilizable con un conjunto de datos utilizados para rellenar el formulario y mostrar un resultado. Jinja2 ofrece sustitución de variables, condicionales y construcciones de bucle sencillas. En efecto, Jinja2 es su propio "mini lenguaje de programación". Las plantillas Jinja2 no son exclusivas de Junos; permiten construir cualquier archivo de texto a partir de una "plantilla" o "formulario" y un conjunto de datos. Cualquier archivo de configuración basado en texto, incluidos los archivos de configuración de Junos en sintaxis curly brace, set o incluso XML, puede generarse a partir de una plantilla Jinja2.

Veamos un ejemplo de plantilla muy sencillo que configura un nombre de host en el router basándose en una plantilla. En este ejemplo, la configuración se expresa mediante una sentencia set. También podría haberse expresado fácilmente en formato texto o XML. Aquí tienes la configuración set declaración para configurar un nombre de host de r0.new:

set system host-name r0.new

Guarda esta declaración en un archivo llamado hostname. set en el directorio de trabajo actual.

Como has visto en la sección anterior, se carga un archivo de configuración en la configuración candidata del dispositivo con el método load():

>>> result = r0.cu.load(path='hostname.set')
>>>

Jinja2 utiliza llaves dobles para indicar una expresión, y la expresión Jinja2 más sencilla es sólo el nombre de la variable:

{{ variable_name }}

Una expresión de variable de Jinja2 se evalúa al valor de la variable. Los nombres de variable de Jinja2 siguen las mismas reglas sintácticas que los nombres de variable de Python.

Aplica esta sintaxis al archivo hostname.set de una línea. Sustituye el nombre de host r0.new codificado por una referencia a la variable host_name. El archivo hostname. set actualizado es ahora:

set system host-name {{ host_name }}

Como acabas de ver, un archivo de configuración se carga en la configuración candidata del dispositivo pasando el argumento path al método load(). De forma similar, una configuración de plantilla se carga en la configuración candidata del dispositivo pasando el argumento template_path al método load(). Sin embargo, las plantillas también requieren un argumento adicional al método load(). El argumento template_vars toma como valor un diccionario. Cada variable de la plantilla Jinja2 debe ser una clave de este diccionario. He aquí un ejemplo que utiliza la plantilla hostname.set para configurar el nombre de host foo:

>>> result = r0.cu.load(template_path='hostname.set',
...                     template_vars= {'host_name' : 'foo'})
>>> r0.cu.pdiff()

[edit system]
-  host-name r0;
+  host-name foo;

>>>

El argumento template_path puede especificar un nombre de archivo absoluto o relativo. Los nombres de archivo relativos se buscan en el directorio de trabajo actual y en un subdirectorio de plantillas de la ruta del módulo.7 Al igual que con el argumento path, la extensión del nombre de archivo del argumento template_path determina el formato esperado de la plantilla. La misma asignación de extensión de nombre de archivo a formato de configuración especificada en la Tabla 4-6 también se aplica al argumento template_path.

Ahora que ya has visto los aspectos básicos de la creación y carga de una plantilla, veamos algo de sintaxis adicional de Jinja2. En primer lugar, una expresión puede contener cero o más filtros separados por el carácter |. De forma similar a los filtros de visualización de una tubería Unix o de una tubería Junos (|), los filtros Jinja2 son conjuntos de funciones que pueden encadenarse para modificar una expresión. Los filtros Jinja2 modifican la variable al principio de una expresión.

Por ejemplo, puedes utilizar un filtro Jinja2 para proporcionar un valor por defecto a una variable. Observa lo que ocurre con la plantilla anterior cuando no hay ninguna clave host_name en el argumento template_vars del método load():

>>> result = r0.cu.load(template_path='hostname.set',tempate_vars={})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/utils/config.py", l...
    return try_load(rpc_contents, rpc_xattrs)
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/utils/config.py", l...
    raise ConfigLoadError(cmd=err.cmd, rsp=err.rsp, errs=err.errs)
jnpr.junos.exception.ConfigLoadError: ConfigLoadError(severity: error, bad_el...

La falta de la variable host_name provoca la generación de una declaración de configuración set incompleta, lo que hace que se genere una excepción jnpr.junos.exception.ConfigLoadError . En este caso, un error podría ser una respuesta adecuada para indicar al usuario de la plantilla que host_name es una variable obligatoria. Sin embargo, en otros casos, es mejor proporcionar un valor por defecto cuando falta una variable. Para ello puede utilizarse el filtro default() proporcionado por Jinja2.

Aquí se utiliza el filtro default() para proporcionar un valor por defecto de Missing.Hostname. También se utiliza el filtro Jinja2 lower para poner en minúsculas el nombre del host:

set system host-name {{ host_name | default('Missing.Hostname') | lower }}

Ahora mira el resultado de omitir la clave host_name del argumento template_vars:8

>>> result = r0.cu.load(template_path='hostname.set')
>>> r0.cu.pdiff()

[edit system]
-  host-name r0;
+  host-name missing.hostname;

>>>

El nombre de host missing.hostnamese configura como resultado de encadenar los filtros default() y lower.

Si se especifica un nombre de host Foo en template_vars, se configura un nombre de host foo:

>>> result = r0.cu.load(template_path='hostname.set',
...                     template_vars= {'host_name' : 'Foo'})
>>> r0.cu.pdiff()

[edit system]
-  host-name r0;
+  host-name foo;

>>>

Consulta la documentación de plantillas de Jinja2 para obtener una lista de los filtros incorporados disponibles. También es posible escribir tu propia función Python que funcione como un filtro personalizado. Consulta la documentación de la API deJinja2 para obtener más detalles sobre los filtros personalizados.

Además de la sustitución de variables y los filtros, Jinja2 ofrece etiquetas que controlan la lógica de representación de las plantillas. Las etiquetas están encerradas entre delimitadores {% tag %} delimitadores. Una de estas etiquetas es la sentencia condicional if. Una sentencia if permite que se incluya contenido diferente en el archivo renderizado dependiendo de si una expresión se evalúa como verdadera o falsa.

El siguiente ejemplo muestra cómo configurar el nombre de host del dispositivo en función de si tiene o no dos motores de enrutamiento. Si hay dos motores de enrutamiento, el nombre de host se configura dentro de los grupos de configuración especiales re0 y re1 y tiene re0. o re1. como prefijo. Si el dispositivo tiene un único motor de enrutamiento, el nombre de host se configura en el nivel jerárquico de configuración [edit system] como antes. Esta vez, la configuración se especifica en sintaxis de texto (también conocida como llaves) y se guarda en el archivo hostname.conf en el directorio de trabajo actual. Éste es el contenido de hostname.conf:

{% if dual_re %}
groups {
    re0 {
        system {
            host-name {{ "re0." + host_name }};
        }
    }
    re1 {
        system {
            host-name {{ "re1." + host_name }};
        }
    }
}
{% else %}
system {
    host-name {{ host_name }};
}
{% endif %}

Observa que la plantilla comienza con una declaración {% if expression %} que depende del valor de la variable dual_re. La sentencia {% else %} marca el final del texto que se mostrará si dual_re es verdadero; también marca el principio del texto que se mostrará si dual_re es falso. La sentencia {% endif %} cierra el bloque condicional.

Fíjate también en la expresión utilizada para representar el nombre de host en el caso dual-RE. Utiliza una cadena estática y el carácter de concatenación de cadenas + de Python para añadir el valor apropiado a la variable host_name.

Aquí tienes un ejemplo de renderización de esta plantilla en un dispositivo con dual REs. Observa que el valor de la clave dual_re se establece a partir del valor de r1.facts['2RE'] proporcionado por la recopilación de hechos por defecto de PyEZ:

>>> r1.facts['2RE']
True
>>> result = r1.cu.load(template_path='hostname.conf',
...                     template_vars= { 'dual_re' : r1.facts['2RE'],
...                                      'host_name' : 'r1' })
>>> r1.cu.pdiff()

[edit groups re0 system]
+   host-name re0.r1;
[edit groups re1 system]
+   host-name re1.r1;

>>>

La misma plantilla aplicada a un dispositivo con un único RE establece el nombre de host en el nivel jerárquico de configuración [edit system]:

>>> r0.facts['2RE']
False
>>> result = r0.cu.load(template_path='hostname.conf',
...                     template_vars= { 'dual_re' : r0.facts['2RE'],
...                                      'host_name' : 'r0' })
>>> r0.cu.pdiff()

[edit system]
+  host-name r0;

>>>

Observa que la clave dual_re se suministra de nuevo a partir de la información de los hechos. Utilizar una plantilla con un bloque condicional genera la configuración correcta para el dispositivo, generada automáticamente en función de si hay o no motores de enrutamiento duales.

Aunque las plantillas Jinja2 ofrecen una plétora de capacidades adicionales, terminemos esta sección examinando la construcción de bucle {% for scalar_var in list_var %} construcción de bucle. En este ejemplo, el bucle itera sobre los elementos de un diccionario y utiliza la clave y los valores del diccionario para configurar un conjunto de direcciones IPv4 en un conjunto de interfaces. Esta vez, la configuración se especifica en sintaxis XML y se guarda en el archivo interfaz_ip.xml en el directorio de trabajo actual. A continuación se muestra el contenido de interface_ip.xml:

<interfaces>
{% for key, value in interface_ips.iteritems() %}
    <interface>
        <name>{{ key }}</name>
        <unit>
            <name>0</name>
            <family>
                <inet>
                    <address>{{ value }}</address>
                </inet>
            </family>
        </unit>
    </interface>
{% endfor %}
</interfaces>

El contenido del bucle for es muy similar al de una sentencia Python for. De hecho, este ejemplo utiliza el método de diccionario iteritems() para iterar sobre cada par clave/valor del diccionario. En la sintaxis de Jinja2, el bucle termina con una sentencia {% endfor %}.

Ahora crea un diccionario interface_ipscon un conjunto de nombres de interfaz como claves y un conjunto de direcciones IPv4 y longitudes de prefijo como valores:

>>> interface_ips = {
...     'ge-0/0/0' : '10.0.4.1/30',
...     'ge-0/0/1' : '10.0.2.1/30',
...     'ge-0/0/2' : '10.0.3.1/30',
...     'ge-0/0/3' : '10.0.5.1/30' }

Utilicemos este diccionario y esta plantilla para configurar direcciones IP en r0. El diccionario interface_ips diccionario se convierte en el valor de la clave interface_ips en el argumento template_vars:

>>> result = r0.cu.load(template_path='interface_ip.xml',
...                     template_vars= { 'interface_ips' : interface_ips })
>>> r0.cu.pdiff()

[edit interfaces ge-0/0/0 unit 0 family inet]
+       address 10.0.4.1/30;
[edit interfaces ge-0/0/1 unit 0 family inet]
+       address 10.0.2.1/30;
[edit interfaces ge-0/0/2 unit 0 family inet]
+       address 10.0.3.1/30;
[edit interfaces ge-0/0/3 unit 0 family inet]
+       address 10.0.5.1/30;

>>>

Como era de esperar, la configuración resultante añade las direcciones correctas en varias interfaces utilizando el bucle for.

Las plantillas de Jinja2 ofrecen muchas funciones adicionales que no podemos cubrir directamente en este libro. Echa un vistazo a la documentación de Jinja2 y experimenta con las características que puedan ser aplicables en tu entorno particular de .

Ver las diferencias de configuración

Has visto el método pdiff() utilizado en ejemplos anteriores para mostrar las diferencias que se han configurado en la configuración candidata. El método pdiff() está pensado para fines de depuración, que es como lo hemos estado utilizando en esos ejemplos. Simplemente imprime las diferencias entre la configuración candidata y una configuración rollback. Por defecto, el método pdiff() compara la configuración candidata con la configuración rollback 0. La configuración rollback 0es la misma que la configuración actualmente confirmada:

>>> r0.cu.pdiff()

[edit system]
-  host-name r0;
+  host-name r0.new;

>>>

En lugar de imprimir las diferencias, el método diff() devuelve una cadena que contiene las diferencias entre las configuraciones candidata y de retroceso:

>>> r0.cu.diff()
'\n[edit system]\n-  host-name r0;\n+  host-name r0.new;\n'

Los métodos diff() y pdiff() toman un argumento opcional sin nombre. El argumento es un número entero que representa el ID del retroceso:

>>> r0.cu.pdiff(1)

[edit system]
-  host-name r0;
+  host-name r0.new;
[edit system login]
+    user bar {
+        uid 2002;
+        class super-user;
+        authentication {
+            encrypted-password "$5$0jDrGxJT$PXj0TWwtu5LPJ4Nvlc1YpmCKy7yAwOUH...
+        }
+    }
+    user foo {
+        uid 2003;
+        class super-user;
+        authentication {
+            encrypted-password "$5$WfsXdd11$4LXuWOIwQA5HsWvF8oxMZGyzodIHsnZP...
+        }
+    }

>>>

Ahora que hemos visto cómo ver las diferencias de configuración, veamos cómo confirmar la configuración candidata modificada.

Confirmar los cambios de configuración

La clase Config de PyEZ proporciona un método commit_check() para validar la configuración candidata y un método commit() para confirmar los cambios. El método commit_check() devuelve True si la configuración candidata supera todas las comprobaciones de confirmación, y lanza una excepción jnpr.junos.exception.CommitError si la configuración candidata no supera alguna comprobación de confirmación, incluidas las advertencias:

>>> r0.cu.load("set system host-name r0")
<Element load-configuration-results at 0x7f6503180ea8>
>>> r0.cu.commit_check()
True
>>> r0.cu.load("set protocols bgp export FooBar")
<Element load-configuration-results at 0x7f650317d488>
>>> r0.cu.commit_check()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/utils/config.py", l...
    raise CommitError(cmd=err.cmd, rsp=err.rsp)
jnpr.junos.exception.CommitError: CommitError(edit_path: [edit groups junos-d...
>>>

El método commit() toma varios argumentos opcionales, que se detallan en la Tabla 4-7. Devuelve True si la configuración candidata se consigna correctamente y lanza una excepción jnpr.junos.exception.CommitError si se produce un error o advertencia al consignar la configuración.

Tabla 4-7. Argumentos del método commit()
ArgumentoDescripción
commentEl valor es una cadena de comentario que describe la confirmación.
confirmEl valor es un número entero que especifica el número de minutos que hay que esperar para que se confirme la confirmación. La confirmación se realiza invocando de nuevo a commit() antes de que expire el temporizador de confirm.
syncUna bandera booleana. commit synchronize Si True, realiza una commit synchronize, que consigna la nueva configuración en ambos motores de enrutamiento de un sistema dual-RE. Es posible que se produzca una de todos modos si el usuario ha configurado la sentencia synchronizeen el nivel de jerarquía de configuración [edit system commit].
detailUna bandera booleana. Si es True, el método commit() devuelve un objeto lxml.etree.Element con detalles adicionales sobre el proceso de confirmación. Este argumento sólo debe utilizarse con fines de depuración.
force_syncUna bandera booleana. Si es True, realiza un commit synchronize force. Este argumento sólo debe utilizarse con fines de depuración.
fullUna bandera booleana. Si True, todos los demonios Junos son notificados y reparan su configuración completa, aunque no se hayan realizado cambios de configuración que afecten a los demonios. Este argumento sólo debe utilizarse con fines de depuración.

Aquí tienes un ejemplo de una confirmación correcta y otra fallida:

>>> r0.cu.load("set system host-name r0")
<Element load-configuration-results at 0x7f650317b5a8>
>>> r0.cu.commit(comment='New hostname',sync=True)
True
>>> r0.cu.load("set protocols bgp export FooBar")
<Element load-configuration-results at 0x7f65031765f0>
>>> r0.cu.commit()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/utils/config.py", l...
    raise CommitError(cmd=err.cmd, rsp=err.rsp)
jnpr.junos.exception.CommitError: CommitError(edit_path: [edit groups junos-d...
>>>
Nota

Si la base de datos de configuración se ha bloqueado con el método lock(), no se desbloquea llamando a commit(). Debes seguir invocando el método unlock() para liberar el bloqueo de la configuración.

Utilizar la Configuración de Rescate

La clase Config también proporciona un método rescue()para realizar acciones en la configuración de rescate del dispositivo Junos. La configuración de rescate está pensada para ayudar a la recuperación del dispositivo en caso de un cambio de configuración erróneo. La idea es que el usuario defina una configuración mínima necesaria para restaurar el dispositivo a un estado bueno conocido y guarde esa configuración mínima como configuración de "rescate". El método rescue()toma un argumento sin nombre que indica la acción a realizar sobre la configuración de rescate. Los valores válidos de este argumento son get, save, delete, y reload. Si la acción es get, se puede especificar un segundo argumento opcional con nombre, format. El formato por defecto es text,, que indica que la configuración está en la sintaxis de texto (también conocida como llaves). El argumento format también puede establecerse como xml, que recupera la configuración como un objeto lxml.etree.Element.

El siguiente ejemplo demuestra cómo borrar, guardar, obtener y luego volver a cargar la configuración de rescate utilizando el atributo de configuración del dispositivo r0.cu existente:

>>> r0.cu.rescue('delete')
True
>>> r0.cu.rescue('save')
True
>>> r0.cu.rescue('get',format='xml')
<Element rescue-information at 0x7f885ac76128>
>>> resp = r0.cu.rescue('reload')
>>> from lxml import etree
>>> etree.dump(resp)
<load-configuration-results>
<ok/>
</load-configuration-results>

>>>

Observa que tas acciones save y delete devuelven un valor booleano que indica éxito o fracaso. La acción save guarda la configuración actual comprometida como configuración de rescate, mientras que la acción delete borra la configuración de rescate actual. La acción get devuelve la configuración de rescate actual. En el ejemplo anterior, esta configuración se devuelve como un objeto lxml.etree.Element porque se especificó format='xml'. Si no se hubiera especificado el argumento format, en su lugar se habría devuelto una cadena que contiene la configuración de rescate. La acción reload carga la configuración de rescate en la configuración candidata. Devuelve la misma respuesta que el método load(). Al igual que el método load (), rescue('reload') sólo modifica la configuración candidata. Se debe emitir un commit() para activar la configuración de rescate .

Utilidades

PyEZ también proporciona un conjunto de métodos de utilidad que pueden utilizarse para realizar tareas comunes en el sistema operativo Junos. Estas tareas proporcionan acceso al sistema de archivos o al shell de Junos a nivel Unix, realizan copias seguras y ejecutan actualizaciones del software Junos. Las utilidades del sistema de archivos definidas en la clase FS del módulo jnpr.junos.utils.fs proporcionan comandos comunes que acceden al sistema de archivos del dispositivo Junos. El módulo jnpr.junos.utils.scpdefine una clase SCP para realizar copias seguras hacia o desde el dispositivo Junos. El módulo jnpr.junos.utils.start_shell proporciona una clase StartShell que permite iniciar una conexión SSH a un dispositivo Junos. Se proporcionan métodos StartShell adicionales para ejecutar comandos a través de la conexión SSH y esperar una respuesta esperada. Por último, la clase SWdel módulo jnpr.junos.utils.sw proporciona un conjunto de métodos para actualizar el software Junos en un dispositivo, así como para reiniciar o apagar el dispositivo.

Como estas utilidades están fuera de la funcionalidad base de PyEZ, y como se amplían con cada nueva versión de PyEZ, este libro no intenta cubrir cada utilidad en detalle. En su lugar, te animamos a que utilices la función help() incorporada en Python para mostrar las cadenas de documentación de estas clases. He aquí un ejemplo de visualización de las cadenas de documentación de la clase FS:

>>> from jnpr.junos.utils.fs import FS
>>> help(FS)
Help on class FS in module jnpr.junos.utils.fs:

class FS(jnpr.junos.utils.util.Util)
 |  Filesystem (FS) utilities:
 |  
 |  * :meth:`cat`: show the contents of a file
 |  * :meth:`checksum`: calculate file checksum (md5,sha256,sha1)
 |  * :meth:`cp`: local file copy (not scp)
 |  * :meth:`cwd`: change working directory
 |  * :meth:`ls`: return file/dir listing
 |  * :meth:`mkdir`: create a directory
 |  * :meth:`pwd`: get working directory
 |  * :meth:`mv`: local file rename
 |  * :meth:`rm`: local file delete
 |  * :meth:`rmdir`: remove a directory
 |  * :meth:`stat`: return file/dir information
 |  * :meth:`storage_usage`: return storage usage
 |  * :meth:`storage_cleanup`: perform storage storage_cleanup
...

Se puede seguir la misma receta para mostrar las cadenas de documentación de las demás clases de utilidad de PyEZ.

Un ejemplo de PyEZ

Ahora que has visto las capacidades de PyEZ, es hora de poner en práctica estos conocimientos reescribiendo el ejemplo lldp_interface_descriptions_rest.py tratado en "Uso de las API RESTful en Python". Este ejemplo utiliza la función RPC bajo demanda de PyEZ para consultar los vecinos LLDP actuales y las descripciones de interfaz. Maneja las respuestas utilizando las bibliotecas lxml y jxmlease. La nueva configuración se aplica utilizando una plantilla Jinja2.

El objetivo y la arquitectura general del script siguen de cerca los del ejemplo anterior lldp_interface_descriptions_rest.py. El nuevo script se llama lldp_interface_descriptions_pyez.py y utiliza PyEZ para descubrir y monitorizar la topología de red de uno o varios dispositivos Junos que ejecuten el protocolo LLDP (Link Layer Discovery Protocol). La información descubierta desde LLDP se almacena en los campos de descripción de interfaz de la configuración del dispositivo. La información actual descubierta por LLDP se compara con la información anterior almacenada en el campo de descripción de la interfaz, y se notifica al usuario cualquier cambio en el estado de LLDP (arriba, cambio o abajo) desde la instantánea anterior.

El script es invocado por un usuario en la línea de comandos y toma uno o más nombres de dispositivos o direcciones IP como argumentos de la línea de comandos. La sintaxis es:

user@h0$ python lldp_interface_descriptions_pyez.py r0 r1 r2 r3 r4 r5

Para que se ejecute correctamente, el servicio NETCONF-sobre-SSH debe estar configurado en cada dispositivo, y en cada uno de ellos debe configurarse un nombre de usuario y una contraseña comunes con la autorización adecuada:

user@r0> show configuration system services 
netconf {
    ssh;
}

user@r0> show configuration system login                       
user user {
    uid 2001;
    class super-user;
    authentication {
        encrypted-password "$1$jCvocDbA$KeOycEvIDtSV/VOdPRHo5."; ## SECRET-DATA
    }
}

El script solicita el nombre de usuario y la contraseña que se utilizarán para conectarse a los dispositivos y, a continuación, imprime su salida en el terminal del usuario. Imprime una notificación por cada dispositivo comprobado. Si el script detecta algún cambio en el estado LLDP desde la última instantánea, esos cambios se imprimen en el terminal. Se configuran las nuevas descripciones de interfaz y un mensaje indica si la configuración del dispositivo se ha actualizado correctamente o no. El siguiente ejemplo muestra una salida de muestra del script:

user@h0$ ./lldp_interface_descriptions_pyez.py r0 r1 r2 r3 r4 r5
Device Username: user
Device Password: 
Connecting to r0...
Getting LLDP information from r0...
Getting interface descriptions from r0...
    ge-0/0/4 LLDP Change. Was: r7 ge-0/0/6 Now: r1 ge-0/0/0
    ge-0/0/3 LLDP Up. Now: r5 ge-0/0/0
    ge-0/0/2 LLDP Up. Now: r3 ge-0/0/2
    ge-0/0/0 LLDP Up. Now: r4 ge-0/0/2
    ge-0/0/5 LLDP Down. Was: r6 ge-0/0/8
    Successfully committed configuration changes on r0.
    Closing connection to r0.
Connecting to r1...
Getting LLDP information from r1...
Getting interface descriptions from r1...
    ge-0/0/2 LLDP Down. Was: r2 ge-0/0/0
    Successfully committed configuration changes on r1.
    Closing connection to r1.
Connecting to r2...
Getting LLDP information from r2...
Getting interface descriptions from r2...
    ge-0/0/2 LLDP Down. Was: r0 ge-0/0/1
    ge-0/0/1 LLDP Down. Was: r3 ge-0/0/0
    ge-0/0/0 LLDP Down. Was: r1 ge-0/0/2
    Successfully committed configuration changes on r2.
    Closing connection to r2.
Connecting to r3...
Getting LLDP information from r3...
Getting interface descriptions from r3...
    ge-0/0/0 LLDP Down. Was: r2 ge-0/0/1
    Successfully committed configuration changes on r3.
    Closing connection to r3.
Connecting to r4...
Getting LLDP information from r4...
Getting interface descriptions from r4...
    No LLDP changes to configure on r4.
    Closing connection to r4.
Connecting to r5...
Getting LLDP information from r5...
Getting interface descriptions from r5...
    ge-0/0/2 LLDP Up. Now: r4 ge-0/0/0
    ge-0/0/0 LLDP Up. Now: r0 ge-0/0/3
    Successfully committed configuration changes on r5.
    Closing connection to r5.
user@h0$

A medida que el script recorre secuencialmente cada dispositivo especificado en la línea de comandos, realiza los siguientes pasos:

  1. Recopila información de vecinos LLDP.

  2. Recopila descripciones de interfaz; analiza la información de vecino LLDP que se almacenó previamente en las descripciones de interfaz.

  3. Compara la información de vecinos LLDP actual y anterior; imprime mensajes LLDP de subida, cambio y bajada; calcula nuevas descripciones de interfaz.

  4. Construye, carga y confirma una configuración candidata con descripciones de interfaz actualizadas.

Nota

Al igual que el script de ejemplo de la API RESTful, este ejemplo intenta proporcionar una función útil y realista, a la vez que se concentra en el código que demuestra PyEZ. Se aplican las mismas advertencias. En concreto, el script de ejemplo es invocado por el usuario en la línea de comandos, pero un programa más complejo podría ser invocado por un evento o en un horario. Además, la salida podría integrarse en un sistema existente de monitoreo o alerta de la red, en lugar de imprimirse simplemente en el terminal. Una implementación más realista almacenaría la información LLDP obtenida del dispositivo en una base de datos fuera del dispositivo o en declaraciones de configuración en el dispositivo apply-macro en lugar de en las declaraciones de configuración de la interfaz description. Por último, cada dispositivo podría consultarse y configurarse en paralelo para acelerar la ejecución del programa.

Analicemos el código Python utilizado para realizar cada uno de estos pasos. De nuevo, te recomendamos que sigas el ejemplo y escribas cada línea de los listados del programa en tu propio archivo de secuencia de comandos llamado lldp_interface_descriptions_pyez.py. Después de completar "Ponerlo todo junto", tendrás un ejemplo de trabajo para ejecutar en tu propia red .

Preámbulo

El primer paso en nuestro script de ejemplo es importar las bibliotecas necesarias y realizar alguna inicialización única. Las llamadas dan más información sobre cada línea del listado del programa:

#!/usr/bin/env python 1
"""Use interface descriptions to track the topology reported by LLDP.

This includes the following steps:
1) Gather LLDP neighbor information.
2) Gather interface descriptions.
3) Parse LLDP neighbor information previously stored in the descriptions.
4) Compare LLDP neighbor info to previous LLDP info from the descriptions.
5) Print LLDP Up / Change / Down events.
6) Store the updated LLDP neighbor info in the interface descriptions.

Interface descriptions are in the format:
[user-configured description ]LLDP: <remote system> <remote port>[(DOWN)]

The '(DOWN)' string indicates an LLDP neighbor which was previously
present, but is now not present.
"""

import sys 2
import getpass

import jxmlease 3

from jnpr.junos import Device 4
from jnpr.junos.utils.config import Config
import jnpr.junos.exception

TEMPLATE_PATH = 'interface_descriptions_template.xml' 5

# Create a jxmlease parser with desired defaults.
parser = jxmlease.EtreeParser() 6

class DoneWithDevice(Exception): pass 7
1

La línea #! (a veces llamada línea hashbang o shebang) permite la opción de ejecutar el script sin especificar el comando python. (Este mecanismo funciona en plataformas tipo Unix y en plataformas Windows utilizando el Lanzador de Python para Windows). En otras palabras, podrías ejecutar el script con path/lldp_interface_descriptions_pyez.py r0 r1 r2 r3 r4 r5 en lugar de pythonpath/lldp_interface_descriptions_pyez.py r0 r1 r2 r3 r4 r5. Ejecutar el script directamente requiere establecer permisos de ejecución en el archivo lldp_interface_descriptions_pyez.py. Utilizar el comando shell /usr/bin/env para invocar Python significa que el script no depende de la ubicación del comando python.

2

Se importan dos módulos estándar de Python, sys y getpass. El módulo sys proporciona acceso a objetos mantenidos por el intérprete de Python, y el módulo getpass permite que el script pregunte interactivamente por la contraseña requerida sin hacer eco de la entrada en el terminal del usuario.

3

La biblioteca jxmlease analiza XML en estructuras de datos nativas de Python. Debes asegurarte de que este módulo está instalado en tu sistema.

4

Se importan tres módulos PyEZ. La clase PyEZ Device se utiliza para crear una instancia de dispositivo. Es la clase PyEZ básica para interactuar con un dispositivo Junos. La clase PyEZ Config ofrece métodos para tratar la configuración del dispositivo Junos. El módulo exception define varias excepciones específicas de PyEZ que pueden aparecer para indicar un posible problema.

5

La variable TEMPLATE_PATH se utiliza como constante para identificar el nombre de la plantilla de configuración de Jinja2 utilizada para generar la configuración de nuevas descripciones de interfaz.

6

Se crea una instancia del analizador jxmlease para analizar objetos lxml.etree.Element y convertirlos en estructuras de datos Python. El método jxmlease.EtreeParser() crea una instancia de la clase jxmlease.EtreeParser con un conjunto de parámetros predeterminados. Mientras que un analizador creado con el método jxmlease.Parser() espera un documento XML de entrada como una cadena, una instancia de la clase jxmlease.EtreeParser espera un documento XML de entrada como un objeto lxml.etree.Element.

7

Se crea una clase personalizada DoneWithDevicepara indicar cuándo se ha completado el procesamiento en cada dispositivo del bucle del dispositivo principal. Esta nueva clase es una subclase de Exception.

Cada uno de los pasos principales del script se ha encapsulado en una función Python que se ejecutará desde la función main() del archivo lldp_interface_descriptions_pyez.py.

Recorre en bucle cada dispositivo

Empecemos analizando en la función main(), que solicita un nombre de usuario y una contraseña y, a continuación, realiza un bucle sobre cada dispositivo especificado en la línea de comandos. La función main() llama a varias funciones, que se analizarán en secciones posteriores:

def main():
    """The main loop.

    Prompt for a username and password.
    Loop over each device specified on the command line.
    Perform the following steps on each device:
    1) Get LLDP information from the current device state.
    2) Get interface descriptions from the device configuration.
    3) Compare the LLDP information against the previous snapshot of LLDP
       information stored in the interface descriptions. Print changes.
    4) Build a configuration snippet with new interface descriptions.
    5) Commit the configuration changes.

    Return an integer suitable for passing to sys.exit().
    """

    if len(sys.argv) == 1: 1
        print("\nUsage: %s device1 [device2 [...]]\n\n" % sys.argv[0])
        return 1

    rc = 0 2

    # Get username and password as user input.
    user = raw_input('Device Username: ')
    password = getpass.getpass('Device Password: ') 3

    for hostname in sys.argv[1:]: 4
        try:
            print("Connecting to %s..." % hostname)
            dev = Device(host=hostname, 5
                         user=user,
                         password=password,
                         normalize=True)
            dev.open() 6

            print("Getting LLDP information from %s..." % hostname)
            lldp_info = get_lldp_neighbors(device=dev) 7
            if lldp_info == None: 8
                print("    Error retrieving LLDP info on " + hostname +
                      ". Make sure LLDP is enabled.")
                rc = 1
                raise DoneWithDevice

            print("Getting interface descriptions from %s..." % hostname)
            desc_info = get_description_info_for_interfaces(device=dev) 9
            if desc_info == None:
                print("    Error retrieving interface descriptions on %s." %
                      hostname)
                rc = 1
                raise DoneWithDevice

            desc_changes = check_lldp_changes(lldp_info, desc_info) 10
            if not desc_changes:
                print("    No LLDP changes to configure on %s." % hostname)
                raise DoneWithDevice

            if load_merge_template_config( 11
                device=dev,
                template_path=TEMPLATE_PATH,
                template_vars={'descriptions': desc_changes}):
                print("    Successfully committed configuration changes on %s." %
                      hostname)
            else:
                print("    Error committing description changes on %s." %
                      hostname)
                rc = 1
                raise DoneWithDevice
        except jnpr.junos.exception.ConnectError as err: 12
            print("    Error connecting: " + repr(err))
            rc = 1
        except DoneWithDevice:
            pass
        finally: 13
            print("    Closing connection to %s." % hostname)
            try:
                dev.close()
            except:
                pass
    return rc
1

El script requiere que se especifique al menos un dispositivo como argumento de la línea de comandos. La lista sys.argv contendrá el nombre del script en el índice 0. Los argumentos especificados por el usuario comienzan en el índice 1. Si no hay argumentos especificados por el usuario (len(sys.argv) == 1), se imprime el mensaje de uso y el script sale con un código de estado de 1 para indicar un error.

2

La variable rc contiene el código de estado que se devolverá al final del script. El valor se inicializa a 0, que indica éxito. Más adelante, si se produce un error, rc se establecerá en 1 y el procesamiento continuará con el siguiente dispositivo especificado en la línea de comandos.

3

Esta sentencia y la anterior solicitan al usuario el nombre de usuario y la contraseña utilizados para establecer la sesión NETCONF con los dispositivos Junos. Se supone que cada dispositivo está configurado con las mismas credenciales de autenticación de usuario. La función getpass() de la biblioteca estándar de Python solicita al usuario una contraseña y desactiva el eco de la respuesta en la pantalla.

4

Este bucle for itera sobre cada nombre de dispositivo (o dirección IP) especificado en los argumentos de la línea de comandos. Como sys.argv[0] es el nombre del script, se utiliza el corte sys.argv[1:] para devolver la lista de dispositivos.

5

Se crea una instancia de dispositivo y se asigna a la variable dev. El nombre de usuario y la contraseña introducidos por el usuario se pasan como argumentos user y password. El argumento normalize=True garantiza que se aplique la normalización de la respuesta a cada llamada RPC bajo demanda que utilice la instancia de dispositivo dev.

6

El método open() se invoca en la instancia del dispositivo dev. Esto establece una sesión NETCONF con el dispositivo e invoca la recopilación de datos por defecto de PyEZ. Si la llamada a open() falla, se lanza una jnpr.junos.exception.ConnectError (o una de sus subclases). Esta posible excepción se gestiona más adelante, en la función main().

7

La función get_lldp_neighbors()se invoca en la instancia actual del dispositivo, dev. El resultado se almacena en el diccionario lldp_info.

8

Si get_lldp_neighbors()informa de un error (devolviendo el valor None), se imprime un mensaje de error, rc se establece en 1 para indicar el error, y se lanza la excepción DoneWithDevice. Esta excepción hace que la ejecución salte a la línea except DoneWithDevice hacia el final de la función main().

9

La función get_description_info_for_interfaces() se ejecuta en el dispositivo actual, dev. El resultado se almacena en el diccionario desc_info. De forma similar a la función get_lldp_neighbors(), si la función get_description_info_for_interfaces() informa de un error (devolviendo el valor None), se imprime un mensaje de error, rc se establece en 1 para indicar el error, y se lanza la excepción DoneWithDevice y se lanza la excepción. Esta excepción hace que la ejecución salte a la línea except DoneWithDevice hacia el final de la función main().

10

La función get_lldp_description_changes() analiza los diccionarios lldp_info y desc_info y devuelve las nuevas descripciones de interfaz en el diccionario desc_changes. Si no ha cambiado ninguna descripción, se lanza la excepción DoneWithDevice . En este caso, la excepción no indica un error. Simplemente se salta la sección de código que carga y consigna el cambio de configuración si no hay una nueva configuración que aplicar.

11

Se llama a la función load_merge_template_config(). El argumento template_pathse establece en el valor de TEMPLATE_PATH y la clave descriptions del argumento template_vars se establece en el diccionario desc_changes. El valor de retorno de la función indica si la nueva configuración se ha confirmado correctamente. Si se produce un error, se imprime un mensaje de error, rc se establece en 1 para indicar el error, y se invoca DoneWithDevice.

12

Una excepción jnpr.junos.exception.ConnectError (o cualquier subclase) indica que no se ha abierto la sesión NETCONF al dispositivo. En este caso, se imprime el error, rc se establece en 1 para indicar el error, y el script continúa con el bloque finally.

13

Colocar la invocación al método dev.close()dentro de un bloque finally garantiza que la sesión NETCONF se cierre correctamente, independientemente de si se ha producido una excepción al recopilar información o al confirmar la nueva configuración. Como dev.close() podría producir una excepción, la sentencia se coloca dentro de un bloque try. El bloque except correspondiente simplemente ignora cualquier excepción planteada por dev.close().

Ahora, veamos cada una de las funciones a las que se llama desde el bucle for con más detalle .

Recopilar información de vecinos LLDP

La primera función, get_lldp_neighbors(), utiliza la función RPC bajo demanda de PyEZ para consultar la RPC get-lldp-neighbors-information. La respuesta RPC tiene el formato por defecto de los objetos lxml Element. La función recopila la información del sistema y del puerto del vecino LLDP para cada interfaz local utilizando métodos lxml, y devuelve la información a la persona que llama en un diccionario:

def get_lldp_neighbors(device): 1
    """Get current LLDP neighbor information.

    Return a two-level dictionary with the LLDP neighbor information.
    The first-level key is the local port (aka interface) name.
    The second-level keys are 'system' for the remote system name
    and 'port' for the remote port ID. On error, return None.

    For example:
    {'ge-0/0/1': {'system': 'r1', 'port', 'ge-0/0/10'}}
    """

    lldp_info = {}
    try: 2
        resp = device.rpc.get_lldp_neighbors_information()
    except (jnpr.junos.exception.RpcError,
            jnpr.junos.exception.ConnectError)as err:
        print "    " + repr(err)
        return None

    for nbr in resp.findall('lldp-neighbor-information'): 3
        local_port = nbr.findtext('lldp-local-port-id') 4
        remote_system = nbr.findtext('lldp-remote-system-name')
        remote_port = nbr.findtext('lldp-remote-port-id')
        if local_port and (remote_system or remote_port):
            lldp_info[local_port] = {'system': remote_system, 5
                                     'port': remote_port}

    return lldp_info
1

La función get_lldp_neighbors()requiere un argumento device. Este argumento es una instancia de dispositivo PyEZ que tiene abierta una sesión NETCONF activa.

2

La llamada RPC bajo demanda y el procesamiento de la respuesta RPC están rodeados por un bloque try para evitar la finalización inesperada del script. Si se produce una excepción, se imprime un mensaje de error y se devuelve el valor None a la persona que llama para indicar el error. La función PyEZ RPC on Demand invoca el RPC XML get-lldp-neighbors-information.

3

El método lxml findall() devolverá una lista de todos los objetos lxml.etree.Element que coincidan con la expresión XPath lldp-neighbor-information. Esto resulta en un bucle a través de cada vecino LLDP. A la variable nbr se le asigna el objeto lxml.etree.Element que representa al vecino LLDP actual.

4

El método lxml findtext() selecciona el primer elemento XML que coincida con una expresión XPath. Se utiliza para seleccionar los valores de las variables local_port, remote_system y remote_port.

5

Los valores remote_system y remote_port se almacenan en el diccionario lldp_info. Este diccionario tiene como clave el valor local_port extraído del vecino LLDP actual.

Recopilar y analizar descripciones de interfaces

La siguiente función, get_description_info_for_interfaces(), realiza otra consulta PyEZ RPC bajo demanda utilizando la RPC get-interface-information. El parámetro descriptions se añade a la RPC para recuperar sólo las descripciones de las interfaces. Esta vez, la salida es analizada por la biblioteca jxmlease para producir un objeto jxmlease.XMLDictNode. El contenido del campo de descripción de la interfaz se analiza a partir de esta respuesta basándose en una convención sencilla que se ha elegido para este script de ejemplo. Vuelve a consultar "Recopilar y analizar descripciones de interfaz" si necesitas un repaso de la convención utilizada. La función se define como sigue:

def get_description_info_for_interfaces(device): 1
    """Get current interface description for each interface.

    Parse the description into the user-configured description, remote
    system, and remote port components.

    Return a two-level dictionary. The first-level key is the
    local port (aka interface) name. The second-level keys are
    'user_desc' for the user-configured description, 'system' for the
    remote system name, 'port' for the remote port, and 'down', which is
    a Boolean indicating if LLDP was previously down. On error, return None.

    For example:
    {'ge-0/0/1': {'user_desc': 'test description', 'system': 'r1',
                  'port': 'ge-0/0/10', 'down': True}}
    """

    desc_info = {}
    try: 2
        resp = parser(device.rpc.get_interface_information(descriptions=True))
    except (jnpr.junos.exception.RpcError,
            jnpr.junos.exception.ConnectError) as err:
        print "    " + repr(err)
        return None

    try:
        pi = resp['interface-information']['physical-interface'].jdict() 3
    except KeyError:
        return desc_info

    for (local_port, port_info) in pi.items(): 4
        try:
            (udesc, _, ldesc) = port_info['description'].partition('LLDP: ') 5
            udesc = udesc.rstrip()
            (remote_system, _, remote_port) = ldesc.partition(' ')
            (remote_port, down_string, _) = remote_port.partition('(DOWN)')
            desc_info[local_port] = {'user_desc': udesc,
                                     'system': remote_system,
                                     'port': remote_port,
                                     'down': True if down_string else False}
        except (KeyError, TypeError): 6
            pass
    return desc_info
1

get_description_info_for_interfaces() requiere un argumento device. Este argumento es una instancia de dispositivo PyEZ que tiene abierta una sesión NETCONF activa.

2

De nuevo, la llamada RPC bajo demanda está rodeada por un bloque try para gestionar las condiciones de excepción. La función RPC bajo demanda se utiliza para ejecutar la RPC XML "obtener-información-de-interfaz". El argumento descriptions se pasa a la RPC y especifica que sólo deben devolverse descripciones de interfaz. El objeto lxml.etree.Elementse pasa a la instancia jxmlease.EtreeParser(), parser().

3

A la variable pi se le asigna un diccionario basado en la información de la interfaz física de la respuesta RPC. El método jdict() (comentado en "objetos jxmlease") se utiliza para devolver un diccionario a partir de la información de la interfaz física. El método jdict() produce automáticamente un diccionario basado en el valor de los elementos <name> de cada elemento <pyhsical-interface> de la respuesta. Una excepción KeyError indica que falta el elemento <interface-information> o <physical-interface>en la respuesta. Esto indica simplemente que no hay ninguna descripción de interfaz configurada actualmente.

4

El bucle for itera sobre local_port, la clave del diccionario pi, y port_info, el valor del diccionario pi.

5

Las siguientes líneas analizan la descripción de la interfaz existente en componentes. Las descripciones se almacenan en el diccionario desc_info, cuya clave es el valor local_port. Consulta "Recopilar y analizar descripciones de interfaz" en el script de ejemplo de la API RESTful si este código no está claro.

6

Al acceder a los datos, se lanza una excepción KeyError cuando la clave especificada no existe. Se lanza una excepción TypeError cuando el tipo al que se accede no coincide con la estructura de datos. Si se da cualquiera de estas condiciones, se ignora la excepción. El procesamiento continúa con el siguiente local_port en el diccionario pi.

Comparar la información del vecino LLDP actual y anterior

La función check_lldp_changes()compara la información LLDP anterior encontrada en los campos de descripción y almacenada ahora en el diccionario desc_info con la información LLDP almacenada ahora en el diccionario lldp_info. Como esta función opera únicamente con los diccionarios desc_info y lldp_info devueltos por las funciones anteriores, su contenido es exactamente el mismo que el de la versión RESTful API. La función se incluye aquí sin repetir las explicaciones de cada línea. Si no estás seguro de la finalidad o el comportamiento de alguna línea, vuelve a consultar las explicaciones de "Comparar la información de vecinos LLDP actual y anterior". La definición de la función es la siguiente

def check_lldp_changes(lldp_info, desc_info):
    """Compare current LLDP info with previous snapshot from descriptions.

    Given the dictionaries produced by get_lldp_neighbors() and
    get_description_info_for_interfaces(), print LLDP up, change,
    and down messages.

    Return a dictionary containing information for the new descriptions
    to configure.
    """

    desc_changes = {}

    # Iterate through the current LLDP neighbor state. Compare this
    # to the saved state as retrieved from the interface descriptions.
    for local_port in lldp_info:
        lldp_system = lldp_info[local_port]['system']
        lldp_port = lldp_info[local_port]['port']
        has_lldp_desc = desc_info.has_key(local_port)
        if has_lldp_desc:
            desc_system = desc_info[local_port]['system']
            desc_port = desc_info[local_port]['port']
            down = desc_info[local_port]['down']
            if not desc_system or not desc_port:
                has_lldp_desc = False
        if not has_lldp_desc:
            print("    %s LLDP Up. Now: %s %s" %
                  (local_port,lldp_system,lldp_port))
        elif down:
            print("    %s LLDP Up. Was: %s %s Now: %s %s" %
                  (local_port,desc_system,desc_port,lldp_system,lldp_port))
        elif lldp_system != desc_system or lldp_port != desc_port:
            print("    %s LLDP Change. Was: %s %s Now: %s %s" %
                  (local_port,desc_system,desc_port,lldp_system,lldp_port))
        else:
            # No change. LLDP was not down. Same system and port.
            continue
        desc_changes[local_port] = "LLDP: %s %s" % (lldp_system,lldp_port)

    # Iterate through the saved state as retrieved from the interface
    # descriptions. Look for any neighbors that are present in the
    # saved state, but are not present in the current LLDP neighbor
    # state.
    for local_port in desc_info:
        desc_system = desc_info[local_port]['system']
        desc_port = desc_info[local_port]['port']
        down = desc_info[local_port]['down']
        if (desc_system and desc_port and not down and
            not lldp_info.has_key(local_port)):
            print("    %s LLDP Down. Was: %s %s" %
                  (local_port,desc_system,desc_port))
            desc_changes[local_port] = "LLDP: %s %s(DOWN)" % (desc_system,
                                                              desc_port)

    # Iterate through the list of interface descriptions we are going
    # to change. Prepend the user description, if any.
    for local_port in desc_changes:
        try:
            udesc = desc_info[local_port]['user_desc']
        except KeyError:
            continue
        if udesc:
            desc_changes[local_port] = udesc + " " + desc_changes[local_port]

    return desc_changes

Construye, aplica y confirma la configuración candidata

La función load_merge_template_config() toma como argumentos una instancia de dispositivo, una plantilla Jinja2 y variables de plantilla. Utiliza RPC bajo demanda, así como los métodos de configuración de PyEZ load() y commit() para realizar el equivalente de los comandos CLI configure private, load merge y commit. También comprueba los resultados en busca de posibles errores:

def load_merge_template_config(device,
                               template_path,
                               template_vars): 1
    """Load templated config with "configure private" and "load merge".

    Given a template_path and template_vars, do:
        configure private,
        load merge of the templated config,
        commit,
        and check the results.

    Return True if the config was committed successfully, False otherwise.
    """

    class LoadNotOKError(Exception): pass 2

    device.bind(cu=Config) 3

    rc = False

    try:
        try:
            resp = device.rpc.open_configuration(private=True) 4
        except jnpr.junos.exception.RpcError as err: 5
            if not (err.rpc_error['severity'] == 'warning' and
                    'uncommitted changes will be discarded on exit' in
                    err.rpc_error['message']):
                raise

        resp = device.cu.load(template_path=template_path,
                              template_vars=template_vars,
                              merge=True) 6
        if resp.find("ok") is None: 7
            raise LoadNotOKError
        device.cu.commit(comment="made by %s" % sys.argv[0]) 8
    except (jnpr.junos.exception.RpcError, 9
            jnpr.junos.exception.ConnectError,
            LoadNotOKError) as err:
        print "    " + repr(err)
    except:
        print "    Unknown error occurred loading or committing configuration."
    else: 10
        rc = True
    try: 11
        device.rpc.close_configuration()
    except jnpr.junos.exception.RpcError as err:
        print "    " + repr(err)
        rc = False
    return rc
1

Los argumentos device, template_path y template_vars son parámetros obligatorios de la función load_merge_template_config().

2

Se define una nueva subclase Exception. Esta clase se levanta, y se maneja, cuando hay un problema al cargar la nueva configuración.

3

Se crea una nueva instancia de configuración PyEZ y se vincula al atributo cu del dispositivo.

4

El método RPC bajo demanda llama al RPC XML de configuración abierta . El argumento private hace que este RPC sea equivalente al comando CLI configure private.

5

jnpr.junos.exception.RpcError Las excepciones son capturadas y gestionadas por este bloque except. Es normal que el RPC XML de abrir configuración devuelva un uncommitted changes will be discarded on exit advertencia cuando se especifica el argumento private. Esta advertencia esperada se ignora. Todas las demás excepciones jnpr.junos.exception.RpcError se detectan y gestionan en el bloque try/except.

6

Esta sentencia pasa los argumentos template_path y template_vars al método load()de la instancia de configuración. La configuración se genera a partir de la plantilla y de los valores proporcionados por el usuario. El argumento merge=True hace que la nueva configuración se fusione con la configuración candidata existente.

7

Un elemento XML <ok> en la respuesta XML del método load() indica que la nueva configuración se ha cargado correctamente. Se lanza una excepción LoadNotOKError, definida anteriormente en esta función, cuando no se encuentra el elemento XML <ok> en la respuesta. En la mayoría de las situaciones, un error al cargar la nueva configuración candidata lanzará alguna subclase de la excepción PyEZ RpcError. Este bloque simplemente maneja la posible situación en la que el método load() no lance una excepción y devuelva una respuesta RPC inesperada.

8

El método commit() de la configuración se utiliza para confirmar la nueva configuración candidata. Se añade un comentario que incluye el nombre del script a la confirmación con el argumento comment.

9

jnpr.junos.exception.RpcError, jnpr.junos.exception.ConnectError, y LoadNotOKError las excepciones y subclases son capturadas y manejadas por este bloque except. El contenido de estas excepciones simplemente se imprime. Se imprime un mensaje diferente para todas las demás excepciones que se hayan producido durante el bloque try.

10

Este bloque else sólo se ejecuta si no se ha producido ninguna excepción. Esto indica que la configuración se ha cargado y confirmado correctamente. En este caso, se asigna a rc el valor de True para indicar el éxito. (Anteriormente, rc se inicializaba a False.)

11

Independientemente de que se haya producido una excepción previa, se ejecuta este bloque try y se cierra la configuración privada candidata mediante el RPC cerrar-configuración. Si se produce un error al cerrar la configuración, se imprime la excepción y se asigna rc False para indicar el error.

La función load_merge_template_config() está escrita para ser independiente de la configuración que se aplique. Pasando la instancia del dispositivo, el archivo de plantilla Jinja2 y las variables de la plantilla, puedes utilizarla para aplicar cualquier configuración al dispositivo. El trabajo real de producir la configuración se realiza en la plantilla Jinja2. En este ejemplo, la plantilla se encuentra en el archivo interface_descriptions_template.xml del directorio de trabajo actual. Como implica la extensión .xml del nombre del archivo, esta plantilla genera una configuración en formato XML. Las llamadas explican cada línea de la plantilla:

<configuration>
    <interfaces> 1
    {% for key, value in descriptions.iteritems() %} 2
        <interface> 3
            <name>{{ key }}</name> 4
            <description>{{ value }}</description> 5
        </interface>
    {% endfor %} 6
    </interfaces>
</configuration>
1

Esta plantilla genera una configuración en sintaxis XML. Sólo es necesario incluir las etiquetas XML relacionadas con la configuración. La etiqueta XML <interfaces> corresponde al nivel [edit interfaces] de la jerarquía de configuración de Junos.

2

Esta línea es un bucle Jinja2 forque itera sobre los elementos del diccionario descriptions. La clave del diccionario descriptions es el nombre de la interfaz, y el valor es la nueva descripción que se va a configurar.

3

La etiqueta XML de apertura de cada interfaz. Esta etiqueta se repetirá para cada interfaz en el diccionario descriptions.

4

El nombre de la interfaz. La expresión {{ key }} se evalúa con cada clave del diccionario descriptions.

5

La descripción de las interfaces. La expresión {{ value }} se evalúa con cada valor del diccionario descriptions.

6

La etiqueta {% endfor %} identifica el final del bucle forque itera sobre cada interfaz en el diccionario descriptions.

Ponerlo todo junto

El último bloque de código del script de ejemplo llama a la función main() cuando se ejecuta el script. Tras introducir este bloque de código, el script de ejemplo es funcional:

if __name__ == "__main__":
  sys.exit(main())

Llegados a este punto, el script está completo y listo para probarlo en un conjunto de dispositivos Junos que ejecuten el servicio NETCONF-sobre-SSH y el protocolo LLDP. Si encuentras errores al ejecutar el script, revisa y compara cuidadosamente tu código con el del ejemplo. El archivo completo lldp_interface_descriptions_pyez.py también está disponible en en GitHub.

Limitaciones

Como todas las herramientas de automatización de tratadas en este libro, PyEZ desempeña un papel muy útil en la automatización de las redes Junos, pero no es la solución a todos los problemas de automatización de redes. La primera limitación, y la más obvia, es que PyEZ es específico de Python. Si dominas Python o estás aumentando un sistema actual escrito en Python, puede que veas esto como una ventaja. Sin embargo, si tu proyecto requiere otro lenguaje, puede que haya otras opciones. Consulta la siguiente sección para obtener más información sobre las formas de acceder a los dispositivos Junos utilizando NETCONF en el lenguaje de tu elección.

Otra limitación (casi tan obvia) es que PyEZ requiere NETCONF. Como todos los dispositivos Junos soportados actualmente admiten NETCONF, este requisito no es una gran limitación. Sin embargo, sí requiere que el servicio NETCONF sobre SSH, o el servicio SSH independiente, esté configurado y accesible desde tu host de automatización. Cuando utilices NETCONF sobre SSH, asegúrate de que el puerto de destino TCP 830 no está siendo filtrado a lo largo de la ruta de red entre tu host de automatización y el dispositivo Junos de destino.

Bibliotecas NETCONF para otros lenguajes

Aunque este libro no trata en detalle , existen diversas bibliotecas para varios lenguajes que implementan el protocolo NETCONF y proporcionan cierto nivel de abstracción que te evita tener que enviar RPC NETCONF directas. La mayoría de estas bibliotecas no soportan las abstracciones de alto nivel, como tablas y vistas, que soporta Junos PyEZ, pero aún así pueden ser extremadamente útiles.

La Tabla 4-8 ofrece un estudio actual de algunas de estas bibliotecas. Consulta el enlace correspondiente para obtener más información sobre cada biblioteca.

Tabla 4-8. Bibliotecas NETCONF disponibles
LenguaDescripciónEnlace
RubyPopular biblioteca NETCONF de código abierto para Ruby. Fácil de instalar, dependencias limitadas y soporte activo.http://rubygems.org/gems/netconf
JavaBiblioteca NETCONF de código abierto para Java. Ya la utilizan clientes empresariales. Fácil instalación sin dependencias.http://www.juniper.net/support/downloads/?p=netconf
PerlSoportada por Juniper Networks JTAC. La más antigua de las bibliotecas NETCONF. La instalación puede ser difícil, con múltiples dependencias.http://www.juniper.net/support/downloads/?p=netconf#sw
PHPBiblioteca NETCONF de código abierto para PHP. En desarrollo activo y puede que no esté lista para su uso en producción.https://github.com/Juniper/netconf-php
PythonLibrería NETCONF de código abierto agnóstica de proveedor para Python. Utilizada por PyEZ.https://github.com/leopoul/ncclient

Estas bibliotecas pueden ser útiles si tu proyecto requiere compatibilidad directa con NETCONF o un lenguaje de desarrollo específico; sin embargo, PyEZ es muy recomendable para el desarrollo de la nueva automatización de Junos.

Resumen del capítulo

En resumen, Junos PyEZ ofrece un agradable equilibrio entre sencillez y potencia. Este equilibrio lo convierte en la elección obvia para los usuarios ya familiarizados con Python. PyEZ ofrece funciones de recopilación de datos y utilidades que simplifican muchas tareas comunes, y simplifica la ejecución RPC proporcionando una capa de abstracción construida sobre el protocolo NETCONF.

Gran parte de la potencia de PyEZ consiste en manejar las respuestas RPC resultantes. Para obtener la máxima potencia y flexibilidad, utiliza expresiones XPath con la biblioteca lxml. Para un mapeo rápido y sencillo entre XML y estructuras de datos nativas de Python, utiliza la biblioteca jxmlease para analizar la respuesta lxml. Para un mapeo reutilizable entre elementos XML complejos y estructuras de datos Python sencillas, elige el mecanismo de tablas y vistas.

Además de los RPC operativos, PyEZ ofrece herramientas para realizar cambios en la configuración. Estas herramientas incluyen una potente función de "construcción de plantillas" que permite generar configuraciones grandes y complejas con un esfuerzo mínimo.

1 ncclient es un proyecto de código abierto dirigido y apoyado por la comunidad, alojado en GitHub.

2 Además de los requisitos del software Junos PyEZ, la instalación desde el repositorio GitHub requiere tener instalado git.

3 La representación en cadena de una instancia de la clase jnpr.junos.Device es simplemente Device(host).

4 Es más seguro proteger la clave privada con una frase de contraseña. De hecho, una frase de contraseña debería considerarse la mejor práctica para los entornos de red de producción.

5 Ten en cuenta que PyEZ elimina los espacios de nombres XML de la respuesta. Así, mientras que la salida CLI incluye elementos XML con atributos en el espacio de nombres junos, como <up-time junos:seconds="102120">1 day, 4:22​</up-time>, el atributo en el objeto de respuesta es seconds en lugar de junos:seconds.

6 Recuerda que PyEZ elimina los espacios de nombres de las respuestas XML. Así, el atributo junos:seconds en la respuesta XML sin procesar se especifica simplemente como seconds dentro de PyEZ.

7 En el host de automatización de ejemplo, la ruta del módulo es /usr/local/lib/python2.7/dist-packages/jnpr/junos, pero la ubicación es específica de la instalación y puede ser diferente en tu máquina. Puedes descubrir la ubicación correcta para tu máquina siguiendo las instrucciones que aparecen al principio de "Tablas y vistas operativas preempaquetadas".

8 Si el argumento template_varsse omite por completo, como en este ejemplo, por defecto será {}, un diccionario Python vacío.

Get Automatizar la administración de Junos now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.