La Lanterne Rouge

Warning: Geek Inside

Synchronisation de dossiers "over SSH"

- Posted in Sans catégorie by

Je vais aborder aujourd'hui l'utilisation de deux softs assez sympathiques sous notre environnement préféré : j'ai nommé Linux. J'en profiterai bien sûr pour enrober cette présentation d'un contexte tiré de mon expérience personnelle.

Tout d'abord, qui dit synchronisation, dit pour tout administrateur réseau ou simple gestionnaire de plusieurs machines : rsync. Bravo vous aviez trouvé, vous êtes trop fort.

Le deuxième est probablement moins connu, mais s'avère être d'une aide précieuse : c'est incrond. Un écouteur d'événements inotify, permettant d'exécuter des commandes lors d'opérations sur le système de fichiers.

Mise à jour 11/07/2014 : je présente une version du système de capture de mouvement avec un Raspberry Pi et le logiciel motion ici

LanterneRouge.getPost().getContext()

Le rêve geekien

En bon geek qui se respecte, mon rêve secret est de pouvoir contrôler tous les objets à distance uniquement par la pensée. Comme les progrès dans ce domaine sont assez peu perceptibles depuis environ quelques centaines d'années, je me rabats sur des méthodes bien plus actuelles et basées sur les technologies de l'information (mais hélas, encore bien limitées).

Mon nouveau défi est ici somme toute assez simple, mais m'a demandé d'effectuer des recherches pour trouver les bons éléments permettant d'atteindre l'objectif fixé :

  • visualiser une webcam placée dans mon appart depuis une machine distante,
  • prendre des clichés lors de la détection de mouvements,
  • envoyer ces clichés directement sur mon serveur distant Sihaya (Ubuntu 10.04) comme sauvegarde

Le matériel (geekien aussi)

Webcam / logiciel de capture

Pour la webcam, ça sera une Logitech C510 à prix raisonnable pour une qualité raisonnable. Elle est reliée à mon PC principal sous Windows XP et la détection et diffusion des images se fait grâce à Yawcam que je recommande très très fortement pour toute utilisation similaire (capture et/ou diffusion).

Yawcam1 Yawcam2

Il est gratuit, efficace, et c'est un vrai bonheur de tomber là-dessus après avoir essayé 4 ou 5 autres logiciels bridés ou payants, dont les fonctionnalités sont en plus loin de celles de Yawcam. Bon, pour nuancer un peu cet avis, je tiens à dire qu'il est en Java, donc ne vous étonnez pas trop d'avoir 200Mo de RAM utilisés au bout de quelques jours d'exécution...

Faisons un rapide tour de ses fonctionnalités principales :

  • HTTP : une page simple affichant une capture de l'image au moment du chargement de la page
  • FTP : un cliché est pris à intervalle régulier (paramétrable) et envoyé par FTP à un serveur
  • File : idem que FTP, mais en local
  • Stream : une page affiche les images prises par la webcam en continu. On peut choisir un rafraichissement par Javascript (pas de plugin nécessaire) ou par une applet Java (qui n'a pas voulu fonctionner avec moi)
  • Motion : sauve une image en local à chaque détection de mouvement (on peut définir très finement la sensibilité et la zone à surveiller)
  • Planificateur : Toutes les fonctions précédentes peuvent en plus être programmées indépendamment selon le jour de la semaine. On choisit donc, à l'heure près, quelles sont celles que l'on veut automatiquement exécuter.

C'est surtout les deux dernières fonctionnalités qui m'intéressent ici, bien que les autres soient les bienvenues aussi. Le seul problème avec cette détection c'est qu'on est limité au système de fichier local. On va voir qu'on peut contourner ce problème.

Stockage local

La webcam, bien que branchée sur mon poste Windows, enregistre les clichés sur un disque réseau monté avec Samba. Ce disque n'est autre qu'un dossier partagé sur mon serveur Usul (Debian Squeeze) situé sur le même LAN. À partir du moment où on a nos fichiers JPG sur la machine Linux, on peut commencer à s'amuser avec.

Tout d'abord, une petite modification de la configuration d'Apache et voilà nos images issues de la détection de mouvement accessibles depuis un navigateur. Bon, ça c'est pas super secure quand même si on ne met pas de mot de passe, donc on restreint cet accès aux IP provenant du VPN (on peut aussi mettre une authentification basique par Apache).

Hmm... le FancyIndexing d'Apache est pas top. Si on installe Visión (EDIT 20/06/2013 : le script n'est plus disponible en ligne mais on peut avantageusement le remplacer par Single File PHP Gallery), on obtient en deux minutes une interface web propre et efficace pour visualiser nos images (avec pré-chargement en prime). J'ai quand même dû adapter un peu le script pour agrandir l'aperçu, épurer l'affichage, trier les fichiers par date décroissante et afficher le nom du fichier courant ainsi que sa position. Allez, disons qu'en 20 minutes c'était au poil.

Les clichés sont désormais stockés sur Usul, mais restent à transférer automatiquement vers Sihaya. Le contexte me semble planté, passons maintenant aux étapes qui m'ont permises d'arriver à une solution satisfaisante.

LanterneRouge.getPost().dispatchSolutions()

Pré-requis

Pas besoin de le répéter, on va utiliser rsync. La commande de base utilisant SSH est celle-ci :

# rsync -av -e ssh /dossier/source/ utilisateur@hote:/dossier/cible

Simple non ?

Attention à bien noter le caractère "/" final dans le chemin du dossier source. Il signifie qu'on souhaite synchroniser le contenu du dossier source uniquement. Autrement, celui-ci serait préalablement créé dans le dossier cible.

Cette commande envoie les nouveaux fichiers ou les fichiers modifiés vers la cible. Mais elle ne permet pas de supprimer les fichiers qui n'existent plus dans le dossier source. Si c'est ce que l'on veut, il faut ajouter un paramètre comme dans cette deuxième commande :

# rsync -av -e ssh --delete /dossier/source/ utilisateur@hote:/dossier/cible

Notre commande rsync est prête. Mais si on essaye de la lancer manuellement, on va remarquer qu'il faut saisir le mot de passe de notre utilisateur distant afin que la synchronisation s'exécute. Un peu normal quand même, on est en SSH ne l'oublions pas. Mais si on prévoit de lancer cette commande automatiquement comme on le verra plus loin, ça risque d'être sérieusement handicapant.

On va donc tout d'abord générer une paire de clés (DSA ou RSA, au choix) et envoyer la partie publique à l'utilisateur distant. Cela permettra à l'utilisateur local de se connecter avec le compte de l'utilisateur distant sans être obligé de fournir son mot de passe.

On va supposer que :

  • cette directive (authentification par clé publique) est activée du côté du serveur distant,
  • que tous les droits sont corrects au niveau des répertoires concernés (OpenSSH est très à cheval sur ce point; sinon, vérifier les logs, c'est très parlant)
  • qu'on aura pris la peine de créer des utilisateurs aux droits restreints, tant au niveau local que distant, afin d'avoir un niveau de sécurité acceptable

Résumé des commandes (sur la machine locale, Usul pour moi) :

# ssh-keygen -t rsa
# ssh-copy-id -i ~/.ssh/id_rsa.pub utilisateur@hote

(pour les détails, la page d'ubuntu-fr peut aider, mais il y en a plein d'autres)

À partir de maintenant, l'utilisateur local n'aura plus besoin de rentrer de mot de passe pour se connecter au compte de l'utilisateur distant. Si on relance la commande rsync précédente, la synchronisation s'effectuera directement. Bon point !

Il nous reste à l'automatiser afin de ne plus être dépendant de ses petits doigts boudinés...

1ère solution : appelez-moi Bourrin

Dans la famille Bourrin, on fait pas dans la finesse vous vous en doutez. Alors depuis huit générations, on élève nos scripts au CRON, ça leur fait un poil brillant et ça améliore leur performances.

La solution est donc : on met en place un job CRON qui va tourner... pff... allez, à la louche on va dire toutes les 5 minutes.

Examinons maintenant les avantages et inconvénients :

(+) Ça marche et ça nous a pris 30 secondes

(-) C'est toutes les 5 minutes (donc maximum 4'59 d'attente pour le rafraîchissement)

(-) Ça fait beaucoup de connexions inutiles dans le lot

(-) Si on veut avoir une planification fine (selon le jour de la semaine), ça devient vite le b... une maison close

J'ajouterais un point quand on décide de recevoir un rapport par mail à chaque exécution de job :

(-) Autant de mails de rapport que d'exécutions (donc 288 par jour; j'ai pas autant de spams donc c'est clairement excessif)

Le bilan est plutôt... comment dire... disons qu'au gouvernement ils appelleraient ça "mitigé". Mais personne n'est dupe (si seulement...) et c'est clairement négatif. C'est pas grave, c'est de ses erreurs qu'on apprend. (qu'on apprend à ne plus les refaire hein, on n'est pas au gouvernement là)

2ème solution : appelez-moi Apprenti

On rentre ici dans la finesse, toute relative, mais finesse quand même. Et pour cela on va utiliser un outil/daemon fort sympathique : incrond et son accolyte incrontab. Je suppose qu'ils sont installés et que le fichier de configuration (/etc/incron.allow) autorise l'utilisateur local que nous avons choisi à créer sa table d'écouteurs sur inotify.

Tout d'abord, notre commande rsync actuelle est un peu longue et on verra après qu'elle sera vite complétée par quelques structures de contrôle et décorations diverses. On va donc l'intégrer à un script (j'ai choisi Bash) qui sera plus facile à appeler. On suppose maintenant que notre commande est contenue dans le script rsync-now.sh (son contenu final sera présenté plus loin).

Nous souhaitons lancer la synchronisation lorsqu'un fichier a été créé dans le dossier source ou supprimé. En regardant la page du manuel d'incrontab on voit qu'il faut utiliser les deux événements suivants :

  • IN_CLOSE_WRITE pour les fichiers écrits puis fermés (donc créés dans notre cas)
  • IN_DELETE pour les fichiers supprimés

On lance alors l'éditeur d'incrontab :

# incrontab -e

puis on ajoute la ligne qui va permettre d'exécuter notre script :

/dossier/source IN_DELETE,IN_CLOSE_WRITE /dossier/du/script/rsync-now.sh

(attention, les deux séparateurs doivent impérativement être des espaces) On sauve et on ferme, puis on vérifie que la table a bien été mise à jour :

# incrontab -l

La ligne ajoutée depuis l'éditeur devrait être affichée en réponse.

Dès à présent, la modification d'un fichier dans le dossier source ou sa suppression va automatiquement exécuter notre script, qui va à son tour exécuter notre commande rsync, qui va finalement envoyer les nouveaux fichiers vers notre machine distante par SSH, et supprimer de celle-ci ceux qui n'existent plus sur le poste local.

Cela semble idéal. "Semble" uniquement, car faisons un point sur les avantages et inconvénients de cette méthode :

(+) La synchronisation est immédiate dès la modification du contenu du dossier

(+) Exécution uniquement si nécessaire (donc charge, connexions et mails de rapport fortement diminués)

(-) Si on a plusieurs créations/suppressions d'affilé, on lance autant de rsync en parallèle qui se marchent dessus

(-) L'envoi de mails de rapport n'est plus automatique : il nous faut créer un script pour le gérer "manuellement" (inconvénient très relatif cela dit)

Le premier point négatif est assez critique. J'ai en effet un job CRON qui tourne tous les jours à 01:00 et qui supprime les fichiers datés de plus de 7 jours. Comme j'ai environ 5 à 20 faux positifs chaque jour (changement de lumière, nuages, etc.), cela signifie que 7 jours plus tard, autant de fichiers seront supprimés simultanément, et donc qu'autant de processus rsync seront lancés en parallèle.

Même si ces exécutions parallèles ne posent – a priori – pas de problèmes critiques (type corruption de données), le système reste quand même assez crade. Surtout que cela pourrait être aisément évité, sans rien perdre au niveau de l'efficacité.

2ème solution (améliorée) : appelez-moi Seigneur

On garde le principe mais on ajoute une "sur-finesse" en utilisant un fichier lock qui va conditionner l'exécution de la commande rsync. Le principe de base d'un fichier lock est d'empêcher plusieurs processus d'accéder simultanément à une ressource : si le fichier lock existe, le processus s'interrompt, s'il n'existe pas, le processus le crée avant de continuer son traitement, puis le supprime à la fin (principe éculé du sémaphore).

Ici, on va "doper" un peu notre fichier lock en se servant de lui comme canal de communication entre nos différents processus exécutant nos scripts. Au lieu d'avoir un fichier vide, il contiendra le PID du dernier processus lancé. Chaque instance du script va donc commencer par vérifier l'existence du fichier lock. Si le fichier existe, il va remplacer son contenu par son PID, puis se terminer.

Le premier processus (celui qui crée le lock) ne va pas exécuter de suite sa commande. À la place, il va attendre une durée arbitraire (j'ai choisi 2 secondes), puis au bout de ce temps, va lire le fichier lock et comparer la valeur obtenue et celle de référence (son propre PID).

  • Si ces valeurs sont égales, c'est qu'aucune autre instance du script ne s'est exécutée entre temps, et qu'il est donc probable que l'opération sur le système de fichiers ne concernait qu'un seul fichier. La synchronisation peut donc démarrer.
  • Dans le cas inverse, c'est que plusieurs fichiers ont été traités simultanément (et que par conséquent, plusieurs instances du script ont été exécutées, chacune ayant écrit son PID dans le fichier), et qu'il est préférable d'attendre encore un peu avant d'exécuter la synchronisation. On va simplement remplacer notre PID de référence (le nôtre, contenu dans une variable locale), par celui que l'on vient de lire, puis patienter encore 2 secondes et retester la valeur contenue dans le fichier. Et ainsi de suite jusqu'à ce que la valeur de référence soit égale à celle relevée dans le fichier lock.

De cette manière, on garantit que seul un processus de notre script pourra exécuter la synchronisation à un instant t, et qu'il ne risque plus d'y avoir deux rsync qui se "marchent dessus".

Je tiens à faire remarquer que je ne prétends pas avoir inventé ici quoi que ce soit et que je suis prêt à refuser le Nobel d'informatique (s'il existe un jour) si on me le proposait (par pure modestie bien sûr, ha ha). J'expose juste une manière qui m'a semblé élégante pour optimiser un processus automatisé, et qui pourrait très bien resservir à quelqu'un, voire simplement me resservir plus tard. Car voyez-vous, ceci est un peu mon carnet de notes, et ma mémoire – ressemblant souvent plus à de la RAM qu'on priverait d'électricité toutes les nuits, qu'à une bonne vieille bande magnétique – ne s'en sortirait pas sans ce type de notes sur "mémoire de masse" (c'est toi la masse).

Mais reprenons notre liste avantages/inconvénients pour cette méthode 2bis :

(+) Comme la précédente (je vais pas tout réécrire hein)

(+) On garantit que seul un processus rsync sera exécuté à la fois

(-) Il reste le mail à gérer depuis le script

Le score est éloquent. Pour un peu je m'applaudirais, mais les autres voyageurs du train risquent de ne pas comprendre, et leur expliquer les raisons d'un tel geste prendrait sûrement trop de temps. Le Monde n'est pas prêt à comprendre de telles merveilles, il va me falloir prendre mon mal en patience, en espérant qu'un jour mon génie sera reconnu à sa juste valeur.

Voici enfin le script final qui représente la mise en pratique de la dernière méthode. Une copie est disponible sur pastebin avec coloration syntaxique.

NOTE (21/02/2011) : Le script suivant ne garantit pas la non-concurrence des processus. Voir le correctif ici.

#!/bin/bash
# rsync-now.sh

#echo "($$) Process started"

METHOD=ssh
ARGS="--delete --progress"
SRC=/dossier/source/
DEST=utilisateur@hote:/dossier/cible
OUTPUT_MAIL=root@localhost
#OUTPUT_MAIL=0

SCRIPTNAME=$(basename "$0")
LOCK=$SCRIPTNAME.lock
RSYNC_CMD="rsync -e $METHOD -av $ARGS $SRC $DEST"

pid=$$
if [ -f $LOCK ]; then
    #echo "($$) Lock already exists, updating PID & exiting"
    echo $pid > $LOCK
    exit
fi;

#echo "($$) Lock does not exist, creating"
echo $pid > $LOCK

runCmd=0
while (( ! $runCmd )); do
    #echo "($$) Waiting 2s more..."
    sleep 2
    lockPid=`cat $LOCK`
    #echo "($$) $lockPid // $pid"
    if [ $lockPid == $pid ]; then
        runCmd=1
    else
        pid=$lockPid
    fi
done

#echo "($$) Lock PID unchanged, executing commands"

out="`date` Starting sync...\n"
while (( $runCmd )); do
    out="$out`$RSYNC_CMD` \n"
    
    lockPid=`cat $LOCK`
    #echo "($$) $lockPid // $pid"
    if [ $lockPid == $pid ]; then
        runCmd=0
        rm $LOCK;
    else
        pid=$lockPid
    fi
done
out="$out`date` Done.\n"

if [ $OUTPUT_MAIL != 0 ]; then
    echo -e "$out" | mail -s "$SCRIPTNAME" $OUTPUT_MAIL
else
    echo -e "$out"
fi;

Les observateurs remarqueront que j'ai utilisé une deuxième boucle autour de l'exécution de la commande rsync. En effet, cette commande peut, selon les aléas de la connexion et le nombre de fichiers concernés, mettre un temps conséquent à envoyer les nouveaux fichiers vers la machine distante. Si entre temps d'autres opérations sont effectuées sur le dossier, elles seront ignorées. La deuxième boucle impose une vérification du fichier lock après l'exécution de rsync, et relance celui-ci si des modifications ont été effectuées. Et ainsi de suite jusqu'à ce que la synchronisation soit parfaite.

LanterneRouge.getPost().close()

Encore un article qui n'intéressera probablement pas grand monde. Heureusement que je le fais d'abord pour moi, sinon je pourrais être déçu de recevoir si peu de reconnaissance.

Plus sérieusement (mais j'étais déjà sérieux), je n'ai pas encore trouvé d'anomalie à la dernière méthode mais il lui reste encore à passer l'épreuve du temps et des bizarreries aléatoires inhérentes au monde du binaire. Je vérifie régulièrement mes logs et les rapports de jobs CRON donc si un problème survient je modifierai le script en conséquence.

Dès que je trouve une autre activité inutile de ce style, promis, je la poste ici avec les autres.