Le blog de Genma
Vous êtes ici : Accueil » Informatique & Internet » GNU/Linux, Logiciels Libres » Rsync Checker petit script Python sans prétention

Rsync Checker petit script Python sans prétention

D 7 janvier 2019     H 09:00     A Genma     C 2 messages   Logo Tipee

TAGS : Planet Libre Sauvegarde Python

Après mon billet Borg Checker, petit script Python sans prétention, voici un autre billet d’un petit outil simple mais effiace, là encore en deux étapes.

Les besoins

Nous avons un Rsync qui se fait dans le sens "Machine distance" en source, "Machine locale" en cible, le tout à travers SSH, lancé avec sudo - pour avoir les droits root et donc aller où on veut et s’affranchir des problèmes de permission.

Nous aimerions valider que les commandes rsync exécutées sont valables /se sont bien déroulées / ne sont pas tombées en erreur. Sachant que nous avons un script shell global qui lance plusieurs scripts shells différents qui eux-même lancent plusieurs commandes Rsync au sein du même script.

SSH

Les connexions SSH se font depuis la machine Backup en tant que client du serveur SSH qui est sur la machine à sauvegarder.

La connexion se fait via une connexion par clef (la clef publique de la machine de sauvegarde, Backup, a été ajoutée sur la machine à sauvegarder.

Rsync avec Sudo à travers SSH

Pour pouvoir copier les fichiers en rsync avec sudo, sans avoir à saisir de mot de passe, il faut faire un sudo visudo ce qui va permettre d’éditer le fichier /etc/sudoers et d’autoriser le lancement de la commande sudo rsync sans avoir à saisir de mot de passe pour l’utilisateur désigné, ici Genma.

sudo visudo

On ajoute en bas de fichier la ligne

genma ALL= NOPASSWD:/usr/bin/rsync

Astuce pour enregister / quitter dans le cas où l’éditeur par défaut est VI :

:wq!

La sauvegarde via Rsync

Exemple de script lançant des commandes Rsync la nuit via une tâche Cron. On écrit des traces / des logs dans un fichier en local. Ce fichier de log permettra de valider l’exécution des commandes (cf section ultérieure dans le fichier).

#!/bin/bash

LOG=/Backup/Machine_distante_A/Sauvegarde_rsync_ssh_<span class="base64" title="PGNvZGUgY2xhc3M9InNwaXBfY29kZSBzcGlwX2NvZGVfaW5saW5lIiBkaXI9Imx0ciI+ZGF0ZSArJVktJW0tJWQ8L2NvZGU+"></span>.log

# Fonction qui permet d'écrire des logs dans le fichier de log 
# Le texte en paramètre de la fonction sera écrit à la suite de la date et heure
Inlog()
{
    echo <span class="base64" title="PGNvZGUgY2xhc3M9InNwaXBfY29kZSBzcGlwX2NvZGVfaW5saW5lIiBkaXI9Imx0ciI+ZGF0ZSArJyVZLSVtLSVkICVIOiVNOiVTJzwvY29kZT4="></span>": $1" >> $LOG
}

# La variable "$?" contient le code de retour de la dernière opération.
# Ici c'est le code d'execution de la commande "rsync". 
# "0" - la commande "rsync" s'executé correctement, 
# Autre valeur en cas échéant, c'est que l'on a donc une erreur
check_rsync()
{
  result_rsync=$(echo $?)
  # La variable "result_rsync" recupere la valeur de "$?"
  # Si "result_rsync" <>"0" alors il avait une erreur lors d'execution de la commande "rsync" et le script s'arrête
  if [ "$result_rsync" == "0" ]; then
      Inlog "$1 Rsync OK"
  else
      Inlog "$1 Rsync KO"
  fi
}

Inlog "Début Rsync Machine_distante_A /var_www sur Serveur_Sauvegarde"
rsync -avz --rsync-path="sudo rsync" -e "ssh  -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --ignore-existing genma@IP_Machine_Distante:/var/www /Backup/Machine_distante_A/var/www/
# Pour appeler la fonction de check du retour de la commande Rsync
check_rsync "Rsync Machine_distante_A /var_www sur Serveur_Sauvegarde"
Inlog "Fin Rsync Machine_distante_A /var_www sur Serveur_Sauvegarde"

Inlog "Début Rsync Machine_distante_A /data/sql sur Serveur_Sauvegarde"
rsync -avz --rsync-path="sudo rsync" -e "ssh  -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --ignore-existing genma@IP_Machine_Distante:/Backup/mysql/ /Backup/Machine_distante_A/data/sql/
# Pour appeler la fonction de check du retour de la commande Rsync
check_rsync "Rsync Machine_distante_A /data/sql sur Serveur_Sauvegarde"
Inlog "Fin Rsync Machine_distante_A /data/sql sur Serveur_Sauvegarde"

Un peu de Python

Un peu comme on avait plusieurs commandes Borg lancées et qu’on vérifiait que chacune d’elles était correctes (cf Borg Checker, petit script Python sans prétention), on vérifiera que les différents scripts et les différentes commandes Rsync se sont déroulées sans soucis.

Le besoin est donc de savoir quelles sont les Rsync qui ont posés soucis pour ensuite traiter manuellement ces erreurs / cas particuliers.

Pour ce faire, la machine de sauvegarde lance un script Python (via une tâche cron à une heure bien postérieure à celles du déroulement des sauvegardes).

Dans ce script Python, on vérifie la présence du fichier de log à la date du jour : cela valide qu’au moins la commande Cron a bien lancé le script Shell contenant les commandes rsync. Le nom du fichier de log est standardisé (cf script Shell ci-dessus) et se trouve dns un dossier. Le fichier est récupéré via la dernière commande de rsync dans un dossier local du serveur.

Le fichier de log étant présent, on parcourt le fichier de logs à la recherche d’une ligne KO et on affiche la dite ligne si besoin. Cf le script Shell et sa fonction check_rsync() qui teste le retour de la commande rsync.

**Fichier Config.ini** : contient les chemins vers les fichiers de logs de chaque machine sauvegardée.

Un script de sauvegarde Shell contenant des commandes rsync génère un fichier de log par exécution, fichier de log ayant dans son nom la date du jour.

[FichiersLogsRsyncSsh]
Machine_distante_A = /Backup/Machine_distante_A/
Machine_distante_B = /Backup/Machine_distante_B/

**Fichier CheckRsyncSSH.py**

#!/usr/bin/python
# -*-coding:Utf-8 -*
import configparser
import sys
import os.path
import datetime

# Initialisation des chemins
# On a un fichier avec
# * en clef : la sauvegarde à valider
# * en valeur : le chemin dans lequel on vérifie la présence d'un fichier de log
config = configparser.ConfigParser()
config.optionxform = str
config.read('./Config.ini')
configRsync = config['FichiersLogsRsyncSsh']

# Code de la fonction
def fctFichierLogRsynsSSH():
	print(" ")
	print(--------------------------------------------")
	print(" CHECK DES RSYNC QUOTIDIENS")
	print(" via un check de présence des fichiers logs")
	print("-------------------------------------------")

	now = datetime.datetime.now().strftime('%Y-%m-%d')

	# Vérification qu'on a bien un fichier de log à la date du jour
	for key,value in config.items('FichiersLogsRsyncSsh'):
		fichierAtrouver = key + "_" + now + ".log"
		found = 0;
		for fileName in os.listdir(value):
			if (fichierAtrouver == fileName):
				found = 1;
				break;
		if (found == 0):
			print(key + ': statut ' + '\x1b[6;31m' + 'KO' + '\x1b[0m' + ' Fichier absent : ' + fichierAtrouver + "!!!")
		if(found == 1):
			print(key + ': statut ' + '\x1b[6;32m' + 'OK' + '\x1b[0m' ' Fichier trouvé : ' + fichierAtrouver);

		# Vérification du contenu du fichier
		chemin = value + fichierAtrouver
		f = open(chemin,'r')
		lignes = f.readlines()
		f.close()
		ligneKO = 0;
		for ligne in lignes:
			# est ce que la ligne contient un KO
			# KO inscrit car le rsync a renvoyé une erreur (cf script Shell)
			# si oui, la ligne est KO
			if "KO" in ligne:
				# On s'arrête au 1er KO, vu qu'on ira voir le fichier en détail du coup
				ligneKO = 1;
				break
		if (ligneKO == 0):
			print(key + ': statut ' + '\x1b[6;32m' + 'OK' + '\x1b[0m' + ' Tous les Rsync sont OK.')
		if (ligneKO == 1):
			print(key + ': statut ' + '\x1b[6;31m' + 'KO' + '\x1b[0m' ' Au moins un rsync contient un KO !!!')
			print("La ligne concernée par un KO est : " +ligne)
		ligneKO = 0;
		found = 0;
		print (" ")
	return 0;
	
# Appel de la fonction principal
fctFichierLogRsynsSSH()

Pour le lancer le script :

python3 CheckRsyncSSH.py

Quel résultat ?

Si on lance le script manuellement, la sortie est donc sur la ligne de commande.

On aura donc un OK en vert ou un KO en rouge qui apparait dans le terminal ce qui permet de facilement distinguer / voir les lignes à analyser. Le Nom du fichier (et par conséquence de la sauvegarde) en échec apparaît. Il faut alors analyser ensuite plus finement en allant relancer le script global ou la commande rsync incriminée, en regardant les logs dans le terminal...

2 Messages

  • Sinon tu as https://www.nongnu.org/rdiff-backup/ qui est un Rsync incremental over SSH.

    Il te crée une copie exacte d’un répertoire accessible à tous moment et ensuite il va simplement créer un diff des changements et ne sauvegarder que ce diff. Ainsi tu peux à tout moment récupérer un fichier tel qu’il était lors de la dernière sauvegarde ou bien tel qu’il était il y a 5 sauvegarde avant.

    Perso je lance avec une tache cron (en root) 2 fois par jour, et j’ai un autre cron qui vire les backup plus vieux que 30 jours.


  • Une ou 2 petites remarques sur le code python :

    + il y a beaucoup de commentaires souvent les commentaires deviennent nécessaire lorsque le code manque d’expressivité. Maintenir des commentaires est beaucoup plus compliqué que du code, c’est pour ça qu’on s’en passe autant que possible.
    + Les commentaires comme «  code de la fonction  » ou «  Appel de la fonction principal  » sont a proscrire
    + Il y a 2 fois où tu cherche si un élément d’une liste valide une condition, pour cela tu utilise une boucle que tu casse avec break (ce qui est très proche de l’utilisation d’un goto). Tu gagnerais en lisibilité avec l’utilisation de la méthode any()*
    + la méthode any() te renverrais un booléen ce qui est un type bien plus explicite pour found ou ligneKO
    + l’utilisation de la structure if/then/else est largement plus expressive que 2 if à la suite et est bien plus simple à maintenir quand on fait évoluer le code (on ne se répète pas)
    + l’ouverture du fichier peu poser problème, il n’est pas fermé en cas de levé d’exception. En python on préférera utiliser utiliser le mot clef with qui permet une libération automatique des ressources
    + je ne vois pas à quoi sert le return 0 ni le dernier print " "
    + certaines lignes finissent par un ; d’autres pas
    + il manque le fameux if __name__ == '__main__': (http://sametmax.com/pourquoi-if-__name__-__main__-en-python)

    * : je retouche ton code en même temps que j’écris ce commentaire et je me rend compte maintenant que ce n’est pas du tout la méthode any() dont tu aurais besoin. Pour moi ça montre que le code n’est pas forcément si lisible, on a l’impression que ces 2 blocs sortent juste un booléens alors que pour le second ce n’est pas le cas. Donc :

    Pour la recherche du fichier de log, au lieu de lister les fichiers contenu dans le dossier, tu peut vérifier l’existence du fichier directement puisque tu connais son chemin complet : os.path.isfile(value + '/' + key + '_' + now + '.log'). Le code sera plus simple tout en étant plus efficace (si tu as beaucoup de fichier dans ce dossier ça peut être utile) et enfin plus fiable on vérifie qu’il s’agit bien d’un fichier (et pas d’un dossier par exemple)

    Pour la vérification du log en lui même, tu ne veux pas savoir s’il existe une erreur, mais lister les erreurs. Tu peux donc utiliser les compréhensions de liste : linesKO = [line for line in lines if 'KO' in line]. Cela te permet de lister toutes les erreurs et tu vérifie s’il existe des erreurs avec len(linesKO) == 0

    Voila tout ça pour faire quelques remarques si ça peut aider :) (moi en tout cas ça m’a amusé)