ici & ailleurs

Mountwatch, Sauvegarde pour geek avec Mac OS

Une petite histoire pour commencer. Le 3 décembre 2007, alors que j'étais au boulot, mon disque dur est tombé en panne. Aussi vite que vous venez de lire cette phrase. Même si j'ai eu un peu de chance d'avoir sauvegardé mes photos quelques jours avant, j'ai perdu une immense partie de mes données et deux semaines de travail non commité.

J'ai racheté un disque dur et un disque externe de la même taille pour pouvoir faire des sauvegardes régulières. On prend les bonnes résolutions quand on s'est vraiment bien planté, ça doit être dans la nature humaine.

Le pire est que je n'ai pas tout perdu par absence de procédure de sauvegarde mais parce que celle-ci était compliquée. Il fallait brancher le disque et lancer un script. Oui, vous avez bien lu, trop compliquée.

Les jours qui ont suivi, j'ai tout fait pour que ma procédure de sauvegarde se résume à une seule étape : brancher le disque externe.

Launchd à la rescousse.

Pour pouvoir appliquer une telle procédure il faut pouvoir dire au système d'exploitation de lancer tel script quand tel volume est monté.

Sur MacOS, Launchd permet de scruter les changements sur un répertoire. Il n'est donc pas compliqué de faire un script qui sera appelé à tout changement de /Volumes. Tout ceci est très bien expliqué dans Backups with Launchd et j'ai employé cette technique jusqu'à hier.

Limites et nouveaux besoins

Cette technique, même si elle fonctionne très bien, montre quelques limites :

Par ailleurs, je me suis mis à utiliser intensivement les Sparse Bundle pour pouvoir disposer de volumes chiffrés. Une sauvegarde en clair protège bien d'un crash disque, moins d'un cambriolage.

Le problème avec un sparse bundle est qu'il faut le monter pour pouvoir l'utiliser (et éventuellement donner son mot de passe). Encore une étape de plus.

Je me suis pris à rêver d'un agent système qui au montage de tel volume s'occuperait de monter le sparse bundle s'y trouvant et lancerait la sauvegarde dessus. Ou, pour tel autre volume, ouvrirait les sparse bundle que j'utilise régulièrement. Ou encore, la possibilité de lancer un script de sauvegarde réseau quand je monte tel autre sparse bundle.

Tout ce dont j'avais besoin était d'un agent qui au montage d'un volume appellerait un script en lui passant le chemin du volume. C'est à dire ce qu'à oublié Apple dans Launchd.

Essais en tout genre

J'ai commencé à faire tout ceci en Python avec PyObjc. Ce fut fait rapidement et assez facilement mais il y avait un petit problème. En lançant le moniteur d'activité j'ai pu constater que le script utilisait 64M de mémoire vive. C'est 20 fois trop pour un script dont le seul boulot est de lancer une boucle qui attend un événement. En fait, c'était le second script, une première version lançait un diskutil list toutes les 5 secondes mais je vais éviter d'en parler ;)

Le problème venant des imports de PyObjc, la seule solution est de faire ce programme en Objective C. Comme je n'en ai quasiment jamais fait, ça a pris un petit moment.

Voici donc mountwatch. Dans le fichier zip, vous trouverez les sources et le binaire. La compilation se fait avec gcc -framework Cocoa mountwatch.m -o mountwatch. Le programme est sous licence WTFPL

Utilisation de mountwatch

Comment s'en sert-on, me direz vous ? Vous pouvez déjà tester que le programme fonctionne en le lançant dans un terminal et en regardant ce qui s'affiche. Il vous faut un script pour tester.

Au même endroit que mountwatch, créez un script test.sh contenant ceci :

#!/bin/sh
echo $1

Rendez le exécutable (chmod a+x test.sh) puis lancez la commande ./mountwatch test.sh. Branchez une clé USB ou n'importe quoi qui va se monter. Vous devriez voir une ligne indiquant le montage du volume et une autre donnant son adresse.

Pour installer définitivement mountwatch, vous devez en faire un agent. Si vous n'avez pas de répertoire Library/LaunchAgents dans votre répertoire utilisateur, créez en un. Dans ce répertoire créez un fichier localhost.mountwatch.plist contenant ceci :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Disabled</key>
    <false/>
    <key>Label</key>
    <string>localhost.mountwatch</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/mountwatch</string>
        <string>VOTRE SCRIPT</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

Vous prendrez soin de remplacer VOTRE SCRIPT par le chemin complet vers votre script. Copiez également mountwatch dans /usr/local/bin.

Un détail important et ça sera presque terminé. Mountwatch pourrait fonctionner comme démon système mais si votre script essaye d'interagir avec l'espace utilisateur (notifications growl, demande de confirmation, etc.) il ne pourra pas, c'est normal. Pour cette raison, il fonctionne comme agent utilisateur. Mountwatch fonctionne avec l'utilisateur en cours ce qui peut-être gênant pour lancer une sauvegarde de tout votre disque. Pour le faire fonctionner en root, vous pouvez faire ceci :

sudo chown root:wheel /usr/local/bin/mountwatch && chmod u+s /usr/local/bin/mountwatch

Ainsi, mountwatch, lancé par n'importe quel utilisateur, aura les droits root. Soyez sympa, faites très attention à ce que fait votre script car lui aussi aura les droits root.

Vous n'avez plus qu'à relancer votre session. Vous devriez voir mountwatch dans le moniteur d'activité.

Ce que peut faire votre script

Votre script sera appelé à chaque montage d'un volume, vous pouvez donc le modifier sans avoir à relancer mountwatch.

Votre script reçoit l'URL vers le volume monté. Avec cette information, vous pouvez exécuter diskutil info $1 pour, par exemple, récupérer le UUID du volume afin de l'identifier correctement.

Pour ouvrir un sparse bundle sur un volume dont vous avez le point de montage (Mount Point) dans les informations, il suffit de simplement lancer open point-de-montage/votre-bundle. Si un mot de passe doit être indiqué, une fenêtre s'ouvrira.

Pour faire des notifications Growl, installez growlnotify qui se trouve dans les extra de Growl.

Si vous faites votre script en Python, vous pouvez demander les infos d'un volume avec l'option -plist et utiliser plistlib pour lire le résultat.

Par exemple, une procédure de sauvegarde dans un sparse bundle :

  1. Le disque est monté,
  2. Le script est lancé pour ce disque et ouvre le sparse bundle
  3. Le script est lancé et démarre la sauvegarde dans le sparse bundle

Enfin, si vous sauvegardez l'intégralité de votre disque système, coupez l'indexation SpotLight sinon ça va durer des heures. Avant et après votre sauvegarde exécutez mdutil -i off /Volumes/votre-volume.

Avant qu'on me le fasse remarquer, je sais que certains logiciels de sauvegarde peuvent démarrer au branchement d'un disque mais aucun ne me proposait la flexibilité et les possibilités qui vont avec.