(**) Translated with www.DeepL.com/Translator
To automatically deploy services, system administrators use “scripts” to sequence desired operations.
This requires a culture, research, implementation of good execution tests, etc….
Today there are several systems for creating “recipes”.
These tools make it possible to industrialize installation and maintenance operations on several operating systems at the same time.
Not having in my job any use of these tools, I preferred to make one, “old school” while in bash :) This tool is not intended to be distributed, the tools on the market are obviously very sophisticated and adapted to hybrid environments.
I started from a simple example provided by “Ansible”, and built a tool adapted to the size of my infrastructure (exclusively for Debian).
The example is available on this page. It explains how to make your first Ansible playbook and concerns the installation of a nginx service, including the installation of a specific index.html file. It is therefore a question of writing this :
---
- 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
In this example, we can see the “yum” command and deduce that the “yum” command will be used by specifying the “epl-release” source:
tasks:
- name: Add epel-release repo
yum:
name: epel-release
state: present
- name: Install nginx
yum:
name: nginx
state: present
What I want
In my case I will use native commands, check that each command has been executed, create a “Capitalization” tree structure in order to reuse the scripts. The big problem with shells is the repetition of code, because of the good execution tests that must be performed after each order.
I wish:
- Execute system commands, on the local server or on remote server(s) through SSH communication (authentication by key exchange)
- Add comments (important)
- Copy files (cp for local, scp for remote servers via SSH, to simplify I will always use the scp command, whose behavior is similar to the cp command for local use)
- Perform string replacements in a file (sed or awk)
- Allow the capitalization of these order sequences (storage space, organization, metadata)
- ….
Translation of the script Ansible
I start from the Ansible script presented above and transform it in a very basic way into shell instructions ( for DEBIAN). This will give us a shell :
File “./script” :
apt update
apt install nginx
scp index.html /usr/share/nginx/html/index.html
systemctl start nginx
Each of the indicated commands will follow one another without ever knowing if the previous one ended well, so its complete correct execution is not guaranteed.
Comments
Add some comments :
# Update of the list of DEBIAN applications available
apt update
# Installation of the nginx service
apt install nginx
# copy of the internal file installed by default
# on all new installations of nginx to check by PLC that it works
scp index.html /usr/share/nginx/html/index.html
# Starting the nginx service
systemctl start nginx
Building an interpreter
First of all, I will make a program that will read the contents of the “script” file and execute the indicated commands, one by one, checking that each of the commands has been correctly executed.
Retrieve the list of commands in order and execution
The POC will simply be done with the Bash language. The name of the script : “execute.sh”
#!/bin/bash
while IFS= read -r line; do
# Does not process comments (starts with space++++# or #
# Comparison with the use of regular expressions
if [[ $line =~ ^[[:space:]]*# ]];then
continue
fi
# command execution
$line
done < ./script
Save the file, then: chmod 755 execute.sh which makes the shell executable, and execute the shell : ./execute.sh
The commands are executed one by one…. Since I use a “no admin” account, I inevitably get an error !!!
Introducing a test of good execution
#!/bin/bash
while IFS= read -r line; do
# Does not process comments (starts with space++++# or #
if [[ $line =~ ^[[:space:]]*# ]];then
continue
fi
# command execution
$line
# Retrieving error code
ERR=$?
if [ ! "$ERR" = "0" ];then
echo ""
echo "---------------"
echo "[**ERR] - Une erreur s'est produite - [Commande :$line - code erreur: $ERR]"
echo "---------------"
echo ""
# Process stop, exit code = last code Error intercepted
exit $ERR
fi
done < ./script
Save, then run the shell : ./execute.sh
The script now stops at the first error encountered. To perform this type of operation, you must have “root” privileges. You can therefore execute the operation by prefixing the command with: “sudo” and set up your account for this type of use, or connect to “root”.
Running in “root” mode
Root connection to the console, or use of the “sudo” command (requires system configuration for your account)
**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]**
An error occurs because the apt command is waiting for a user response : Y | N for respectively “Yes”, “No”. I want to install whatever happens, so I will indicate the parameter “-y” to the command “apt install nginx” in the script file
# Updating the content of DEBIAN repositories for the system
apt update
# Installation of the nginx service
apt -y install nginx
# copy of the internal file installed by default on all new installations of nginx in order to check by PLC that it works
scp index.html /usr/share/nginx/html/index.html
# Starting the nginx service
systemctl start nginx
Save, then run the 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]**
The script asks you to copy an index.html file to the destination /usr/share/nginx/html/index.html, but this file does not exist. I will therefore plan and in the spirit of “Package” a directory for resources and thus allow to keep the context of execution of this PLC. One could imagine a structure such as :
–[Name of the automation project] file : script Directory : ressources file : ressource1.txt file : ressource2.txt file : ….
but I’m not going to change the content of the “script” file that will remain: scp index.html /usr/share/nginx/html/html/index.html and consider that all sources to be copied without prefix (../, ./test/, /root/ etc) are available in the “resources” directory of the project. You are a system admin, you know what you are doing, so you explicitly know the destination of the file on the host.
mkdir ressources
touch ressources/index.html
So we now have an empty file to copy
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
End of execution warning
I add a little blabla at the end of “execute.sh”, to indicate the end and proper execution of the automation script.
echo ""
echo "---------------"
echo "[*FINOK] - Le script d'automatisation s'est correctement exécuté"
echo "---------------"
echo ""
exit 0
Save, then run the shell : ./execute.sh
./execute.sh
Result obtained :
[*FINOK] - Le script d'automatisation s'est correctement exécuté
With a basic script written in bash, and a list of tasks to be executed on a local server, the method works perfectly. You will also notice that the scp command behaves like the “cp” command when executing locally.
Running on a remote host
here we can imagine two methods:
- Passing from the list of hosts in parameters to the execute.sh command
- or pass as a parameter to the execute.sh command of a file containing the list of servers to be processed.
It should be noted that in both cases one or more parameters must be indicated.
Passing one or more hosts to parameters
Passing a host as a parameter is the same as executing this command: Execute.sh [host name or IP address] [host name or IP address]…
Because of the use of ssh, you will need to copy your ~/.ssh/idrsa.pub key to the remote host before attempting an automatic ssh connection. If this key does not exist, use the ssh-keygen command to generate the private/public key pair associated with your user account. Let us consider that the key is installed and that the SSH connection does not require a password. In this example, the remote host for me is the IP address: 192.168.1.1.56
Running the operation suite automatically will require you to connect, so I will adapt the shell **_execute.sh**
Detection of host parameters in “execute.sh”
I will consider that there is always at least one host in the parameters. If the command does not have a parameter, then the host will be the local server or “local”. If I want to process several hosts including the local server, I will add “local” to the list of servers. Without parameter the local server will be the only server processed.
#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
After this processing I will have access to the list of hosts to be processed. Simply iterate this list and execute the destination commands on the host. I’m adding a few more checks along the way :
#!/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
And still from the script file, I run execute.sh by providing a list of several servers. Operations are proceeding normally.
Conclusion
As said at the beginning it’s “old school” but it works and is enough for me in my use…
Going further
These first lines of code in Bash are at the origin of the “Mytinydc-automation” project [Click here to access the Gitlab (french language only)].
Document licence : Creative Commons (CC BY-NC-ND 4.0)
THIS DOCUMENTATION IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND AND DISTRIBUTED FOR EDUCATIONAL PURPOSES ONLY. THE AUTHOR, CONTRIBUTORS TO THIS DOCUMENTATION OR ©MYTINYDC.COM SHALL IN NO EVENT BE LIABLE FOR ANY DIRECT OR INDIRECT DAMAGE THAT MAY RESULT FROM THE APPLICATION OF THE PROCEDURES IMPLEMENTED IN THIS DOCUMENTATION, OR FROM THE INCORRECT INTERPRETATION OF THIS DOCUMENT.