Scripts shell dans Ansible

Supposons qu'un client vous ait demandé d'aider à la migration de script pour déployer un fichier sudoers centralisé sur les serveurs RHEL et AIX.



Eh bien, c'est un scénario très courant, et avec son exemple, vous pouvez démontrer l'utilisation des fonctionnalités avancées d'Ansible, ainsi que la façon dont l'approche change - d'un script qui effectue une certaine tâche, à une description idempotente (sans apporter de modifications) et en surveillant la conformité avec l'état de l'instance.

Prenez le script:

#!/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 

Il y a 212 lignes de code, sans contrôle de version dans le fichier sudoers. Le client dispose déjà d'un processus qui s'exécute une fois par semaine et vérifie la somme de contrôle du fichier pour assurer la sécurité. Bien que le script contienne une référence à Solaris, pour ce client, nous n'avons pas eu à transférer cette exigence.

Commençons par créer un rôle et placer le fichier sudoers dans Git pour le contrôle de version. Entre autres choses, cela nous permettra de nous débarrasser de la nécessité de monter des volumes NFS.

Avec les paramètres de validation et de sauvegarde pour les modules de copie et de modèle , nous pouvons éliminer la nécessité d'écrire du code pour sauvegarder et restaurer le fichier. Dans ce cas, la validation est effectuée avant que le fichier ne soit placé au point de destination, et si la validation échoue, le module génère une erreur.

Pour chaque rôle, nous devons spécifier des tâches, des modèles et des variables. Voici la structure du fichier correspondant:



Le fichier de script de rôle (playbook), sudoers.yml , a une structure simple:

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

Les variables de rôle se trouvent dans le fichier vars / main.yml . Ce fichier contient le fichier de contrôle et inclut / exclut les directives qui seront utilisées pour créer une logique spéciale pour ignorer les hôtes Lawson et inclure le fichier sudoers.d uniquement dans les hôtes hd.

Voici le contenu du fichier vars / main.yml :

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

Si nous utilisons les modules copy et lineinfile , le rôle ne sera pas idempotent. Le module de copie installera le fichier de base et lineinfile réinsérera include à chaque démarrage. Comme ce rôle sera lancé sur Ansible Tower , l'idempotence est un must. Nous convertirons le fichier en un modèle jinja2.

Dans la première ligne, nous ajoutons la commande suivante pour contrôler les espaces et les retraits :

#jinja2: lstrip_blocks: True, trim_blocks: True

Notez que les versions plus récentes du module de modèle incluent des paramètres pour trim_blocks (ajoutés dans Ansible 2.4).

Voici le code qui insère la ligne d' inclusion à la fin du fichier:

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

Nous utilisons la construction conditionnelle ({% if%}, {% endif%}) pour la commande shell qui insère une ligne pour les hôtes dont les noms commencent par les caractères "hd". Nous utilisons des faits Ansible et un filtre [0: 2] pour analyser le nom d'hôte.

Passons maintenant aux tâches. Tout d'abord, vous devez établir un fait pour analyser le nom d'hôte. Nous utiliserons le fait «parhost» dans la construction conditionnelle.

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

Il n'y a pas de paramètre csum sur le serveur de stock RHEL. Si nécessaire, nous pouvons utiliser un autre fait pour indiquer conditionnellement le nom du fichier binaire avec la somme de contrôle. Veuillez noter qu'un code supplémentaire peut être requis si ces fonctionnalités sont différentes sous AIX, Solaris et Linux.

De plus, nous devons résoudre le problème avec les différences de groupes racine sous AIX et 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' }}"

L'utilisation de blocs nous permet de définir la condition pour l'ensemble de la tâche. Nous utiliserons la condition à la fin du bloc pour exclure les hôtes «la».

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

Le module modèle valide et installe le fichier. Nous fixons le résultat afin de pouvoir déterminer si la tâche a changé. L'utilisation du paramètre de validation dans ce module vous permet de vérifier la validité du nouveau fichier sudoer avant de le placer sur l'hôte.

##
# 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 un nouveau modèle a été installé, exécutez le script shell pour générer le fichier de somme de contrôle. La construction conditionnelle met à jour le fichier de somme de contrôle lors de l'installation du modèle sudoers, ou si le fichier de somme de contrôle est manquant. Étant donné que le processus en cours surveille également d'autres fichiers, nous utilisons le code shell fourni dans le script source:

- 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

Le module de fichiers vérifie l'installation des autorisations nécessaires:

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

Étant donné que le paramètre de sauvegarde ne fournit aucune option pour le traitement des sauvegardes précédentes, nous devrons nous occuper de créer le code approprié nous-mêmes. Dans l'exemple ci-dessous, nous utilisons pour cela le paramètre «register» et le champ «stdout_lines».

##
# 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 != ""

Achèvement du bloc:

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

Le cas d'utilisation prévu est de jouer ce rôle sur Ansible Tower. Les alertes Ansible Tower peuvent être configurées de sorte qu'en cas d'échec de l'exécution du travail, les alertes soient envoyées par e-mail, à Slack ou d'une autre manière. Ce rôle s'exécute dans Ansible, Ansible Engine ou Ansible Tower.

En conséquence, nous avons supprimé tout ce qui était superflu du script et créé un rôle complètement idempotent qui peut fournir l'état souhaité du fichier sudoers. L'utilisation de SCM permet un contrôle de version, fournit une gestion des changements et une transparence plus efficaces. Les CI / CD avec Jenkins ou d'autres outils vous permettent de configurer des tests automatisés du code Ansible pour les modifications futures. Le rôle d'auditeur dans Ansible Tower vous permet de surveiller et d'appliquer les exigences organisationnelles.

Le code de travail avec les sommes de contrôle pourrait être supprimé du script, mais pour cela, le client devrait d'abord consulter son service de sécurité. Si nécessaire, le modèle sudoers peut être protégé avec Ansible Vault. Enfin, l'utilisation de groupes évite d'écrire la logique en utilisant les inclusions et les exclusions.

→ Vous pouvez télécharger le rôle depuis GitHub sur ce lien

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


All Articles