Inhaltsverzeichnis

Allgemein

Ein automatisches Backupscript auf Basis von Perl zum Backup des Systems.

dump vs. tar

Das Backupscript kann seit Version 0.7 zusätzlich zu dump das Programm tar zum Erzeugen von Backups verwenden. Ob dump oder tar zum Erzeugen der Backups eingesetzt wird ist abhängig vom Anwendungsfall und muss im Einzelnen entschieden werden.

dump

Ermöglicht das Backup von ganzen Partitionen (und nur von ganzen Partitionen) im laufenden Betrieb. Es erzeugt hierzu einen Snapshot des Dateisystems. Das Erzeugen von inkrementellen Backups erlaubt das Vorhalten einer gewissen Historie ohne dabei große Mengen Backup-Speicher zu verbrauchen. Das Erzeugen eines dump ist jedoch durch das Erzeugen des Snapshots recht ressourcen-hungrig und führt meist dazu, dass das System für einige Sekunden bis Minuten voll ausgelastet ist.

Die Verwendung von dump auf Partitionen die mit Journaling Softupdates formatiert wurden ist derzeit nicht möglich und führt zum Systemabsturz!

tar

Da tar Verzeichnisorientiert arbeitet ist es damit möglich ausgewählte Teile des Systems zu sichern, nicht jedoch ganze Partitionen. Es ist nicht möglich mit tar inkrementelle Backups zu erzeugen. Da es direkt auf den Dateien arbeitet (nicht wie dump auf Snapshots) ist das Sichern geöffneter Dateien problematisch.

WARNUNG

Das folgende Script verwendet wahlweise das Programm dump zum Erzeugen der Backups. Dieses führt jedoch unter FreeBSD 9.0 zu Systemabstürzen wenn es in Kombination mit Journaling Softupdates verwendet wird. Anwender von Journaling Softupdates sollten daher hier Vorsicht walten lassen oder auf die Backupmethode tar zurückgreifen. Wird kein Journal verwendet (also nur die normalen Softupdates) treten diese Probleme nicht auf.

Konfigurationsdatei

Beschreibung der Parameter

Das Backupscript setzt eine Konfigurationsdatei voraus. Diese muss unter /usr/local/etc/backupscriptrc abgelegt sein. Das Script muss folgenden Aufbau haben:

parameter = wert

Leerzeilen sowie Zeilen die mit # beginnen (Kommentare) werden ignoriert. Es darf kein Kommentar innerhalb einer Parameter-Zeile verwendet werden.

Folgende Parameter sind zulässig:

Parameter Beschreibung des Wertes Hinweise
methode Mögliche Werte sind dump (standard) und tar. Hiermit wird festgelegt mit Hilfe welcher Methode das Backup erzeugt wird. Globaler Wert
zip Einzig möglicher Wert ja. Bei Setzen dieses Werts wird definiert, dass die Backups mit gzip komprimiert werden Globaler Wert
cipher Mögliche Werte sind alle gültigen Werte für openssl enc (siehe man enc). Ist dieser Parameter gesetzt wird das Backup entsprechend des ausgewählten Algorithmus verschlüsselt. Wird der Parameter nicht gesetzt erfolgt keine Verschlüsselung. (Benötigt key oder keyfile). Globaler Wert
Benötigt key oder keyfile.
key Ist cipher gesetzt kann hier das Passwort angegeben werden welches zur Verschlüsselung verwendet werden soll. Aus Sicherheitsgründen ist hiervon abzuraten und statt dessen keyfile zu verwenden. Globaler Wert
Benötigt cipher
keyfile ist cipher gesetzt kann hier eine Datei angegeben werden welche das Passwort zur Verschlüsselung enthält. Diese Methode ist der Verwendung von key vorzuziehen. Es ist jedoch darauf zu achten, dass ein Backup des Keyfiles existiert und, dass dieses nur von root gelesen werden kann. Globaler Wert
Benötigt cipher
nodump Verzeichnis oder Datei welche nicht gesichert werden soll. Darf mehrmals verwendet werden. Globaler Wert
ziel Zielverzeichnis in welches die Dumps gespeichert werden sollen. Globaler Wert
mounten Gibt an ob das Zielverzeichnis auf einer extra Partition liegt welche zuvor gemountet werden muss. Dass die Platte nur zum Backup gemountet wird erhöht dich Sicherheit des Backups. Damit das automatische Mounten funktioniert muss das entsprechend zu mountende Verzeichnis in /etc/fstab (ideal per noauto) angegeben sein. Mehr Infos im nächsten Punkt. Der einzig erlaubte Wert ist ja. Ist kein Mounten gewünscht ist der Parameter weg zu lassen. Globaler Wert
mountpfad Falls ziel nicht direkt das Verzeichnis ist welches gemountet werden muss so ist hier das Mountverzeichnis anzugeben. Sollen die Backups z.B. unter /data/backups abgelegt werden, jedoch /data muss hierzu gemountet werden (backup ist demnach ein Unterverzeichnis auf der /data-Partition) dann wäre der entsprechende Parameter /data. Sind Zielverzeichnis und Mountverzeichnis identisch muss dies nicht extra angegeben werden (z.B. wenn die Dumps direkt nach /data gespeichert werden sollten). Globaler Wert
device Die Devicenode welche die zu sichernde Partition beschreibt. z.B. /dev/ad4p1. Mehrfachnennungen möglich. Nur für Methode dump
tarfile Definiert den „Basisnamen“ des zu sichernden Tar-Files. z.B. „backup“ ergibt später den Namen „backup-20121217.tar.gz“ falls das Backup am 17.12.2012 durchgeführt und gezipt wurde. Nur für Methode tar
quelle Verzeichnis welches gesichert werden soll. Nur für Methode tar

Beispiel Konfiguration mit dump

backupscriptrc
methode   = dump
 
# Nicht zu sichernde Verzeichnisse
nodump    = /usr/ports
nodump    = /usr/obj
nodump    = /usr/src
 
# Zu sichernde Partitionen
device    = /dev/ad4p2
device    = /dev/ad4p4
device    = /dev/ad4p6
 
#/data muss gemountet werden. Die Backups landen dann unter /data/backup
mounten   = ja
mountpfad = /data
ziel      = /data/backup

Beispiel Konfiguration mit tar, AES-256-Verschlüsselung und gzip-komprimiert

backupscriptrc
methode = tar
ziel    = /backups
zip     = ja
 
cipher  = aes256
keyfile = /usr/local/etc/backupkey/backup.key
 
quelle  = /root
quelle  = /etc
quelle  = /usr/local/etc
 
nodump  = /root/testVerzeichnis

Backupscript

Das Backupscript sieht wie folgt aus und sollte ausführbar gemacht unter /usr/local/bin abgelegt werden.

Das aktuelle Script ist immer unter https://github.com/rakor/backupscript zu finden.

backupscript
#!/usr/bin/perl
 
$version = "0.8";
$T_SCRIPTNAME = "backupscript";
 
$OPENSSL 	= "/usr/bin/openssl";
$TAR		= "/usr/bin/tar";
$DUMP		= "/sbin/dump";
$CHFLAGS	= "/bin/chflags";
$MOUNT		= "/sbin/mount";
$UMOUNT		= "/sbin/umount";
$GZIP		= "/usr/bin/gzip";
 
$KONFIGDATEI = "/usr/local/etc/backupscriptrc";
@KONFWERTE = ("nodump", "device", "ziel", "mounten", "mountpfad", "cipher", "methode", "key", "keyfile", "quelle", "tarfile", "zip");
 
$methode = "dump"; # Standard und somit Kompartible zu ver < 0.7
$tarErweiterung = "tar";
$default_tar_name = "backup";
 
## Ausgabetexte ##
$T_KONFDATEINICHTDA = "Konfigurationsdatei $KONFIGDATEI nicht vorhanden!\n";
$T_KONFDATEINICHTOEFFNEN = "Konnte Konfigurationsdatei nicht oeffnen!\n";
$T_KONFDATEIDEFEKT = "Konfigurationsdatei $KONFIGDATEI ist fehlerhaft!\n";
$T_KONFDATEIDEFEKT_TAR_DEVICE = "Konfigurationsdatei $KONFIGDATEI ist fehlerhaft! Wenn die Methode tar gewählt ist darf kein \"device\" definiert sein. \n";
#$T_KONFDATEIDEFEKT_TAR_OUTPUT = "Konfigurationsdatei $KONFIGDATEI ist fehlerhaft! Wenn die Methode tar gewählt ist muss der Outputname in \"tarfile\" definiert sein. \n";
$T_KONFDATEIDEFEKT_DUMP_TAROPTS = "Konfigurationsdatei $KONFIGDATEI ist fehlerhaft! Wenn die Methode dump gewählt ist darf weder \"tarfile\" noch \"quelle\" definiert sein. \n";
$T_KONFDATEIDEFEKT_KEY_KEYFILE = "Konfigurationsdatei $KONFIGDATEI ist fehlerhaft! Es darf entweder \"key\" oder \"keyfile\" definiert sein, nicht beide.\n";
$T_KONFDATEIDEFEKT_KEY_NOCIPHER = "Konfigurationsdatei $KONFIGDATEI ist fehlerhaft! Um die Verschlüsselung der Zieldatei zu aktivieren muss \"cipher\" mit einer openssl-kompatiblen Verschlüsselung gesetzt werden. (z.B. aes256)\n";
$T_KONFDATEIDEFEKT_CIPHER_NOKEY = "Konfigurationsdatei $KONFIGDATEI ist fehlerhaft! \n";
$T_MOUNTFEHLER = "Fehler beim Mounten des Zielpfades\n";
$T_UMOUNTFEHLER = "Fehler beim Unmounten des Zielpfades\n";
$T_MOUNTVARIABLEFALSCH = "Die Variable 'mounten' wurde gesetzt enthaelt aber keinen zulaessigen Wert. Es wird kein Mountversuch unternommen.\n";
$T_NODUMPVERZFALSCH = "Ein 'nodump'-Verzeichnis ist falsch";
$T_ZIELVERZFALSCH = "Das Ziel-Verzeichnis existiert nicht.\n";
$T_UMOUNTWEGENFEHLER = "Wegen eines Fehlers wurde das Mountverzeichnis vorzeitig getrennt.\n";
$T_DEVICEVERZFALSCH = "Ein 'device' ist falsch";
$T_SCHLUESSELLAENGEZUKURZ = "Die Länge des gewählten Schlüssels zur Verschlüsselung ist zu kurz. Zur Sicheren Verschlüsselung bitte einen längeren Schlüssel definieren\n";
$T_KEYFILENICHTNUTZBAR = "Das Keyfile zur Verschlüsselung wurde nicht gefunden. Bitte prüfen Sie ob es existiert und gelesen werden kann.\n";
$T_HILFETEXT = "usage:\t$T_SCRIPTNAME [OPTION]\n\n[Options]:\n\t0-9\tDumplevel - Eine Ziffer die das Dumplevel angibt (nur bei Backups mit dump)\n\t-h\tLange Hilfe\n\n";
$T_LANGEHILFE = "Version: $version
 
Das Script dient der erleichterten Erstellung von Backups unter FreeBSD unter
Verwendung des Systemprogrammes 'dump' oder 'tar'. Zusätzlich können die
Backups mittels 'OpenSSL' verschlüsselt werden. Zur Steuerung wird eine
Konfigurationsdatei unter folgendem Link benoetigt:
 
\t$KONFIGDATEI
 
Die Datei bietet folgende Parameter:
        Allgemeine Parameter
            method     :   Methode mit welcher das Backup erzeugt werden soll.
                           Mögliche Parameter sind 'dump' oder 'tar'.
            nodump     :   Verzeichnisse die NICHT gesichert werden sollen.
            ziel       :   Zielverzeichnis fuer die Backups.
            mounten    :   'ja' falls vor dem Backup eine Partition gemountet 
                           werden muss.
            mountpfad  :   Falls das zu mountende Verzeichnis von 'ziel'
                           abweicht.
            zip        :   'ja' falls das Backup komprimiert werden soll.
                           Zur Komprimierung wird gzip verwendet.
 
        Parameter für Verschlüsselung
            cipher     :   Eine OpenSSL-kompatible Verschlüsselungsmethode.
                           z.B. aes256
            key        :   Passwort welches zur Verschlüsselung verwendet
                           werden soll.
                           Die empfohlene Methode ist 'keyfile' zu verwenden.
                           (schliesst die Verwendung von keyfile aus).
            keyfile    :   Datei welche das zu verwendende Passwort enthält.
                           (schliesst die Verwendung von key aus).
 
        Parameter ausschließlich für dump-Backups	
            device     :   Partition die gesichert werden soll
 
        Parameter ausschliesslich für tar-Backups
            tarfile    :   Name des erzeugten tar-Archivs. Nur der Name,
                           die Erweiterung wird automatisch ergänzt.
            quelle     :   Verzeichnisse die gesichert werden sollen. Je ein
                           Eintrag pro Verzeichnis.
 
Das zu mountende Verzeichnis, egal ob extra angegeben oder nicht, muss immer
unter /etc/fstab aufgefuehrt sein. Es sollte die 'noauto'-Option in /etc/fstab
verwendet werden damit die Backupplatte nicht unnoetig gemountet bleibt.
 
Beispiel einer $KONFIGDATEI für normale nichtverschlüsselte Backups mit dump:
------------------------------------------------
method = dump
nodump = /usr/ports
nodump = /usr/obj
nodump = /usr/src
 
device = /dev/ad4p2
device = /dev/ad4p4
device = /dev/ad4p6
 
mounten = ja
mountpfad = /data
ziel = /data/backup
-------------------------------------------------
 
 
Beispiel einer $KONFIGDATEI für tar-basierte, verschlüsselte Backups:
------------------------------------------------
method    = tar
tarfile   = backup
zip       = ja
ciper     = aes256
key       = testpasswort
 
nodump    = /usr/home/unimportant
 
quelle    = /usr/home
quelle    = /etc
mounten   = ja
mountpfad = /data
ziel      = /data/backup
-------------------------------------------------
\n";
 
###########################################################################
 
sub zeigeHilfe{
	print $T_HILFETEXT;
	exit;
}
 
###########################################################################
 
sub zeigeLangeHilfe{
	print $T_LANGEHILFE;
	exit;
}
 
###########################################################################
 
sub KonfdateiEinlesen{
#
# Liest die Datei ein und speichert die Werte im Array
# Incl. Ueberpruefung der Werte gegen @KONFWERTE.
#
 
	open KDATEI, "<",$KONFIGDATEI or die $T_KONFDATEINICHTOEFFNEN;
	foreach (<KDATEI>){
		unless (/^\s*#/ or /^\s*$/){ # keine Leerzeilen oder Kommentare
			$ok = 0;
			$_ =~/^\s*(\S+)\s*=\s*(.*?)\s*$/; # $1=Parameter; $2=Wert
			foreach (@KONFWERTE){	# Testen ob Werte zulaessig sind
				$ok = 1 if ($_ eq $1);
			}
			die $T_KONFDATEIDEFEKT unless $ok;
			## Verarbeitung der Daten
				if ($1 eq "nodump"){
					push @nodump, $2;
				}
				elsif($1 eq "device"){
					push @device, $2;
				}
				elsif($1 eq "quelle"){
					push @quellverzeichnisse, $2;
				}
				elsif($1 eq "ziel"){
					$ziel = $2;
				}
				elsif($1 eq "zip"){
					if ($2 =~/^[jJ][aA]$/){
						$zip = 1;
					}
					else{
						warn $T_ZIPVARIABLEFALSCH;
					}
				}
				elsif($1 eq "mounten"){
					if ($2 =~ /^[jJ][aA]$/){
						$mounten = 1;
					}
					else{
						warn $T_MOUNTVARIABLEFALSCH; 
					}
				}
				elsif($1 eq "mountpfad"){
					$mountpfad = $2;
				}
				elsif($1 eq "cipher"){
					$cipher = $2;
				}
				elsif($1 eq "tarfile"){
					$tarOutput = $2;
				}
				elsif($1 eq "methode"){
					unless ($2 eq "tar" or $2 eq "dump"){
						warn $T_METHODENVARIABLEFALSCH;
					}
					if ($2 eq "tar"){
						$methode = "tar";
					} else {
						$methode = "dump";
					}
				}
				elsif($1 eq "key"){
					$key = $2;
				}
				elsif($1 eq "keyfile"){
					$keyfile = $2;
				}
			## Ende Verarbeitung der Daten		
		}
	}
	close $KDATEI;
} # Ende sub KonfdateiEinlesen
 
###########################################################################
 
sub zielpfadZuschneiden{
	$ziel =~s/^(.*?)\/?$/$1/;
}
 
###########################################################################
 
sub istZielGemountet{
	$gefunden = 0;
	@gemountet = `mount`;
	foreach (@gemountet){
		$gefunden = 1 if (/$_[0]/);
	}
	$gefunden;
}
 
###########################################################################
 
sub zielMounten{
	unless (defined $mountpfad){
		$mountpfad = $ziel;
	}
	unless (&istZielGemountet($mountpfad)){
		!system "$MOUNT $mountpfad" or die "$T_MOUNTFEHLER";
	}
}
 
###########################################################################
 
sub zielUmounten{
	unless (defined $mountpfad){
		$mountpfad = $ziel;
	}
	if (&istZielGemountet($mountpfad)){
		!system "$UMOUNT $mountpfad" or die "$T_UMOUNTFEHLER";
	}
}
 
###########################################################################
 
sub konfigAufSinnTesten{
	die $T_KONFDATEIDEFEKT_TAR_DEVICE	if ($methode eq "tar" and defined @device);
#	die $T_KONFDATEIDEFEKT_TAR_OUTPUT 	if ($methode eq "tar" and not defined $tarOutput);
	die $T_KONFDATEIDEFEKT_DUMP_TAROPTS if (($methode eq "dump" and defined @quellverzeichnisse) or ($methode eq "dump" and defined $tarOutput));
	die $T_KONFDATEIDEFEKT 			unless (defined $ziel and (defined @device or defined @quellverzeichnisse));
	die $T_KONFDATEIDEFEKT_KEY_KEYFILE  if (defined $key and defined $keyfile);
	die $T_KONFDATEIDEFEKT_CIPHER_NOKEY if (defined $cipher and not (defined $keyfile or defined $key));
	die $T_KONFDATEIDEFEKT_KEY_NOCIPHER if ((defined $keyfile or defined $key) and not defined $cipher);
}
 
###########################################################################
 
sub VerzeichnissePruefen{
	foreach (@nodump){
		warn "$T_NODUMPVERZFALSCH: $_\n" unless (-e $_);
	}
 
	unless (-d $ziel){	
		&zielUmounten if (defined $mounten);
		warn $T_UMOUNTWEGENFEHLER;
		die $T_ZIELVERZFALSCH;
	}
 
	foreach (@device){
		die "$T_DEVICEVERZFALSCH: $_\n" unless (-e $_);
	}
}
 
###########################################################################
 
sub cipherpipeErzeugen{
	if (defined $cipher){
		$cipherpipe = "| $OPENSSL enc -$cipher -e -salt -pass";
		if (defined $key){
			warn $T_SCHLUESSELLAENGEZUKURZ if (length $key < 8);
			$cipherpipe = $cipherpipe." pass:$key"
		} elsif (defined $keyfile){
			die $T_KEYFILENICHTNUTZBAR unless (-e $keyfile and -r $keyfile);
			$cipherpipe = $cipherpipe." file:$keyfile";
		}
		return "-";
	} else {
		$cipherpipe = "";
		return $_[0];
	}
}
 
###########################################################################
 
sub dateiinfosAusgeben{
	@Einheiten = ("B", "kB", "MB", "GB", "TB");
	$division = 0;
	if (-e $_[0]){
		$groesse = -s $_[0];
		while ($groesse >= 1024){
			$groesse = $groesse / 1024;
			$division += 1;
		}
		$ausgabe = sprintf("%s %.2f %s",$_, $groesse, $Einheiten[$division]);
	} else {
		$ausgabe = "$_ wurde nicht angelegt!";
	}
	print "$ausgabe\n";
}
 
###########################################################################
 
# Parameter abpruefen
&zeigeLangeHilfe if ($ARGV[0] eq "-h");
 
# Ist Konfigdatei da?
die $T_KONFDATEINICHTDA unless (-f $KONFIGDATEI);
 
 
# Konfigdatei abarbeiten
&KonfdateiEinlesen;
&konfigAufSinnTesten;
 
if ($methode eq "dump"){
	&zeigeHilfe unless ($ARGV[0] =~/^\d$/);
	$dumplevel = $ARGV[0];
} else {
	&zeigeHilfe if ($#ARGV > -1);
}
 
&zielpfadZuschneiden;
# Muessen wir mounten? Dann los.
&zielMounten if (defined $mounten);
# Zielverzeichnis kann ggf erst nach dem Mounten geprueft werden.
&VerzeichnissePruefen;
 
# Datumsstring erzeugen
($sek, $min, $std, $mtag, $mon, $jahr, , ,) = localtime(time); 
$datum = sprintf("%04d%02d%02d",$jahr+1900,$mon+1,$mtag); 
 
 
if ($methode eq "dump"){	# FALLS MIT DUMP GEARBEITET WIRD
	# Vorarbeiten... gemountete Laufwerke einlesen
	@mounts = `mount`;
 
	foreach (@device){
		$deviceName = $_;
		foreach (@mounts){
			$_ =~/^\s*(.*) on (.*)\s+\(/;
			$devicenodepfadausmount = $1;
			$devicemountpfad = $2;
			if ($devicenodepfadausmount=~/$deviceName$/){
				if ($devicemountpfad eq "/") { $devicemountpfad="root" }
				else{
					$devicemountpfad =~ s/^\/(.*)$/$1/;
					$devicemountpfad =~ s/\//_/g;
			 	}
				$devicemountpfad{$deviceName}=$devicemountpfad;
			}
		}
	}
 
	# 'nodump' setzen falls dump verwendet wird
	foreach (@nodump){
		system ("$CHFLAGS -R nodump $_") if (-e $_);
	}
 
	# dumps durchfuehren
	foreach (@device){
		/^.*?([^\/]+)$/;
		$deviceKurzName = $1;
		$dumpDateiname = "$ziel/$devicemountpfad{$_}-$deviceKurzName-$datum-L$dumplevel";
		&cipherpipeErzeugen($dumpDateiname);
		if (defined $zip){
			$zippipe = " | $GZIP";
			$dumpDateiname = "$dumpDateiname.gz";
		} else {
			$zippipe = "";
		}
		push @erzeugteDateien, $dumpDateiname;
		system ("$DUMP -$dumplevel -h0 -Lauf - $_ $zippipe $cipherpipe | dd of=$dumpDateiname");
	}
} elsif ($methode eq "tar"){	# FALLS MIT TAR GEARBEITET WIRD
	if (defined @nodump){
		$exclude = "";
		foreach (@nodump){
			$exclude = $exclude." --exclude $_";
		}
	}
	$tarOutput = $default_tar_name unless (defined $tarOutput);
	$tarOutput =~ s/^([^\.]*)\.(.*?)$/$1/;;
	if (length $2 > 1){
		$tarErweiterung = $2;
	} 
	$output = "$ziel/$tarOutput-$datum.$tarErweiterung";
	if (defined $zip){
		$z = "-z";
		$output = "$output.gz";
	} else {
		$z ="";
	}
	push @erzeugteDateien, $output; 
	$tarOutputDateiname = &cipherpipeErzeugen($output);
	system ("$TAR -c $z -f - $exclude --exclude $output @quellverzeichnisse $cipherpipe | dd of=$output");
}
 
# Dateigroessen ausgeben
print "\nEs wurden folgende Backups durch $methode angelegt"; 
print " und mittels $cipher verschlüsselt" if (defined $cipher);
print ":\n";
foreach (@erzeugteDateien){
	&dateiinfosAusgeben($_);
 
}
 
&zielUmounten if (defined $mounten);
</code>
 
===== Anwendung =====
Wird das Backupscript mit //dump// verwendet erwartet es als einzigen Parameter das Dumplevel welches angewendet werden soll. Ein Dump-0 wird also wie folgt gestartet:
<code>
# backupscript 0
</code>
 
Bei Verwendung von //tar// ist kein Parameter notwendig und das Backupscript wird einfach wie folgt gestartet:
<code>
# backupscript

Automatische Backups per periodic

Das Script kann verwendet werden um automatische Backups per periodic durchzuführen. Hierzu werden entsprechende Dateien unter /etc/periodic angelegt. Vorrausgesetzt es sollen Backups nach folgendem Schema durchgeführt werden. (Bei Verwendung von dump).

Ein entsprechendes monatliches Periodicscript (z.B. /etc/periodic/monthly/970.backup-L0) hat dann folgenden Inhalt:

#!/bin/sh
/usr/local/bin/backupscript 0

Das Script muss natürlich ausführbar sein. Ähnlich ist mit den anderen Scripten vorzugehen.