Agiles Backup-Skript: Nutze Rsync & Tar-Archive!

In der digitalen Ära, in der Daten zu einem unverzichtbaren Bestandteil unseres Lebens geworden sind, ist deren Schutz von entscheidender Bedeutung. Regelmäßig kontaktieren mich daher Leser, die auf der Suche nach einem zuverlässigen Backup-Skript sind. Ob für das persönliche Notebook oder den heimischen Server hält sich dabei erstaunlicherweise die Wage.

Angesichts der wiederkehrenden Anfragen habe ich beschlossen, ein modular nutzbares Backup-Skript zu entwickeln. Dabei sollen verschiedene Szenarien berücksichtigt werden. Wie das Erstellen von gezippten Tar-Archiven auf USB-Festplatten oder NAS-Freigaben sowie die Synchronisierung der Daten mittels rsync auf einen Server. Eine Log-Datei wird übrigens auch noch erstellt.

Das Backup-Skript wurde funktionsgetrieben entwickelt, um eine breite Palette von Anwendungsszenarien abzudecken. Dadurch hast du die Freiheit, den Ablauf entsprechend deinen individuellen Anforderungen zusammenzustellen. Zusätzlich können die einzelnen Funktionen problemlos in deine eigenen Skripte integriert werden, wenn es am Ende doch eine Eigenentwicklung werden soll.

Inbetriebnahme des Skripts:

Im Rahmen der einfacheren Einrichtung habe ich das Skript in zwei Dateien aufgeteilt. Unter backup.sh kannst du den jeweiligen Funktionen Argumente übergeben, die Reihenfolge der Ausführung an deine Bedürfnisse anpassen oder unerwünschte Funktionen streichen. Die ganze Magie steckt dann in der Datei functions.sh, wo du in der Regel keine Adaptationen vornehmen musst.

Beachte lediglich, dass beide Dateien im selben Verzeichnis platziert werden müssen und das Ausführungsbit gesetzt werden muss. Während der Skript-Ausführung wird automatisch ein Logfile namens backup.log im aktuellen Verzeichnis erstellt. Weiterhin nutzt das Skript verschiedene Formatierungen für einen klar strukturierten Output in der Shell wie auch in der Log-Datei.

Das Backup-Skript wurde auf Debian- und Ubuntu-Systemen getestet, da diese Betriebssysteme 98 Prozent der Leseranfragen abdecken. Es ist jedoch darauf ausgelegt, auch auf anderen Distributionen zu funktionieren. In solchen Fällen müsste mindestens der genutzten Paket-Manager in der requirements-Funktion angepasst werden. Alternativ dazu kann man die Funktion auch auskommentieren.

Nun genug der Umschweife, hier ist der fertige Code:

#!/bin/bash 

##########################################################
# Dieses Skript ruft die Funktionen aus functions.sh auf #
##########################################################

readonly functions=$(pwd)"/functions.sh"
source $functions 

######################
# Ablauf des Backups #
######################

# Begrüßung des Nutzers:
hello "\n$(date +%d.%m.%y): Erstellung eines Backups sowie eines Rsync-Jobs auf die NAS\n"

# Abfrage von Passwörtern (keine Speicherung in Dateien):
passwords "rsync" "sudo"

# Installation von zwingend notwendiger Software:
requirements "nfs-common" "rsync"

# Hartes Beenden von störenden Programmen:
killall "firefox" "chrom"

# Einhängen einer NAS-Freigabe
mountnas "nfs" "192.168.178.254:/freigbe" "/mnt/mountpoint" 

# Anfertigen eines gezippten TAR-Archivs:
tarbackup "/mnt/mountpoint/Backups/Notebook/Vollsicherung-$(date +%d.%m.%y).tar.gz" "/home/user /random-dic /another/random/dic"

# Behalten der 15 neuesten Sicherungen:
prunetar "15" "/mnt/mountpoint/Backups"

# NAS-Freigabe aushängen:
umountnas "/mnt/mountpoint"

# Datensynchronisation auf einen Rsync-Server:
rsyncbackup "/home/user /random-dic /another/random/dic" "rsync://user@192.168.178.253:873/Rsync-Verzeichnis/Notebook"

# Erfolgsmeldung zum Abschluss
ciao "Die Backups waren allesamt erfolgreich!\n"

An dieser Stelle möchte ich noch kurz auf ein paar Dinge hinweisen: Zwischen den Funktionsaufrufen können individuelle Code-Passagen platziert werden. Falls bestimmte Teile nicht benötigt werden, können sie durch Hinzufügen von # am Anfang der Zeile auskommentiert werden. Natürlich kann man die Zeile auch einfach entfernen.

Es ist außerdem sinnvoll Programme, die den Datensatz während des Backup-Laufs verändern, manuell zu schließen, anstatt dies dem Skript zu überlassen. Dieses beendet diese Prozesse nämlich knallhart, was für Komplikationen bei der erneuten Programmausführung sorgen kann. Bei Chromium und Firefox konnte ich ein solches Verhalten jedoch nich feststellen.

#!/bin/bash 

###########
# Logging #
###########

function logger() {

local logfile="$(pwd)/$1"

if [[ -f $logfile ]]; then
    tee -a "$logfile"
else 
    touch "$logfile"
    tee -a "$logfile"
fi

}

#########################
# Styling Shell-Ausgabe #
#########################

function style() {

local style_selection="$1"
local shell_output="$2"
local logger_selection="$3"

if [[ -n $4 ]]; then
    local exit_code="exit $4"
else 
    local exit_code=""
fi

if [[ "$logger_selection" == "logging" ]]; then

case $style_selection in
    [Gg][Rr][Ee][Ee][Nn])
        echo -e "\e[32m${shell_output}\e[0m" | logger backup.log
        ${exit_code}
        ;;
    [Rr][Ee][Dd])
        echo -e "\e[31m${shell_output}\e[0m" | logger backup.log
        ${exit_code}
        ;;
    [Bb][Oo][Ll][Dd])
        echo -e "\e[1m${shell_output}\e[0m" | logger backup.log
        ${exit_code}
        ;;
    [Nn][Oo][Rr][Mm][Aa][Ll])
        echo -e "${shell_output}" | logger backup.log
        ${exit_code}
        ;;
esac

fi

if [[ ! "$logger_selection" == "logging" ]]; then

case $style_selection in
    [Gg][Rr][Ee][Ee][Nn])
        echo -e "\e[32m${shell_output}\e[0m" 
        ${exit_code}
        ;;
    [Rr][Ee][Dd])
        echo -e "\e[31m${shell_output}\e[0m" 
        ${exit_code}
        ;;
    [Bb][Oo][Ll][Dd])
        echo -e "\e[1m${shell_output}\e[0m"
        ${exit_code}
        ;;
    [Nn][Oo][Rr][Mm][Aa][Ll])
        echo -e "${shell_output}"
        ${exit_code}
        ;;
esac

fi

}

#######################
# Systemanforderungen #
#######################

function requirements() {

    local req_list=("$@") 

    for package in "${req_list[@]}"; do

        if ! dpkg -l | grep -i "$package" >/dev/null 2>&1; then

            echo "$sudopw" | sudo -S apt install "$package" -y >/dev/null 2>&1
            style "normal" "${package} musste noch installiert werden.\n" "logging"

        fi
    
    done
}

#####################
# Hilfe / Erklärung #
#####################

function help() {

  style "normal" "\nDieses Skript führt ein Backup von festgelegten Ordnern durch.\n" "logging_no"
  style "bold" "Aufruf: $(basename "$0") ohne Optionen oder Argumente\n" "no_logging"

}

if [[ $1 == "-h" || $1 == "--help" ]]; then
    help
    exit 0
fi

####################
# Nutzer-Begrüßung #
####################

function hello() {

    local greeting="$1"

    style "bold" "${greeting}" "logging"

}

####################
# Passwort-Abfrage #
####################

function passwords() {
    local rsync_yes="$1"
    local sudo_yes="$2"

    if [ "$rsync_yes" == "rsync" ]; then

        style "normal" "\nGib bitte das Passwort für den Rsync-Server ein: " "no_logging"
        read rsyncpw
        export RSYNC_PASSWORD="$rsyncpw"

    fi

    if [ "$sudo_yes" == "sudo" ]; then

        style "normal" "\nGib bitte dein Sudo-Passwort ein, damit Abhängigkeiten installiert werden können und die Freigabe eingehängt werden kann: " "no_logging"
        read sudopw

    fi

}

#################################################################################################################
# Hinweis, dass alle unnötigen Prozesse beendet werden müssen, damit konsistente Backups erstellt werden können #
#################################################################################################################

function killall() {

    local programs=("$@")

    style "normal" "\nDamit es nicht zur Fehlermeldung >> Geänderte Dateien während der Sicherung << kommt, müssen folgende Programme beendet werden: ${programs[*]}    [J/n]" "no_logging"
    read ps

    if [ "$ps" == "n" ] || [ "$ps" == "N" ]; then
  
        style "bold" "\nDas Skript wird umgehend beendet!" "no_logging" "0"

    else

        style "normal" "\nAlle störenden Programme werden jetzt hart beendet, um das Backup durchzuführen:\n" "no_logging"
            for program in "${programs[@]}"; do
                pkill -e "${program}"
        done

    fi

}

#################################
# Tar-Vollsicherung auf die NAS #
#################################

function tarbackup() {

    local backup_name="$1"
    local backed_up_dic="$2"

    style "bold" "\nAnfertigung einer komprimierten Vollsicherung mittels tar\n" "logging"

    tar --exclude-caches --ignore-failed-read --totals -czvf "${backup_name}" ${backed_up_dic} ||  style "red" "\nEs ist ein Fehler aufgetreten. Die Vollsicherung mit tar konnte nicht erstellt werden.\n" "logging" "1" && style "green" "\nDie Vollsicherung mit tar wurde erfolgreich erstellt.\n" "logging"
    
}

###############################
# Rsync-Sicherung auf die NAS #
###############################

function rsyncbackup() {

    local backed_up_dic="$1"
    local ip_dic="$2"

    style "bold" "\nSicherung der Daten mit Rsync\n" "logging"

    rsync -avh --progress --delete --stats --exclude={"/proc/*","/sys/*","/tmp/*","/var/tmp/*","/run/*","/mnt/*","/media/*"} ${backed_up_dic} "${ip_dic}" || style "red" "\nEs ist ein Fehler aufgetreten. Die Sicherung mit rsync konnte nicht durchgeführt werden.\n" "logging" "1" && style "green" "\nDie Sicherung mit rsync wurde erfolgreich durchgeführt.\n" "logging"
	
    unset RSYNC_PASSWORD

}

####################
# Abschlussmeldung #
####################

function ciao() {

    local closing_report="$1"

    style "green" "\n${closing_report}\n" "logging"

}

##################
# Backup Pruning #
##################

function prunetar() {

    local keep_backups="$1"
    local backup_share="$2"
    local files="$(ls -1tr ${backup_share})"
    local num_files=${#files[@]}

    if ((num_files > keep_backups)); then

        cd ${nas_share}
   
            for ((i = 0; i < num_files - keep_backups; i++)); do
                rm "${files[i]}"
                style "normal" "\nAltes Backup ${files[i]} gelöscht.\n" "logging"
            done

    fi
}

###############
# NAS mounten #
###############

function mountnas() {
    
    local share_type="$1"
    local ip_share="$2"
    local mount_point="$3"

    if ! mount | grep -q "${mount_point}"; then

        echo "$sudopw" | sudo -S mount -t "${share_type}" "${ip_share}" "${mount_point}" || style "red" "Fehler: Die "$(echo $share_type | tr '[:lower:]' '[:upper:]')"-Freigabe konnte nicht eingehängt werden!" "logging" "1" 
        echo ""
    
    fi

}

################
# NAS umounten #
################

function umountnas() {

    local mount_point="$1"

    echo "$sudopw" | sudo -S umount -f ${mount_point}  ||  style "red" "\nEs ist ein Fehler aufgetreten. Die Freigabe konnte nicht ausgehängt werden.\n" "logging" "1"

}

Von Fabian Wüst

Er ist leidenschaftlicher Open-Source-Benutzer und ein begeisterter Technologie-Enthusiast. Als kreativer Kopf hinter Homelabtopia bringt Fabian hier seine umfangreiche Erfahrung als Linux-Admin ein. Um sicherzustellen, dass du aus seinen Beiträgen den größtmöglichen Nutzen ziehen kannst, führt er ausgiebige Tests durch und errichtet dafür immense Setups.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert