Pragmatische Backups für das Heimnetzwerk

Public Key Hermes: Download
Public Key Backupserver:
Download
Sonstige Downloads

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

Auf dem Backupserver kommt eine minimalistische Debianinstallation zum Einsatz (Basiskonfiguration ohne spezielle Zusatzpackete). Auf dieser werden dann ssh, sudo und noch ein Paar weitere Packete eingerichtet (siehe auch Sudo einrichten). rsync wird später die Backups ausführen, mit mercurial können Änderungen an den Backupskripten aufgezeichnet werden.

$ apt-get install rsync openssh-server sudo byobu mercurial

Ebenso wird der root-Account gesperrt:

$ sudo usermod -e 0 root
$ sudo passwd -l root

Dann muss sichergestellt werden, dass die USB-Festplatte beim Hochfahren des Backupservers automatisch eingehängt wird. Dabei ist bekannt, dass die Platte mit dem ReiserFS formatiert wurde und nur ein Dateisystem besitzt.

$ sudo nano /etc/fstab
/dev/sdb1   /media/backup   reiserfs    notail,noatime  0   2
$ sudo mkdir /media/backup
$ sudo mount /dev/sdb1

Aus Komfortgründen soll die Platte auch unter ~/media verfügbar sein, da auf allen Rechnern überwiegend der gleiche Benutzer (dennis) verwendet wird.

$ cd ~
$ mkdir media
$ ln -s /media/backup /home/dennis/media

Weiter geht's mit der Einrichtung von SSH. Auch hier muss erst ein Schlüsselpaar generiert werden:

$ ssh-keygen

In ~/.ssh/id_rsa.pub befindet sich dann der Public Key des Servers, der in die Datei ~/.ssh/authoirzed_keys jedes Clients kopiert werden muss. Andernfalls kann sich der Backupserver nicht an den Clients anmelden und somit das Backup nicht ausführen. In die Datei ~/.ssh/authoirzed_keys des Backupservers muss der Public Key von Hermes kopiert werden, damit dieser die jeweiligen Backupprogramme antoßen kann.

Die Backupskripte liegen in ~/bin und werden mit Mercurial verwaltet:

$ mkdir ~/bin
$ cd ~/bin
$ hg init

Bei den Skripten selbst handelt es sich um einfache Shellskripte, die alle mit backup- beginnen müssen, um als Backup Target aufgerufen werden zu können. Dabei unterscheide ich nach backup-host_name-Skripten, die einen einzelnen Rechner sichern und backup-many_name-Skripten, die eine Sammlung von Rechnern sichern und hierfür einefach die jeweiligen backup-host_name-Skripte aufrufen.

Ein backup-many_name-Skript sieht daher so aus:

~/bin/backup-many_wol (Beispiel)
#! /bin/bash
## des - Mon Jul 12 2010: Full backup of all WoL-capable hosts

# Include common functions
BIN=$(dirname $0)
. $BIN/_backup-functions.sh

# Do backup
echo_section_header "Full backup of all WakeOnLan capable systems"

$BIN/backup-host_hermes; echo
$BIN/backup-host_developer-showcase; echo
$BIN/backup-host_blues-mobil; echo
$BIN/backup-host_eeepc; echo
$BIN/backup-host_jake; echo
$BIN/backup-host_orphilia; echo
$BIN/backup-host_backup; echo

# Print disk usage statistics
echo_end_sections
df -h;echo
echo_end_sections

backup-host_name-Skript sieht hingegen so aus:

~/bin/backup-host_demo (Beispiel)
#! /bin/bash
## des - Mon Jul 12 2010: backup demo
set +e

# Include common functions
BIN=$(dirname $0)
. $BIN/_backup-functions.sh

# Do backup
host=demo
login=willy
SSH="ssh $host -l $login"
dpkg_file="/home/$login/dpkg-selections.txt"

echo_new_backup $host $login
date

wake_host $host
wake_rc=$?

if [ $wake_rc == $WAKE_ERROR ]; then
    exit 1
fi

$SSH "dpkg --get-selections > $dpkg_file"
echo
backup_remote_dir $host $login /home
echo_separator
backup_remote_dir $host $login /etc
$SSH "rm $dpkg_file"

shutdown_host $host $login
date

Hier sieht man schön, wie der Rechner erst hochgefahren wird und vor dem eigentlichen Backup noch allgemeine Aufgaben erledigt werden, zum Beispiel um eine Liste aller installierten Packete zu erstellen, die dann ebenfalls Teil des Backups wird.

Hinzu kommt ein leicht abgewandeltes Skript, um den Backupserver selbst zu sichern:

~/bin/backup-host_backup
#! /bin/bash
## des - Mon Jul 12 2010: self-backup
set +e

# Include common functions
BIN=$(dirname $0)
. $BIN/_backup-functions.sh

# Do backup
host=backup
login=dennis
dpkg_file="/home/$login/dpkg-selections.txt"

echo_new_backup $host $login
date

dpkg --get-selections > $dpkg_file
echo
backup_local_dir /home
echo_separator
backup_local_dir /etc
rm $dpkg_file

date

All diese Skripte beziehen sich auf allgemeine Funktionen, die in _backup_functions.sh stecken: Download _backup_functions.sh

Zuletzt muss noch dafür gesorgt werden, dass der Rechner ohne Passwortabfrage heruntergefahren werden kann. Hierfür eignet sich sudo:

$ sudo nano /etc/sudoers.d/poweroff
# DES: Sun Jul 11, 2010 - Don't require password for shutdown
Cmnd_Alias SHUTDOWN_CMDS = /sbin/shutdown, /sbin/halt, /sbin/reboot, /sbin/poweroff
%sudo  ALL=(ALL)   NOPASSWD: SHUTDOWN_CMDS
$ sudo chmod 440 /etc/sudoers.d/poweroff

Mehr gibt es auf dem Backupserver nicht zu sehen. Die gesamte Funktionalität steckt einfach in einer Hand voll Shellskripten, die in regelmäßigen Abständen ausgeführt werden. Dabei muss beachtet werden, dass der Aufruf vom Heimserver aus ohne den backup-Prefix geschieht. Statt backup-host_demo heißt das Backupprogramm dort nur host_demo. Somit wird verhindert, dass andere als die dedizierten Backupskripte ausgeführt werden können.

Konfiguration der Clients

Für die zu sichernden Clients gilt, dass diese per WakeOnLan hochgefahren werden müssen. Dies wird von eigentlich allen Netzwerkkarten unterstützt, muss in der Regel aber im BIOS aktiviert werden. Bei älteren Linux-Distributionen muss noch ein Trick angewandt werden, damit der Treiber der Netzwekkarte WakeOnLan nicht wieder deaktiviert. Siehe: Wake-On-Lan unter Debian/Linux

Anschließend muss sichergestellt werden, dass sich der Backupserver mit dem Client verbinden und rsync für das Backup aufrufen kann:

$ apt-get install openssh-server rsync

Anschließend muss in die Datei ~/.ssh/authorized_keys der Public Key des Backupservers eingetragen werden. Durch einen gegenseitigen Aufruf beider Systeme über dier SSH-Shell, kann der Zugriff getestet werden. Dabei wird man bei der ersten Verbindung gefragt, ob man sich wirklich mit dem anderen System verbinden will. Vergisst man diesen Schritt, klappt das Backup nicht.

$ ssh backup
$ ssh eigener-host
$ exit
$ exit

Das war's schon fast. Jetzt muss nurnoch die Möglichkeit geschaffen werden, dass der Client ohne Passwortabfrage heruntergefahren werden kann. Dies erfolgt analog zur Einrichtung des Backupservers. Für dem Heimserver entfällt dieser Schritt, da dieser nicht ausgeschaltet werden darf!

$ sudo nano /etc/sudoers.d/poweroff
# DES: Sun Jul 11, 2010 - Don't require password for shutdown
Cmnd_Alias SHUTDOWN_CMDS = /sbin/shutdown, /sbin/halt, /sbin/reboot, /sbin/poweroff
%sudo  ALL=(ALL)   NOPASSWD: SHUTDOWN_CMDS
$ sudo chmod 440 /etc/sudoers.d/poweroff

Dieses Vorgehen ist im unteren Drittel von https://help.ubuntu.com/community/Sudoers beschrieben, wobei die Benutzergruppe unter Ubuntu %admin statt %sudo heißt.

Das ist alles.


attachments

imageappend Append an Image
>