scripts/rpi-backup.sh
2020-03-01 14:45:04 +01:00

468 lines
19 KiB
Bash
Executable File

#!/bin/bash
#----------------------------------------------------------------------------------------------------
#~ Nom : rpi-backup
#~ Description : Sauvegarde le contenu d'un Raspberry pi
#~ Auteur : Etienne Girault <etienne.girault@gmail.com>
#~
#~ Usage : [-h] [-D] [-V] [-t] [-q] [-o] [-b] [-v] [-c]
#~ Usage : [-f [<filename>]
#~ Usage : [-l "DIR1 DIR2 DIR3"...] or [-l DIR1,DIR2,DIR3...]
#~
#~ Options : -h, --help ; Affiche l'aide
#~ Options : -V, --version ; Affiche la version
#~ Options : -D, --debug ; Mode debug
#~ Options : -f, --file=[FILE] ; Détermine le chemin de la sauvegarde
#~ Options : -t, --test ; Mode test
#~ Options : -q, --quiet ; Mode silencieux
#~ Options : -l, --list ; Spécifie les répertoires à sauvegarder
#~ Options : -p, --print ; Affiche la taille et le chemin de la dernière sauvegarde
#~ Options : -b, --background ; Lancement du script en arrière-plan
#~ Options : -v, --view ; Affiche les derniers logs
#~ Options : -c, --configure ; Configure les variables du script
#----------------------------------------------------------------------------------------------------
# Déclaration des variables
#----------------------------------------------------------------------------------------------------
script_name="$(grep '^#~ Nom' "$0" | sed 's/^.*: //')"
script_version="$(grep '^#~ v' "$0" | tail -1 | sed -e 's/^#~ //' -e 's/ *\-.*$//')"
backup_user=backup
host_name=$(hostname -s)
# Répertoire où la sauvegarde sera déposée
backup_dir=/backup
# Nom et chemin de la sauvegarde
backup_name=backup_${host_name}_$(date +"%Y%m%d")
backup_path="${backup_dir}/${backup_name}"
# Répertoire où les listes des exports sera déposée
export_dir=${backup_dir}/export
# Localisation des fichiers config des repo git (dans un tableau pour prendre en compte '*')
git_configs=(/appli/source/*/.git/config)
# Répertoires depuis la racine à sauvegarder
saved_dir="appli/data backup/export boot etc home root seedbox srv"
# Répertoire d'archive
archive_dir=/data/backup
# Répertoire des anciennes archives
old_archive_dir=${archive_dir}/old
# Fichier de log
log_file=/var/log/${script_name}.log
# Variables configurables
configurable_vars="backup_user backup_dir export_dir archive_dir old_archive_dir saved_dir git_configs log_file"
#----------------------------------------------------------------------------------------------------
# Déclaraition des fonctions
#----------------------------------------------------------------------------------------------------
print_script_name_version() { echo "$script_name $script_version" && sed -n '/^#~ Version/,$p' "$0" | grep '^#~' | sed 's/^#~ //'; }
print_script_description() { grep '^#~ Description' "$0" | sed 's/^.*: //'; }
print_script_usage() { grep '^#~ Usage' "$0" | sed -e 's/^.*: //' -e '1 s/^/Usage: '"$script_name"' /' -e '2,$ s/^/'"$(printf '%*s' "$(echo "Usage: $script_name" | wc -c)")"'/'; }
print_script_options() { echo "Options" && grep '^#~ Options' "$0" | sed -e 's/^.*://' -e 's/^ --/ --/' | column -ts \;; }
print_script_help() { echo "$script_name $script_version" && print_script_description && print_script_usage && print_script_options; }
# Séquence de test avant le lancement du script
prelaunch_test() {
# Relance le script avec droits sudo si pas deja fait
[[ "$(whoami)" = root ]] || { sudo "$0" "$@"; exit; }
# Détection du mode interactif ou silencieux
[[ ! "$mode_quiet" && -t 0 && -t 1 ]] && mode_interactive=0
# Décalage des arguments en fonction du nombre d'options choisies
shift $((OPTIND-1))
# Si un argument reste en fin de chaîne, c'est qu'il y a un problème sur le nombre d'argument
[[ "$1" ]] && argument_error="$1"
# Si une erreur d'argument a été trouvée, l'affiche et sort du script
if [[ "$argument_error" ]]; then
msg_log 3 "Argument incompris: $argument_error"
exit 1
fi
# Test si l'utilisateur de backup existe, active le mode standalone sinon
if ! id "$backup_user" &>/dev/null; then
msg_log 3 "utilisateur $backup_user absent sur le système"
mode_standalone=0
fi
# Vérifie si un chemin de sauvegarde personnalisé a été demandé
custom_backup_path 1
# Test la présence du répertoire de sauvegarde (création si besoin)
test_directory "$backup_dir" 0
# Test la présence du répertoire d'export (création si besoin)
test_directory "$export_dir" 0
# Test la présence du répertoire d'archive, active le mode standalone sinon
test_directory "$archive_dir" || mode_standalone=0
test_directory "$old_archive_dir" 0
# Test de la présence des répertoires à sauvegarder, les supprime de la liste si absent
for directory in $saved_dir; do
test_directory "/${directory}" || saved_dir="${saved_dir/$directory/}"
done
# Si le dossier de log n'existe pas, active le flag no_logging
test_directory "$(dirname $log_file)" || no_logging=0
# Test des commandes d'export, affiche son absence si c'est le cas
test_command dpkg || msg_log 3 "l'outil dpkg est introuvable"
test_command pip2 || msg_log 3 "l'outil pip2 est introuvable"
test_command pip3 || msg_log 3 "l'outil pip3 est introuvable"
# Mode test pour dev
[[ "$mode_test" ]] && fonction_test "$@"
return 0
}
# Test la présence d'une commande, retourne 1 si elle n'existe pas
# $1 : Commande testée
test_command() { type "$1" &>/dev/null && return 0 || return 1; }
# Test la présence d'un réperoire, la possiblité de le créer, retourne 1 si pas présent/créé
# $1 : Répertoire testé
# [$2] : Si initialisée, le répertoire sera créé si possible
test_directory() {
# si le répertoire n'existe pas
[[ -d "$1" ]] && return
# si $2 est initialisée et si le répertoire parent existe
if [[ "$2" && -d "$(dirname $1)" ]]; then
# demande de creation du répertoire
if ask_question "Répertoire $1 absent, dois-je le créer ?"; then
msg_log 1 "Création du répertoire $1"
msg_log 2 "mkdir $1"
return 0
# si l'utilisateur répond non, retourne 1
else return 1; fi
# sinon, $2 n'est pas initialisée ou le répertoire parent n'existe pas, retourne 1
else return 1; fi
}
# Pose une question à l'utilisateur, si pas intéractif, la réponse est oui par défaut
# $1 : Question posée
ask_question() {
[[ ! "$mode_interactive" ]] && return 0
echo -e "$1 \c"
read -r -n 1 question_answer < /dev/tty
echo ""
case $question_answer in
o|O|y|Y) return 0;;
*) return 1;;
esac
}
# Affiche les étapes du script sur la sortie standard en mode interactif ou le fichier de log en mode silencieux
# $1 : Mode (0=message, 1=début de tâche, 2=fin de tache, 3=message sur le stderr, 4=debut/fin de script)
# $2 : Message à afficher ou code retour de la précédente commande
msg_log() {
# Si mode interactif, affichage des messages sur la console
if [[ "$mode_interactive" ]]; then
case $1 in
0) echo "$2";;
1) echo -e "${2} \c";;
2) if eval "$2" &> /dev/null; then
echo -e "\033[32;1mOK\033[0m"
else
echo -e "\033[31;1mErreur\033[0m"
fi;;
3) echo "$2" >&2;;
esac
# Sinon, mode non interactif, affichage des messages sur le fichier de log
else
[[ "$no_logging" ]] && return 0
format_date="$(date "+%h %d %T") ${host_name}:"
case $1 in
0|3) echo "$format_date $2" >> "$log_file";;
1) echo -e "$format_date ${2} \c" >> "$log_file";;
2) if eval "$2" &> /dev/null; then
echo -e "\033[32;1mOK\033[0m" >> "$log_file"
else
echo -e "\033[31;1mErreur\033[0m" >> "$log_file"
fi;;
4) echo "$format_date --- $2" >> "$log_file";;
esac
fi
}
# Export des listes de paquets/librairies/repos installés dans le dossier d'export
export_lists() {
msg_log 1 "Export des listes de paquets/librairies/repos"
# Export de la liste des paquets apt
test_command dpkg && dpkg --get-selections > "${export_dir}/apt.list"
# Export de la liste des librairies python2
test_command pip2 && pip2 freeze > "${export_dir}/pip2.list"
# Export de la liste des librairies python3
test_command pip3 && pip3 freeze > "${export_dir}/pip3.list"
# Extraction des URLs des repos github
ls ${git_configs[*]} >/dev/null 2>&1 && sed -n '/url/p' ${git_configs[*]} | sed 's/^.*= //' > "${export_dir}/appli.list"
msg_log 2
return 0
}
# Création de la sauvegarde
create_backup() {
# Suppression d'éventuel reliquat d'archive
[[ -f "$backup_path".tar ]] && rm -f "$backup_path".tar
# Création du fichier de sauvegarde et application du user de backup
touch "$backup_path".tar
[[ ! "$mode_standalone" ]] && chown "${backup_user}:${backup_user}" "$backup_path".tar
# Sauvegarde de la liste des répertoires dans backup_name.tar dans le backup_dir
cd / || exit
for directory in $saved_dir; do
msg_log 1 "Sauvegarde du dossier /$directory"
msg_log 2 'tar --append --warning=no-file-ignored --file "$backup_path".tar "$directory"'
done
msg_log 1 "Compression de la sauvegarde"
# Compression de l'archive tar en tar.gz
[[ -f "$backup_path".tar ]] && msg_log 2 'gzip -1 "$backup_path".tar'
[[ -f "$backup_path".tar.gz ]] && mv "$backup_path".t{ar.gz,gz}
return 0
}
# Supprime les précédentes sauvegardes du dossier de sauvegarde
clear_backup() {
[[ "$mode_standalone" ]] && return
msg_log 1 "Suppression des précédentes sauvegardes du dossier $backup_dir"
msg_log 2 'find "$backup_dir" -maxdepth 1 ! -name "$backup_name".tgz -user "$backup_user" -print0 | xargs -r0 rm'
return 0
}
# Rotations des sauvegardes réalisées par le user backup
rotate_backup() {
# Si le répertoire d'archive est injoignable, retour
[[ "$mode_standalone" ]] && return
msg_log 1 "Rotations des sauvegardes dans les archives $archive_dir"
for day in 01 10 20; do
# Déplace les sauvegardes de ce serveur datant du 01, 10 et 20 du mois, qui ont plus de 3 jours, du dossier archives vers les anciennes archives
su "$backup_user" -s /bin/bash -c "find $archive_dir -maxdepth 1 -name *${host_name}*${day}.tgz -type f -mtime +1 -print0 | xargs -r0 mv -t $old_archive_dir"
# Supprime les sauvegardes de ce serveur qui ont plus de 30 jours du dossier des anciennes archives
[[ ! "$day" = 01 ]] && su "$backup_user" -s /bin/bash -c "find $old_archive_dir -maxdepth 1 -name *${host_name}*${day}.tgz -mtime +30 -type f -print0 | xargs -r0 rm"
done
# Supprime les sauvegardes de ce serveur de plus de 5 jours du dossier d'archive
msg_log 2 'su "$backup_user" -s /bin/bash -c "find $archive_dir -maxdepth 1 -name *${host_name}* -type f -mtime +1 -print0 | xargs -r0 rm"'
msg_log 1 "Copie la sauvegarde $backup_name dans le dossier d'archive $archive_dir"
# Copie la sauvegarde dans le dossier d'archive
msg_log 2 'su "$backup_user" -s /bin/bash -c "cp $backup_path.tgz $archive_dir"'
return 0
}
test_nextarg() {
[[ "${1::1}" = "-" ]] && argument_error="$1"
return 0
}
# Spécifie au script un chemin de sauvegarde personnalisé
# $1 : Mode (0=Initialise la variable backup_path avec $2, 1=Vérifie l'état)
# $2 : valeur du backup_path demandé
custom_backup_path() {
# Si la fonction est appelé avec un argument
case $1 in
0) # $1 = 0, le backup_path est associé à $2
backup_path="$2"
# Le drapeau custom_backup_path_set est levé
custom_backup_path_set=0;;
1) # $1 = 1, on vérifie l'état des drapeaux, si custom_backup_path_flag pas levé, on retourne 0
[[ ! "$custom_backup_path_flag" ]] && return 0
# Si mode interactif
if [[ "$mode_interactive" ]]; then
# Si custom_backup_path_set n'est pas levé, on demande a l'utilisateur la valeur de backup_path
if [[ ! "$custom_backup_path_set" ]]; then
echo -e "Chemin de la sauvegarde ? \c"
read -r backup_path < /dev/tty;
fi
# Si mode interactif
# Tant que le répertoire de la valeur de backup_path n'existe pas, on redemande la valeur de backup_set à l'utilisateur
until test_directory "$(dirname $backup_path)"; do
echo -e "Chemin de sauvegarde invalide, réessaye :\c"
read -r backup_path < /dev/tty;
done
# Sinon, mode silencieux
else
# Si le drapeau custom_backup_path_set est levé
if [[ "$custom_backup_path_set" ]]; then
# Si le répertoire de la valeur de backup_path n'existe pas, affiche l'erreur sur les logs et quitte le script
test_directory "$(dirname $backup_path)" || { msg_log 3 "Le chemin de la sauvegarde $backup_path n'est pas valable"; exit 1;}
# Sinon, le drapeau custom_backup_path_set n'est pas levé, incompatible en mode silencieux, affiche l'erreur sur les logs et quitte le script
else
msg_log 3 "Le chemin de la sauvegarde $backup_path n'est pas spécifié"
exit 1;
fi
fi
# Si le backup_path est donné en relatif, transformation en chemin absolu
[[ ! "${backup_path::1}" = "/" ]] && backup_path="$(pwd)/$backup_path"
return 0;;
esac
# Le drapeau custom_backup_path_path est levé
custom_backup_path_flag=0
mode_standalone=0
return 0
}
# Sauvegarde les bases MySQL/MariaDB si le service est lancé
save_mysql() {
# Test si le service MySQL est démarré, retourne 0 sinon
systemctl is-active mysql --quiet || return 0
msg_log 1 "Sauvegarde des bases MySQL"
# Dump des bases de données dans le répertoire d'export
msg_log 2 'mysqldump --all-databases > "${export_dir}/mysql-dump.sql"'
return 0
}
# Affiche la taille et le nom de la dernière sauvegarde
print_backup_name() { ls -lth "$backup_dir"/*.tgz | head -1 | awk '{printf "%-6s %s\n", $5, $9}'; }
# Lance un éditeur de texte avec les variables configurables
configure_variable() {
mode_interactive=0
# Crée un fichier de configuration temporaire
conf_file=$(mktemp)
# Affiche chaque variable dans le fichier de configuration
for each_variable in $configurable_vars; do
sed -n "/^${each_variable}=/p" "$0" >> "$conf_file"
done
# Edite le fichier de configuration
vi "$conf_file"
# Changement de l'IFS pour la lecture du fichier de configuration
IFS='='
# Pour chaque ligne du fichier de configuration
while read -r the_variable new_value; do
# Vérifie si la variable écrite sur le fichier est modifiable
if echo "$configurable_vars" | grep -q "$the_variable"; then
# Si la valeur de la variable a été modifiée
if ! grep -q -F "$the_variable=$new_value" "$0"; then
# Demande confirmation à l'utilisateur pour modifier la variable
if ask_question "Modifier la variable '$the_variable=$new_value' ?"; then
# script_path=$(readlink -f "$0")
msg_log 1 "Modification de la variable '$the_variable'"
# Modifie la valeur de la variable directement dans le script
msg_log 2 'sed -i "s#^$the_variable=.*#$the_variable=$new_value#" "$(readlink -f $0)"'
fi
fi
else
msg_log 0 "Impossible de changer la variable '$the_variable'"
fi
done < "$conf_file"
# Supprime le fichier de configuration temporaire
rm "$conf_file"
}
# Affiche les derniers logs
view_log() {
# Cherche le motif '--- Début', le stock quand il le trouve et ajoute au stock, si
sed -n '/--- Début/h;/--- Début/!H;$!b;x;p' "$log_file"
}
# Fonction de test pour le developpement
fonction_test() {
sed -n '/url/p' ${git_configs[*]} | sed 's/^.*= //'
msg_log 0 "test ok"
exit 0
}
#----------------------------------------------------------------------------------------------------
# Début du script
#----------------------------------------------------------------------------------------------------
main() {
prelaunch_test "$@"
msg_log 4 "Début de la sauvegarde"
export_lists
save_mysql
create_backup
clear_backup
rotate_backup
msg_log 4 "Fin de la sauvegarde"
exit 0
}
#----------------------------------------------------------------------------------------------------
# Gestion des options
#----------------------------------------------------------------------------------------------------
while getopts ":-:hVDf:tql:pbvc" OPTION; do
case $OPTION in
h) print_script_help; exit;;
V) print_script_name_version; exit;;
D) set -x;;
f) test_nextarg "$OPTARG" && custom_backup_path 0 "$OPTARG";;
t) mode_test=0;;
q) mode_quiet=0;;
l) test_nextarg "$OPTARG" && saved_dir="${OPTARG//,/ }"; custom_backup_path;;
p) print_backup_name; exit;;
b) nohup "$script_name" "-q" "${@/-b/}" &> /dev/null & exit;;
v) view_log; exit;;
c) configure_variable; exit;;
:) [ "$OPTARG" = "f" ] && custom_backup_path;;
\?) echo "-$OPTARG: option invalide" && print_script_usage; exit;;
-) case $OPTARG in
help) print_script_help; exit;;
version) print_script_name_version; exit;;
debug) set -x;;
file) custom_backup_path 0 "$OPTARG";; # VERIF
test) mode_test=0;;
quiet) mode_quiet=0;;
list) saved_dir="${OPTARG//,/ }"; custom_backup_path;;
print) print_backup_name; exit;;
background) nohup "$script_name" "-q" "${@/-b/}" &> /dev/null & exit;;
view) view_log; exit;;
configure) configure_variable; exit;;
*) echo "--$OPTARG: option invalide" && print_script_usage; exit;;
esac ;;
esac
done
# Reception de signaux d'interruptions
trap 'exit 1' INT TERM
set -o errexit
main "$@"
#----------------------------------------------------------------------------------------------------
# Fin du script
#----------------------------------------------------------------------------------------------------
#~ Version changelog
#~ v0.1 - Création du script
#~ v0.2 - Sauvegarde totale de la carte SD (~25min)
#~ v0.2.1 - Sauvegarde uniquement les répertoires utiles à la restauration (~2min)
#~ v0.2.2 - Nommage de la sauvegarde en fonction de la date
#~ v0.2.3 - Ajout des exports des listes de paquets/librairies/URLs github
#~ v0.2.4 - Ajout de l'option file pour définir un chemin à la sauvegarde
#~ v0.3 - Réécriture du script suivant les best practice bash
#~ v0.4 - Ajout des fonctions test_command et test_directory
#~ v0.4.1 - Ajout de la fonction ask_question
#~ v0.4.2 - Ajout du mode verbeux et interactif
#~ v0.4.3 - Ajout du mode standalone et la creation des répertoires nécessaires au script
#~ v0.5 - Utilisable multi serveur
#~ v0.5.1 - Mode verbeux par défaut en interactif, ajout du mode silencieux
#~ v0.5.2 - Refonte de la fonction msg_log
#~ v0.5.3 - Ajout du logging en mode non interactif
#~ v0.6 - Ajout de l'option list pour spécifier les répertoires à sauvegarder, à associer à l'option file
#~ v0.6.1 - Ajout de la fonction custom_backup_path pour gérer les erreurs d'un backup_path personnalisé
#~ v0.6.2 - Ajout de la fonction test_nextarg pour gérer les problèmes d'argument
#~ v1.0 - Publication pour mise en fonctionnement
#~ v1.1 - Ajout de la sauvegarde des bases MySQL/MariaDB
#~ v1.1.1 - Modification de la fonction msg_log pour la prise en compte du code retour
#~ v1.2 - Ajout de l'option print pour l'affichage de la taille et du nom de la dernière sauvegarde
#~ v1.3 - Ajout de l'option background pour le lancement en arriere plan
#~ v1.4 - Ajout de l'option view pour le l'affichage des derniers logs
#~ v1.5 - Ajout de l'option configure pour la reconfiguration des variables
#~ v1.5.1 - Améliorations de synthaxe
#~ v1.5.2 - Correction de l'utilisation de la variable git_config
# TODO - Restauration