diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..383f345 --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +cp *.sh ../../bin/backup diff --git a/cron.sh b/cron.sh new file mode 100755 index 0000000..df9fad3 --- /dev/null +++ b/cron.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# +# Aufruf via ssh auf dem jew. Server +# +# $1 - Logfile für den Backup-Lauf +# + +# cron job auf nathan +# +# je Server: +# +# ssh nathan sudo /var/srv/nas/Server/bin/backup/cron.sh /var/srv/nas/backups/log/nathan-backup-full.log --daily +# ssh hamiller sudo /var/srv/nas/Server/bin/backup/cron.sh /var/srv/nas/backups/log/hamiller-backup-full.log --daily +# ssh synology sudo /volume1/nfs/Server/bin/backup/cron.sh /volume1/nfs/backups/log/synology-backup-full.log --daily +# ssh synology sudo /volume1/nfs/Server/bin/backup/cron.sh /volume1/nfs/backups/log/synology-backup-media-full.log --weekly +# + +LOG="$1" +shift + +HERE=$(dirname "$0") +$HERE/main.sh -a "$@" >"$LOG" 2>&1 & diff --git a/hamiller.sh b/hamiller.sh new file mode 100755 index 0000000..1b8cf67 --- /dev/null +++ b/hamiller.sh @@ -0,0 +1,51 @@ +#!/usr/bin/eval bash + +# TODO: cmd line handling +# fn für div. Aktionen (gemeinsame?) + +BACKUP_ROOT=/var/docker/tmp + +backup_data() { + TARGET_DIR="$BACKUP_ROOT/backup.$DAY" + + NAS_ROOT=/var/srv/nas/backups + + LOG_FILE="$NAS_ROOT/log/$HOST_NAME-backup.log" + + log INFO "*** *** Begin hamiller server backup" + delete_dir $TARGET_DIR + check_directory $TARGET_DIR + + # TODO: docker down + + backup_all_dirs "/var" "$TARGET_DIR" # docker + # backup_all_dirs "/var/docker" "$TARGET_DIR" vol tmp + # backup_all_dirs "/var/docker/vol" "$TARGET_DIR" + + # TODO: docker up + + # TODO: copy backup files to NAS + + log INFO "*** End of hamiller server backup" + list_archives $BACKUP_ROOT +} + +# TODO: restore fn + + +while :; do + case $1 in + --daily) + backup_data + break + ;; + --restore) + # TODO + break + ;; + *) # Default case: No more options, so break out of the loop. + break + esac + + shift +done diff --git a/main.inc.sh b/main.inc.sh new file mode 100755 index 0000000..bc5419f --- /dev/null +++ b/main.inc.sh @@ -0,0 +1,222 @@ +HOST_NAME=$(hostname) +DAY=$(date +%u) + +PASSPHRASE=1234567890 +EXT=tgz.gpg +LIST_EXT=txt + +# check if function $1 exists +# +# returns true/false +# +isFunction() { declare -Ff "$1" ; } + +# Log-Datei schreiben. +# +# Die Ausgabe geht auch auf STDOUT, dadurch kann die vollständige Ausgabe, +# einschließlich Status- und Fehlermeldungen der Programme in ein erweitertes +# Log für die Fehlersuche umgeleitet werden. +# +# $1 - Typ, z.B. INFO, WARN, ERROR, etc. +# $2 ff. - die Meldung, die ins Log geschreiben wird +# +log() { + local type=$1 + if [ "$type" == "ERROR" ]; then ERROR=0; fi + shift 1 + echo "$(date '+%d.%m.%Y %H:%M:%S') - $type $@" | sudo tee -a $LOG_FILE + +} + +# Ist $1 in der Liste der restlichen args $2, $3 ... +# +# zB. if is_in "foo" "foo" "bar" "foo bar" ; then ... +# Leere Liste liefert immer false +# +is_in() { + local search="$1" + shift 1 + local e + for e in "$@"; do [[ "$e" == "$search" ]] && return 0; done + return 1 +} + +# Alle Verzechnisnamen in einem Verzeichnis holen. +# +# $1 - Verzeichnis in dem gesucht wird als absoluter Pfad +# $2 ff. - auszuschliessende Verzeichnisse (kann leer bleiben) +# +iterate_dirs_in() { + local dirname + local root=$1 + shift 1 + for dir in $(sudo find "$root" -maxdepth 1 -type d \! -wholename "$root"); do + dirname=$(basename $dir) + if ! is_in "$dirname" "$@"; then + echo "$dirname" + fi + done +} + +# create directory if it not exists +# +# $1 - dir to check +# +check_directory() { + local dir=$1 + if [ -d "$1" ]; then + return 0 + else + mkdir -p "$1" && return 0 + sudo mkdir -p "$1" && return 0 + log ERROR "### could not create directory $1!" + # no chance to recover + exit 1 + fi +} + +# Verzeichnis löschen +# +# Im Fehlerfall wird rc=1 zurückgegeben +# +# $1 - zu löschendes Verzeichnis +# +delete_dir() { + local dir=$1 + sudo rm -rf "$dir" && return 0 + log ERROR "### could not delete directory $1!" + # return error, so caller can decide what to do + return 1 +} + +# Slashes "/" im Pfadnamen durch Dashes "-" ersetzen, so dass der String als Dateiname verwendet werden kann. +# +# $1 - path +backup_file_prefix() { + echo "$1" | sed -E 's/^\///' | sed -E 's/\//-/g' +} + +# Verzeichnise in .tgz.gpg-Dateien sichern. +# +# $1 - Verzeichnis, das gesichert wird: alle Verzeichnisse darunter werden einzeln als .tgz.gpg-Datei gesichert +# $2 - Zielverzeichnis für .tgz.gpg-Dateien +# $3 ff. - Verzeichnisse, die nicht gesichert werden sollen (kann leer bleiben) +# +backup_all_dirs() { + local root=$1 + local target=$2 + local prefix=$(backup_file_prefix $root) + local tar_file + local dir + shift 2 + + for dir in $(iterate_dirs_in "$root" "$@"); do + tar_file="$target/$prefix-$dir.$EXT" + backup_dir "$root" "$tar_file" "$dir" + create_list "$tar_file" "$dir" + test_integrity "$tar_file" + done +} + +# Einzelnes Verzeichnis sichern. +# +# $1 - Pfad um Verzeichnis, das gesichert werden soll +# $2 - Zielpfad und -name für das Archiv +# $2 - zu sicherndes Verzeichnis +# +backup_dir() { + local root=$1 + local archive=$2 + local dir=$3 + log INFO "--- Backup $root/$dir -> $archive" + sudo tar -c -z --numeric-owner --one-file-system --exclude "@eaDir" -C "$root" "$dir" | sudo gpg --batch -c --passphrase $PASSPHRASE -o "$archive" + local rc=$? + if [ "$rc" == "0" ] ; then + log INFO "*** success" + else + log ERROR "### see full log" + fi +} + +# Inhalt des Archivs in eine Textdatei ausgeben. +# +# $1 - Archiv +# +create_list() { + local archive=$1 + log INFO "--- List $archive" + sudo gpg --batch --decrypt --passphrase $PASSPHRASE "$archive" | sudo tar -tzv | sudo tee "$archive.$LIST_EXT" >/dev/null + local rc=$? + if [ "$rc" == "0" ] ; then + log INFO "*** success" + else + log ERROR "### see full log" + fi +} + +# Integrität des Archivs testen, indem es entschlüsselt, entpackt +# und der Inhalt auf /dev/null ausgegeben wird. +# +# $1 - Archiv +# +test_integrity() { + local archive=$1 + log INFO "--- Check integrity of $archive" + sudo gpg --batch --decrypt --passphrase $PASSPHRASE "$archive" | sudo tar -xzO >/dev/null + local rc=$? + if [ "$rc" == "0" ] ; then + log INFO "*** success" + else + log ERROR "### see full log" + fi +} + +# Restore aus Archiv +# +# Wird nach pwd ausgegeben! +# +# $1 - Archiv, ggf. mit Pfad +# $2 ff. - zurückzusichernde Dateien/Verzeichnisse +# +restore() { + local archive=$1 + shift 1 + sudo gpg --batch --decrypt --passphrase $PASSPHRASE "$archive" | sudo tar -xzv $@ >/dev/null + local rc=$? + if [ "$rc" == "0" ] ; then + log INFO "*** success" + else + log ERROR "### see full log" + fi +} + +# Liste der Archives auf stdout ausgeben +# +# Zeigt alle Archive incl. Verzeichnis (ohne $1) an +# +# z.B. +# $1 = /volume1/nfs/backups +# ... +# /daily/synology/backup.3/volume1-nfs-Server.tgz.gpg +# /weekly/synology/volume1-media-books.tgz.gpg +# +# $1 - Verzeichnis unter dem alle Backups liegen +# +list_archives() { + local target=$1 + local targ=${target//\//\\\/} # slashes escapen + echo "#$targ#" + sudo find "$target" -type f -name "*.$EXT" | grep "${HOST_NAME}/" | sed -E "s/^$targ//" +} + +# Inhalt des Archivs $2 auf stdout ausgeben +# +# $1 - Verzeichnis unter dem alle Backups liegen +# $2 - Archiv-Datei mit Pfad ab $1 +# +list_archive_content() { + local root=$1 + local archive=$2 + + sudo cat "$root/${archive/#\//}.$LIST_EXT" +} diff --git a/main.sh b/main.sh new file mode 100755 index 0000000..28b229b --- /dev/null +++ b/main.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# +# entrypoint for all actions +# + + +HERE=$(dirname "$0") +. $HERE/main.inc.sh + + + +show_help() { + echo "usage: $0 options" + echo " -a, --auto start nightly backup on local machine" + echo " --list list archives" + echo ' --content ' + echo " show contents of archive" + echo ' -r, --restore ' + echo " restore files from archive to pwd" + echo " -h, -?. --help show this help" +} + +echo " ### main.sh" + +# Initialize all the option variables. +# This ensures we are not contaminated by variables from the environment. +file= +archive= + +while :; do + case $1 in + -h|-\?|--help) + show_help # Display a usage synopsis. + exit 0 + ;; + -a|--auto) + shift + . $HERE/$HOST_NAME.sh "$@" # >$LOGF 2>&1 + break # exit kehrt nicht zu run.sh zurück + ;; + --list) + . $HERE/$HOST_NAME.sh -l + break; + ;; + --content) + if [ "$2" ]; then + . $HERE/$HOST_NAME.sh -c $2 + else + echo 'ERROR: "--content" requires a non-empty option argument.' + fi + break + ;; + -r|--restore) + if [ "$2" ]; then + echo "not yet implemented" + archive=$2 + shift + #. $HERE/$HOST_NAME.sh -r $archive $@ + else + echo 'ERROR: "--restore" requires a non-empty option argument.' + fi + break + ;; + + # --- example -> https://mywiki.wooledge.org/BashFAQ/035 + -f|--file) # Takes an option argument; ensure it has been specified. + if [ "$2" ]; then + file=$2 + shift + else + die 'ERROR: "--file" requires a non-empty option argument.' + fi + ;; + --file=?*) + file=${1#*=} # Delete everything up to "=" and assign the remainder. + ;; + --file=) # Handle the case of an empty --file= + die 'ERROR: "--file" requires a non-empty option argument.' + ;; + --) # End of all options. + shift + break + ;; + -?*) + echo "WARN: Unknown option (ignored): $1" + echo + show_help + exit + ;; + *) # Default case: No more options, so break out of the loop. + break + esac + + shift +done + diff --git a/nathan.sh b/nathan.sh new file mode 100755 index 0000000..f0257ff --- /dev/null +++ b/nathan.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# TODO: cmd line handling +# fn für div. Aktionen (gemeinsame?) + + +BACKUP_ROOT=/var/docker/tmp + +backup_data() { + TARGET_DIR="$BACKUP_ROOT/backup.$DAY" + + NAS_ROOT=/var/srv/nas/backups + + LOG_FILE="$NAS_ROOT/log/$HOST_NAME-backup.log" + + log INFO "*** *** Begin nathan server backup" + delete_dir "$TARGET_DIR" + check_directory "$TARGET_DIR" + + # TODO: docker down + + backup_all_dirs "/var" "$TARGET_DIR" docker + backup_all_dirs "/var/docker" "$TARGET_DIR" vol tmp + backup_all_dirs "/var/docker/vol" "$TARGET_DIR" + + # TODO: docker up + + # TODO: copy backup files to NAS + + log INFO "*** End of nathan server backup" + list_archives $BACKUP_ROOT +} +# TODO: restore fn + + +while :; do + case $1 in + --daily) + backup_data + break + ;; + --restore) + # TODO + break + ;; + *) # Default case: No more options, so break out of the loop. + break + esac + + shift +done diff --git a/synology.sh b/synology.sh new file mode 100755 index 0000000..05b10ee --- /dev/null +++ b/synology.sh @@ -0,0 +1,96 @@ +#!/usr/bin/eval bash + +# TODO: cmd line handling +# fn für div. Aktionen (gemeinsame?) + +NAS_ROOT=/volume1/nfs/backups + +backup_media() { + TARGET_DIR="$NAS_ROOT/weekly/$HOST_NAME" + + LOG_FILE="$NAS_ROOT/log/$HOST_NAME-backup-media.log" + + log INFO "*** *** Begin NAS backup (media/books, media/music)" + + EXCLUDE_DIRS=("@eaDir" "Temporary Items" "lost+found" serien tmp video "video.archive") + check_directory $TARGET_DIR + # media/books media/music + backup_all_dirs "/volume1/media" "$TARGET_DIR" "${EXCLUDE_DIRS[@]}" + + log INFO "*** End of NAS backup" + list_archives $TARGET_DIR + +} + +backup_data() { + + TARGET_DIR="$NAS_ROOT/daily/$HOST_NAME/backup.$DAY" + + LOG_FILE="$NAS_ROOT/log/$HOST_NAME-backup.log" + + log INFO "*** *** Begin NAS backup (data, nfs)" + EXCLUDE_DIRS=("@eaDir" "Temporary Items" "lost+found") + delete_dir $TARGET_DIR # bei Fehler werden die Dateien hoffentlich überschrieben + check_directory $TARGET_DIR + # data + backup_all_dirs "/volume1/data" "$TARGET_DIR" "${EXCLUDE_DIRS[@]}" + + EXCLUDE_DIRS=("@eaDir" "Temporary Items" "lost+found" backups) + # nfs + backup_all_dirs "/volume1/nfs" "$TARGET_DIR" "${EXCLUDE_DIRS[@]}" + + log INFO "*** End of NAS backup" + list_archives $TARGET_DIR +} + +# TODO: restore fn + + +## +# auto backup handling +# +# hostname-scripte setzen vars und definieren Funktionen backup_daily1, 2, usw. und backup_weekly1, 2, usw. +# in den Funktionen werden die vars für den einzelnen backuü gesetzt (target, excludes, usw.) +# im main-script wird dann dass hostname-script gesourced und in einer Schleife werden die +# entsprechenden backup_*-Funktionen aufgerufen +# +# count=1 +# while [ $(isFunction backup_daily$count) ]; do +# backup_daily$count # setzt die nötigen vars für den backup +# do_backup "daily" +# let "count += 1" +# done +# + +while :; do + case $1 in + --daily) + backup_data + break + ;; + --weekly) + backup_media + break + ;; + -l|--list) + list_archives "$NAS_ROOT" + break; + ;; + -c|--content) + if [ "$2" ]; then + list_archive_content "$NAS_ROOT" "$2" + else + echo 'ERROR: "--content" requires a non-empty option argument.' + fi + break + ;; + --restore) + # TODO + break + ;; + *) # Default case: No more options, so break out of the loop. + break + esac + + shift +done