Script de sauvegarde rsync

Présentation d´un script shell personnel permettant de mettre en place une sauvegarde de données grâce au fameux outil rsync, via Internet, sur un schéma de snapshots. Autrement dit, d’images des données à un instant précis. Le script permet la rotation des sauvegardes et une distribution non linéaire des plus anciennes.

Le contexte

Pour stocker mes précieuses données numériques personnelles j’utilise un NAS, basé sur FreeNAS, dont on a vu tout le processus de configuration dans les deux pages dédiées ici et . Pour sauvegarder ces mêmes données dans un lieu physiquement séparé j’utilise un second NAS situé 500km plus loin. Entre les deux il y a … des lignes téléphoniques de particuliers, toutes simples, avec les limitations d’upload que cela implique : soit à peut près 100ko/s maximum.

Le système d’exploitation est FreeNAS, dérivé de FreeBSD, ce script est donc un script bash, il devrait tourner sans problème à priori sur tout système Unix, Linux et dérivés.

Le principe « snapshots-like », l’utilisation de liens en durs (on va en reparler), et la suppression partielle des anciennes sauvegardes sont inspirés des scripts et informations de François Scheurer, Mike Rubel et Michael Jakl.

Philosophie du script

Tous les possesseurs de Mac connaissent Time Machine, cet utilitaire de sauvegarde extrêmement bien conçu et à ma connaissance malheureusement sans équivalent sous Windows. Et bien en gros ce script de sauvegarde fonctionne de façon similaire, la jolie interface graphique en moins.

Le script crée quotidiennement une image des données sources sur la machine de destination, ainsi on a sur le NAS de sauvegarde un dossier par jour qui contient l´image complète de la source. Pour restaurer un fichier ou un dossier il suffit de choisir la date voulue et on retrouve toute l’arborescence telle qu’elle était à ce moment là. Les sauvegardes sont numérotées : 000 pour la plus récente puis 001 pour celle de la veille, 002 pour l’avant veille, etc…il y a donc une rotation quotidienne des plus anciennes en ajoutant 1 à tous les numéros de dossier. Le script maintient également un dossier « courante » qui est en fait un lien symbolique vers le dossier de la dernière sauvegarde valide.

Listage des sauvegardes distantesAprès un certain temps, avoir une sauvegarde par jour ne sert plus à grand chose, l’idée est donc de supprimer quelques une des plus anciennes sauvegardes en partant du principe que plus elle sont anciennes et moins on a de chance d’en avoir besoin. Donc plus ça va et plus on les espace : une tous les 10 jours, puis 30 jours, etc…

Le fonctionnement en détail

Structure générale

Je vous ai dit que l’on faisait une image des données sources par jour, alors évidemment on ne va pas transférer l’ensemble des données tous les jours. Rsync dispose d’une option magique (–link-dest= »reference ») qui compare les fichiers du dossier source avec le dossier « reference », si l’un est manquant il l’envoi dans la destination mais si le fichier existe dans la référence alors il créé simplement un lien en dur dans la destination vers celui ci. Ainsi pas de transfert de tout ce qui existe déjà !

Sauf que, problème ! Si un utilisateur renomme son dossier « Images » en « Photos », rsync ne le reconnaît pas dans le dossier de reference et on aura peut être 100, 300, 500Go ou plus à renvoyer vers la destination… à 100ko/s je vous laisse faire le calcul. :(Pour éviter ce problème de fichiers ou dossiers renommés, lié à la distance entre les NAS il existe un petit patch qui ajoute à rsync l’option –detect-renamed. –detect-renamed permet donc de repérer et d’éviter cela en utilisant simplement les données déjà à destination pour faire l’image du jour. Du coup je n’utilise pas l’option –link-dest.

Comme finalement on n’utilisera pas l’option link-dest, afin d’avoir les liens en durs vers les fichiers préexistant on va donc commencer par faire une copie complète avec des liens puis on mettra celle-ci à jour. Pour fonctionner correctement en cas de renommage de dossier ou de déplacement de fichier dans la source vers un nouveau dossier, detect-renamed doit avoir dans la destination l’arborescence identique à la source.

Si j’ajoute un contrôle d’unicité au lancement afin de ne pas se retrouver avec 2 scripts qui font la même chose en parallèle, ce qui collerai un gros bazar, ainsi qu’un retour d’erreur par mail à a fin, on obtient en gros cette structure :

#
# Y'a t'il un script précédent encore lancé ?
#	 -> si oui on le supprime
#
#Copie en liens durs de la sauvegarde "courante" vers le dossier 000
#Mise à jour de l'arborescence de la destination à partir de la source
#     -> si erreur dans la mise à jour, on stoppe, envoi mail
#Transfert des fichiers modifiés
#     -> si erreur dans le transfert, on stoppe, envoi mail
#
#Si tout c'est bien terminé on effectue la rotation : 000 devient 001, 001->002, etc...
#Création du lien pour dire que la nouvelle sauvegarde courante est la 001
#Suppression sélective de certaines sauvegardes anciennes
#Envoi de mail et enregistrement du log
#

A cela s’ajoute la contrainte du débit en upload limité, du coup en cas de gros volume à transférer il se pourrait qu’au bout de 24h la sauvegarde de la veille ne soit pas terminée. Pour traiter ce cas on ajoute un suffixe _incomplete au nom de dossier et on utilise cette sauvegarde comme nouvelle référence pour la suivante, ainsi le travail effectué n’est pas perdu. Le suffixe permettra à la fonction de suppression de ne pas garder la sauvegarde longtemps car elle n’a pas d’interêt.

De plus comme l’upload est limité et toujours en cas de gros volume il peut être contraignant de saturer la ligne téléphone avec la sauvegarde. J’utilise donc un autre patch de rsync : –stop-at. Il permet d’indiquer à rsync de se fermer à une heure prévue. Ainsi de minuit à 8h j’upload presque sans limitation, à 8h rsync se ferme, le script le détecte et relance une instance de rsync avec un débit limité à 50% environ jusqu’à 23h50.

Contrôle de l’unicité

Au lancement, le script créé un fichier « flag » qui sert à enregistrer les différents retours texte en cas de succès ou d’échec. A la fin, ce fichier est copié dans les logs puis supprimé. Le script teste donc si un fichier flag est déjà présent, trahissant ainsi soit une autre instance de script, soit un plantage ou arrêt système ayant empêché le script précédent de se terminer correctement. Dans le premier cas on ferme le rsync précédent, dans le second on supprime la sauvegarde précédente partiellement échouée et on reprend sur des bases saines.

Ceci est géré par cette partie du script :

#Si il y a déjà un fichier flag (anormal)=> la synchro de la veille n'est soit pas terminée (mauvaise programmation heure de fin) ou stoppée due à plantage ou arrêt système)
if [ -e $flag ]; then
	pkill -SIGUSR1 -f -u $USER_CIBLE rsync								#tente de fermer Rsync avec le signal SIGUSR1 (le signal est testé par le script de la veille et lui signal de se fermer également)
	sleep 120															#Tempo 120 secondes

	if [ -e $flag ]; then												#Re-teste présence fichier flag. Si oui c'est que le script précédent n etait plus lance => il y a eu plantage ou arret de la machine l hébergeant
		date_veille=$(head -n 1 $flag | cut -c15-)						#récupère la date sauvegarde interrompue (dans 1ere ligne du fichier flag)
		echo -e '\n!!!!!!!!!!\nScript du' $(date +%Y-%m-%d) >> $flag
		echo -e 'Suppression du dossier : 000_'$date_veille >> $flag
		rm -r $CIBLE/000_$date_veille >> $flag 2>&1						#supprime le dossier 000_ dans lequel la précédente sauvegarde a echoue
		envoi_mail 2													#mail sauvegarde non terminée cause arrêt NAS ou plantage script
		mv $flag $DOSS_SCRIPT/log/$date_veille-erreur$EXTENSION			#déplace le fichier de flag dans le dossier des logs et le renomme
	fi
fi

 

Transfert des fichiers, analyse du code retour

Pour s’assurer du bon déroulement des étapes critiques de la sauvegarde, à chaque exécution de rsync le code de retour est analysé. Parmis les codes remarquables citons :

  • 0 : code signalant que rsync s’est terminé normalement
  • 30 : signalant un arrêt sur timeout => peut être généré par l’option –stop-at, donc on s’en servira pour traiter le cas de l’arrêt programmé à heure fixée
  • 19 : signale un arrêt suite à reception du signal SIGUSER1. C’est ce signal que l’on envoi avec la commande pkill (voir l’extrait de code ci-dessus) si l’instance précédente n’est pas fermée. Le script précédent peut ainsi savoir qu’un nouveau est lancé.

Mise en place et paramétrage du script

Script complet

Voici le script au complet, j’ai essayé de largement commenter le tout, cela sera bien plus lisible en le collant dans un éditeur comme Notepad++ ou autre.

NOTE : Mise à jour du script le 14 octobre 2014 avec une correction d’un bug au niveau du dossier des transferts partiels

#!/bin/bash

#	Version 3.2 - sauvegarde_nuit
#
#Update depuis 3.1 :
#		- déplacement "tempRsync" (dossier des incomplets) hors arborescence sauvegardée pour le préserver du --delete-excluded
#		- suppression du dossier "tempRsync" en cas d'erreur lors de la synchro. Non touché en cas d'erreur autre (ex : connexion impossible)
#		- amélioration test après une fin par time-out pour mieux détecter un run-time terminé (sort souvent en 3eme ligne avant 
#fin de fichier mais parfois en 2ème ligne, donc ajout d'un "ou" dans le test
#		- Suppression de l'option de compression -z (compression désormait assurée au niveau de ssh)
#
#-------------------------------------------------------------------------------------------------------------------------------------------#
#	SYNCHRONISATION DISTANTE DES DONNES PAR RSYNC ENTRE 2 NAS																				#
#																																			#
#Créé un fichier "flag", témoin de son lancement, puis démarre une sauvegarde miroir depuis une machine distante vers celle qui l'héberge	#
#Lors du démarrage, il contrôle la présence du fichier flag, si celui ci est présent c'est qu'une synchro rsync est déjà en cours,			#
#on la stoppe avec le signal SIGUSR1 (code retour 19 de rsync). Ceci fermera également le script qui l'a lancé								#
#																																			#
#Le script lance ensuite un "dry run" pour tester la connexion et lister le volume de données à envoyer. Les stats sont envoyées dans le	#
#fichier de flag. Si tout s'est bien passé on lance la synchronisation réelle : d'abord mise à jour des dossiers puis des fichiers			#
#																																			#
#En cas d'erreur de rsync ou de sauvegarde de la veille non terminée, on envoie un mail pour alerter l'administrateur						#
#En cas de bonne exécution, le fichier flag est transformé en fichier log dans le dossier de la sauvegarde									#
#																																			#
#Le script utilise une version patchée de rsync incluant les options --detect-renamed pour ne pas retransférer des fichiers renommés ou 	#
#déplacés et --stop-at pour donner une heure d'arrêt de rsync. La version patchée de rsync doit être installée sur les 2 machines			#
#-------------------------------------------------------------------------------------------------------------------------------------------#

#VARIABLES ET CONFIGURATION
#**********************************************************************************

#Configuration :
LIMITATION_JOUR=1											#Mettre à 1 pour activer une limitation de la bande passante différente en journée, 0 = pas de différence
	DEBUT_JOUR="08:00"										#Heure de début de limitation pour le jour, format hh:mm impératif
	BP_LIMITE_JOUR=60										#Bande passante maximale en ko/s.   0 = pas de limite

FIN_SAUVEGARDE="23:50"										#Heure de fin de la sauvegarde, format hh:mm (avant l'heure de début de la suivante)
BP_LIMITE=100												#Bande passante maximale en ko/s.   0 = pas de limite

MAIL_SUCCES=0												#Mettre à 0 pour ne pas recevoir le rapport de sauvegarde par mail en cas de succès. Aucune influence sur les rapports d'erreur

DOSS_SCRIPT="/mnt/pool_sauvegarde/Scripts"					#Dossier abritant ce script
NOM_FLAG="flag"												#Nom du fichier "flag" (sans extension)
EXTENSION=".log"											#Extension à ajouter aux fichiers logs générés

DOSSIER_SOURCE="/mnt/Tank/"									#Dossier source à sauvegarder
NAS_DISTANT=xxx.xxx.xxx.xxx									#Adresse du serveur source
PORT=22														#Port du service ssh serveur source
USER="User"													#Nom d'utilisateur source
USER_MAIL="user@monmail.com"								#E-mail pour la reception des rapports

CIBLE="/mnt/pool_sauvegarde/Sauvegardes"					#Dossier de destination des sauvegardes (un dossier par date est créé ensuite à l'intérieur)
USER_CIBLE="UserSauvegarde"									#Nom d'utilisateur serveur destination

EXCLUS="/mnt/pool_sauvegarde/Scripts/liste.txt"				#Liste des fichiers et dossiers à exclure de la sauvegarde

#FIN DES VARIABLES ET DE LA CONFIGURATION, NE PAS MODIFIER PLUS BAS
#*******************************************************************************************************************************************

# La fonction envoi_mail gère l'ajout des textes explicatifs et l'envoi des mails aux utilisateurs
#
#Le mail envoyé dépend de l'argument passé à cette fonction :
#	1 = Synchro précédente non terminée
#	2 = Fin anormale de la synchro précédente (plantage script ou panne électrique)
#	3 = Erreur lors du premier rsync d'analyse
#	4 = Erreur lors du rsync de sauvegarde
#	5 = Erreur lors du rsync de mise à jour arborescence
#	6 = Le script a ete stoppé par une nouvelle occurence du script => mauvaise programmation des heures
#	
#	9 = Sauvegarde terminée avec succès
#--------------------------------------------------------------------------------------------------------------------
function envoi_mail()
{
	if [ $1 -eq 1 ]; then
			echo -e '\n*** ATTENTION ! ***' >> $flag
			echo -e 'cette sauvegarde distante n a pas pu être complétée dans les dernières 24h\n' >> $flag
			echo -e 'aide :' >> $flag
			echo -e 'number of files = nombre total de fichiers dans la source' >> $flag
			echo -e 'number of files transferred = nombre de fichiers qu il y avait à envoyer : absent -ou modifiés- de la destination' >> $flag
			echo -e 'total transferred size = Volume des données qu il y avait à envoyer\n' >> $flag
			mail -s 'NAS Caution: Sauvegarde distante non terminée' $USER_MAIL < $flag
		elif [ $1 -eq 2 ]; then
			echo -e '\n*** ATTENTION ! ***' >> $flag
			echo -e 'cette sauvegarde distante n a pas été terminée correctement' >> $flag
			echo -e 'Cela peut être du à un arrêt du NAS cible (hebergeant le script et les sauvegardes), une panne électrique ou un plantage du script\n' >> $flag
			echo -e 'aide :' >> $flag
			echo -e 'number of files = nombre total de fichiers dans la source' >> $flag
			echo -e 'number of files transferred = nombre de fichiers qu il y avait à envoyer : absent -ou modifiés- de la destination' >> $flag
			echo -e 'total transferred size = Volume des données qu il y avait à envoyer\n' >> $flag
			mail -s 'NAS Caution: Sauvegarde distante interrompue' $USER_MAIL < $flag
		elif [ $1 -eq 3 ]; then
			echo -e '\n ==> ERREUR RSYNC LORS DU DRY-RUN, ARRET DU SCRIPT, erreur Rsync n°'$retval >>$flag
			mail -s 'NAS Warning : ECHEC DE SAUVEGARDE DISTANTE LORS DE L ANALYSE' $USER_MAIL <$flag
		elif [ $1 -eq 4 ]; then
			echo -e '\n ==> ERREUR RSYNC LORS DE LA SYNCHRO, ARRET DU SCRIPT. Erreur n°'$retval >> $flag
			mail -s 'NAS Warning : ECHEC DE SAUVEGARDE DISTANTE' $USER_MAIL <$flag
		elif [ $1 -eq 5 ]; then
			echo -e '\n ==> ERREUR RSYNC LORS DE LA MISE A JOUR DE L ARBORESCENCE, ARRET DU SCRIPT. Erreur n°'$retval >> $flag
			mail -s 'NAS Warning : ECHEC DE SAUVEGARDE DISTANTE' $USER_MAIL <$flag
		elif [ $1 -eq 6 ]; then
			echo -e '\n*** ATTENTION ! ***' >> $flag
			echo -e 'Cette sauvegarde a été interrompue suite au demarrage d une autre sauvegarde' >> $flag
			echo -e 'VERIFIEZ LA CONFIGURATION DE L HEURE DE FIN QUI DOIT ETRE PROGRAMMEE AVANT LE DEBUT DE LA SUIVANTE' >> $flag
			mail -s 'NAS Warning : ECHEC SAUVEGARDE, VERIFIEZ LA PROGRAMMATION' $USER_MAIL <$flag
		elif [ $1 -eq 9 ]; then
			if [ $MAIL_SUCCES -ne 0 ]; then															#Envoie le mail si MAIL_SUCCES n'est pas 0 dans la conf
				echo 'Sauvegarde terminée avec succès'  >> $flag
				mail -s 'NAS : Sauvegarde distante réussie' $USER_MAIL <$flag
			fi
	fi
}

# La fonction copie_liens permet de créer au début de la sauvegarde l'image précedente en faisant une copie de la
#sauvegarde N-1 par des liens en durs, cette copie sera ensuite exploitée par rsync pour la mettre à jour.
# L'utilisation de ce système à la place de l'option --link-dest de rsync permet d'utiliser l'option --detect-renamed de
#rsync
#----------------------------------------------------------------------------------------------------------------------
function copie_liens()
{
cp -alL $CIBLE/courante $CIBLE/000_$date_jour 2>> $flag			#copie (liens durs) de la sauvegarde courante. Sert de base et est mise à jour ensuite
echo -e '\nCopie image courante terminée...' >> $flag
}

# La fonction lien_courante permet de mettre à jour le lien symbolique "courante" sur la sauvegarde distante
# qui pointe vers la sauvegarde actuelle (c'est l'image courante)
# La fonction reçoit un argument qui est le dossier à lier au dossier "courante"
#----------------------------------------------------------------------------------------------------------------------
function lien_courante()
{
rm -rf $CIBLE/courante 2>> $flag							#Suppression du dossier de la sauvegarde courante...
ln -s $CIBLE/$1 $CIBLE/courante 2>> $flag					#... et recréation (en fait un simple lien vers la sauvegarde du jour)
}

# La fonction rotation_sauvegardes permet d'incrémenter le numéro des dossiers contenant les sauvegardes en conservant la date, par
# exemple : 005_2012-12-12 est renommé en 006_2012-12-12. La sauvegarde numero 999 est supprimée.
# Cette fonction supprime également les sauvegardes incompletes (nom de dossier se terminant par _incomplete) à partir de la 25eme sauvegarde
#----------------------------------------------------------------------------------------------------------------------
function rotation_sauvegarde()
{
if ls $CIBLE/999_* > /dev/null 2>&1; then				#suppression de la sauvegarde numéro 999 si elle existe
	echo "suppression de la sauvegarde 999" >> $flag
	rm -rf $CIBLE/999_* 2>> $flag
fi
for i in {999..0}										#boucle de 999 à 0
do
	ichaine=$(printf "%.3d" $i)							#génère le numéro de sauvegarde sur 3 chiffres
	if [ $i -gt 25 ] && find $CIBLE/${ichaine}_*_incomplete -maxdepth 0 > /dev/null 2>&1; then	#à partir de la 25eme sauvegarde (et après) on teste si c'est une incomplète
		echo "suppression de la sauvegarde incomplète ${ichaine}" >> $flag
		rm -r $CIBLE/${ichaine}_*_incomplete 2>> $flag	#si c'est une incomplète, on la supprime
	fi
	if ls $CIBLE/${ichaine}_* > /dev/null 2>&1; then	#test de l'existence de la sauvegarde n
		for k in $CIBLE/${ichaine}_*; do				#si existe on récupère son nom complet (avec chemin et date)
			let j=$i+1									#génére le numéro n+1 sur 3 chiffres
			j=$(printf "%.3d" $j)
			k=${k#${CIBLE}/}							#supprime la partie chemin du nom de sauvegarde
			mv $CIBLE/$k $CIBLE/${j}${k#$ichaine} 2>> $flag		#renomme la sauvegarde n en n+1
		done
	fi
done
}

# La fonction suppression sauvegarde permet de supprimer les anciennes sauvegardes en en gardant 1 seule par interval défini. Elle reçoit
# 3 paramètres : une butée haute, une butée basse et un interval (dans cet ordre). La série entre les deux butées est divisées en intervals
# de la longueur définie par le paramètre interval puis une seule sauvegarde est conservée dans celui ci.
#Note : si la longueur de la série entre les butées n'est pas divisible par l'interval, le reste ne correspondant pas à un interval complet est
#ignoré (ex : de 100 à 75 par pas de 10 : on garde une sauvegarde entre 100 et 91, une entre 90 et 81 mais rien n'est supprimé entre 80 et 75
#-----------------------------------------------------------------------------------------------------------------------------------------------
function suppression_sauvegardes()
{
let start=$1							# initialise la première boucle avec la butée haute passée en paramètre
let stop=$start-$3+1					# initialise la première boucle avec le calcul de stop (start-pas+1)
let fin=$2								# récupère la butée basse en paramètre n°2

while [ $stop -ge $fin ]				# Cette boucle génère les bornes entre les butées hautes, basses et avec l'interval passé en paramètre
do	
	f=0									#initialise le flag
	for ((i=$start;i>=$stop;--i))		#boucle entre les bornes start - stop
	do
		ichaine=$(printf "%.3d" $i)
		if ls $CIBLE/${ichaine}_* > /dev/null 2>&1; then		#teste si on a une sauvegarde qui porte ce numéro
			if [ $f -eq 0 ]; then								#si on en a une on regarde si c'est la première
				let f=1											#si oui, on positionne le flag pour dire qu'on en a une
			else
				k=$(find $CIBLE/${ichaine}_* -maxdepth 0)
				echo "suppression de la sauvegarde "${k#${CIBLE}/} >> $flag	#si ce n'est pas la première, on la supprime
				rm -r $CIBLE/${k#${CIBLE}/} 2>> $flag
			fi
		fi
	done
	let start=$stop-1					#calculs prochaine bornes
	let stop=$start-$3+1
done
}

#La fonction finalisation permet d'effectuer les dernières opérations communes à la fin de la sauvegarde normale :
#Ces opérations sont la rotation des sauvegardes puis la mise à jour du lien vers la sauvegarde courante et enfin la 
#gestion de la suppression des plus anciennes sauvegardes selon une répartition spécifique et variable dans le temps
#-----------------------------------------------------------------------------------------------------------------------------------------------
function finalisation()
{
echo -e '\nSauvegarde terminée à '$(date +%H:%M:%S)'\nRotation des sauvegardes...'  >> $flag
rotation_sauvegarde																			#Effectue le renommage de toutes les sauvegardes n en n+1 (rotation)
echo 'Rotation terminée, effacement et recréation du dossier de sauvegarde courante...'  >> $flag
lien_courante 001_$date_jour																#Suppression du dossier de la sauvegarde courante et recréation (en fait un simple lien vers la sauvegarde du jour)
suppression_sauvegardes 1000 401 200														#Suppression de quelques anciennes sauvegardes avec une répartition variable selon l'ancienneté
suppression_sauvegardes 400 131 30
suppression_sauvegardes 130 41 10
envoi_mail 9
mv $flag $DOSS_SCRIPT/log/$date_jour$EXTENSION												#...déplace et renomme le fichier flag dans les logs
}

#La fonction finalisation_incomplete permet d'effectuer les dernières opérations communes à la fin d'une sauvegarde incomplete :
#renommage du dossier de sauvegarde avec ajout du suffixe "incomplete", rotation des sauvegardes puis la mise à jour du lien vers la sauvegarde courante
#-----------------------------------------------------------------------------------------------------------------------------------------------
function finalisation_incomplete()
{
echo -e '\nSauvegarde incomplete terminée à '$(date +%H:%M:%S)'\nRotation des sauvegardes...'  >> $flag
mv $CIBLE/000_$date_jour $CIBLE/000_${date_jour}_incomplete
rotation_sauvegarde
echo 'Rotation terminée, effacement et recréation du dossier de sauvegarde courante...'  >> $flag
lien_courante 001_${date_jour}_incomplete								#Mise à jour de la sauvegarde courante
envoi_mail 1
mv $flag $DOSS_SCRIPT/log/$date_jour-incomplete$EXTENSION				#déplacement fichier flag en log avec spécification incomplete
}

#La fonction finalisation_erreur permet d'effectuer les dernières opérations communes à la fin d'une sauvegarde qui se termine en erreur :
#renommage du dossier de sauvegarde avec ajout du suffixe "incomplete", rotation des sauvegardes puis la mise à jour du lien vers la sauvegarde courante
#-----------------------------------------------------------------------------------------------------------------------------------------------
function finalisation_erreur()
{
echo -e '\nErreur d execution à '$(date +%H:%M:%S)'\nSuppression du dossier 000_'$date_jour >> $flag
rm -r $CIBLE/000_$date_jour >> $flag 2>&1								#supprime le dossier 000 dans lequel on faisait la sauvegarde
if [ $1 -eq 4 ] && [ -e $DOSS_SCRIPT/tempRsync ]; then					#si erreur lors de la synchro (1er argument = 4) on supprime aussi le dossier
	echo -e '\nSuppression du dossier tempRsync' >> $flag				#des incomplets rsync
	rm -r $DOSS_SCRIPT/tempRsync
fi
envoi_mail $1															#envoi du mail selon le numéro d'erreur reçu en argument
mv $flag $DOSS_SCRIPT/log/$date_jour-erreur$EXTENSION					#déplace le fichier de flag dans le dossier des logs et le renomme
}

#PROGRAMME PRINCIPAL, contrôle l'état actuel, prépare le dossier de sauvegarde, lance une analyse (dry-run rsync), 
#puis copie l'arborescence des dossiers (sans supprimer les anciens) afin de permettre le bon fonctionnement de --detect-renamed
#en cas de fichiers déplacés ou dossiers renommés et enfin lance la sauvegarde effective des fichiers.
#Inclus le contrôle des codes retour des fonctions principales pour s'assurer de la bonne exécution.
#----------------------------------------------------------------------------------------------------------------------
flag=$DOSS_SCRIPT/$NOM_FLAG$EXTENSION									#définition du fichier de flag
SOURCE=$USER@$NAS_DISTANT:$DOSSIER_SOURCE								#et de la source
#Si il y a déjà un fichier flag (anormal)=> la synchro de la veille n'est soit pas terminée (mauvaise programmation heure de fin) ou stoppée due à plantage ou arrêt système)
if [ -e $flag ]; then
	pkill -SIGUSR1 -f -u $USER_CIBLE rsync								#tente de fermer Rsync avec le signal SIGUSR1 (le signal est testé par le script de la veille et lui signal de se fermer également)
	sleep 120															#Tempo 120 secondes

	if [ -e $flag ]; then												#Re-teste présence fichier flag. Si oui c'est que le script précédent n etait plus lance => il y a eu plantage ou arret de la machine l hébergeant
		date_veille=$(head -n 1 $flag | cut -c15-)						#récupère la date sauvegarde interrompue (dans 1ere ligne du fichier flag)
		echo -e '\n!!!!!!!!!!\nScript du' $(date +%Y-%m-%d) >> $flag
		echo -e 'Suppression du dossier : 000_'$date_veille >> $flag
		rm -r $CIBLE/000_$date_veille >> $flag 2>&1						#supprime le dossier 000_ dans lequel la précédente sauvegarde a echoue
		envoi_mail 2													#mail sauvegarde non terminée cause arrêt NAS ou plantage script
		mv $flag $DOSS_SCRIPT/log/$date_veille-erreur$EXTENSION			#déplace le fichier de flag dans le dossier des logs et le renomme
	fi
fi

date_jour=$(date +%Y-%m-%d)												#Enregistrement date du jour
#Effacement et reCréation du fichier de flag :
echo 'SAUVEGARDE DU '$date_jour > $flag									#NE PAS MODIFIER CETTE LIGNE

#Création du dossier de sauvegarde du jour : on recopie en liens l'image courante :
copie_liens

#on commence par un lancement de rsync en mode dry run pour lister dans le fichier de flag les fichiers à synchroniser (uniquement pour les statistiques)
echo -e 'Début analyse du volume à '$(date +%H:%M:%S) >> $flag
echo -e 'Rapport analyse, à synchroniser ce soir :\n----------------------------------------------' >> $flag
$DOSS_SCRIPT/rsync -ahzn -e "ssh -p "$PORT --stats --timeout=60 --delete --exclude-from=$EXCLUS $SOURCE $CIBLE/000_$date_jour >> $flag 2>&1
retval=$?																						#enregistre le code retour de rsync
if [ $retval -ne 0 ];	then																	#contrôle du code  retour
	#si erreur (clés ssh, connexion, serveur distant éteind, indisponible, ...) : on finalise et on quitte
	finalisation_erreur 3
	exit
fi

#Copie de l'arborescence des dossiers uniquement (ordre des option --includes/--exclude important, pas de --delete)
echo -e '\nDébut mise à jour arborescence à '$(date +%H:%M:%S) >> $flag
$DOSS_SCRIPT/rsync -ahz -e "ssh -p $PORT" --timeout=60 --exclude-from=$EXCLUS --include='*/' --exclude='*' $SOURCE $CIBLE/000_$date_jour >> $flag 2>&1
retval=$?																						#enregistre le code retour de rsync
if [ $retval -ne 0 ];	then																	#contrôle du code  retour
	#si erreur (clés ssh, connexion, serveur distant éteind, indisponible, ...) : on finalise et on quitte
	finalisation_erreur 5
	exit
fi
echo -e 'Mise à jour arborescence terminée à '$(date +%H:%M:%S) >> $flag

#Lancement de la synchro réelle :
if [ $LIMITATION_JOUR -ne 0 ]; then								#Si débit different pour le jour on règle l'heure de fin en conséquence
	heure_fin=$DEBUT_JOUR
	else
		heure_fin=$FIN_SAUVEGARDE
fi

echo -e '\n\nDébut de sauvegarde à '$(date +%H:%M:%S) >> $flag
echo -e 'Rapport de sauvegarde :\n----------------------------------------------' >> $flag
$DOSS_SCRIPT/rsync -ahz -e "ssh -p $PORT" --stats --delete --delete-excluded --detect-renamed --timeout=60 --bwlimit=$BP_LIMITE --stop-at=$heure_fin --partial --partial-dir="tempRsync" --exclude-from=$EXCLUS $SOURCE $CIBLE/000_$date_jour >> $flag 2>&1
retval=$?																					#enregistrement et contrôle du code retour

if [ $retval -eq 0 ]; then			#si rsync est quitté normalement, terminé avant l'heure maxi, déroulement normal...
	finalisation																			#on appelle la fonction de finalisation
	exit
elif [ $retval -eq 19 ]; then		#si rsync est quitté avec le code 19 (SIGUSR1) alors finalise et quitte le script car une nouvelle occurence s'est lancée...
	finalisation_erreur 6
	exit
elif [ $retval -eq 30 ]; then 		#si rsync est quitté avec un code 30 (timeout) on analyse si c'est l'heure de fin ou une autre erreur											
	if [ "$(tail -n -3 $flag | head -n 1)" = "run-time limit exceeded" ] || [ "$(tail -n -2 $flag | head -n 1)" = "run-time limit exceeded" ]; then				#lecture 3ème ou 2ème ligne en partant de la fin de flag, si c'est bien une fin par run-time terminé et...
		if [ $LIMITATION_JOUR -ne 0 ]; then													#...Si on a programmé un débit different pour la suite		
			echo -e '\nFin de sauvegarde de nuit à '$(date +%H:%M:%S) >> $flag				#on l'écrit et cela se poursuivra dessous
		else																				#Sinon la sauvegarde est terminée et elle est incomplete
			finalisation_incomplete															#on finalise (renommage, rotation, lien courante)
			exit
		fi
	else																					#si on a eu une erreur 30 mais que c'est inattendu, c'est un vrai timeout => erreur
		finalisation_erreur 4
		exit
	fi
elif [ $retval -ne 0 ]; then		#si on a une erreur autre, on finalise et on quitte
	finalisation_erreur 4
	exit
fi

#Lancement de la sauvegarde de jour si on a pas quitté le script au dessus
echo -e '\nDebut de la sauvegarde de jour' >> $flag
$DOSS_SCRIPT/rsync -ahz -e "ssh -p $PORT" --stats --delete --delete-excluded --detect-renamed --timeout=60 --bwlimit=$BP_LIMITE_JOUR --stop-at=$FIN_SAUVEGARDE --partial --partial-dir="tempRsync" --exclude-from=$EXCLUS $SOURCE $CIBLE/000_$date_jour >> $flag 2>&1
retval=$?

if [ $retval -eq 0 ]; then			#si rsync est quitté normalement, terminé avant l'heure maxi, déroulement normal...
	finalisation																			#on appelle la fonction de finalisation
	exit
elif [ $retval -eq 19 ]; then		#si rsync est quitté avec le code 19 (SIGUSR1) alors finalise et quitte le script car une nouvelle occurence s'est lancée...
	finalisation_erreur 6
	exit
elif [ $retval -eq 30 ]; then 		#si rsync est quitté avec un code 30 (timeout) on analyse si c'est l'heure de fin ou une autre erreur
	if [ "$(tail -n -3 $flag | head -n 1)" = "run-time limit exceeded" ] || [ "$(tail -n -2 $flag | head -n 1)" = "run-time limit exceeded" ]; then				#Si c'est bien l'heure de fin, c'est une incomplète
		finalisation_incomplete																#on finalise (renommage, rotation, lien courante)
		exit
	else																					#si on a eu une erreur 30 mais que c'est inattendu, c'est un vrai timeout => erreur
		finalisation_erreur 4
	fi
elif [ $retval -ne 0 ]; then		#si on a une erreur autre, on finalise et on quitte
	finalisation_erreur 4
	exit
fi

 

Le script est prévu pour être exécuté via une tâche cron une seule fois par jour et ce sur la machine hébergeant les sauvegardes, cela me semblais plus robuste pour la création des liens, la rotation et suppression des sauvegarde en cas de coupure internet entre les machines.

Ici il est sur un pool dédié contenant 2 dossiers : Scripts et Sauvegardes. Script héberge le script et les logs, Sauvegardes, l’ensemble des sauvegardes. Bien sûr vous pouvez modifier tout cela dans les paramètres.

Version patchée de rsync

ATTENTION : le script en l’état utilise 2 options de rsync qui n’existent pas dans la version « officielle » actuelle. Il s´agit de –detect-renamed et de –stop-at dont on a parlé précédemment.

Pour inclure ces patches il est nécessaire de compiler rsync vous même en les incluant. Pour cela les sources se situent sur https://rsync.samba.org/ftp/rsync/ dans le dossier /src. Ou plus simplement dans les ports FreeBSD.

Pour ceux qui utilisent FreeNAS en version 64 bits, vous pouvez télécharger ma version ici même.

 Paramétrage du script

L’ensemble des paramètres réglable est situé en tête du script (lignes surlignées en jaune ci-dessus) et sont normalement décrit. L’heure de fin est à mettre avant l’heure de début de la sauvegarde suivante (lancée par cron) pour un fonctionnement plus propre.

Ensuite évidemment il faut régler l’IP du serveur source, le nom d’utilisateur, le port et votre mail. Pour que tout fonctionne bien il faut avoir préalablement paramétré une authentification ssh par clé depuis le serveur distant hébergeant le script vers votre serveur local abritant les fichiers à sauvegarder. La connexion ssh doit avoir été ouverte au moins une fois pour ajouter la « key fingerprint » à la liste des serveurs connus.

La liste des exclus est un fichier contenant une liste des dossiers et des fichiers à ne pas envoyer vers la sauvegarde distante. Il suffit de faire précéder le nom des dossiers ou fichier par – pour les exclure comme sur cet exemple :

#
# Liste des dossiers / fichiers à exclure de la sauvegarde distante

# Exclusion des dossiers
– Jail
– Plugins
– Medias
– Scripts
– Sauvegarde_MID/T??l??chargements
– Sauvegarde_MID/zip et exe*
– Sauvegarde_AD/T??l??chargements
– Sauvegarde_MD

# Exclusion des fichiers et dossiers commençant par un point
#tels que .ssh et autres
– .*

ATTENTION : là encore un piège, il est indispensable d’avoir des sauts de ligne codés au format UNIX. Dans Notepad++ cela se fait avec Edition->Convertir les sauts de lignes.

Et voilà ! Normalement si tout ce passe bien vous allez recevoir un rapport quotidien (à condition d’avoir activé l’option). Celui ci contient les stats rsync du dry-run, s’exécutant donc AVANT l’envoi réel des fichier. Par conséquent quand on y lit « transfered file » il ne s’agit en fait pas des fichiers réellement transféré mais qu’il VA transférer. Si tout se termine en moins de 24h, c’est pareil, mais si vous avez trop de données à envoyer et que cela prend plus de 24h cela correspondra donc à ce qu’il reste à envoyer. Vous voyez la subtilité ?

N’hésitez pas à faire part de vos remarques ou suggestions, je suis preneur de tout ce qui peut l’améliorer car il n’est certainement pas parfait.

 


Vous avez aimé ? N'hésitez pas à partager :
Facebook Twitter Google+ Mail

5 réflexions sur « Script de sauvegarde rsync »

  1. Bonjour,

    Je trouve votre script super bien, par contre j’ai une difficulté, je ne sais pas comment on fait pour compiler rsync avec les options –detect-renamed et de –stop-at

    Est ce que vous auriez un tuto ?

    Pour infos je suis sur ubuntu.

    Merci d’avance pour votre aide

    1. Bonjour Olivier,

      Merci pour le compliment. J’en ai profité pour mettre en ligne une petite mise à jour du script, pas grand chose mais j’avais apporté des modifs alors autant les partager.

      Pour compiler sur Ubuntu c’est très simple : télécharge le code source de rsync (prend la dernière version tant qu’à faire !) et des patches associés à la version ici : https://rsync.samba.org/ftp/rsync/src/. Ce sont les fichiers .tar.gz
      Ensuite tu les décompresses où tu veux pour avoir un dossier rsync-3.1.1 contenant les sources et également le dossier « patches » avec… les patches !

      En ligne de commande, tu te met dans le dossier rsync et tu exécutes dans l’ordre:

      patch -p1 < patches/time-limit.diff patch -p1 < patches/detect-renammed.diff ./configure make

      Et voilà ! tu retrouves dans le dossier des sources l'exécutable "rsync" et si tu lances "./rsync --help" tu verras les nouvelles options dans la liste.

      Pour finir tu peux remplacer les rsync d'origine dans /usr/bin/rync par cette version patchée comme ça c'est cette version qui s’exécutera en tapant simplement rsync

      1. Bonjour Mickaël,

        Merci beaucoup pour ta réponse et pour ton partage, je vais essayer ce week-end les infos que tu me donnes.

        Pour infos j’utilise déjà le rsync pour sauvegarder les données de mon entreprise sur un serveur externe du mais ton code me semble tellement génial que j’ai hâte de le mettre en place.

        Au passage encore félicitation pour ton travail, étant également codeur en PHP si je peux un jour t’apporter mon aide ce serait avec un très grand plaisir.

        Cordialement,

      2. Bonjour
        Je suis nouveau sur Linux et je suis intéressé par votre script de sauvegarde.
        Je suis sur Ubuntu 16.04.2LTS.
        Or, j’ai lancé la procédure de compilation de rsync-3.1.2 avec les 2 patches. L’adjonction des 2 patches se passe bien, au détail près qu’il faut faire SUDO et qu’il y a une faute sur la 2ème commande « patch » : renamed ALD renammed.
        Par contre, pour la dernière étape « ./configure make », impossible d’y parvenir…
        Voici le message d’erreur obtenu :
        nicolas@bureau:~/rsync-3.1.2$ sudo ./configure make
        configure.sh: WARNING: you should use –build, –host, –target
        configure.sh: Configuring rsync 3.1.2
        checking build system type… Invalid configuration `make’: machine `make’ not recognized
        configure.sh: error: /bin/bash ./config.sub make failed
        nicolas@bureau:~/rsync-3.1.2$ ./rsync –help
        bash: ./rsync: Aucun fichier ou dossier de ce type

        Pouvez-vous m’aider à ce stade car l’esprit de votre script correspond exactement à ce que je recherche aujourd’hui ?
        Merci d’avance,
        Nicolas

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *