En el artículo Sysadmin vago: cómo actualizar todos los servidores de tu organización ejecutando un único comando expliqué de qué forma es posible lanzar actualizaciones de múltiples servidores GNU/Linux (Debian y CentOS) desde un simple script Bash, utilizando un servidor de administración centralizado y SSH con autenticación con clave pública.

La limitación de este esquema era que trabajaba de forma secuencial, es decir, se actualizaba de a un sistema por vez. Por lo tanto en este artículo voy a demostrar cómo he mejorado mi script para lanzar todas las actualizaciones en paralelo (so pena de saturar un poco el enlace y/o Web proxy) sin necesariamente perder el control del proceso.

Recordemos que no es recomendable actualizar un sistema a ciegas esperando que todo funcione. Muchas actualizaciones requieren reinicio de servicios o nuevas versiones de archivos de configuración. Sin contar con los conflictos producidos por aplicaciones a medida atadas a versiones específicas de paquetes. Sin contar con actualizaciones de kernel que pueden provocar conflictos con software compilado a la medida de un kernel específico. Por ello, como administradores de sistemas, debemos examinar lo que cada gestor de paquetes ofrece actualizar, para evaluar si se debe proceder o se requiere alguna clase de intervención manual.

Sin embargo, existen dos factores que permiten atenuar conflictos. Primero, como sysadmins medianamente responsable, muy seguramente sabremos de antemano qué actualizaciones hay disponibles para cada versión de cada sistema operativo. Ya sea porque estamos suscriptos a las diferentes listas de correo de las distribuciones que utilizamos en nuestra organización, o porque usamos un script para notificar las actualizaciones disponibles por mail. Segundo, es posible lanzar todas las actualizaciones en paralelo sin perder las salidas si se redirigen a diferentes archivos, para luego analizarlas si es necesario. Sólo queda pendiente detectar cuándo un gestor desea actualizar el kernel Linux, para decidir si proceder o no con cada sistema en particular. Luego es posible relajarse y ver cómo Bash, SSH y sudo hacen todo por nosotros.

El formato del archivo de configuración (ahora llamado actualizar_servidores-paralelo.conf) permanece sin cambios:

usuario:host:puerto:distro

En cambio, el script Bash (ahora llamado actualizar_servidores-paralelo.sh) cambia bastante:

#!/usr/bin/env bash

# Localización del archivo de configuración
CONF="actualizar_servidores-v2.conf"
LOGDIR="log"

# Comandos para verificar actualizaciones
CMD_DEBIAN="LANG=C sudo apt-get -s upgrade | grep upgraded | tail -n 1 | cut -d' ' -f1"
CMD_CENTOS="sudo yum check-update"

# Comandos para verificar actualizaciones de kernel
CMD_DEBIAN_KERN="LANG=C sudo apt-get -s upgrade 2>/dev/null | grep linux | wc -l"
CMD_CENTOS_KERN="sudo yum check-update 2>/dev/null"

# Comandos para actualizar
CMD_DEBIAN_UPDT="LANG=C sudo apt-get -q -y upgrade && sudo apt-get -q -y clean"
CMD_CENTOS_UPDT="LANG=C sudo yum -y update"

# Configuración del diálogo
OPCIONES=""
INDICE=1
KERN=0

# Datos de los servidores
USERS="users"
HOSTS="hosts"
PORTS="ports"
OSES="oses"

# Obtener fecha y hora
FECHAHORA=$(date +%Y-%m-%d_%H-%M-%S)
echo === Iniciando actualizaciones \($FECHAHORA\) ===

# CONF - username:host:port:os
for SERVER in $(cat $CONF)
do
    # Parsear linea de configuración
    USER=$(echo $SERVER | cut -d ':' -f1)
    HOST=$(echo $SERVER | cut -d ':' -f2)
    PORT=$(echo $SERVER | cut -d ':' -f3)
    OS=$(echo $SERVER | cut -d ':' -f4)

    echo -n Verificando $HOST...

    # Verificar actualizaciones disponibles
    case $OS in
      "debian")
          UPDATES=$(ssh -p $PORT $USER@$HOST $CMD_DEBIAN)
          KERN=$(ssh -p $PORT $USER@$HOST $CMD_DEBIAN_KERN)
          ;;
      "centos")
          UPDATES=$(ssh -t -p $PORT $USER@$HOST "$CMD_CENTOS" > /dev/null 2>&1; echo -n $?)
          KERN=$(ssh -t -p $PORT $USER@$HOST $CMD_CENTOS_KERN 2>/dev/null | grep kernel | wc -l)
          ;;
    esac

    if [ $UPDATES -eq 0 ]
    then
      echo ""
    else
      if [ $OS == "centos" ]
      then
          echo " Hay actualizaciones disponibles."
      else
          echo " Hay "$UPDATES" actualizaciones disponibles."
      fi
    fi

    # Si hay actualizaciones, agregar a la lista
    if [ $UPDATES -gt 0 ]
    then
        if [ $KERN -eq 0 ]
        then
            OPCIONES=("${OPCIONES[@]}" $INDICE "$HOST" off)
        else
            OPCIONES=("${OPCIONES[@]}" $INDICE "$HOST (KERNEL)" off)
        fi
        USERS=("${USERS[@]}" $USER)
        HOSTS=("${HOSTS[@]}" $HOST)
        PORTS=("${PORTS[@]}" $PORT)
        OSES=("${OSES[@]}" $OS)
        ((INDICE++))
    fi

done

# Imprimir diálogo selector de opciones
SELECCION=$(/usr/bin/dialog --separate-output --checklist "Seleccione los sistemas a actualizar" 22 76 16"${OPCIONES[@]}" 2>&1 > /dev/tty)

# Lanzar actualizaciones en paralelo
for SERV in $SELECCION
do
    echo Actualizando $SERV ${USERS[$SERV]}\@${HOSTS[$SERV]}\:${PORTS[$SERV]} ${OSES[$SERV]}

    # Actualizar el servidor
    case ${OSES[$SERV]} in
        "debian") (ssh -p ${PORTS[$SERV]} ${USERS[$SERV]}@${HOSTS[$SERV]} $CMD_DEBIAN_UPDT; echo $?.FINALIZADO) > $LOGDIR/$FECHAHORA-${HOSTS[$SERV]}.log 2>&1 &;;
        "centos") (ssh -t -t -p ${PORTS[$SERV]} ${USERS[$SERV]}@${HOSTS[$SERV]} "$CMD_CENTOS_UPDT"; echo $?.FINALIZADO) > $LOGDIR/$FECHAHORA-${HOSTS[$SERV]}.log 2>&1 &;;
    esac
done

# Esperar que finalicen todas las actualizaciones
while [ 1 ]
do
    # Esperar 2 segundos
    sleep 2

    FIN=1

    # Revisar todos los archivos de log
    for SERV in $SELECCION
    do
        # Determinar si finalizó
        FINALIZADO=$(tail $LOGDIR/$FECHAHORA-${HOSTS[$SERV]}.log 2>/dev/null | grep ".FINALIZADO" | wc -l)
        if [ $FINALIZADO -eq 0 ]
        then
            FIN=0
            break
        fi
    done

    if [ $FIN -gt 0 ]
    then
        break
    fi
done

echo "Finalizado"

# Verificar códigos de retorno
ERRORES=0
for SERV in $SELECCION
do
    # Obtener código de retorno
    RET=$(grep ".FINALIZADO" $LOGDIR/$FECHAHORA-${HOSTS[$SERV]}.log | cut -d'.' -f1)

    if [ $RET -gt 0 ]
    then
        echo "*****" ${HOSTS[$SERV]} ha finalizado con ERRORES
        ERRORES=1
    else
        echo "·" ${HOSTS[$SERV]} ha finalizado correctamente
    fi
done

if [ $ERRORES -eq 1 ]
then
    echo
    echo "***** ADVERTENCIA ****"
    echo "1 o más actualizaciones han finalizado con errores"
    echo
fi

read -p "Presione [Enter] para continuar..."

# Revisar logs
for LOGFILE in $LOGDIR/$FECHAHORA*
do
    less $LOGFILE
done

 

¿Cómo funciona este nuevo script Bash?

En primer ciclo for se encarga de determinar si existen actualizaciones disponibles (identificando además si se va a actualizar el kernel) para cada servidor en el archivo de configuración.

Para cada servidor que posee actualizaciones disponibles, mantiene cuatro listas donde guarda los parámetros del archivo de configuración.

Una vez que ha determinado cuáles servidores disponen de actualizaciones, elabora un cuadro de diálogo, utilizando la herramienta dialog (man dialog), que permite escoger cuales se desean actualizar.

El segundo ciclo for se encarga de lanzar todas las actualizaciones en paralelo. El paralelismo se logra lanzando las actualizaciones en segundo plano (&) y redireccionando, tanto la salida estándar como la error estándar, a diferentes archivos.

El siguiente ciclo (while) se utiliza para esperar que todos los servidores finalicen su actualización. Cada 2 segundos verifica todos los archivos de salida generados. La finalización se marca con una línea que posee la siguiente sintaxis:

n.FINALIZADO

Donde n indica el código de retorno del gestor de paquetes.

Una vez que todos los servidores finalizaron su proceso de actualización, imprime "Finalizado" en la salida y reporta en qué estado ha finalizado cada uno. En caso de que alguno haya finalizado con errores mostrará además una advertencia.

En el último ciclo for muestra por pantalla, utilizando la herramienta less, la salida de cada actualización, para analizar detenidamente en caso de errores.

Ejemplo de actualización en paralelo

A continuación, un ejemplo de corrida del script. La primera parte, donde se verifican las actualizaciones disponibles en cada servidor, ocurre de manera secuencial. Esto es algo que voy a paralelizar en una próxima versión, de la misma forma que hice con el proceso de actualización en sí.

sysadmin@mngmntsrv:~/scripts$ ./actualizar_servidores-paralelo.sh 
=== Iniciando actualizaciones (2015-02-13_10-25-09) ===
Verificando mngmntsrv.linuxito.com...
Verificando appssrv.linuxito.com... 1 actualizaciones disponibles.
Verificando server05.linuxito.com... 4 actualizaciones disponibles.
Verificando webapps-db.linuxito.com... 5 actualizaciones disponibles.
Verificando www-devel.linuxito.com... 2 actualizaciones disponibles.
Verificando www.linuxito.com... 1 actualizaciones disponibles.
Verificando webapps-db2.linuxito.com... 6 actualizaciones disponibles.
Verificando bckpsrv.linuxito.com... 3 actualizaciones disponibles.
Verificando webapps2-testing.linuxito.com...
Verificando webapps2.linuxito.com... 4 actualizaciones disponibles.
Verificando crm-db.linuxito.com... 11 actualizaciones disponibles.
Verificando crm.linuxito.com... 12 actualizaciones disponibles.
Verificando ns1.linuxito.com... 16 actualizaciones disponibles.
Verificando ns2.linuxito.com...
Verificando www-devel.linuxito.com... 2 actualizaciones disponibles.
Verificando webapps3-db.linuxito.com...
Verificando webapps3.linuxito.com...

Luego de verificar las actualizaciones surge el cuadro de diálogo:

Cabe destacar que el script no informa qué paquetes se actualizarán en cada servidor, excepto al finalizar su ejecución. Recordar que el script asume que el administrador conoce previamente qué se actualizará en cada sistema.

En caso de que un sistema disponga de una actualización del kernel, mostrará la palabra "KERNEL", en mayúsuclas, y entre paréntesis, para que sea fácil de identificar.

Luego de seleccionar y presionar "Aceptar", se lanzan las actualizaciones en paralelo. Los mensajes "Actualizando..." aparecen secuencialmente, pero de forma inmediata uno tras otro.

Actualizando 1 sysadmin@appssrv.linuxito.com:2626 debian
Actualizando 4 sysadmin@www-devel.linuxito.com:2626 debian
Actualizando 5 sysadmin@www.linuxito.com:2626 debian
Actualizando 6 sysadmin@webapps-db2.linuxito.com:2626 debian
Actualizando 9 sysadmin@crm-db.linuxito.com:2626 debian
Finalizado
· appssrv.linuxito.com ha finalizado correctamente
· www-devel.linuxito.com ha finalizado correctamente
· www.linuxito.com ha finalizado correctamente
· webapps-db2.linuxito.com ha finalizado correctamente
· crm-db.linuxito.com ha finalizado correctamente
Presione [Enter] para continuar...
sysadmin@mngmntsrv:~/scripts$

Una vez que finaliza el último servidor, imprime "Finalizado" y reporta los estados de cada uno. Al terminar muestra secuencialmente todas las salidas, las cuales se cierran presionando la tecla 'Q'.

Los archivos de log no son borrados, y pueden ser consultados más tarde:

sysadmin@mngmntsrv:~/scripts$ ls -1 log/2015-02-13_10-25-09-*
log/2015-02-13_10-25-09-appssrv.linuxito.com.log
log/2015-02-13_10-25-09-www-devel.linuxito.com.log
log/2015-02-13_10-25-09-www.linuxito.com.log
log/2015-02-13_10-25-09-webapps-db2.linuxito.com.log
log/2015-02-13_10-25-09-crm-db.linuxito.com.log

Espero que sea útil. Hasta la próxima entrega.