• en: SysAdmin – Automation ()
  • 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/id_rsa.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.

    Poster un commentaire

    Désolé, la réponse à la question Captcha est incorrecte
    Merci. le message est dans le tuyau :)
    Désolé, il y a un problème dans le tuyau, veuillez réessayer plus tard :(
    Un champs est invalide

    Commentaires (non traduits)

    Pas de commentaires