Pragmatische Backups für das Heimnetzwerk

Motivation und Ausgangslage

Ein- oder zweimal im Jahr kommt es bei mir vor, dass sich von irgend einem meiner Rechner mal wieder die Festplatte verabschiedet und sich entschließt, plötzlich und unerwartet vor ihren Schöpfer zu treten. Und das obwohl sich die Rechner kaum von der Stelle bewegen, nur durchschnittlich oft benutzt und immer sorgfältig behandelt werden. Da ich somit schon viel zu oft irgendwelche Daten verloren habe, musste ich irgendwann zu der Einsicht kommen, dass ein Backup nur dann ein gutes Backup ist, wenn es vollautomatisch und ohne jegliches Zutun von Anwenderseite durchgeführt werden kann. Insbesondere darf es nicht sein, dass ein Backup erst manuell angestoßen werden muss (zum Beispiel durch Einstecken einer Backupplatte), da dies in der Realität viel zu oft vergessen wird. Beim letzten Plattencrash hat es ausgerechnet meinen Hauptcomputer erwischt, wodurch ich ganze zwei Monate zurückfiel, weil eben genauso lange das letzte Backup her war.

Damit sich ein solcher Fall nicht nochmal ereignen kann, beschloss ich einen gerade günstig gekauften Minirechner (400 MHz VIA CPU, 10 EUR gebraucht) als vollautomatischen Backupserver zu konfigurieren. Das Prinzip ist einfach: Mit Hermes habe ich ohnehin einen Rechner, der rund um die Uhr läuft, den ich aber nicht als Backupserver verwenden will. Er weckt jede Nacht um vier Uhr den Backupserver auf (Cronjob) und sorgt per SSH dafür, dass ein vordefiniertes Backupprogramm angestoßen wird. Der Backupserver kümmert sich dann darum, alle Computer im Netzwerk hochzufahren, zu sichern und wieder herunterzufahren. Anschließend schaltet er sich von selbst aus.

Der Aufbau sieht also wie folgt aus:

Abb.: Systemlandschaft mit Backupserver
[Backupserver]---USB---[Festplatte 1.5 TB]
      |
      +---LAN-----+-------------+
                  |             |
               [Router]   [Heimserver]
                  |
      +--LAN--+---+---+-------+
      |       |       |       |
    [PC1]   [PC2]   [PC3]   [PC4]   ...

[Heimserver] weckt [Backupserver].

Backupserver weckt [PC1] und führt Backup durch

Backupserver fährt [PC1] runter, wenn niemand angemeldet

...

Backupserver fährt runter

Natürlich werden nur die Rechner aufgeweckt, die nicht schon ohnehin eingeschaltet sind. Der Heimserver muss also nicht aufgeweckt werden, da er immer an ist. Die Existenz der anderen Rechner kann mit Ping überprüft werden. Damit wird auch überprüft, ob nach einem Wake-On-Lan der Rechner tatsächlich hochgefahren wurde. Beim Abschalten gilt umgekehrt, dass der Heimserver nicht heruntergefahren darf, da somit die automatischen Backups nicht mehr angestoßen werden könnten. Alle anderen Computer dürfen im Prinzip heruntergefahren werden, jedoch nur, wenn kein Benutzer mehr angemeldet ist. Arbeitet noch jemand an dem Rechner, soll der Rechner also einfach eingeschaltet bleiben.

Hinzu kommt, dass manche meiner Rechner nicht wake-on-lan fähig oder nicht ans Netzwerk angebunden sind. Hierbei handelt es sich um historische Rechner mit DR-DOS oder ATARI TOS statt Linux. Aber auch die AW4416 Audio Workstation von Yamaha zählt hierzu. Diese Geräte können nicht vollautomatisch gesichert werden. Für sie muss der Zugriff auf den Backuprechner per FTP oder SFTP möglich sein, um manuelle Backups durchführen zu können.

Benannte Hosts aufwecken

Voraussetzung für den obigen Plan ist, dass alle Rechner mit einer festen IP-Adresse oder einem festen Domainnamen angesprochen werden können. Hierfür gibt es verschiedene Lösungsansätze; in meinem Fall ist es so, dass dnsmasq als kombinierter DHCP/DNS-Server jedem Rechner anhand seiner MAC-Adresse eine IP und eine Subdomain zuordnet.

Hinzu kommt ein eigenes Python-Script, welches das wakeonlan Kommando (muss unter Debian nachinstalliert werden!) einhüllt. Denn wakeonlan funktioniert nur mit einer MAC-Adresse und die kenne ich in der Regel nicht auswendig. Stattdessen greift das Skript auf eine zentral im Netzwerk abgelegte Konfigurationsdatei zurück, die jeder MAC-Adresse einen Namen zuordnet.

Zuordnung von Namen zu MAC-Adressen
192.168.178.004 00:50:45:5D:89:0C * jake
192.168.178.005 00:40:63:C9:BD:F3 * backup
192.168.178.020 00:A0:D1:9B:79:6E * blues-mobil

Demnach sind folgende Aufrufe identisch:

$ wake-on-lan  00:40:63:C9:BD:F3
$ ./wakehost backup

Hier der Quellcode des Skripts:

wakehost.py
#! /usr/bin/env python
#encoding=utf-8

# wakehost.py
# Simple wakeonlan wrapper in order to wake named hosts
#
# Copyright 2010 Dennis Schulmeister <dennis(at)developer-showcase.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.

# Configuration values
HOST_FILE_URL = "http://hermes/hosts.txt"
TIMEOUT = 10

# Imported modules
import subprocess, sys, urllib2

# Function definitions
def main():
    '''
    Main routine of this script. Returns system return code.
    '''
    try:
        host_file = urllib2.urlopen(HOST_FILE_URL, None, TIMEOUT)
    except urllib2.URLError as ex:
        print str(ex)
        return 1

    try:
        hostname = sys.argv[1]
    except IndexError:
        hostname = ""

    error = False

    if hostname:
        mac = ""

        for line in host_file:
            line = line.lstrip().rstrip()
            host = line.split(" ")

            if host[3] == hostname:
                mac = host[1]
                break

        if mac:
            try:
                subprocess.call(["wakeonlan", mac])
            except OSError:
                print "OS Error: Unable to run wakeonlan"
                error = True
        else:
            print "Unknown host: %s" % hostname
            error = True
    else:
        for line in host_file:
            line = line.lstrip().rstrip()
            host = line.split(" ")
            print "%s: %s" % (host[1], host[3])

    host_file.close()

    if error:
        return 1
    else:
        return 0

# Program start
if __name__ == "__main__":
    sys.exit(main())

Konfiguration des Heimservers

Aufgabe des Heimservers ist es, das Backup der Cronjob täglich anzustoßen. Das Ergebnis des Backups soll dann per Mail an den Administrator geschickt werden.

Da die Kommunikation mit dem Backupserver nur via SSH funktioniert, muss zunächst ein Schlüsselpaar generiert werden. Der Public Key findet sich dann in der Datei ~/.ssh/id_rsa.pub.

$ ssh-keygen

Anschließend wird ein neuer Cronjob eingeplant:

/etc/cron.d/DES-backup
# Start periodic backup on backup server
# Runs mon, wed, fri at 3:30am
# des, Sun Jul 11 2010

#--------------------------------------------------------
# 0=sun, 1=mon, 2=tue, 3=wed, 4=tue, 5=fri, 6=sat, 7=sun
#--------------------------------------------------------
# NOTE: many_small contains hermes but not blues-mobil!
#--------------------------------------------------------

00 4  * * * dennis  /home/dennis/bin/run_backup.sh -q many_silent
00 14 * * 3 dennis  /home/dennis/bin/run_backup.sh -q host_jake

Dieser Job startet ein Shellscript, das als Parameter mindestens einen Namen des auszuführenden Backupprogramms entgegennimmt. Dieses muss auf dem Backupserver angelegt sein (siehe unten), wodurch sich Art und Umfang des Backups bestimmt.

Das Skript selbst unterteilt sich in zwei Dateien, um die eigentliche Skriptfunktion (das Anstoßen des Backups) vom E-Mailversand des Backupergebnisses zu trennen. Zwar verschickt Cron automatisch alle Ausgaben eines Kommandos per E-Mail, diese landen bei mir aber nicht an der richtigen Stelle. (Cron schickt die Ausgaben an den ausführenden Benutzer, dennis in diesem Fall. Ich will jedoch, dass die Mails an root gehen, weil sie dann automatisch in einen speziellen IMAP-Ordner für Systemmeldungen einsortiert werden können. Mehr dazu etwas weiter unten.) Deshalb werden durch den Parameter -q sämtliche Assgaben unterdrückt und stattdessen vom Skript per E-Mail verschickt. Will man das Skript interaktiv ausführen, lässt man den Parameter natürlich weg; die Ausgaben werden trotzdem per E-Mail verschickt.

~/bin/run_backup.sh (Aufruf und E-Mailversand)
#! /bin/bash
## des - Sun Jul 11, 2010: Start backup server in order to run a full backup
## This script is called automaticaly by a cron job and sends the results back
## to the system adminstrator.

BIN=$(dirname $0)
quiet=0

if [ -z "$1" ]; then
    echo "Usage: $0 [-q] <backup-target> <backup-target> ..."
    exit 1
fi

until [ -z "$1" ]; do
    case $1 in
        -q)
            quiet=1
            ;;
        *)
            target=$1
            mailfile=$(tempfile)

            if [ $quiet == "0" ]; then
                $BIN/_run_backup.sh $target | tee -a "$mailfile"
            else
                $BIN/_run_backup.sh $target &>> "$mailfile"
            fi

            mail -s "[HERMES] Results of backup for target $target" root < "$mailfile"
            rm "$mailfile"
           
            sleep 60
            ;;
    esac

    shift
done

Die eigentliche Funktion steckt in _run_backup.sh. Hier wird erst geprüft, ob der Backupserver mit ping erreicht werden kann und der Server bei Bedarf hochgefahren. Anschließend wird das geforderte Backupprogramm ausgeführt und der Rechner wieder heruntergefahren. Über ein Lockfile wird sichergestellt, dass nicht zwei Backups gleichzeitig laufen können.

~/bin/_run_backup.sh (Eigentliches Anstarten des Backups)
#! /bin/bash
## des - Sun Jul 11, 2010: Start backup server in order to run a full backup
## This script is called automaticaly by a cron job.
set +e
BIN=$(dirname $0)
LOCK_FILE="$BIN/.lock_backup"

# Find out backup target
if [ -n "$1" ]; then
    echo "Target: $1"
    target=$1
else
    echo "Target: NONE --> Doing full backup"
    target="all"
fi

echo "backup target: $target" | mail -s "[HERMES] Starting backup for target $target" root

# Check no other backup is currently running
if [ -e "$LOCK_FILE" ]; then
    echo "ERROR: Cannot aquire lock file. Is another backup running?"
    echo "Delete $LOCK_FILE if an old backup aborted unforseen."
    exit 1
else
    touch $LOCK_FILE
fi

# Check whether server is already running
ping -q -c 5 -w 5 backup > /dev/null
SERVER_ON=$?

# Start server if necessary
if [ "$SERVER_ON" != 0 ]; then
    echo "Backup server not running. Wake up."
    $BIN/wakehost backup

    COUNT=0
    while [ $COUNT -lt 18 ]; do
        let COUNT=COUNT+1
        echo -n "*"

        ping -q -c 5 backup &> /dev/null
        SUCCESS=$?

        if [ "$SUCCESS" == 0 ]; then
            break
        else
            sleep 10
        fi
    done

    echo
    if [ "$SUCCESS" != 0 ]; then
        echo "ERROR: Backup server didn't wake up!"
        exit 1
    fi

    sleep 10
else
    echo "Backup server already running."
fi

# Do backup
echo
ssh backup /home/dennis/bin/backup-$target
echo

# Shutdown server if it wasn't running before
USER_COUNT=$(ssh backup who | wc -l)
if [ "$USER_COUNT" -eq "0" ]; then
    echo "Noone else logged in. Shutdown backup server."
    ssh backup sudo poweroff
else
    echo "$USER_COUNT users still loged in. Can't shutdown backup server."
fi

# Release lock file
rm $LOCK_FILE
exit 0

Jetzt fehlt nurnoch ein kleiner Kniff, damit die beim Backup verschickten Mails in einem gesonderten Ordner meines Postfaches auftauchen. Hier kommt die Tatsache zum Tragen, dass Hermes auch der IMAP-Server für meine Mails ist und diese daher in einem Maildir liegen, das per fdm regelmäßig aus verschiedenen Quellen befüllt wird.

Zunächst wird ein neuer Systembenutzer und ein Alias für root auf diesen Benutzer benötigt. Somit wird erreicht, dass die Mails an den Administrator im Postfach des neuen Benutzers landen. Dieses kann dann von fdm via IMAP ausgelsen werden, um die Mails durch eine neue Regel in einen eigenen Ordner einzusortieren:

$ sudo adduser sysmail
Password:
$ sudo nano /etc/aliases
root:sysmail

Folgende Zeilen müssen in ~/.fdm.conf ergänzt werden:

Anpassungen an ~/.fdm.conf
account "hermes-sysmail" imap server "localhost" user "sysmail" pass "..."

action "md_system" maildir "${maildir}/.System"

match account "hermes-sysmail"
   or "^(To:|Cc:|Bcc:).*root@" in headers
   or "^(To:|Cc:|Bcc:|X-Envelope-To:).*login@" in headers
   or "^(To:|Cc:|Bcc:|X-Envelope-To:).*system@" in headers
   or "^(To:|Cc:|Bcc:|X-Envelope-To:).*security@" in headers
   or "^(To:|Cc:|Bcc:|X-Envelope-To:).*sicherheit@" in headers
   action "md_system"

Zuletzt muss der Heimserver noch wei ein normaler Client eingerichtet werden, damit auch er automatisch gesichert werden kann. Diese Prozedur unterscheidet sich nicht von der unten beschriebenen Konfiguration der Clients.

Konfiguration des Backupservers

xxx

Konfiguration der Clients

xxx


attachments

imageappend Append an Image
>