#!/bin/bash #---------------------------------------------------------------------------------------------------- #~ Nom : rpi-backup #~ Description : Sauvegarde le contenu d'un Raspberry pi #~ Auteur : Etienne Girault #~ #~ Usage : [-h] [-D] [-V] [-t] [-q] [-o] [-b] [-v] [-c] #~ Usage : [-f [] #~ 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