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 ncclient
1 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.git
2 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
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(host='
r0
',user='user
',password='user123
')
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.
Antes de poder utilizar la clase
jnpr.junos.Device
, primero hay que importarla. Esta línea importa el paquete Pythonjnpr.junos
y copia el nombreDevice
en el espacio de nombres local, lo que te permite simplemente hacer referencia aDevice()
. Una sintaxis alternativa esimport jnpr.junos
. De nuevo, importa el paquete Pythonjnpr.junos
, pero no copia el nombreDevice
en el espacio de nombres local. Si utilizas esta sintaxis, deberás hacer referencia a la clase como atributo dejnpr.junos
utilizando la sintaxisjnpr.junos.Device()
.Al llamar al objeto de clase
Device
con la sintaxisDevice()
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 llamadar0
. No hay nada especial en el nombrer0
, y podría utilizarse cualquier nombre válido de variable Python en su lugar. Los parámetros de la llamada aDevice()
establecen los valores iniciales de los atributos de la instancia. En este ejemplo, los parámetroshost
,user
, ypassword
se han establecido con los valores adecuados para el dispositivo Junos con nombre de hostr0
.
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.
Parámetro | Descripción | Valor por defecto |
---|---|---|
host | Un 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). |
port | El 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 |
user | El 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 $USER para 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:>>> |
password | La 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_facts | Un 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_probe | Esta 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_probe es 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_probe puede cambiarse para todas las instancias de dispositivo estableciendo Device.auto_probe = antes de instanciar cualquier instancia de dispositivo). |
ssh_config | La 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_file | La 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 ). |
normalize | Un 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 Device
no 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
uid 2001; class super-user; authentication { ssh-dsa "ssh-dss AAAAB3Nzauser
...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 user
en 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.
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 facts
de 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_facts
en 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('
/usr/local/lib/python2.7/dist-packages/jnpr/junos/devi... warnings.warn("CLI command is for debug use only!", ...) >>>show system uptime'
)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 timeout
de la instancia del dispositivo. Establecer la propiedad timeout
afecta 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.
Excepción | Descripción |
---|---|
jnpr.junos. exception. ConnectClosedError | Se 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.RpcTimeoutError | Se 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.PermissionError | Aparece si la autorización de , como se explica en "Autenticación y autorización", no permite que se ejecute la RPC. |
jnpr.junos.exception.RpcError | Se 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.PermissionError
indicarí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
from
time
import
sleep
MAX_ATTEMPTS
=
3
WAIT_BEFORE_RECONNECT
=
10
# Assumes r0 already exists and is a connected device instance
for
attempt
in
range
(
MAX_ATTEMPTS
)
:
try
:
routes
=
r0
.
rpc
.
get_route_information
(
)
except
jnpr
.
junos
.
exception
.
ConnectClosedError
:
sleep
(
WAIT_BEFORE_RECONNECT
)
try
:
r0
.
open
(
)
except
jnpr
.
junos
.
exception
.
ConnectError
:
pass
else
:
# Success. No exception was raised.
# break will skip the for loop's else.
break
else
:
# 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 ...
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ónsleep()
del módulotime
al espacio de nombres local.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.El bucle
for
intentará ejecutar la RPC hastaMAX_ATTEMPTS
veces.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 bloquetry
. El resultado de la RPC se almacena en la variableroutes
. Esta RPC puede tener éxito sin ninguna excepción, o puede lanzar cualquiera de las excepciones enumeradas en la Tabla 4-3.Si la sentencia del bloque
try
lanza una excepción, se comprueba si coincide con una excepción dejnpr.junos.exception.ConnectClosedError
. Si la excepción coincide, se ejecutan las sentencias de este bloqueexcept
. Si la excepción no coincide con este bloqueexcept
, 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 bloqueexcept
se probará en orden hasta que se encuentre una coincidencia o se agote la lista de excepciones.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 duranteWAIT_BEFORE_RECONNECT
segundos. A continuación, la ejecución pasa a la siguiente línea. El métodoopen()
se envuelve en un bloquetry
para capturar cualquier excepción que se produzca si la llamada falla porque la condición de red subyacente sigue existiendo. La sentenciaexcept: pass
atrapará e ignorará todas las excepcionesjnpr.junos.exception.ConnectError
planteadas porr0.open()
.Nota
Todas las excepciones planteadas por
r0.open()
se enumeran en la Tabla 4-2. Todas estas excepciones son subclases dejnpr.junos.
exception.
ConnectError
Por lo tanto, si se captura esta clase de excepción, se capturan todas las excepciones creadas porr0.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.El bloque
else
de una sentencia compuestatry
/except
/else
sólo se ejecuta si no se han producido excepciones en el bloquetry
. En otras palabras, el bloqueelse
sólo se ejecuta sir0.rpc.get_route_information()
se ha ejecutado correctamente. La sentenciabreak
saldrá del buclefor
cuando la RPC tenga éxito. Al salir de un buclefor
con una sentenciabreak
se omite la cláusulaelse
del bucle.La cláusula
else
de un bucle Pythonfor
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 r0
ya 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.ConnectClosedError
persiste 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 tag
de 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.Element
pueden 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 xml
que 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 [
y el diccionario de atributos tag
='text
']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.
Element
attrib
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 lxml
está 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 [
de la expresión XPath debe coincidir exactamente. Mostrar explícitamente el valor de la etiqueta tag
='value
']<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_table
con 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_table
con 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_table
con todas las entradas de la tabla ARP y, a continuación, muestra arp_table
's type is jnpr.junos.
factory.
OpTable.ArpTable
que es una subclase de jnpr.junos.
factory.
OpTable
Al 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 T
es 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
:
rpc
:
get-arp-table-information
item
:
arp-table-entry
key
:
mac-address
view
:
ArpView
ArpView
:
fields
:
mac_address
:
mac-address
ip_address
:
ip-address
interface_name
:
interface-name
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.
El RPC XML de Junos(get-arp-table-information) que se invoca para recuperar los datos de los elementos de la tabla.
Una expresión XPath utilizada para seleccionar cada elemento de la tabla en la respuesta RPC.
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.
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.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.
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.La expresión XPath
mac-address
se utiliza para establecer el valor de la clavemac_address
en el objeto vista nativo de Python. Dado quemac_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).De nuevo,
ip-address
es una expresión XPath yip_address
es el nombre de la clave en el objeto vista de Python.La clave final de cada objeto vista de Python es
interface_name
. El valor de la claveinterface_name
viene determinado por la expresión XPathinterface-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.
Nombre clave | Obligatorio u opcional | Descripción |
---|---|---|
rpc | Necesario | El 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). |
args | Opcional | Una 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_key | Opcional | El nombre de un primer argumento opcional sin nombre del método Esta definición permite al usuario llamar a: >>> Lo que provoca que se envíe la siguiente RPC al dispositivo Junos:
|
item | Necesario | Una 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. |
key | Opcional, 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 |
view | Necesarioa | 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 el nombre de una vista definida en el mismo documento YAML. |
a Si la clave |
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 users
que 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 view
de 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_
El ejemplo utiliza la más sencilla de estas claves, groupname
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 seconds
6 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_
para definir los campos que comparten el prefijo XPath. En este caso, la clave groupname
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 User
Table
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 UserExtView
definido 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_table
a 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 path
a 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
.
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 private
se 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.hostname
se 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 {%
delimitadores. Una de estas etiquetas es la sentencia condicional tag
%}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
que depende del valor de la variable expression
%}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
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:scalar_var
in
list_var
%}
<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_ips
con 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 0
es 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.
Argumento | Descripción |
---|---|
comment | El valor es una cadena de comentario que describe la confirmación. |
confirm | El 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 . |
sync | Una 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 synchronize en el nivel de jerarquía de configuración [edit system
commit] . |
detail | Una 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_sync | Una bandera booleana. Si es True , realiza un commit synchronize force . Este argumento sólo debe utilizarse con fines de depuración. |
full | Una 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... >>>
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.scp
define 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 SW
del 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:
Recopila información de vecinos LLDP.
Recopila descripciones de interfaz; analiza la información de vecino LLDP que se almacenó previamente en las descripciones de interfaz.
Compara la información de vecinos LLDP actual y anterior; imprime mensajes LLDP de subida, cambio y bajada; calcula nuevas descripciones de interfaz.
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
"""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
import
getpass
import
jxmlease
from
jnpr.junos
import
Device
from
jnpr.junos.utils.config
import
Config
import
jnpr.junos.exception
TEMPLATE_PATH
=
'
interface_descriptions_template.xml
'
# Create a jxmlease parser with desired defaults.
parser
=
jxmlease
.
EtreeParser
(
)
class
DoneWithDevice
(
Exception
)
:
pass
La línea
#!
(a veces llamada línea hashbang o shebang) permite la opción de ejecutar el script sin especificar el comandopython
. (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 conpath
/lldp_interface_descriptions_pyez.py
r0 r1 r2 r3 r4 r5
en lugar depython
path
/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 comandopython
.Se importan dos módulos estándar de Python,
sys
ygetpass
. El módulosys
proporciona acceso a objetos mantenidos por el intérprete de Python, y el módulogetpass
permite que el script pregunte interactivamente por la contraseña requerida sin hacer eco de la entrada en el terminal del usuario.La biblioteca
jxmlease
analiza XML en estructuras de datos nativas de Python. Debes asegurarte de que este módulo está instalado en tu sistema.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 PyEZConfig
ofrece métodos para tratar la configuración del dispositivo Junos. El móduloexception
define varias excepciones específicas de PyEZ que pueden aparecer para indicar un posible problema.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.Se crea una instancia del analizador
jxmlease
para analizar objetoslxml.etree.Element
y convertirlos en estructuras de datos Python. El métodojxmlease.EtreeParser()
crea una instancia de la clasejxmlease.EtreeParser
con un conjunto de parámetros predeterminados. Mientras que un analizador creado con el métodojxmlease.Parser()
espera un documento XML de entrada como una cadena, una instancia de la clasejxmlease.EtreeParser
espera un documento XML de entrada como un objetolxml.etree.Element
.Se crea una clase personalizada
DoneWithDevice
para indicar cuándo se ha completado el procesamiento en cada dispositivo del bucle del dispositivo principal. Esta nueva clase es una subclase deException
.
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
:
(
"
\n
Usage:
%s
device1 [device2 [...]]
\n
\n
"
%
sys
.
argv
[
0
]
)
return
1
rc
=
0
# Get username and password as user input.
user
=
raw_input
(
'
Device Username:
'
)
password
=
getpass
.
getpass
(
'
Device Password:
'
)
for
hostname
in
sys
.
argv
[
1
:
]
:
try
:
(
"
Connecting to
%s
...
"
%
hostname
)
dev
=
Device
(
host
=
hostname
,
user
=
user
,
password
=
password
,
normalize
=
True
)
dev
.
open
(
)
(
"
Getting LLDP information from
%s
...
"
%
hostname
)
lldp_info
=
get_lldp_neighbors
(
device
=
dev
)
if
lldp_info
==
None
:
(
"
Error retrieving LLDP info on
"
+
hostname
+
"
. Make sure LLDP is enabled.
"
)
rc
=
1
raise
DoneWithDevice
(
"
Getting interface descriptions from
%s
...
"
%
hostname
)
desc_info
=
get_description_info_for_interfaces
(
device
=
dev
)
if
desc_info
==
None
:
(
"
Error retrieving interface descriptions on
%s
.
"
%
hostname
)
rc
=
1
raise
DoneWithDevice
desc_changes
=
check_lldp_changes
(
lldp_info
,
desc_info
)
if
not
desc_changes
:
(
"
No LLDP changes to configure on
%s
.
"
%
hostname
)
raise
DoneWithDevice
if
load_merge_template_config
(
device
=
dev
,
template_path
=
TEMPLATE_PATH
,
template_vars
=
{
'
descriptions
'
:
desc_changes
}
)
:
(
"
Successfully committed configuration changes on
%s
.
"
%
hostname
)
else
:
(
"
Error committing description changes on
%s
.
"
%
hostname
)
rc
=
1
raise
DoneWithDevice
except
jnpr
.
junos
.
exception
.
ConnectError
as
err
:
(
"
Error connecting:
"
+
repr
(
err
)
)
rc
=
1
except
DoneWithDevice
:
pass
finally
:
(
"
Closing connection to
%s
.
"
%
hostname
)
try
:
dev
.
close
(
)
except
:
pass
return
rc
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 de1
para indicar un error.La variable
rc
contiene el código de estado que se devolverá al final del script. El valor se inicializa a0
, que indica éxito. Más adelante, si se produce un error,rc
se establecerá en1
y el procesamiento continuará con el siguiente dispositivo especificado en la línea de comandos.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.Este bucle
for
itera sobre cada nombre de dispositivo (o dirección IP) especificado en los argumentos de la línea de comandos. Comosys.argv[0]
es el nombre del script, se utiliza el cortesys.argv[1:]
para devolver la lista de dispositivos.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 argumentosuser
ypassword
. El argumentonormalize=True
garantiza que se aplique la normalización de la respuesta a cada llamada RPC bajo demanda que utilice la instancia de dispositivodev
.El método
open()
se invoca en la instancia del dispositivodev
. Esto establece una sesión NETCONF con el dispositivo e invoca la recopilación de datos por defecto de PyEZ. Si la llamada aopen()
falla, se lanza unajnpr.junos.exception.ConnectError
(o una de sus subclases). Esta posible excepción se gestiona más adelante, en la funciónmain()
.La función
get_lldp_neighbors()
se invoca en la instancia actual del dispositivo,dev
. El resultado se almacena en el diccionariolldp_info
.Si
get_lldp_neighbors()
informa de un error (devolviendo el valorNone
), se imprime un mensaje de error,rc
se establece en1
para indicar el error, y se lanza la excepciónDoneWithDevice
. Esta excepción hace que la ejecución salte a la líneaexcept DoneWithDevice
hacia el final de la funciónmain()
.La función
get_description_info_for_interfaces()
se ejecuta en el dispositivo actual,dev
. El resultado se almacena en el diccionariodesc_info
. De forma similar a la funciónget_lldp_neighbors()
, si la funciónget_description_
info_for_interfaces()
informa de un error (devolviendo el valorNone
), se imprime un mensaje de error,rc
se establece en1
para indicar el error, y se lanza la excepciónDoneWith
Device
y se lanza la excepción. Esta excepción hace que la ejecución salte a la líneaexcept DoneWithDevice
hacia el final de la funciónmain()
.La función
get_lldp_description_changes()
analiza los diccionarioslldp_info
ydesc_info
y devuelve las nuevas descripciones de interfaz en el diccionariodesc_changes
. Si no ha cambiado ninguna descripción, se lanza la excepciónDoneWithDevice
. 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.Se llama a la función
load_merge_template_config()
. El argumentotemplate_path
se establece en el valor deTEMPLATE_PATH
y la clavedescriptions
del argumentotemplate_vars
se establece en el diccionariodesc_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 en1
para indicar el error, y se invocaDoneWithDevice
.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 en1
para indicar el error, y el script continúa con el bloquefinally
.Colocar la invocación al método
dev.close()
dentro de un bloquefinally
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. Comodev.close()
podría producir una excepción, la sentencia se coloca dentro de un bloquetry
. El bloqueexcept
correspondiente simplemente ignora cualquier excepción planteada pordev.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
)
:
"""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
:
resp
=
device
.
rpc
.
get_lldp_neighbors_information
(
)
except
(
jnpr
.
junos
.
exception
.
RpcError
,
jnpr
.
junos
.
exception
.
ConnectError
)
as
err
:
"
"
+
repr
(
err
)
return
None
for
nbr
in
resp
.
findall
(
'
lldp-neighbor-information
'
)
:
local_port
=
nbr
.
findtext
(
'
lldp-local-port-id
'
)
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
,
'
port
'
:
remote_port
}
return
lldp_info
La función
get_lldp_neighbors()
requiere un argumentodevice
. Este argumento es una instancia de dispositivo PyEZ que tiene abierta una sesión NETCONF activa.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 valorNone
a la persona que llama para indicar el error. La función PyEZ RPC on Demand invoca el RPC XML get-lldp-neighbors-information.El método lxml
findall()
devolverá una lista de todos los objetoslxml.etree.Element
que coincidan con la expresión XPathlldp-neighbor-information
. Esto resulta en un bucle a través de cada vecino LLDP. A la variablenbr
se le asigna el objetolxml.etree.Element
que representa al vecino LLDP actual.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 variableslocal_port
,remote_system
yremote_port
.Los valores
remote_system
yremote_port
se almacenan en el diccionariolldp_info
. Este diccionario tiene como clave el valorlocal_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
)
:
"""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
:
resp
=
parser
(
device
.
rpc
.
get_interface_information
(
descriptions
=
True
)
)
except
(
jnpr
.
junos
.
exception
.
RpcError
,
jnpr
.
junos
.
exception
.
ConnectError
)
as
err
:
"
"
+
repr
(
err
)
return
None
try
:
pi
=
resp
[
'
interface-information
'
]
[
'
physical-interface
'
]
.
jdict
(
)
except
KeyError
:
return
desc_info
for
(
local_port
,
port_info
)
in
pi
.
items
(
)
:
try
:
(
udesc
,
_
,
ldesc
)
=
port_info
[
'
description
'
]
.
partition
(
'
LLDP:
'
)
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
)
:
pass
return
desc_info
get_description_info_for_interfaces()
requiere un argumentodevice
. Este argumento es una instancia de dispositivo PyEZ que tiene abierta una sesión NETCONF activa.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 argumentodescriptions
se pasa a la RPC y especifica que sólo deben devolverse descripciones de interfaz. El objetolxml.etree.Element
se pasa a la instanciajxmlease.EtreeParser()
,parser()
.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étodojdict()
(comentado en "objetos jxmlease") se utiliza para devolver un diccionario a partir de la información de la interfaz física. El métodojdict()
produce automáticamente un diccionario basado en el valor de los elementos<name>
de cada elemento<pyhsical-interface>
de la respuesta. Una excepciónKeyError
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.El bucle
for
itera sobrelocal_port
, la clave del diccionariopi
, yport_info
, el valor del diccionariopi
.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 valorlocal_port
. Consulta "Recopilar y analizar descripciones de interfaz" en el script de ejemplo de la API RESTful si este código no está claro.Al acceder a los datos, se lanza una excepción
KeyError
cuando la clave especificada no existe. Se lanza una excepciónTypeError
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 siguientelocal_port
en el diccionariopi
.
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
:
(
"
%s
LLDP Up. Now:
%s
%s
"
%
(
local_port
,
lldp_system
,
lldp_port
))
elif
down
:
(
"
%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
:
(
"
%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
)):
(
"
%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
)
:
"""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
device
.
bind
(
cu
=
Config
)
rc
=
False
try
:
try
:
resp
=
device
.
rpc
.
open_configuration
(
private
=
True
)
except
jnpr
.
junos
.
exception
.
RpcError
as
err
:
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
)
if
resp
.
find
(
"
ok
"
)
is
None
:
raise
LoadNotOKError
device
.
cu
.
commit
(
comment
=
"
made by
%s
"
%
sys
.
argv
[
0
]
)
except
(
jnpr
.
junos
.
exception
.
RpcError
,
jnpr
.
junos
.
exception
.
ConnectError
,
LoadNotOKError
)
as
err
:
"
"
+
repr
(
err
)
except
:
"
Unknown error occurred loading or committing configuration.
"
else
:
rc
=
True
try
:
device
.
rpc
.
close_configuration
(
)
except
jnpr
.
junos
.
exception
.
RpcError
as
err
:
"
"
+
repr
(
err
)
rc
=
False
return
rc
Los argumentos
device
,template_path
ytemplate_vars
son parámetros obligatorios de la funciónload_merge_template_config()
.Se define una nueva subclase
Exception
. Esta clase se levanta, y se maneja, cuando hay un problema al cargar la nueva configuración.Se crea una nueva instancia de configuración PyEZ y se vincula al atributo
cu
del dispositivo.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 CLIconfigure private
.jnpr.junos.exception.RpcError
Las excepciones son capturadas y gestionadas por este bloqueexcept
. Es normal que el RPC XML de abrir configuración devuelva ununcommitted
changes will be discarded on exit
advertencia cuando se especifica el argumentoprivate
. Esta advertencia esperada se ignora. Todas las demás excepcionesjnpr.junos.exception.RpcError
se detectan y gestionan en el bloquetry
/except
.Esta sentencia pasa los argumentos
template_path
ytemplate_vars
al métodoload()
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 argumentomerge=True
hace que la nueva configuración se fusione con la configuración candidata existente.Un elemento XML
<ok>
en la respuesta XML del métodoload()
indica que la nueva configuración se ha cargado correctamente. Se lanza una excepciónLoadNotOKError
, 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 PyEZRpcError
. Este bloque simplemente maneja la posible situación en la que el métodoload()
no lance una excepción y devuelva una respuesta RPC inesperada.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 argumentocomment
.jnpr.junos.exception.RpcError
,jnpr.junos.exception.ConnectError
, yLoadNotOKError
las excepciones y subclases son capturadas y manejadas por este bloqueexcept
. 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 bloquetry
.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 arc
el valor deTrue
para indicar el éxito. (Anteriormente,rc
se inicializaba aFalse
.)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 asignarc
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
>
{%
for
key
,
value
in
descriptions
.iteritems
(
)
%}
<interface
>
<name
>
{{
key
}}
</name>
<description
>
{{
value
}}
</description>
</interface>
{%
endfor
%}
</interfaces>
</configuration>
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.Esta línea es un bucle Jinja2
for
que itera sobre los elementos del diccionariodescriptions
. La clave del diccionariodescriptions
es el nombre de la interfaz, y el valor es la nueva descripción que se va a configurar.La etiqueta XML de apertura de cada interfaz. Esta etiqueta se repetirá para cada interfaz en el diccionario
descriptions
.El nombre de la interfaz. La expresión
{{ key }}
se evalúa con cada clave del diccionariodescriptions
.La descripción de las interfaces. La expresión
{{ value }}
se evalúa con cada valor del diccionariodescriptions
.La etiqueta
{% endfor %}
identifica el final del buclefor
que itera sobre cada interfaz en el diccionariodescriptions
.
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.
Lengua | Descripción | Enlace |
---|---|---|
Ruby | Popular biblioteca NETCONF de código abierto para Ruby. Fácil de instalar, dependencias limitadas y soporte activo. | http://rubygems.org/gems/netconf |
Java | Biblioteca 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 |
Perl | Soportada 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 |
PHP | Biblioteca 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 |
Python | Librerí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_vars
se 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.