blog-image

Administration système - Automatisation

  • dHENRY
  • 05/07/2019
  • (Durée de lecture : 10 mn)

Pour déployer automatiquement des services, les admins systèmes utilisent des “scripts” permettant l’enchaînement d’opérations désirées.
Cela nécessite une culture, des recherches, de mettre en place des tests de bonne exécution, etc…
Aujourd’hui il existe plusieurs systèmes pemettant des créer des “recettes”.

Ces outils permettent d’industrialiser les opérations d’installations, de maintenances et ce sur plusieurs systèmes d’exploitations en même temps.

N’ayant dans mon job aucune utilité de ces outils, j’ai préféré en fabriquer un, “old school” tout en bash :) Cet outil n’a pas vocation d’être diffusé, les outils du marché sont bien évidemment bien plus perfectionnés et adaptés à des environnements hybrides.

Je suis parti d’un exemple simple fourni par “Ansible”, puis construit un outil adapté à la taille de mon infrastructure (exclusivement pour Debian).

L’exemple est disponible à cette page. Elle explique comment fabriquer votre premier playbook Ansible et concerne l’installation d’un service nginx, comprenant la mise en place d’un fichier index.html spécifique. Il s’agit par conséquent d’écrire ceci :

---
- name: Install nginx
    hosts: host.name.ip
    become: true

    tasks:
    - name: Add epel-release repo
    yum:
        name: epel-release
        state: present

    - name: Install nginx
    yum:
        name: nginx
        state: present

    - name: Insert Index Page
    template:
        src: index.html
        dest: /usr/share/nginx/html/index.html

    - name: Start NGiNX
    service:
        name: nginx
        state: started

Dans cet exemple, on peut voir la commande “yum” et en déduire que la commande “yum” sera utilisée en précisant la source “epl-release” :

    tasks:
      - name: Add epel-release repo
        yum:
          name: epel-release
          state: present

      - name: Install nginx
        yum:
          name: nginx
          state: present

Ce que je souhaite

Dans mon mon cas je vais utiliser des commandes natives, vérifier que chaque commande ait bien été exécutée, créer une arborescence de “Capitalisation” en vue de réutiliser les scripts. Le gros problème des “shells” est la répétition du code, à cause des tests de bonne exécution que l’on doit effectuer après chaque commande.

Je souhaite :

  • Exécuter des commandes systèmes, sur le serveur local ou sur un des serveur(s) distant(s) au travers d’une communication SSH (authentification par échange de clés)
  • Ajouter des commentaires (important)
  • Copier des fichiers (cp pour le local, scp pour les serveurs distant via SSH, pour simplifier j’utiliserai toujours la commande scp, dont le comportement est similaire à la commande cp pour une utilisation locale)
  • Effectuer des remplacements de chaînes dans un fichier (sed ou awk)
  • Permettre la capitalisation de ces enchaînements de commandes (espace de stockage, organisation, métadonnées)
  • ….

Traduction du script Ansible

Je pars du script Ansible présenté ci-avant et je le transforme d’une manière très basique en instructions shell ( pour DEBIAN). Ceci va nous donner un shell :

Fichier “./script” :

apt update
apt install nginx
scp index.html /usr/share/nginx/html/index.html
systemctl start nginx

Chacune des commandes indiquées vont s’enchaîner sans jamais savoir si la précédente s’est bien terminée, sa bonne exécution complète n’est donc pas assurée.

Commentaires

Ajoutons quelques commentaires :

# Mise à jour de la liste des applications DEBIAN disponible
apt update
# Installation du service nginx
apt install nginx
# copie du fichier interne installé par défaut
# sur toutes les nouvelles installation de nginx afin de vérifier par automate qu'il fonctionne
scp index.html /usr/share/nginx/html/index.html
# Démarrage du service nginx
systemctl start nginx

Construction d’un interpréteur

Je vais en tout premier lieu fabriquer un programme qui va lire le contenu du fichier “script” et exécuter les commandes indiquées, une à une, en vérifiant que chacune des commandes ait été correctement exécutée.

Récupérer la liste des commandes dans l’ordre et exécution

Le POC se fera tout simplement avec le langage bash. Le nom du script : “execute.sh

#!/bin/bash
while IFS= read -r line; do
        #Ne traite pas les commentaires (démarre par space+++# ou bien #
        # Comparaison avec l'utilisation des expressions régulières
        if [[ $line =~ ^[[:space:]]*# ]];then
            continue
        fi
        #Execution de la commande
        $line
done < ./script

Enregistrez le fichier, puis : chmod 755 execute.sh qui permet de rendre le shell exécutable, et exécution du shell : ./execute.sh

Les commandes s’exécutent une à une… Comme j’utilise un compte “non admin”, j’obtiens inévitablement une erreur !!!

Introduire un test de bon exécution

#!/bin/bash
while IFS= read -r line; do
    #Ne traite pas les commentaires (démarre par space+++# ou bien #
    if [[ $line =~ ^[[:space:]]*# ]];then
        continue
    fi
    # Execution de la commande
    $line
    # Récupération code Erreur
    ERR=$?
    if [ ! "$ERR" = "0" ];then
        echo ""
        echo "---------------"
        echo "[**ERR] - Une erreur s'est produite  - [Commande :$line - code erreur: $ERR]"
        echo "---------------"
        echo ""
        # Arrêt du processus, code de sortie = dernier code Erreur intercépté
        exit $ERR
    fi
done < ./script

Enregistrer, puis exécuter le shell : ./execute.sh
Le script s’arrête maintenant à la première erreur rencontrée. Pour exécuter ce type d’opération, vous devez disposer des privilièges “root”. Vous pouvez donc exécuter l’opération en préfixant la commande par : “sudo” et paramétrer votre compte pour ce type d’utilisation, ou bien vous connectez “root”.

Exécution en mode “root”

Connexion root à la console, ou bien utilisation de la commande “sudo” (nécessite un paramétrage système pour votre compte)

**Need to get 1,588 kB of archives.
After this operation, 2,865 kB of additional disk space will be used.
Do you want to continue? [Y/n] Abort.
[**ERR] - Une erreur s’est produite - [Commande :apt install nginx - code erreur: 1]**

Une erreur se produit car la commande apt attend une réponse utilisateur : Y | N pour respectivement “Yes”, “No”. Je souhaite installer quoiqu’il arrive, je vais donc indiquer le paramètre “-y” à la commande “apt install nginx” dans le fichier script

# Mise à jour du contenu des dépôts DEBIAN pour le système
apt update
# Installation du service nginx
apt -y install nginx
# copie du fichier interne installé par défaut sur toutes les nouvelles installation de nginx afin de vérifier par automate qu'il fonctionne
scp index.html /usr/share/nginx/html/index.html
# Démarrage du service nginx
systemctl start nginx

Enregistrer, puis exécuter le shell : ./execute.sh

**…
Setting up nginx-full (1.10.3-1+deb9u2) …
Setting up nginx (1.10.3-1+deb9u2) …
cp: cannot stat ‘index.html’: No such file or directory
[**ERR] - Une erreur s’est produite - [Commande :cp index.html /usr/share/nginx - code erreur: 1]**

Le script demande de copier un fichier index.html vers la destination /usr/share/nginx/html/index.html, mais ce fichier n’existe pas. Je vais donc prévoir et dans l’esprit “Package” un répertoire pour les ressources et permettre ainsi de conserver le contexte d’exécution de cet automate. On pourrais imaginer une structure tel que :

–[Nom du projet d’automatisation] file : script Directory : ressources file : ressource1.txt file : ressource2.txt file : ….

mais je ne vais pas changer le contenu du fichier “script” qui restera : scp index.html /usr/share/nginx/html/index.html et considérer que toutes les sources à copier sans préfixe (../, ./test/, /root/ etc) sont disponibles dans le répertoire “ressources” du projet. Vous êtes admin système, vous savez ce que vous réaliser, vous connaissez donc explicitement la destination du fichier sur l’hôte.
mkdir ressources
touch ressources/index.html
On a donc maintenant un fichier vide à copier

Modification du répertoire de travail de “execute.sh”

#!/bin/bash
# Répertoire des ressources
RESSOURCESDIR="./ressources"
# Nom du script d'automatisation à exécuter
SCRIPT="script"
#Déplacement dans le répertoire de travail "ressources" si existe
if [ -d "$RESSOURCESDIR" ];then
    SCRIPT="../$SCRIPT"
    cd "$RESSOURCESDIR"
else
    SCRIPT="./$SCRIPT"
fi
while IFS= read -r line; do
    #Ne traite pas les commentaires (démarre par space+++# ou bien #
    if [[ $line =~ ^[[:space:]]*# ]];then
        continue
    fi
    # Execution de la commande
    $line
    # Récupération code Erreur
    ERR=$?
    if [ ! "$ERR" = "0" ];then
        echo ""
        echo "---------------"         echo "[*ERR] - Une erreur s'est produite  - [Commande :$line - code erreur: $ERR]"
        echo "---------------"
        echo ""
        # Arrêt du processus, code de sortie = dernier code Erreur intercepté
        exit $ERR
    fi
done < $SCRIPT

Avertissement de fin d’exécution

J’ajoute une petit blabla à la fin de “execute.sh”, pour indiquer la fin et bonne exécution du script d’automatisation.

echo ""
echo "---------------"
echo "[*FINOK] - Le script d'automatisation s'est correctement exécuté"
echo "---------------"
echo ""
exit 0

Enregistrer, puis exécuter le shell : ./execute.sh
./execute.sh
Résultat obtenu :

    [*FINOK] - Le script d'automatisation s'est correctement exécuté

Avec un script basique écrit en bash, et une liste de tâches à exécuter sur un serveur local, la méthode fonctionne parfaitement. Vous remarquerez aussi que la commande scp se comporte comme la commande “cp” lors d’une exécution locale.

Exécution sur un hôte distant

ici on peut imaginer deux méthodes :

  • Passage de la liste des hosts en paramètres à la commande execute.sh
  • ou bien passage en paramètre à la commande execute.sh d’un fichier contenant la liste des serveurs à traiter.

On remarque que dans ces deux cas il faudra indiquer un ou plusieurs paramètres.

Passage d’un ou plusieurs hôtes en paramètres

Passer un hôte en paramètre revient à exécuter cette commande : execute.sh [nom hôte ou adresse IP] [nom hôte ou adresse IP]
Du fait de l’utilisation de ssh, il vous faudra copier votre clé ~/.ssh/idrsa.pub sur l’hôte distant avant de tenter une connexion ssh automatique. Si cette clé n’existe pas utiliser la commande ssh-keygen pour générer la paire cles privé/publique associée à votre compte utilisateur. Considérons que la clé est installé et que la connexion SSH ne demande pas de mot de passe. Dans cet exemple, l’hôte distant pour moi est l’adresse IP : 192.168.1.56
Exécuter la suite d’opération de manière automatisée va nécessiter de se connecter, par conséquent je vais adapter le shell **_execute.sh
**

Détection des paramètres hôte dans “execute.sh”

Je vais considérer qu’il y a toujours au moins un hôte dans les paramètres. Si la commande ne dispose pas de paramètre, alors l’hôte sera le serveur local soit “local”. Si je veux traiter plusieurs hôtes dont le serveur local, j’ajouterai “local” à la liste des serveurs. Sans paramètre le serveur local sera le seul serveur traité.

#Hôtes à traiter
HOSTS=""
#Detection des hosts
for PARAMHOST in "$@"
do
    HOSTS="$HOSTS $PARAMHOST"
done
#Aucun paramètre pas de host donc traitement local
if [ "$HOST" == '' ];then
    HOST="local"
fi

Après ce traitement j’aurai accès à la liste des hôtes à traiter. Il suffit d’itérer cette liste et d’exécuter les commandes à destination sur l’hôte. J’ajoute au passage quelques contrôles supplémentaires :

#!/bin/bash
PROCESSERRFILE=dirname $0
PROCESSERRFILE=$PROCESSERRFILE/log-install.log
touch $PROCESSERRFILE
#Répertoire des ressources
RESSOURCESDIR="./ressources"
#Paramétres de commande SSH
PARAMSSH="-o PasswordAuthentication=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no"
SSHCMD="ssh $PARAMSSH"
SCPCMD="scp $PARAMSSH"
#Nom du script d'automatisation à exécuter
SCRIPT="script"
#Hôtes à traiter
HOSTS=""
# Détection des hosts
for PARAMHOST in "$@"
do
    HOSTS="$HOSTS $PARAMHOST"
done
# Aucun paramètre pas de host donc traitement local
if [ "$HOSTS" == '' ];then
    HOSTS="local"
fi
# Erreurs détectées
ERREURS=""
# Déplacement dans le répertoire de travail "ressources" si existe
if [ -d "$RESSOURCESDIR" ];then
    SCRIPT="../$SCRIPT"
    cd "$RESSOURCESDIR"
else
    SCRIPT="./$SCRIPT"
fi
for SERVER in $HOSTS;
do
ERREURSERVER=""
AUTOMATE=()
while IFS= read -r LINE; do
        AUTOMATE+=("$LINE")
done < $SCRIPT
for LINE in "${AUTOMATE[@]}"
do
    #Ne traite pas les commentaires (démarrés par space+++# ou bien #
    if [[ $LINE =~ ^[[:space:]]# ]];then
            continue
    fi
    # Execution de la commande pour chacun des hosts - trim de la commande
    CMD=echo $LINE|xargs
    if [ $SERVER" != "local" ];then
        CMD="$SSHCMD root@$SERVER $CMD"
    fi
    # Altération de la commande scp pour ajouter l'hôte final
    if [[ $LINE =~ ^scp ]];then
        if [ "$SERVER" != "local" ];then
                CMD=""
                I=0
                for word in $LINE;
                do
                    # substitution de la commande scp par la commande interne
                    if [ $I -eq 0 ];then
                        CMD="$SCPCMD"
                    else
                        #Ajout des infos hosts dans la destination
                        if [ $I -gt 1 ];then
                            # exception si déjà mentionné
                            if [[ ! $word =~ @ ]];then
                                CMD="$CMD root@$SERVER:$word"
                            fi
                        else
                            CMD="$CMD $word"
                        fi
                fi
                I=$(($I+1))
                done
            fi
        fi
        echo "  [Exec : $LINE - CMD : $CMD]"
        $CMD
        # Récupération code Erreur
        ERR=$?
        if [ "$ERR" != "0" ];then
            echo ""
            echo "---------------"
            echo "[ERR] - Une erreur s'est produite  - [Commande :$LINE - code erreur: $ERR]"
            echo "     Commande réellement exécutée : $CMD"
            echo "---------------"
            echo ""
            # Arrêt du processus, code de sortie = dernier code Erreur intercepté
            ERREUR="$ERREUR###$SERVER###$LINE###$CMD###$ERR"
            ERREURSERVER=$ERR
            break
    fi
done
if [ "$ERREURSERVER" = "" ];then
echo ""
echo "---------------" echo "[FINOK] - Le script d'automatisation s'est correctement exécuté pour : $SERVER"
echo "---------------"
echo ""
fi
done
if [ "$ERREUR" != "" ];then
    echo "Erreurs detéctées : $ERREUR"
fi
exit 0

Et toujours à partir du fichier script, j’exécute execute.sh en fournissant une liste de plusieurs serveurs. Les opérations se déroulent normalement.

Conclusion

Comme dit au début c’est “old school” mais ca marche et me suffit dans le cadre de mon utilisation…

Aller plus loin

Ces premières lignes de code en Bash sont à l’origine du projet “Mytinydc-automation” [Cliquez ici pour accéder au Gitlab].

Licence de ce document : Creative Commons (CC BY-NC-ND 4.0)

CETTE DOCUMENTATION EST LIVRÉE “EN L’ÉTAT”, SANS GARANTIE D’AUCUNE SORTE ET DISTRIBUÉE DANS UN BUT ÉDUCATIF EXCLUSIVEMENT. L’AUTEUR, CONTRIBUTEURS DE CETTE DOCUMENTATION OU ©MYTINYDC.COM NE SAURAIENT EN AUCUN CAS ÊTRE TENUS RESPONSABLES DES DOMMAGES DIRECTS OU INDIRECTS POUVANT RÉSULTER DE L’APPLICATION DES PROCÉDURES MISES EN ŒUVRE DANS CETTE DOCUMENTATION, OU DE LA MAUVAISE INTERPRÉTATION DE CE DOCUMENT.