#!/usr/bin/env bash set -uo pipefail IFS=$'\n\t' PATH="/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin" SCRIPT_NAME=$(basename "$0") SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" &>/dev/null && pwd) LOGFILE="/var/log/${SCRIPT_NAME%.sh}.log" # checkzfs-Defaults (in config.example überschreibbar, hier zentral statt an mehreren Stellen im Skript) CHECKZFS_MAX_AGE="${CHECKZFS_MAX_AGE:-1500,2000}" CHECKZFS_MAX_SNAPSHOT_COUNT="${CHECKZFS_MAX_SNAPSHOT_COUNT:-180,200}" CHECKZFS_SPOOL_MAXAGE="${CHECKZFS_SPOOL_MAXAGE:-100000}" CHECKZFS_DISABLED="${CHECKZFS_DISABLED:-0}" CHECKZFS_LOCAL="${CHECKZFS_LOCAL:-0}" CHECKZFS_SPOOL="${CHECKZFS_SPOOL:-1}" # Check_MK Spool-Staleness (Sekunden, Dateiname-Prefix) – zentral statt an jeder Stelle hartkodiert SPOOL_MAXAGE_PIGGYBACK="${SPOOL_MAXAGE_PIGGYBACK:-100000}" SPOOL_MAXAGE_CHECKZFS="${SPOOL_MAXAGE_CHECKZFS:-100000}" SPOOL_MAXAGE_CHECKPBS="${SPOOL_MAXAGE_CHECKPBS:-1200000}" SPOOL_MAXAGE_PBS_STATUS="${SPOOL_MAXAGE_PBS_STATUS:-90000}" detect_os() { case "$(uname -s)" in Linux) echo "linux" ;; FreeBSD) echo "freebsd" ;; *) echo "unknown" ;; esac } OS_TYPE="$(detect_os)" epoch_to_str() { local epoch="$1" if [[ "$OS_TYPE" == "freebsd" ]]; then date -r "$epoch" "+%Y-%m-%d %H:%M:%S" else date -d "@$epoch" "+%Y-%m-%d %H:%M:%S" fi } log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOGFILE" } usage() { echo "Usage:" echo " $0 -c /path/to/config # Full backup run" echo " $0 [function] # Run individual function" echo " $0 help # Show available functions" exit 1 } CONFIG_FILE="" while getopts "c:" opt; do case "$opt" in c) CONFIG_FILE="$OPTARG" ;; *) usage ;; esac done shift $((OPTIND - 1)) if [[ -n "${CONFIG_FILE:-}" ]]; then if [[ ! -f "$CONFIG_FILE" ]]; then log "ERROR: Configuration file not found: $CONFIG_FILE" exit 1 fi if ! bash -n "$CONFIG_FILE"; then log "Syntax error in configuration file $CONFIG_FILE" exit 1 fi source "$CONFIG_FILE" REQUIRED_VARS=( SOURCEPORT BACKUPSERVER ZSYNC MAINTDAY SHUTDOWN UPDATES SOURCEHOST ZFSTRGT ZPUSHTAG ZPUSHMINKEEP ZPUSHKEEP ZPUSHLABEL PBSHOST PBSPORT BACKUPSTORE BACKUPSTOREPBS BACKUPEXCLUDE REPLEXCLUDE DYNROUTE REBOOT ) if [[ "${DYNROUTE,,}" == "yes" ]]; then REQUIRED_VARS+=(DDNS_GATEWAY DDNS_SERVER) fi MISSING_VARS=() for var in "${REQUIRED_VARS[@]}"; do if [[ -z "${!var:-}" ]]; then MISSING_VARS+=("$var") fi done if [[ "${#MISSING_VARS[@]}" -gt 0 ]]; then log "Missing configuration variables:" for var in "${MISSING_VARS[@]}"; do log " - $var" done log "Aborting — please check your configuration file." exit 1 else log "All required configuration variables are set." fi fi SOURCEUSER="${SOURCEUSER:-root}" PBSUSER="${PBSUSER:-root}" EPIGGYBACK_USER="${EPIGGYBACK_USER:-root}" ECHECKZFS_USER="${ECHECKZFS_USER:-root}" # Für hooks.d/-Scripts verfügbar machen (laufen als eigener Prozess via `bash`) export SOURCEHOST SOURCEPORT SOURCEUSER PBSHOST PBSPORT PBSUSER BACKUPSTOREPBS rssh() { ssh -o BatchMode=yes -o ConnectTimeout=10 -p "$SOURCEPORT" "${SOURCEUSER}@${SOURCEHOST}" "$@"; } rscp() { scp -P "$SOURCEPORT" "$@"; } rpbssh() { ssh -o BatchMode=yes -o ConnectTimeout=10 -p "$PBSPORT" "${PBSUSER}@${PBSHOST}" "$@"; } detect_host_type() { if command -v pveversion >/dev/null 2>&1; then echo "pve" elif command -v proxmox-backup-server >/dev/null 2>&1; then echo "pbs" else echo "unknown" fi } get_sourcehostname() { if [[ -z "${SOURCEHOSTNAME:-}" ]]; then log "SOURCEHOSTNAME is empty, retrieving via SSH from $SOURCEHOST..." SOURCEHOSTNAME=$(rssh hostname) log "Detected SOURCEHOSTNAME: $SOURCEHOSTNAME" fi } zfs_replace() { local status status=$(zpool status) if echo "$status" | grep -q 'scan: resilver in progress'; then return 0 fi if echo "$status" | grep -qE 'replacing-[0-9]'; then return 0 fi return 1 } wait_replace() { local interval_seconds=300 # Alle 5 Minuten prüfen local waited_minutes=0 log "ZFS Replace-Vorgang erkannt – warte unbegrenzt auf Abschluss..." while true; do if ! zfs_replace; then log "Replace abgeschlossen nach $waited_minutes Minuten – fahre jetzt herunter." return 0 fi log "Replace läuft noch – erneut prüfen in $((interval_seconds/60)) Minuten..." sleep "$interval_seconds" ((waited_minutes+=interval_seconds/60)) done } set_wol_g_enabled() { if [[ "$OS_TYPE" == "freebsd" ]]; then log "FreeBSD erkannt – set_wol_g_enabled() wird übersprungen (ethtool/apt nicht verfügbar)." return 0 fi log "Checking if ethtool is installed..." if ! command -v ethtool >/dev/null 2>&1; then log "ethtool is not installed, attempting installation..." apt update && apt install -y ethtool || { log "Error: ethtool could not be installed." return 1 } else log "ethtool is already installed." fi log "Setting Wake-on-LAN (WOL) to 'g' on interfaces with static IP..." for iface in $(ls /sys/class/net | grep -vE '^(lo|tap|vmbr|veth|br|docker|bond|wl)'); do if [[ -e "/sys/class/net/$iface/device" ]]; then log "Processing physical interface: $iface" current_wol=$(ethtool "$iface" 2>/dev/null | awk '/Wake-on:/ {print $2}') if [[ "$current_wol" != "g" ]]; then log "Setting WOL to 'g' for $iface..." ethtool -s "$iface" wol g || log "Error setting WOL on $iface" else log "WOL already set to 'g' for $iface" fi if grep -qE "^\s*iface\s+$iface\s+inet\s+static" /etc/network/interfaces; then if ! grep -A 5 -E "^\s*iface\s+$iface\s+inet\s+static" /etc/network/interfaces | grep -q "post-up ethtool -s $iface wol g"; then log "Adding WOL command in static block for $iface..." sed -i "/^\s*iface\s\+$iface\s\+inet\s\+static/a \ post-up ethtool -s $iface wol g" /etc/network/interfaces else log "WOL command already present in static block for $iface." fi else log "No static entry found for $iface, no changes made." fi fi done } write_zsync_config() { get_sourcehostname local conf_file="/etc/bashclub/$SOURCEHOST.conf" log "Writing zsync config to $conf_file" { echo "target=$ZFSTRGT" echo "source=${SOURCEUSER}@${SOURCEHOST}" echo "sshport=$SOURCEPORT" echo "tag=$ZPUSHTAG" echo "snapshot_filter=\"$ZPUSHFILTER\"" echo "min_keep=$ZPUSHMINKEEP" echo "zfs_auto_snapshot_keep=$ZPUSHKEEP" echo "zfs_auto_snapshot_label=zfs-auto-snap_$ZPUSHLABEL" echo "zfs_auto_snapshot_engine=internal" echo "checkzfs_disabled=$CHECKZFS_DISABLED" echo "checkzfs_local=$CHECKZFS_LOCAL" echo "checkzfs_prefix=miyagi-$SOURCEHOSTNAME-$(hostname)-$ZPUSHTAG" echo "checkzfs_max_age=$CHECKZFS_MAX_AGE" echo "checkzfs_max_snapshot_count=$CHECKZFS_MAX_SNAPSHOT_COUNT" echo "checkzfs_spool=$CHECKZFS_SPOOL" echo "checkzfs_spool_maxage=$CHECKZFS_SPOOL_MAXAGE" } > "$conf_file" } run_zsync() { write_zsync_config if [[ "$ZSYNC" != "no" ]]; then /usr/bin/bashclub-zsync -c "/etc/bashclub/$SOURCEHOST.conf" else log "Zsync is disabled." fi } run_local_apt_updates() { log "Running updates on local system..." local TIMESTAMP TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") local SNAPSHOT_TAG="pve-update-via-miyagi" local MAX_SNAPSHOTS=5 local REBOOT_FLAG="${REBOOT^^}" local UPDATE_LOGFILE="/var/log/proxmox_update.log" log_msg() { local level="$1" local message="$2" local timestamp timestamp=$(date +"%Y-%m-%d %H:%M:%S") echo "[$timestamp] $message" echo "[$timestamp] [$level] $message" >> "$UPDATE_LOGFILE" } local HOST_TYPE HOST_TYPE=$(detect_host_type) local ZFS_DATASETS=() case "$HOST_TYPE" in pve) log_msg "INFO" "Systemtyp erkannt: PVE" ZFS_DATASETS=("rpool/ROOT/pve-1" "rpool/pveconf") ;; pbs) log_msg "INFO" "Systemtyp erkannt: PBS" ZFS_DATASETS=("rpool/ROOT/pbs-1") ;; *) log_msg "WARN" "Systemtyp (PVE/PBS) konnte nicht erkannt werden – ueberspringe ZFS-Snapshots." ;; esac # Prüfen, ob Updates verfügbar sind if ! apt update | tee -a "$UPDATE_LOGFILE" | grep -qi "upgradable"; then log_msg "INFO" "Keine Updates verfügbar." return fi log_msg "INFO" "Updates verfügbar. Erstelle Snapshots." for dataset in "${ZFS_DATASETS[@]}"; do if ! zfs list -H -o name "$dataset" >/dev/null 2>&1; then log_msg "WARN" "Dataset $dataset existiert nicht – übersprungen." continue fi snapshot="${dataset}@${SNAPSHOT_TAG}-${TIMESTAMP}" log_msg "INFO" "Erstelle Snapshot: $snapshot" zfs snapshot "$snapshot" # Alte Snapshots bereinigen log_msg "INFO" "Bereinige alte Snapshots in $dataset" old_snaps=$(zfs list -t snapshot -o name -s creation | grep "^${dataset}@${SNAPSHOT_TAG}-") snap_count=$(grep -c . <<< "$old_snaps") if (( snap_count > MAX_SNAPSHOTS )); then snaps_to_delete=$(echo "$old_snaps" | head -n $((snap_count - MAX_SNAPSHOTS))) while IFS= read -r snap; do log_msg "INFO" "Lösche alten Snapshot: $snap" zfs destroy "$snap" done <<< "$snaps_to_delete" fi done # System-Upgrade log_msg "INFO" "Starte dist-upgrade" if ! apt dist-upgrade -y | tee -a "$UPDATE_LOGFILE"; then log_msg "ERROR" "Fehler während dist-upgrade" return 1 fi log_msg "INFO" "Starte autoremove" apt autoremove -y | tee -a "$UPDATE_LOGFILE" # Kernel-Update prüfen if apt list --upgradable 2>/dev/null | grep -q "linux-image-"; then log_msg "WARN" "Kernel-Update erkannt. Neustart empfohlen." if [[ "$REBOOT_FLAG" == "YES" ]]; then log_msg "INFO" "REBOOT=YES erkannt. System wird neugestartet." reboot fi else log_msg "INFO" "Update abgeschlossen. Kein Neustart erforderlich." fi } run_pbs_apt_updates() { if [[ "${BACKUPSERVER,,}" == "yes" ]]; then log "Running updates on PBS host ($PBSHOST)..." rpbssh apt update && rpbssh apt dist-upgrade -y || { log "Error during updates on $PBSHOST" } else log "PBS updates skipped (BACKUPSERVER=$BACKUPSERVER)" fi } run_remote_updates() { if [[ "${UPDATES,,}" == "yes" ]]; then if [[ "$OS_TYPE" == "freebsd" ]]; then log "FreeBSD erkannt – lokales apt-Update wird übersprungen (apt nicht verfügbar)." else run_local_apt_updates fi # PBS-Host-Update läuft unabhängig vom lokalen Update-Ergebnis/Skript-Host-OS run_pbs_apt_updates run_hooks post-update else log "Updates disabled (UPDATES=$UPDATES)" fi } send_piggyback() { get_sourcehostname local combined_host="miyagi-${SOURCEHOSTNAME}-$(hostname)" local filename="${SPOOL_MAXAGE_PIGGYBACK}_${combined_host}" log "Reminder: Add a host named ${combined_host} in CMK (without Agent, Piggyback enabled)!" log "Creating piggyback file: $filename" { echo "<<<<${combined_host}>>>>" /usr/bin/check_mk_agent echo "<<<<>>>>" } > "$filename" if rscp "$filename" "${SOURCEUSER}@${SOURCEHOST}:/var/lib/check_mk_agent/spool/"; then log "Piggyback data successfully sent to $SOURCEHOST" else log "ERROR: Failed to send piggyback data to $SOURCEHOST" fi rm -f "$filename" } run_pbs_backup() { if [[ "${BACKUPSERVER,,}" != "yes" ]]; then log "PBS Backup übersprungen: BACKUPSERVER='$BACKUPSERVER' (muss 'yes' sein)" return 0 fi get_sourcehostname log "Starte PBS Backup auf Host: $SOURCEHOST" # PBS-Storage prüfen und ggf. aktivieren log "Prüfe, ob PBS-Storage '$BACKUPSTORE' auf $SOURCEHOST aktiviert ist..." if rssh "pvesm status | grep -w '$BACKUPSTORE' | grep -q 'disabled'"; then log "PBS-Storage '$BACKUPSTORE' ist deaktiviert – aktiviere temporär und warte 10 Sekunden..." rssh "pvesm set '$BACKUPSTORE' --disable 0 && sleep 10" pbs_enabled_by_script=true else log "PBS-Storage '$BACKUPSTORE' ist bereits aktiviert." pbs_enabled_by_script=false fi vzdump_success=false # Versuch: vzdump mit Change Detection ## Eintrag muss in /etc/vzdump.conf entries-max 100000000 log "Starte vzdump mit Change Detection..." if rssh vzdump --pbs-change-detection-mode metadata \ --node "$SOURCEHOSTNAME" --storage "$BACKUPSTORE" \ --exclude "$BACKUPEXCLUDE" --mode snapshot --all 1 \ --notes-template '{{guestname}}'; then log "vzdump (Change Detection) erfolgreich." vzdump_success=true else log "vzdump (Change Detection) fehlgeschlagen – versuche Fallback ohne Change Detection..." if rssh vzdump \ --node "$SOURCEHOSTNAME" --storage "$BACKUPSTORE" \ --exclude "$BACKUPEXCLUDE" --mode snapshot --all 1 \ --notes-template '{{guestname}}'; then log "Fallback-vzdump erfolgreich." vzdump_success=true else log "FEHLER: vzdump fehlgeschlagen – auch im Fallback." fi fi # PBS-Storage ggf. wieder deaktivieren if [[ "$vzdump_success" == true && "$pbs_enabled_by_script" == true ]]; then log "Deaktiviere temporär aktiviertes PBS-Storage '$BACKUPSTORE' auf $SOURCEHOST..." rssh "pvesm set '$BACKUPSTORE' --disable 1" fi # Monitoring-Ausgabe if [[ "$vzdump_success" == true ]]; then echo "0 DailyPBS - Daily Backup erfolgreich" > /tmp/cmk_tmp.out else echo "2 DailyPBS - Daily Backup FEHLGESCHLAGEN" > /tmp/cmk_tmp.out fi { echo "<<>>" cat /tmp/cmk_tmp.out } > "/tmp/${SPOOL_MAXAGE_CHECKPBS}_checkpbs" rscp "/tmp/${SPOOL_MAXAGE_CHECKPBS}_checkpbs" "${SOURCEUSER}@${SOURCEHOST}":/var/lib/check_mk_agent/spool \ || log "Fehler beim Übertragen des Monitoring-Outputs via SCP" rm -f /tmp/cmk_tmp.out "/tmp/${SPOOL_MAXAGE_CHECKPBS}_checkpbs" #write_pbs_status } run_maintenance() { if [[ "${BACKUPSERVER,,}" != "yes" ]]; then log "PBS Backup wird übersprungen (BACKUPSERVER=$BACKUPSERVER)" return fi if [[ "$(date +%u)" == "$MAINTDAY" ]]; then log "Running maintenance..." PRUNEJOB=$(rpbssh proxmox-backup-manager prune-job list --output-format json-pretty | grep -m 1 "id" | cut -d'"' -f4) rpbssh proxmox-backup-manager prune-job run "$PRUNEJOB" rpbssh proxmox-backup-manager garbage-collection start "$BACKUPSTOREPBS" rpbssh proxmox-backup-manager verify backup else log "No maintenance scheduled for today." fi } run_scrub_stop() { local mode="$1" # "local" oder "remote" local ssh_cmd=() if [[ "$mode" == "remote" ]]; then ssh_cmd=(ssh -o BatchMode=yes -o ConnectTimeout=10 -p "$SOURCEPORT" "${SOURCEUSER}@${SOURCEHOST}") fi "${ssh_cmd[@]}" bash -c ' for pool in $(zpool list -H -o name); do echo "Stopping scrub on pool: $pool" if zpool status "$pool" | grep -q "scrub in progress"; then if zpool scrub -s "$pool"; then echo "Scrub stopped on $pool" else echo "Error stopping scrub on $pool" fi else echo "No active scrub on $pool" fi done ' } write_pbs_status() { local lib_script="$SCRIPT_DIR/lib/pbs-status.sh" if [[ ! -x "$lib_script" ]]; then log "FEHLER: $lib_script nicht gefunden oder nicht ausführbar." return 1 fi PBSHOST="$PBSHOST" BACKUPSTOREPBS="$BACKUPSTOREPBS" \ PBS_REPO_USER="${PBS_REPO_USER:-}" PBS_PASSWORD="${PBS_PASSWORD:-}" \ PBSBACKUP_STATUS="${PBSBACKUP_STATUS:-}" \ SPOOL_MAXAGE_PBS_STATUS="$SPOOL_MAXAGE_PBS_STATUS" \ "$lib_script" | tee -a "$LOGFILE" } run_hooks() { local stage="$1" local hook_dir="/etc/miyagi-backup/hooks.d/$stage" if [[ ! -d "$hook_dir" ]]; then return 0 fi local hooks=() while IFS= read -r -d '' f; do hooks+=("$f") done < <(find "$hook_dir" -maxdepth 1 -name "*.sh" -executable -print0 | sort -z) if [[ ${#hooks[@]} -eq 0 ]]; then return 0 fi log "Führe $stage-Hooks aus ($hook_dir)..." for hook in "${hooks[@]}"; do log "Hook: $hook" if ! bash "$hook"; then log "WARNUNG: Hook $hook fehlgeschlagen (Exit $?) – fahre fort" fi done } shutdown_now() { if [[ "${SHUTDOWN,,}" == "yes" ]]; then if zfs_replace; then log "ZFS Replace-Vorgang erkannt – warte bis zum Abschluss..." if ! wait_replace; then log "Shutdown abgebrochen – Replace ist nach max. Wartezeit noch nicht abgeschlossen." return fi fi send_piggyback send_piggyback_external send_checkzfs_external run_hooks pre-shutdown log "Shutting down now...in 60sec" sleep 60 shutdown now else log "No shutdown requested." fi } send_piggyback_external() { if [[ "${EPIGGYBACK,,}" != "yes" ]]; then log "Externer Piggyback-Export deaktiviert." return fi get_sourcehostname log "Ermittelter SOURCEHOSTNAME: $SOURCEHOSTNAME" local combined_host="miyagi-${SOURCEHOSTNAME}-$(hostname)" local spool_file="${SPOOL_MAXAGE_PIGGYBACK}_${combined_host}_piggyback_external" local temp_dir temp_dir=$(mktemp -d) log "Erzeuge temporäre Piggyback-Datei: $temp_dir/$spool_file" { echo "<<<<${combined_host}>>>>" /usr/bin/check_mk_agent echo "<<<<>>>>" } > "$temp_dir/$spool_file" if scp -P "$EPIGGYBACK_PORT" "$temp_dir/$spool_file" "${EPIGGYBACK_USER}@${EPIGGYBACK_HOST}:/var/lib/check_mk_agent/spool/"; then log "Piggyback-Daten erfolgreich an $EPIGGYBACK_HOST gesendet: $spool_file" else log "Fehler beim Übertragen der Piggyback-Daten an $EPIGGYBACK_HOST" rm -rf "$temp_dir" return 1 fi rm -rf "$temp_dir" return 0 } send_checkzfs() { if [[ "${ZSYNC,,}" != "yes" ]]; then log "send_checkzfs: ZSYNC nicht aktiv – übersprungen" return 0 fi get_sourcehostname local checkzfs_cmd checkzfs_cmd="$(command -v checkzfs || true)" if [[ -z "$checkzfs_cmd" || ! -x "$checkzfs_cmd" ]]; then log "send_checkzfs: checkzfs nicht gefunden – übersprungen" return 0 fi # Defaults für checkzfs-Parameter (können via zsync-Config überschrieben werden) local checkzfs_prefix="${checkzfs_prefix:-miyagi-${SOURCEHOSTNAME}-$(hostname)-${ZPUSHTAG}}" local checkzfs_max_age="${checkzfs_max_age:-$CHECKZFS_MAX_AGE}" local checkzfs_max_snapshot_count="${checkzfs_max_snapshot_count:-$CHECKZFS_MAX_SNAPSHOT_COUNT}" # Optional zsync-Config laden falls vorhanden (überschreibt Defaults) local zsync_config="/etc/bashclub/${SOURCEHOST}.conf" [[ -f "$zsync_config" ]] && source "$zsync_config" local filter="" while IFS=$'\t' read -r name value src; do if [[ "$src" == "local" && "$value" == "subvols" ]]; then filter+="${name}/|" elif [[ "$src" == "local" && "$value" == "all" ]]; then filter+="${name}|" fi done < <(rssh "zfs get -H -o name,value,source -t filesystem,volume $ZPUSHTAG") filter="#${filter%|}" log "Generierter ZFS-Filter: $filter" local combined="miyagi-${SOURCEHOSTNAME}-$(hostname)-${ZPUSHTAG}" local spoolfile="/tmp/${combined}_checkzfs" local spooldest="${SPOOL_MAXAGE_CHECKZFS}_${combined}_checkzfs" log "Führe checkzfs aus..." { echo "<<>>" "$checkzfs_cmd" \ --source "${SOURCEUSER}@${SOURCEHOST}:${SOURCEPORT}" \ --filter "$filter" \ --replicafilter "^${ZFSTRGT}" \ --prefix "$checkzfs_prefix" \ --threshold "$checkzfs_max_age" \ --maxsnapshots "$checkzfs_max_snapshot_count" \ --output checkmk } > "$spoolfile" if rscp "$spoolfile" "${SOURCEUSER}@${SOURCEHOST}:/var/lib/check_mk_agent/spool/${spooldest}"; then log "CheckZFS-Daten an $SOURCEHOST gesendet: $spooldest" else log "FEHLER: CheckZFS-Daten konnten nicht übertragen werden" fi rm -f "$spoolfile" } send_checkzfs_external() { if [[ "${ECHECKZFS,,}" != "yes" ]]; then log "Externer CheckZFS deaktiviert." return fi local config="/etc/bashclub/${SOURCEHOST}.conf" if [[ ! -f "$config" ]]; then log "Konfigurationsdatei fehlt: $config" return 1 fi source "$config" get_sourcehostname # Setzt SOURCEHOSTNAME, wenn nicht vorhanden local checkzfs_cmd="${checkzfs_cmd:-$(command -v checkzfs)}" if [[ -z "$checkzfs_cmd" || ! -x "$checkzfs_cmd" ]]; then log "checkzfs nicht gefunden oder nicht ausführbar – Abbruch." return 1 fi log "Verwende checkzfs: $checkzfs_cmd" # Filter anhand ZFS-Tags auf dem SOURCEHOST aufbauen local filter="" while IFS=$'\t' read -r name value source; do if [[ "$source" == "local" && "$value" == "subvols" ]]; then filter+="${name}/|" elif [[ "$source" == "local" && "$value" == "all" ]]; then filter+="${name}|" fi done < <(rssh "zfs get -H -o name,value,source -t filesystem,volume $ZPUSHTAG") filter="#${filter%|}" log "Generierter ZFS-Filter: $filter" local combined="miyagi-${SOURCEHOSTNAME}-$(hostname)-${ZPUSHTAG}" local spoolfile="/tmp/${combined}_checkzfs_external" local spooldest="${SPOOL_MAXAGE_CHECKZFS}_${combined}_checkzfs_external" log "Führe checkzfs aus..." { echo "<<>>" "$checkzfs_cmd" \ --source ${SOURCEUSER}@"${SOURCEHOST}:${SOURCEPORT}" \ --filter "$filter" \ --replicafilter "^${ZFSTRGT}" \ --prefix "$checkzfs_prefix" \ --threshold "$checkzfs_max_age" \ --maxsnapshots "$checkzfs_max_snapshot_count" \ --output checkmk } > "$spoolfile" if scp -P "$ECHECKZFS_PORT" "$spoolfile" ${ECHECKZFS_USER}@"$ECHECKZFS_HOST":/var/lib/check_mk_agent/spool/"$spooldest"; then log "Spool-Datei erfolgreich übertragen an $ECHECKZFS_HOST:$spooldest" else log "Fehler beim Übertragen der Spool-Datei an $ECHECKZFS_HOST" fi rm -f "$spoolfile" } wait() { echo echo "###########################################" echo "# Das Backup startet in 60 Sekunden... #" echo "# Drücke [Enter], um sofort fortzufahren, #" echo "# oder warte, um automatisch zu starten. #" echo "###########################################" read -t 60 -p "Fortfahren (Enter drücken) oder warten... " input if [[ $? -eq 0 ]]; then log "Press ENTER to continue." else log "No press ENTER, wait for 60 Sec.." sleep 60 fi } set_dynamic_route() { local target="$SOURCEHOST" local gateway="$DDNS_GATEWAY" local dns_server="$DDNS_SERVER" local current_ip if [[ -z "$target" || -z "$gateway" ]]; then log "Fehler: SOURCEHOST oder DDNS_GATEWAY nicht gesetzt" return 1 fi # Ist SOURCEHOST bereits eine IPv4- oder IPv6-Adresse? if [[ "$target" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ || "$target" == *:* ]]; then current_ip="$target" log "SOURCEHOST ist bereits eine IP-Adresse: $current_ip" else log "Setze temporäre Route zu DNS-Server $dns_server via $gateway..." case "$(uname -s)" in Linux) ip route | grep -q "^$dns_server" || \ ip route add "$dns_server" via "$gateway" ;; FreeBSD) route -n get "$dns_server" >/dev/null 2>&1 || \ route add -host "$dns_server" "$gateway" ;; esac log "Löse IP von $target über DNS $dns_server..." current_ip=$(dig +short @"$dns_server" "$target" | awk 'NF {print; exit}') if [[ -z "$current_ip" ]]; then log "Fehler: Konnte IP für $target nicht auflösen" return 1 fi fi log "Ermittelte IP: $current_ip – setze Route via $gateway..." case "$(uname -s)" in Linux) ip route | grep -q "^$current_ip" || \ ip route add "$current_ip" via "$gateway" ;; FreeBSD) route -n get "$current_ip" >/dev/null 2>&1 || \ route add -host "$current_ip" "$gateway" ;; *) log "Nicht unterstütztes Betriebssystem: $(uname -s)" return 1 ;; esac log "Route erfolgreich gesetzt: $current_ip via $gateway" } # Main execution: if [[ $# -eq 0 ]]; then if [[ -n "$CONFIG_FILE" ]]; then wait log "Running full backup using configuration file: $CONFIG_FILE" if [[ "${DYNROUTE,,}" == "yes" ]]; then log "DYNROUTE ist aktiviert – setze dynamische Route für $SOURCEHOST..." set_dynamic_route else log "DYNROUTE ist deaktiviert oder nicht gesetzt." fi write_zsync_config run_zsync if [[ "${BACKUPSERVER,,}" == "yes" ]]; then log "BACKUPSERVER ist aktiviert, führe Backup aus..." run_pbs_backup run_maintenance run_hooks post-backup else log "BACKUPSERVER ist nicht aktiviert (BACKUPSERVER=$BACKUPSERVER), überspringe Backup." fi run_remote_updates if [[ "${EPIGGYBACK,,}" == "yes" ]]; then send_piggyback_external fi if [[ "${ECHECKZFS,,}" == "yes" ]]; then send_checkzfs_external fi shutdown_now else usage fi else # Funktionen die keine Config brauchen CONFIG_FREE_FUNCTIONS=(help detect_os detect_host_type zfs_replace wait_replace set_wol_g_enabled run_scrub_stop) case "$1" in help) declare -F | awk '{print $3}' | grep -v "^_" | grep -v "^main$" ;; *) if declare -F "$1" >/dev/null 2>&1; then config_required=true for fn in "${CONFIG_FREE_FUNCTIONS[@]}"; do [[ "$1" == "$fn" ]] && config_required=false && break done if [[ "$config_required" == true && -z "${SOURCEHOST:-}" ]]; then log "Funktion '$1' benötigt eine Config. Aufruf: $0 -c /etc/miyagi-backup/backup.conf $1" exit 1 fi "$@" else log "Function $1 not found." usage fi ;; esac fi