Scripts de shell en Ansible

Supongamos que un cliente le pide ayuda con la migración de script para desplegar un archivo sudoers centralizado en servidores RHEL y AIX.



Bueno, este es un escenario muy común y, con su ejemplo, puede demostrar el uso de las funciones avanzadas de Ansible, así como también cómo cambia el enfoque, desde un script que realiza una tarea determinada, hasta una descripción idempotente (sin hacer cambios) y supervisa el cumplimiento del estado de la instancia.

Toma el guión:

#!/bin/sh # Desc: Distribute unified copy of /etc/sudoers # # $Id: $ #set -x export ODMDIR=/etc/repos # # perform any cleanup actions we need to do, and then exit with the # passed status/return code # clean_exit() { cd / test -f "$tmpfile" && rm $tmpfile exit $1 } #Set variables PROG=`basename $0` PLAT=`uname -s|awk '{print $1}'` HOSTNAME=`uname -n | awk -F. '{print $1}'` HOSTPFX=$(echo $HOSTNAME |cut -c 1-2) NFSserver="nfs-server" NFSdir="/NFS/AIXSOFT_NFS" MOUNTPT="/mnt.$$" MAILTO="unix@company.com" DSTRING=$(date +%Y%m%d%H%M) LOGFILE="/tmp/${PROG}.dist_sudoers.${DSTRING}.log" BKUPFILE=/etc/sudoers.${DSTRING} SRCFILE=${MOUNTPT}/skel/sudoers-uni MD5FILE="/.sudoers.md5" echo "Starting ${PROG} on ${HOSTNAME}" >> ${LOGFILE} 2>&1 # Make sure we run as root runas=`id | awk -F'(' '{print $1}' | awk -F'=' '{print $2}'` if [ $runas -ne 0 ] ; then echo "$PROG: you must be root to run this script." >> ${LOGFILE} 2>&1 exit 1 fi case "$PLAT" in SunOS) export PINGP=" -t 7 $NFSserver " export MOUNTP=" -F nfs -o vers=3,soft " export PATH="/usr/sbin:/usr/bin" echo "SunOS" >> ${LOGFILE} 2>&1 exit 0 ;; AIX) export PINGP=" -T 7 $NFSserver 2 2" export MOUNTP=" -o vers=3,bsy,soft " export PATH="/usr/bin:/etc:/usr/sbin:/usr/ucb:/usr/bin/X11:/sbin:/usr/java5/jre/bin:/usr/java5/bin" printf "Continuing on AIX...\n\n" >> ${LOGFILE} 2>&1 ;; Linux) export PINGP=" -t 7 -c 2 $NFSserver" export MOUNTP=" -o nfsvers=3,soft " export PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin" printf "Continuing on Linux...\n\n" >> ${LOGFILE} 2>&1 ;; *) echo "Unsupported Platform." >> ${LOGFILE} 2>&1 exit 1 esac ## ## Exclude Lawson Hosts ## if [ ${HOSTPFX} = "la" ] then echo "Exiting Lawson host ${HOSTNAME} with no changes." >> ${LOGFILE} 2>&1 exit 0 fi ## ## * NFS Mount Section * ## ## Check to make sure NFS host is up printf "Current PATH is..." >> ${LOGFILE} 2>&1 echo $PATH >> $LOGFILE 2>&1 ping $PINGP >> $LOGFILE 2>&1 if [ $? -ne 0 ]; then echo " NFS server is DOWN ... ABORTING SCRIPT ... Please check server..." >> $LOGFILE echo "$PROG failed on $HOSTNAME ... NFS server is DOWN ... ABORTING SCRIPT ... Please check server ... " | mailx -s "$PROG Failed on $HOSTNAME" $MAILTO exit 1 else echo " NFS server is UP ... We will continue..." >> $LOGFILE fi ## ## Mount NFS share to HOSTNAME. We do this using a soft mount in case it is lost during a backup ## mkdir $MOUNTPT mount $MOUNTP $NFSserver:${NFSdir} $MOUNTPT >> $LOGFILE 2>&1 ## ## Check to make sure mount command returned 0. If it did not odds are something else is mounted on /mnt.$$ ## if [ $? -ne 0 ]; then echo " Mount command did not work ... Please check server ... Odds are something is mounted on $MOUNTPT ..." >> $LOGFILE echo " $PROG failed on $HOSTNAME ... Mount command did not work ... Please check server ... Odds are something is mounted on $MOUNTPT ..." | mailx -s "$PROG Failed on $HOSTNAME" $MAILTO exit 1 else echo " Mount command returned a good status which means $MOUNPT was free for us to use ... We will now continue ..." >> $LOGFILE fi ## ## Now check to see if the mount worked ## if [ ! -f ${SRCFILE} ]; then echo " File ${SRCFILE} is missing... Maybe NFS mount did NOT WORK ... Please check server ..." >> $LOGFILE echo " $PROG failed on $HOSTNAME ... File ${SRCFILE} is missing... Maybe NFS mount did NOT WORK ... Please check server ..." | mailx -s "$PROG Failed on $HOSTNAME" $MA ILTO umount -f $MOUNTPT >> $LOGFILE rmdir $MOUNTPT >> $LOGFILE exit 1 else echo " NFS mount worked we are going to continue ..." >> $LOGFILE fi ## ## * Main Section * ## if [ ! -f ${BKUPFILE} ] then cp -p /etc/sudoers ${BKUPFILE} else echo "Backup file already exists$" >> ${LOGFILE} 2>&1 exit 1 fi if [ -f "$SRCFILE" ] then echo "Copying in new sudoers file from $SRCFILE." >> ${LOGFILE} 2>&1 cp -p $SRCFILE /etc/sudoers chmod 440 /etc/sudoers else echo "Source file not found" >> ${LOGFILE} 2>&1 exit 1 fi echo >> ${LOGFILE} 2>&1 visudo -c |tee -a ${LOGFILE} if [ $? -ne 0 ] then echo "sudoers syntax error on $HOSTNAME." >> ${LOGFILE} 2>&1 mailx -s "${PROG}: sudoers syntax error on $HOSTNAME" "$MAILTO" << EOF Syntax error /etc/sudoers on $HOSTNAME. Reverting changes Please investigate. EOF echo "Reverting changes." >> ${LOGFILE} 2>&1 cp -p ${BKUPFILE} /etc/sudoers else # # Update checksum file # grep -v '/etc/sudoers' ${MD5FILE} > ${MD5FILE}.tmp csum /etc/sudoers >> ${MD5FILE}.tmp mv ${MD5FILE}.tmp ${MD5FILE} chmod 600 ${MD5FILE} fi echo >> ${LOGFILE} 2>&1 if [ "${HOSTPFX}" = "hd" ] then printf "\nAppending #includedir /etc/sudoers.d at end of file.\n" >> ${LOGFILE} 2>&1 echo "" >> /etc/sudoers echo "## Read drop-in files from /etc/sudoers.d (the # here does not mean a comment)" >> /etc/sudoers echo "#includedir /etc/sudoers.d" >> /etc/sudoers fi ## ## * NFS Un-mount Section * ## ## ## Unmount /mnt.$$ directory ## umount ${MOUNTPT} >> $LOGFILE 2>&1 if [ -d ${MOUNTPT} ]; then rmdir ${MOUNTPT} >> $LOGFILE 2>&1 fi ## ## Make sure that /mnt.$$ got unmounted ## if [ -f ${SRCFILE} ]; then echo " The umount command failed to unmount ${MOUNTPT} ... We will not force the unmount ..." >> $LOGFILE umount -f ${MOUNTPT} >> $LOGFILE 2>&1 if [ -d ${MOUNTPT} ]; then rmdir ${MOUNTPT} >> $LOGFILE 2>&1 fi else echo " $MOUNTPT was unmounted ... There is no need for user intervention on $HOSTNAME ..." >> $LOGFILE fi # # as always, exit cleanly # clean_exit 0 

Hay 212 líneas de código, sin control de versión en el archivo sudoers. El cliente ya tiene un proceso que se ejecuta una vez por semana y verifica la suma de comprobación del archivo para garantizar la seguridad. Aunque el script contiene una referencia a Solaris, para este cliente no tuvimos que transferir este requisito.

Comencemos creando un rol y colocando el archivo sudoers en Git para el control de versiones. Entre otras cosas, esto nos permitirá deshacernos de la necesidad de montar volúmenes NFS.

Con las opciones de validación y copia de seguridad para los módulos de copia y plantilla , podemos eliminar la necesidad de escribir código para hacer una copia de seguridad y restaurar el archivo. En este caso, la validación se realiza antes de colocar el archivo en el punto de destino, y si la validación falla, el módulo arroja un error.

Para cada rol, necesitamos especificar tareas, plantillas y variables. Aquí está la estructura del archivo correspondiente:



El archivo de script de roles (libro de jugadas), sudoers.yml , tiene una estructura simple:

---
##
# Role playbook
##
- hosts: all
roles:
- sudoers
...

Las variables de rol se encuentran en el archivo vars / main.yml . Este archivo contiene el archivo de suma de verificación e incluye / excluye directivas que se utilizarán para crear una lógica especial para omitir los hosts Lawson e incluir el archivo sudoers.d solo en los hosts hd.

Aquí está el contenido del archivo vars / main.yml :

---
MD5FILE: /root/.sudoer.md5
EXCLUDE: la
INCLUDE: hd
...

Si usamos los módulos copy y lineinfile , entonces el rol no será idempotente. El módulo de copia instalará el archivo base, y lineinfile volverá a insertar incluir cada vez que se inicia. Dado que este rol se lanzará en Ansible Tower , la idempotencia es imprescindible. Convertiremos el archivo a una plantilla jinja2.

En la primera línea, agregamos el siguiente comando para controlar espacios y sangrías :

#jinja2: lstrip_blocks: True, trim_blocks: True

Tenga en cuenta que las versiones más nuevas del módulo de plantilla incluyen parámetros para trim_blocks (agregado en Ansible 2.4).

Aquí está el código que inserta la línea de inclusión al final del archivo:

{% if ansible_hostname[0:2] == INCLUDE %}
#includedir /etc/sudoers.d
{% endif %}

Usamos la construcción condicional ({% if%}, {% endif%}) para el comando de shell que inserta una línea para hosts cuyos nombres comienzan con los caracteres "hd". Utilizamos hechos de Ansible y un filtro [0: 2] para analizar el nombre de host.

Ahora pasemos a las tareas. Primero, debe establecer un hecho para analizar el nombre de host. Usaremos el hecho "parhost" en la construcción condicional.

---
##
# Parse hostnames to grab 1st 2 characters
##
- name: "Parse hostname's 1st 2 characters"
set_fact: parhost={{ ansible_hostname[0:2] }}

No hay ningún parámetro csum en el servidor de valores RHEL. Si es necesario, podemos usar otro hecho para indicar condicionalmente el nombre del archivo binario con la suma de verificación. Tenga en cuenta que es posible que se requiera código adicional si estas características son diferentes en AIX, Solaris y Linux.

Además, tenemos que resolver el problema con diferencias en los grupos raíz en AIX y RHEL.

##
# Conditionally set name of checksum binary
##
- name: "set checksum binary"
set_fact:
csbin: "{{ 'cksum' if (ansible_distribution == 'RedHat') else 'csum' }}"

##
# Conditionally set name of root group
##
- name: "set system group"
set_fact:
sysgroup: "{{ 'root' if (ansible_distribution == 'RedHat') else 'sys' }}"

El uso de bloques nos permite establecer la condición para toda la tarea. Utilizaremos la condición al final del bloque para excluir hosts "la".

##
# Enclose in block so we can use parhost to exclude hosts
##
- block:

El módulo de plantilla valida e instala el archivo. Arreglamos el resultado para que podamos determinar si la tarea ha cambiado. El uso del parámetro de validación en este módulo le permite verificar la validez del nuevo archivo sudoer antes de colocarlo en el host.

##
# Validate will prevent bad files, no need to revert
# Jinja2 template will add include line
##
- name: Ensure sudoers file
template:
src: sudoers.j2
dest: /etc/sudoers
owner: root
group: "{{ sysgroup }}"
mode: 0440
backup: yes
validate: /usr/sbin/visudo -cf %s
register: sudochg

Si se ha instalado una nueva plantilla, ejecute el script de shell para generar el archivo de suma de verificación. La construcción condicional actualiza el archivo de suma de verificación al instalar la plantilla sudoers, o si falta el archivo de suma de verificación. Dado que el proceso en ejecución también monitorea otros archivos, utilizamos el código de shell proporcionado en el script de origen:

- name: sudoers checksum
shell: "grep -v '/etc/sudoers' {{ MD5FILE }} > {{ MD5FILE }}.tmp ; {{ csbin }} /etc/sudoers >> {{ MD5FILE }} ; mv {{ MD5FILE }}.tmp {{ MD5FILE }}"
when: sudochg.changed or MD5STAT.exists == false

El módulo de archivo verifica la instalación de los permisos necesarios:

- name: Ensure MD5FILE permissions
file:
path: "{{ MD5FILE }}"
owner: root
group: "{{ sysgroup }}"
mode: 0600
state: file

Dado que el parámetro de copia de seguridad no proporciona ninguna opción para procesar copias de seguridad anteriores, tendremos que encargarnos de crear el código apropiado nosotros mismos. En el siguiente ejemplo, usamos el parámetro "registrarse" y el campo "stdout_lines" para esto.

##
# List and clean up backup files. Retain 3 copies.
##
- name: List /etc/sudoers.*~ files
shell: "ls -t /etc/sudoers*~ |tail -n +4"
register: LIST_SUDOERS
changed_when: false

- name: Cleanup /etc/sudoers.*~ files
file:
path: "{{ item }}"
state: absent
loop: "{{ LIST_SUDOERS.stdout_lines }}"
when: LIST_SUDOERS.stdout_lines != ""

Finalización de bloque:

##
# This conditional restricts what hosts this block runs on
##
when: parhost != EXCLUDE
...

El caso de uso previsto es desempeñar este papel en Ansible Tower. Ansible Tower Alerts se puede configurar para que, en caso de falla en la ejecución del trabajo, las alertas se envíen por correo electrónico, a Slack o de alguna otra manera. Este rol se ejecuta en Ansible, Ansible Engine o Ansible Tower.

Como resultado, eliminamos todo lo superfluo del script y creamos un rol completamente idempotente que puede proporcionar el estado deseado del archivo sudoers. El uso de SCM permite el control de versiones, proporciona una gestión de cambios y transparencia más eficientes. Los CI / CD con Jenkins u otras herramientas le permiten configurar pruebas automáticas de código Ansible para futuros cambios. El rol de auditor en Ansible Tower le permite monitorear y hacer cumplir los requisitos de la organización.

El código para trabajar con sumas de verificación podría eliminarse de la secuencia de comandos, pero para esto el cliente primero debería consultar a su servicio de seguridad. Si es necesario, el patrón de sudoers se puede proteger con Ansible Vault. Finalmente, el uso de grupos evita escribir lógica usando incluye y excluye.

→ Puede descargar el rol desde GitHub en este enlace

Source: https://habr.com/ru/post/es433792/


All Articles