Ya he hablado mucho de mi visión sobre la automatización y sobre el panorama de la «tecnología» actualmente. Pues ya es hora de untarnos un poco las manos, no es que no lo haya hecho, les recuerdo que ya alguna vez escribí sobre hacer scripts de configuración usando Word y Combinación de correspondencia que es un método bien creativo (y un poco rudimentario) de usar la tecnología existente. En ésta entrada, sí quiero describir un programa que hice en mi trabajo, obviamente modificado para ser publicado, que me parece muy versátil. La idea es listar IPs-Comandos en una hoja de cálculo y escribir en ésta misma el resultado de su ejecución. Disfrutenlo
Prerrequisitos!
Es importante decir varias cosas. 1) No voy a dar ningún soporte sobre el script que voy a describir, la programación es compleja y resolver las dudas depende mucho de cosas por fuera del script mismo, como el entorno de ejecución, la versión de python, la conectividad con los equipos, etc. 2) Evidentemente, éste contenido no es iniciático: se requiere conocer de configuración de equipos y tener bases de programación, si no tiene éstos dos prerrequisitos lo invito a buscar esas bases antes de ésta lectura.
Introducción
Una vez comprendidos los prerrequisitos, veamos qué voy a mostrar. En mi trabajo es muy común que se requiera entrar a varios equipos a hacer muchas consultas para poder saber qué se tiene que configurar con la finalidad de poner en servicio algún tipo de conectividad. Puede ir desde mirar las rutas en todo el camino, hasta verificar estado de puertos o versiones. Otras veces puede ser necesario consultar cosas sobre varios equipos, por ejemplo, consultar el nivel de ocupación de interfaces de acceso o la distribución de hardware para algún informe (por ejemplo cuántas tarjetas de línea de cierto tipo hay en la red). Está fuera de la discusión si los gestores pueden hacer esas tareas, el asunto es que como ingenieros a veces nos toca hacer lo que deberían hacer los gestores (seguramente por problemas burocráticos que los administradores o jefes no resuelven). Dado el escenario anterior, a mí se me ocurrió que sería una gran herramienta, listar las IPs de los equipos y los comandos que quiero ejecutar en cada uno y con la generalidad de Python puedo escribir el resultado en la misma hoja de cálculo. En éste esquema, yo podría ejecutar varios comandos sobre un mismo equipo simplemente repitiendo la IP en diferentes líneas. Más adelante, me dí cuenta de que también sería muy poderoso, poder filtrar la información del comando con una expresión regular pasada al programa, es decir, no sólo recolectar el resultado de cierto comando sino una porción específica de tal comando. Pues eso es lo que les voy a describir.
Esta no es una entrada para enseñar a programar ni para tirar comandos en los dispositivos, es un ejemplo rudimentario de programabilidad de red y puede que requiera muchos ajustes, esos serán responsabilidad de uds., los lectores. También es muy importante tener en cuenta que ésta es una publicación para ingenieros de red y que la programabilidad de la red es un proceso en la administración de una organización, es decir, debe ser gradual, involucrar varias áreas, incluir capacitación, preparación inicial del entorno administrativo y laboral, gestionar el cambio organizacional sin mencionar las estimaciones de costos y compromisos de calidad.
Finalmente, espero que lo que ilustro en ésta publicación les de muchas ideas porque usa las varias herramientas muy poderosas que pueden facilitar la vida de cualquier ing. de red. Si se les ocurre algún programa, me encantaría conocer la idea, dejála en los comentarios y ayúdame a darle inercia a este blog.
Entorno de ejecución
El programa que les voy a mostrar lo escribí después de instalar una distribución de Python llamada Anaconda en Windows 10. Anaconda viene con Python 3 y con un editor de código muy útil llamado Spyder. En éste editor se puede escribir el programa, ejecutarlo e incluso hacer pruebas en tiempo de ejecución, dado que el editor deja las variables creadas por el programa en el ambiente de ejecución y con el «CLI» de python puedes probar código usando las variables creadas por el último programa.
Adicionalmente, instalé dos librerías clave: Paramiko para conexión de red usando SSH y Openpyxl para editar documentos Excel. Si ud. cree que hay mejores librerías puede dejarlas en los comentarios, conozco de una versión de Paramiko especial para redes llamada NetMiko que no he usado por falta de tiempo, también sé que Pandas permite lectura y escritura a Excel de una manera fácil y un poco rudimentaria, pero esa discusión la dejo para los comentarios.
En resumen:
- Máquina: Core i5/8GB de ram
- Windows 10 enterprise
- Python 3.7.3
- Anaconda3-2019.03-Windows-x86_64
- Librerías que hay que instalar: Paramiko, Openpyxl
Librerías
En el programa se usan las librerías re para procesar expresiones regulares, datetime para sacar el timestamp inicial y final y saber cuánto tardó la ejecución del script, time para hacer pausas en la ejecución, tiempos de espera con la famosa función sleep(). Las últimas tres librerías sí son muy importantes y son la esencia de esta publicación, no son tan protagónicas en el script que describo acá, sino para futuras aplicaciones, son como una gran base para que uds. mismos experimenten.
Pandas es una poderosa librería para manipular datos en forma de matrices multidimensionales. En el script descrito no se hace mucho con ella, pero los invito a estudiar un poco y ver las poderosas operaciones de pandas. La idea es que un dataframe es una unidad de datos multidimensional que puede ser indexada por una palabra/llave o por números a manera de posiciones, de hecho mucho más: es como una pequeña base de datos y sobre ella se pueden ejecutar operaciones simples típicas de una BD. En cuanto a las llaves, el dataframe tiene sintaxis especiales para hacer operaciones tipo SQL pero con su propia sintaxis, es decir, escribiendo de manera especial la referencia de la variable se puede extraer una porción de los datos que incluye. En el caso presente, la línea ip_address=df[‘IP’] escribe en la variable ip_address toda la columna llamada IP de la hoja de cálculo indexable mediante posiciones, es decir, obtiene una lista de la columna IP del dataframe df y éste a su vez fue leído inicialmente de la hoja de cálculo. Todo eso lo hizo la librería pandas. Como les decía, ésta es es mucho más poderosa que eso pero no lo voy a describir en ésta entrada (y no sé si en otras).
Paramiko es una popular librería para conexiones ssh, no la he usado mucho más allá de eso y sé que existe una variante especialmente definida para ser usada en redes llamada netmiko. Tal vez cuando la explore les escriba al respecto pero por ahora no tengo planes.
Openpyxl es la última librería importante de mencionar. Ésta permite leer y escribir en archivos de excel, por ende depende de éste, incluyendo las consecuencias, por ejemplo si no hay licencia tampoco hay edición 🙂 Adicionalmente, cuando la usamos también debemos recordar que Excel no puede abrir dos veces el mismo archivo, openpyxl sí, pero cuando va a escribir, detecta que hay otra copia del mismo archivo y falla la ejecución. Eso es particularmente importante si estamos ejecutando un script por 10 o 20 minutos y falla al final por la tontería de no haber cerrado el archivo. Obviamente hay formas de evitar el problema, por ejemplo escribiendo en un archivo con un nombre diferente al abierto, entre otras. Openpyxl permite leer y escribir a un libro de excel usando sus contenidos como listas o diccionarios, por ejemplo, las siguientes líneas abren un archivo y obtienen una hoja de cálculo llamada Sheet1:
wb = load_workbook(nombreArchivo)
sheet=wb["Sheet1"]
Luego las siguientes líneas leen y escriben a la celda indicada:
sheet['f2'].value="CONEXION FALLIDA"
regex_=re.compile(sheet['e'+str(i+2)].value)
En la última línea me sirve para ilustrar el uso de la librería de expresiones regulares re. En la línea se asigna a la variable regex_ un objeto tipo Expresión Regular usando el contenido de la celda ‘E5’ (asumiento que la variable i contiene el valor 5). Este estilo de expresiones lo uso mucho y es un poco engorroso, pero también es muy versátil.
Una parte interesante de Openpyxl que yo no exploto mucho, es que con esta librería se puede manipular el formato de las celdas, por ejemplo cambiar colores de fondo, tipos de letra, estilo, etc.. También hay que saber que no acepta funciones de excel, es decir, lee los contenidos de las celdas. Por ejempo, si una celda contiene =A1*B1 no obtiene el valor mostrado en la celda sino la cadena literal «=A1*B1». Creo que no hay ninguna forma de obligar la ejecución de la fórmula pero todavía me falta investigar un poco más.
Estructura del programa
La estructura tiene varias funciones: 1) conectarse –shh_conn_open, 2) cerrar conexión shh_close_conn, 3) Ejecutar comando –ssh_runcommand, 4) main(). El propósito de las funciones es parametrizar y modularizar el programa, con el fin de que se pueda reutilizar el código más fácilmente. En mi organización se usa una librería de funciones en vez de escribirlas directamente en el programa pero eso sería un tópico intermedio o avanzado de programación. Las funciones de utilidad se las dejo para autoestudio, básicamente lo que hacen es adecuar los parámetros de ingreso/salida y atender las excepciones que son la forma correcta de manejar los errores en tiempo real. La estructura general del programa es:
- Importar librerías
- Definir funciones
- Ejecutar la función main() -Variables de inicialización, bucle principal, guardado y cierre de recursos.
Finalmente, para instalar las librerías recomiendo buscar e instalar pip (en esta página indican que viene instalado en Python 3.4 o superior), con ésta utilidad se instalan de una manera supremamente fácil. Una vez se instale pip pueden ejecutar pip install paramiko y pip install openpyxl
Lo primero que hay que hacer es importar las librerías:
import re
import datetime
import time
import pandas as pd
import paramiko
from openpyxl import load_workbook
casi todas se usan en las primeras líneas de la función main():
nombreArchivo="commandMatch.xlsx"
df=pd.read_excel(nombreArchivo)
start=datetime.datetime.now()
ip_address=df['IP']
wb = load_workbook(nombreArchivo)
sheet=wb["Sheet1"]
usuario='Goofy'
passwd='1S4P3rv'
n=len(ip_address)
# Estimación 1: 20 sec por línea 20 sec/60 sec = 1/3 min
# print("Tiempo estimado (20 s / comando): "&str(n/3)&" min")
# Estimación 2: 10 comandos / min
input("Cierre el archivo "+nombreArchivo+" antes de continuar. Pulse cualquier tecla para continuar")
print("== Tiempo estimado (10 comandos/min): "+str(n/10)+" min\n")
continuar=input("Continuar? [Y|n]")
if re.match('n',continuar,re.IGNORECASE):
wb.close()
exit()
Lo único que quiero resaltar es el uso de la función input(), que pregunta al usuario si después de ver la estimación de tiempo de ejecución quiere continuar. Lo que responda el usuario queda en la variable continuar y luego si la respuesta fue ‘n’ sale del programa. En éste pedazo de código vemos un 2o uso de las expresiones regulares: buscar una porción de contenido en una cadena sin importar si es mayúscula o minúscula. El uso de expresiones regulares en Python no es trivial, las funciones no son muy intuitivas y existen variantes no comunes que las complican un poco. Recomiendo leer y probar por su cuenta el uso de la librería, acá les dejo un buen link para que exploren y para probarlas en línea está ésta otra página: pythex.org.
Python es un lenguaje interpretado, es decir, no se compila para ser ejecutado de manera directa por la máquina, en vez de eso, el programa se traduce en tiempo real a bytecodes que a su vez se convierten en instrucciones al procesador. Esa es la razón por la que yo casi siempre me refiero al programa como script, porque estrictamente hablando no es un programa, toca tener un intérprete de Python para ejecutarlo. Como consecuencia de que sea un lenguaje interpretado, la forma más común de ejecutar programas es escribiendolos y luego enviarlos al motor del lenguaje. En nuestro caso, la ejecución de éste programa consistirá en ejecutar la siguiente línea en el CMD de Windows: python CommandMatchv1.0.py La forma de usar estos scripts puede tener dos estilos: ejecutar alguna rutina para observar el resultado de manera directa en el cmd de manera semi-interactiva o ejecutar una rutina para almacenar algún tipo de resultado. Ambas formas son útiles, sobre todo para nosotros los ing. de red. El caso de querer ver los resultados en tiempo real, así no sea el propósito, lo podemos usar como una forma rudimentaria de depuración (debugging).
Como lo mencioné anteriormente, el programa consiste en leer un excel, por cada línea del archivo se espera una IP, un comando y una expresión regular (opcional). Para cada IP el programa se conecta usando un usuario/contraseña escrito en el programa (lo que llaman hardcoded), luego ejecuta el comando, si existe expresión regular pasa la salida del comando por la expresión regular y finalmente escribe el resultado en la misma hoja de cálculo. Existen un par de variables que sirven para evitar cerrar/volver a abrir una sesión con un equipo si la siguiente IP es la misma. A continuación el bucle principal:
# Comandos para quitar la paginación en Cisco, ALU y Huawei respectivamente. Por defecto usar un enter
comandoIni=["\n", "terminal len 0\n", "environment no more\n", "screen-length 0\n"]
seleccionInicial=input("Quitar paginación de Cisco (1), ALU(2), Huawei(3) o no quitar paginación (0)?")
ComandoNoPaginar=comandoIni[int(seleccionInicial)]
# conectar se usa para evitar desconectar/reconectar a la misma IP
conectar=True
i=0
lineas=[]
# shh_conn_open, ssh_runcommand y shh_close_conn son funciones utilitarias definidas antes de llamarlas
for host in ip_address:
try:
if conectar:
(sshclient,rconn,stat,msg)=shh_conn_open(host,usuario,passwd)
time.sleep(2)
(salida, error)=ssh_runcommand(rconn,stat,ComandoNoPaginar,2)
except:
print("Conexión fallida a: "+ str(host))
sheet['f'+"2"].value="CONEXION FALLIDA"
conectar=True
i=i+1
continue
# Print usado como depuración: qué se lee de la hoja de cálculo?
print(sheet["d"+str(i+2)].value)
(salida, error)=ssh_runcommand(rconn,stat,sheet["d"+str(i+2)].value,3)
lineas=re.split(r'\\r\\n',salida)
# Si la columna e contiene algo, se espera una expresión regular, usela
if sheet['e'+str(i+2)].value:
regex_=re.compile(sheet['e'+str(i+2)].value)
sheet["f"+str(i+2)].value="\n".join([re.search(regex_,x).group(0) for x in lineas if re.search(regex_,x)])
else:
sheet["f"+str(i+2)].value="\n".join(lineas[1:len(lineas)-1])
# Preguntar si la siguiente IP es igual que la actual, en tal caso, no desconectar y al inicio del bucle no reconectar.
if i+1<n:
next_host=ip_address[i+1]
else: # Último host: asegurar que es diferente. Esperemos que 1.1.1.1 no sea una IP válida en nuestra red XD!
next_host='1.1.1.1'
if host!=next_host:
shh_close_conn(sshclient, rconn)
conectar=True
else:
conectar=False
i=i+1
Espero que el programa sea suficientemente claro, sin embargo, creo que la siguiente línea requiere una explicación especial:
[re.search(regex_,x).group(0) for x in lineas if re.search(regex_,x)]
En ella observamos una de las carecterísticas más poderosas del lenguaje, las list comprehension. Toda la expresión genera una lista (para mí un arreglo), pero toma cada valor de la lista lineas y si éste valor de x cumple la condición re.search(regex_,x), es decir un match para la expresión regular, en esa posición queda el valor re.search(regex_,x).group(0), es decir, el valor específico que hizo match con la expresión. Denle un segundo vistazo a la explicación y exploren las posibilidades de esta forma de hacer ciclos. Esta sintaxis es muy poderosa porque en una sola línea nos evitamos un bucle entero. De otro lado, programas que abusen de ésta estructura también serán más difíciles de entender y por ende la documentación se hace más crítica. Como ésta expresión genera una lista, la parte derecha de la igualdad une esos valores con un \n, es decir, un enter.
La alternativa a si hay expresión regular es simplemente guardar el resultado sin procesar:
sheet["f"+str(i+2)].value="\n".join(lineas[1:len(lineas)-1])
en ésta línea también hacemos uso de una sintaxis especial para indizar una lista, pos_inicial:pos_final. Con ella le quitamos la posición final, por ende estamos guardando desde el comando ejecutado hasta una línea antes en la cual sólo saldría el prompt.
Finalmente, guardamos, liberamos los recursos y se imprime el resumen de la ejecución:
wb.save(nombreArchivo)
wb.close()
print("- Iniciado a : "+str(start))
print("- Finalizado a: "+str(datetime.datetime.now()))
print("- Tardó : "+str(datetime.datetime.now()-start))
A continuación dejo el script con su plantilla de Excel para la descarga. Lamentablemente la plataforma de blogging no me deja adjuntar .zip así que tocó en .tar que no es un archivo comprimido.
Mejoras posibles
Muchas validaciones. En programación se conoce como curso normal de eventos al caso ideal que generalmente uno tiene en la cabeza, es el que da la estructura del programa y a su vez es el caso normal con el cual probamos si nuestro programa funciona correctamente o no. Luego uno debe pensar en el curso anormal de eventos, es decir, las condiciones de error o inesperadas que son las más difíciles de detectar porque pueden ser situaciones que a nadie se le ocurren o que pueden ser malintencionadas. Allí entran en juego las excepciones y probablemente las diferentes versiones del programa. Esta versión que expuse es medio trivial: piensa estrictamente en el curso normal de los eventos, por eso se pueden hacer muchas mejoras. Hay muchas áreas donde se puede hacer validaciones, por ejemplo al inicio cuando se pide si continuar o no. La función re.match() va a buscar la letra ‘n’ al inicio del contenido de la variable continuar, si el usuario es malicioso puede ingresar otros valores y el programa podría hacer cosas inesperadas. Así mismo, nunca se verifica que el valor de expresión regular leído del excel sí sea una RegEx, podría ser una porción de código que haga algo por su cuenta! Este no es un asunto trivial, porque con el tiempo, éstos scripts pueden ser ejecutados por alguien ajeno a la organización y podrían generar un problema de seguridad mayor.
Como ven, hay un par de datos que se incluyen o preguntan en el script que se podrían incluír en el programa: usuario/contraseña, el vendor, entre otros, otra opción es agregar código que deduzca o encuentre el fabricante. Finalmente, un compañero hizo una versión paralelizada, es decir, que distribuye las conexiones en varios hilos de ejecución en paralelo. La librería para paralelizar se llama thread y es otra importante forma de mejorarlo.
Otra forma de usar el script que me gusta mucho es agregando opciones o switches, como los que usamos en comandos de CMD o shell de linux, es decir las letras que permiten cambiar cómo funciona un programa. Por ejemplo, yo a veces permito que el programa tenga una opción -s para no imprimir o preguntar nada en pantalla (python CommandMatchv1.0.py -s) o para especificar el nombre del archivo desde el cual leer (python CommandMatchv1.0.py -f RevisionCore.xlsx). La opción de especificar el nombre del archivo me gusta porque permite ejecutar varias versiones del programa en paralelo. Otra opción interesante sería especificar el nombre de archivo de lectura y un nombre de escritura para no usar el mismo archivo, etc.. Para usar parámetros hay que incluir la librería sys y usar la variable sys.argv que es una lista de los parámetros pasados al script. Si se les ocurren más formas de usar el script quedo atento, sería de gran utilidad.
Resumen y continuación
En computación, por lo general es muy importante una lógica simple pero general, éste programa es una ilustración de ese concepto: no pretender hacer un programa que haga todo, sino scripts o rutinas genéricas que sirvan para 1) Permitir una familiarización con la sistematización de la información, programación, etc sin romper directamente con las costumbres de la organización, 2) Tener rutinas reutilizables. El arte de la programación consiste en encontrar el balance entre lo general y lo específico.
Excelente tarde mi estimado, tengo una pregunta, como podría lograr que se capturen cerca de 3000 lineas? llega como a 30 y se corta aprox en 100.
te agradezco de antemano
Hola Jose Luis,
intenta cambiar parámetros de timer o de buffer en la función de ssh_runcommand()