v3: checkzfs/Spool-Staleness Variablen in config.example dokumentieren

This commit is contained in:
patrick committed 2026-09-15 12:36:00 +02:00
commit 42d3c45760
9 files changed
+1614

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
#Edit all Variables for best Experience
UPDATES='yes' # Proxmox VE und PBS Updates nach dem Lauf
SHUTDOWN='no' # System nach Ausführung herunterfahren?
DYNROUTE='yes' # DYNroute ja oder nein?
DDNS_GATEWAY='192.168.66.1' # DYNDNS Gateway
DDNS_SERVER='1.1.1.1' #DNS Server extern
REBOOT='no' # Reboot nach Update?
# Quelle (Proxmox VE System, das gesichert wird)
SOURCEPORT='22' # SSH-Port, normalerweise 22
SOURCEHOST='192.168.50.200' # IP des Quell-Proxmox-Servers
SOURCEUSER='root' # SSH-User für SOURCEHOST
# Replikation (ZFS)
ZFSTRGT='rpool-ssd1/repl/pve200' # Zielpfad für Replikation
# ZFS Zsync Replikation
ZSYNC='yes' # ZSYNC aktivieren (ja/nein)
ZPUSHTAG='bashclub:zsync-198-ssd' # Benutzer-Tag für ZFS
ZPUSHMINKEEP='3' # Mindestens zu behaltende Snapshots
ZPUSHKEEP='14' # Snapshots mit dem Tag, die behalten werden
ZPUSHLABEL='zsync-rz' # Suffix für Snapshot-Autoengine
ZPUSHFILTER='hourly|daily|weekly|monthly' # Weitere Filter (leer lassen oder Muster wie daily| weekly etc.)
# Backup mit Proxmox Backup Server
BACKUPSERVER='no' # Backup via PBS aktivieren?
MAINTDAY='7' # Wartungstag (1=Mo, 7=So)
PBSHOST='192.168.50.199' # IP des Proxmox Backup Servers
PBSPORT='22' # SSH-Port PBS
PBSUSER='root' # SSH-User für PBSHOST
PBSBACKUP_STATUS='86400' # Warnung ab 24h (86400s)
PBS_REPO_USER='root@pam' # PBS-User oder API-Token-ID (z.B. 'user@pbs!tokenname') für write_pbs_status
PBS_PASSWORD='' # Passwort oder API-Token-Secret zu PBS_REPO_USER (write_pbs_status)
BACKUPSTORE='backup' # Datastore auf Quell-Proxmox
BACKUPSTOREPBS='backup' # Datastore auf PBS
BACKUPEXCLUDE='10,3252,3253,3254' # VM/CT-IDs, die vom Backup ausgeschlossen sind
REPLEXCLUDE="$BACKUPEXCLUDE" # Diese auch von Replikation ausschließen
# Zusätzliche Monitoring-Ziele
# External Piggyback Host
EPIGGYBACK='no' # Piggyback-Daten an Monitoring-Ziel senden?
EPIGGYBACK_PORT='222' # SSH-Port für EPIGGYBACK_HOST
EPIGGYBACK_HOST='192.168.66.10' # Monitoring-Zielhost für Piggyback
EPIGGYBACK_USER='root' # SSH-User für EPIGGYBACK_HOST
# External Checkzfs Host
ECHECKZFS='no' # CheckZFS-Daten an externen Monitoring-Host senden?
ECHECKZFS_PORT='2222' # SSH-Port für ECHECKZFS_HOST
ECHECKZFS_HOST='192.168.50.210' # Monitoring-Zielhost für check_zfs
ECHECKZFS_USER='root' # SSH-User für ECHECKZFS_HOST
# checkzfs-Defaults (optional, sonst Skript-interne Defaults)
#CHECKZFS_MAX_AGE='1500,2000' # Schwellenwert Snapshot-Alter warn,crit (Minuten)
#CHECKZFS_MAX_SNAPSHOT_COUNT='180,200' # Schwellenwert Snapshot-Anzahl warn,crit
#CHECKZFS_SPOOL_MAXAGE='100000' # Check_MK Spool-Staleness (Sekunden)
#CHECKZFS_DISABLED='0' # checkzfs in bashclub-zsync-Config deaktivieren (1=ja)
#CHECKZFS_LOCAL='0' # checkzfs lokal statt remote ausführen (1=ja)
#CHECKZFS_SPOOL='1' # checkzfs Spool-Ausgabe aktivieren (1=ja)
# Check_MK Spool-Staleness (optional, sonst Skript-interne Defaults)
#SPOOL_MAXAGE_PIGGYBACK='100000' # Sekunden, für send_piggyback()/send_piggyback_external()
#SPOOL_MAXAGE_CHECKZFS='100000' # Sekunden, für send_checkzfs()/send_checkzfs_external()
#SPOOL_MAXAGE_CHECKPBS='1200000' # Sekunden, für run_pbs_backup() Monitoring-Ausgabe
#SPOOL_MAXAGE_PBS_STATUS='90000' # Sekunden, für write_pbs_status() / lib/pbs-status.sh
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Post-Backup-Hook: alte PBS-Task-Logs löschen (>60 Tage)
#
# Ablage: /etc/miyagi-backup/hooks.d/post-backup/
# Ausführung: chmod +x <hookname>.sh
# Reihenfolge: alphabetisch (01-foo.sh, 02-bar.sh, ...)
# Fehlerverhalten: Exit != 0 erzeugt WARNUNG im Log, weitere Hooks laufen weiter
#
# Migriert aus run_pbs_log_clear() (v3/miyagi-backup.sh). Braucht PBSHOST/
# PBSPORT/PBSUSER aus der miyagi-Config (werden vom Hauptskript exportiert).
set -uo pipefail
[[ -z "${PBSHOST:-}" ]] && { echo "PBSHOST nicht gesetzt – übersprungen."; exit 0; }
echo "Räume alte PBS-Task-Logs auf ($PBSHOST)..."
ssh -o BatchMode=yes -o ConnectTimeout=10 -p "${PBSPORT:-22}" "${PBSUSER:-root}@${PBSHOST}" \
find /var/log/proxmox-backup/tasks/ -type f -mtime +60 -delete \
|| echo "Fehler beim Aufräumen der PBS-Logs auf $PBSHOST"
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Post-Backup-Hook: alte vzdump-Logs auf SOURCEHOST löschen (>30 Tage)
#
# Ablage: /etc/miyagi-backup/hooks.d/post-backup/
# Ausführung: chmod +x <hookname>.sh
# Reihenfolge: alphabetisch (01-foo.sh, 02-bar.sh, ...)
# Fehlerverhalten: Exit != 0 erzeugt WARNUNG im Log, weitere Hooks laufen weiter
#
# Migriert aus run_pve_log_clear() (v3/miyagi-backup.sh). Braucht SOURCEHOST/
# SOURCEPORT/SOURCEUSER aus der miyagi-Config (werden vom Hauptskript exportiert).
set -uo pipefail
[[ -z "${SOURCEHOST:-}" ]] && { echo "SOURCEHOST nicht gesetzt – übersprungen."; exit 0; }
echo "Räume alte vzdump-Logs auf ($SOURCEHOST)..."
ssh -o BatchMode=yes -o ConnectTimeout=10 -p "${SOURCEPORT:-22}" "${SOURCEUSER:-root}@${SOURCEHOST}" \
find /var/log/vzdump -type f -mtime +30 -delete \
|| echo "Fehler beim Aufräumen der vzdump-Logs auf $SOURCEHOST"
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Post-Update-Hook: PVE-Notification-Target mail-to-root deaktivieren
#
# Ablage: /etc/miyagi-backup/hooks.d/post-update/
# Ausführung: chmod +x <hookname>.sh
# Reihenfolge: alphabetisch (01-foo.sh, 02-bar.sh, ...)
# Fehlerverhalten: Exit != 0 erzeugt WARNUNG im Log, weitere Hooks laufen weiter
#
# Migriert aus disable_pve_notifications() (v3/miyagi-backup.sh) — vormals
# unconditional in run_remote_updates() aufgerufen, jetzt hier als Hook,
# also nur aktiv wenn Datei ausführbar hier liegt.
set -uo pipefail
command -v pveversion >/dev/null 2>&1 || exit 0
if [[ "$(hostname -I 2>/dev/null)" == *"${SOURCEHOST:-}"* || "$(hostname)" == "${SOURCEHOST:-}" ]]; then
echo "Lokaler Host ist SOURCEHOST – Notification-Deaktivierung wird hier übersprungen."
exit 0
fi
echo "PVE erkannt – deaktiviere Notification-Target mail-to-root und default-matcher (keine lokalen Mails an root)."
pvesh set /cluster/notifications/endpoints/sendmail/mail-to-root --disable 1 >/dev/null 2>&1 \
|| echo "WARN: Notification-Target mail-to-root konnte nicht deaktiviert werden."
pvesh set /cluster/notifications/matchers/default-matcher --disable 1 >/dev/null 2>&1 \
|| echo "WARN: Notification-Matcher default-matcher konnte nicht deaktiviert werden."
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Beispiel Pre-Shutdown-Hook für miyagi-backup
#
# Ablage: /etc/miyagi-backup/hooks.d/pre-shutdown/
# Ausführung: chmod +x <hookname>.sh
# Reihenfolge: alphabetisch (01-foo.sh, 02-bar.sh, ...)
# Fehlerverhalten: Exit != 0 erzeugt WARNUNG im Log, Shutdown läuft weiter
#
# Dieser Hook wird vor dem 60s-Countdown in shutdown_now() ausgeführt.
# Hinweis: eigene sleep-Aufrufe addieren sich zum 60s-Countdown dazu.
# ── Beispiel 1: Monitoring-Agent vor Shutdown reporten ───────────────────────
#
# if [ -x /usr/local/bin/patchmon-agent ]; then
# /usr/local/bin/patchmon-agent report
# fi
# ── Beispiel 2: CheckMK-Agent aktualisieren ───────────────────────────────────
#
# if [ -x /usr/bin/cmk-agent-update ]; then
# /usr/bin/cmk-agent-update
# fi
# ── Beispiel 3: Dienst neu starten (nur wenn installiert) ────────────────────
#
# if systemctl list-unit-files wazuh-agent.service &>/dev/null; then
# systemctl restart wazuh-agent.service
# fi
# ── Beispiel 4: Eigenes Skript aufrufen ───────────────────────────────────────
#
# /opt/myscript/notify-shutdown.sh
exit 0
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Eigenständiges Script: schreibt PBS-Backup-Status als lokale Check_MK-Spooldatei.
# Wird von miyagi-backup.sh (write_pbs_status()) als eigener Prozess aufgerufen.
# Erwartet folgende Umgebungsvariablen: PBSHOST, BACKUPSTOREPBS, PBS_REPO_USER,
# PBS_PASSWORD, PBSBACKUP_STATUS (optional), SPOOL_MAXAGE_PBS_STATUS (optional).
set -uo pipefail
IFS=$'\n\t'
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
epoch_to_str() {
local epoch="$1"
case "$(uname -s)" in
FreeBSD) date -r "$epoch" "+%Y-%m-%d %H:%M:%S" ;;
*) date -d "@$epoch" "+%Y-%m-%d %H:%M:%S" ;;
esac
}
if [[ -z "${SPOOL_MAXAGE_PBS_STATUS:-}" ]]; then
log "SPOOL_MAXAGE_PBS_STATUS nicht gesetzt – PBS-Status-Ausgabe übersprungen."
exit 1
fi
spool_file="/var/lib/check_mk_agent/spool/${SPOOL_MAXAGE_PBS_STATUS}_checkpbs_local"
repo="${PBS_REPO_USER:-}@${PBSHOST:-}:${BACKUPSTOREPBS:-}"
if ! command -v proxmox-backup-client >/dev/null 2>&1; then
log "proxmox-backup-client nicht installiert – PBS-Status-Ausgabe übersprungen."
exit 0
fi
if [[ -z "${PBS_REPO_USER:-}" || -z "${PBS_PASSWORD:-}" ]]; then
log "PBS_REPO_USER oder PBS_PASSWORD nicht gesetzt – PBS-Status-Ausgabe übersprungen."
exit 1
fi
tmpfile=$(mktemp)
echo "<<<local>>>" > "$tmpfile"
now=$(date +%s)
# === Schwellenwert(e) parsen ===
warn_threshold=0
crit_threshold=86400 # Default kritisch ab 24h
if [[ -n "${PBSBACKUP_STATUS:-}" ]]; then
IFS=',' read -r t1 t2 <<< "$PBSBACKUP_STATUS"
if [[ "$t1" =~ ^[0-9]+$ && -z "$t2" ]]; then
# Nur ein Wert vorhanden
crit_threshold=$t1
log "Verwende einfachen Schwellenwert: CRIT=${crit_threshold}s"
elif [[ "$t1" =~ ^[0-9]+$ && "$t2" =~ ^[0-9]+$ ]]; then
warn_threshold=$t1
crit_threshold=$t2
log "Verwende gestaffelte Schwellenwerte: WARN=${warn_threshold}s, CRIT=${crit_threshold}s"
else
log "WARNUNG: Ungültiges PBSBACKUP_STATUS-Format – Fallback: CRIT=${crit_threshold}s"
fi
fi
# Backup-Daten extrahieren (direkt vom Skript-Host gegen PBSHOST, mit PBS_PASSWORD authentifiziert)
PBS_PASSWORD="$PBS_PASSWORD" proxmox-backup-client list --repository "$repo" --output-format json 2>/dev/null | \
grep -E '"backup-id"|"backup-type"|"backup-time"' | \
sed -E 's/[",]//g; s/^ *//' > "$tmpfile.json"
backup_type="" backup_id="" backup_time=""
while read -r line; do
case "$line" in
backup-type:*)
backup_type="${line#*: }"
;;
backup-id:*)
backup_id="${line#*: }"
;;
backup-time:*)
backup_time="${line#*: }"
if [[ -n "$backup_type" && -n "$backup_id" && -n "$backup_time" ]]; then
age=$((now - backup_time))
timestamp=$(epoch_to_str "$backup_time")
if (( age < warn_threshold )); then
status=0
elif (( age < crit_threshold )); then
status=1
else
status=2
fi
msg="$backup_type/$backup_id last backup $timestamp (age: $((age / 3600))h)"
echo "$status PBS_${backup_type}_${backup_id} - $msg" >> "$tmpfile"
# Reset
backup_type="" backup_id="" backup_time=""
fi
;;
esac
done < "$tmpfile.json"
rm -f "$tmpfile.json"
mv "$tmpfile" "$spool_file"
log "PBS Backup Status lokal geschrieben: $spool_file"
+840
View File
@@ -0,0 +1,840 @@
#!/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 "<<<local>>>"
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 "<<<local>>>"
"$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 "<<<local>>>"
"$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
+315
View File
@@ -0,0 +1,315 @@
#!/usr/bin/env bash
# miyagi-backup Installer v3
set -uo pipefail
FORCE_YES=0
DRY_RUN=0
DEV_MODE=0
CONFIG_FILE=""
INSTALL_DIR="/opt/miyagi-backup"
CONFIG_DIR="/etc/miyagi-backup"
HOOKS_DIR="${CONFIG_DIR}/hooks.d/pre-shutdown"
HOOKS_DIR_POST_UPDATE="${CONFIG_DIR}/hooks.d/post-update"
HOOKS_DIR_POST_BACKUP="${CONFIG_DIR}/hooks.d/post-backup"
SYMLINK="/usr/local/bin/miyagi-backup"
GITHUB_BASE="https://raw.githubusercontent.com/bashclub/miyagi-backup/main"
GITEA_BASE="https://gitea.perlbach24.de/scripte/miyagi-backup/raw/branch/main"
CHECKZFS_URL="https://gitea.perlbach24.de/scripte/check-zfs-replication/raw/branch/main/checkzfs.py"
CHECK_SNAPSHOT_AGE_URL="https://gitea.perlbach24.de/scripte/check-zfs-replication/raw/branch/main/check-snapshot-age"
CLEANSNAPS_URL="https://raw.githubusercontent.com/bashclub/zfs-housekeeping/refs/heads/main/cleansnaps.sh"
DISK_SMART_INFO_URL="https://raw.githubusercontent.com/bashclub/checkmk-smart/main/disk_smart_info.py"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
info() { echo -e "${BLUE}[..]${NC} $*"; }
warn() { echo -e "${YELLOW}[!!]${NC} $*"; }
die() { echo -e "${RED}[ERR]${NC} $*" >&2; exit 1; }
usage() {
cat <<EOF
Usage: $0 [OPTIONS]
-c <file> Konfigurationsdatei laden (für SSH-Check + ZFS-Tagging)
--yes Keine Rückfragen
--dry-run Zeigt nur was gemacht würde
--dev Installiere von Gitea (dev) statt GitHub (stable)
--help Diese Hilfe
EOF
exit 0
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-c) CONFIG_FILE="$2"; shift 2 ;;
--yes) FORCE_YES=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--dev) DEV_MODE=1; shift ;;
--help) usage ;;
*) die "Unbekannter Parameter: $1" ;;
esac
done
if [[ -n "$CONFIG_FILE" ]]; then
[[ ! -f "$CONFIG_FILE" ]] && die "Konfigurationsdatei nicht gefunden: $CONFIG_FILE"
bash -n "$CONFIG_FILE" || die "Syntaxfehler in $CONFIG_FILE"
source "$CONFIG_FILE"
SOURCEUSER="${SOURCEUSER:-root}"
fi
}
run() {
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "[DRY-RUN] $*"
else
"$@"
fi
}
confirm() {
[[ "$FORCE_YES" -eq 1 ]] && return 0
while true; do
read -rp "$1 [j/N]: " yn
case "$yn" in
[JjYy]*) return 0 ;;
[Nn]*|"") return 1 ;;
*) echo "Bitte j oder n eingeben." ;;
esac
done
}
ensure_dialog() {
if ! command -v dialog &>/dev/null; then
info "Installiere dialog..."
run apt-get install -y -qq dialog || die "dialog konnte nicht installiert werden"
fi
}
get_base_url() {
[[ "$DEV_MODE" -eq 1 ]] && echo "$GITEA_BASE" || echo "$GITHUB_BASE"
}
# ── Installationsschritte ─────────────────────────────────────────────────────
install_miyagi() {
local base
base="$(get_base_url)"
local source_label
[[ "$DEV_MODE" -eq 1 ]] && source_label="Gitea (dev)" || source_label="GitHub (stable)"
info "Installiere miyagi-backup von $source_label..."
run mkdir -p "$INSTALL_DIR"
run mkdir -p "$CONFIG_DIR"
run mkdir -p "$HOOKS_DIR"
run mkdir -p "$HOOKS_DIR_POST_UPDATE"
run mkdir -p "$HOOKS_DIR_POST_BACKUP"
run curl -fsSL "${base}/miyagi-backup.sh" -o "${INSTALL_DIR}/miyagi-backup.sh"
run chmod +x "${INSTALL_DIR}/miyagi-backup.sh"
# config.example nur holen, nie bestehende Config überschreiben
run curl -fsSL "${base}/config.example" -o "${CONFIG_DIR}/config.example"
# Example-Hook kopieren (nicht ausführbar = inaktiv bis User chmod +x setzt)
run curl -fsSL "${base}/hooks.d/pre-shutdown/example-hook.sh" \
-o "${HOOKS_DIR}/example-hook.sh"
# Standard-Hooks (ehemals fest im Skript, jetzt aktiv per Default, nicht
# neu überschreiben falls User sie schon selbst angepasst/deaktiviert hat)
if [[ ! -e "${HOOKS_DIR_POST_UPDATE}/10-disable-pve-notifications.sh" ]]; then
run curl -fsSL "${base}/hooks.d/post-update/10-disable-pve-notifications.sh" \
-o "${HOOKS_DIR_POST_UPDATE}/10-disable-pve-notifications.sh"
run chmod +x "${HOOKS_DIR_POST_UPDATE}/10-disable-pve-notifications.sh"
fi
if [[ ! -e "${HOOKS_DIR_POST_BACKUP}/10-pbs-log-clear.sh" ]]; then
run curl -fsSL "${base}/hooks.d/post-backup/10-pbs-log-clear.sh" \
-o "${HOOKS_DIR_POST_BACKUP}/10-pbs-log-clear.sh"
run chmod +x "${HOOKS_DIR_POST_BACKUP}/10-pbs-log-clear.sh"
fi
if [[ ! -e "${HOOKS_DIR_POST_BACKUP}/20-pve-log-clear.sh" ]]; then
run curl -fsSL "${base}/hooks.d/post-backup/20-pve-log-clear.sh" \
-o "${HOOKS_DIR_POST_BACKUP}/20-pve-log-clear.sh"
run chmod +x "${HOOKS_DIR_POST_BACKUP}/20-pve-log-clear.sh"
fi
# Symlink
run ln -sf "${INSTALL_DIR}/miyagi-backup.sh" "$SYMLINK"
ok "miyagi-backup installiert: ${INSTALL_DIR}/miyagi-backup.sh"
ok "Symlink: $SYMLINK"
ok "Config-Verzeichnis: $CONFIG_DIR"
ok "Hook-Verzeichnisse: $HOOKS_DIR, $HOOKS_DIR_POST_UPDATE, $HOOKS_DIR_POST_BACKUP"
}
install_checkzfs() {
info "Installiere checkzfs..."
run curl -fsSL "$CHECKZFS_URL" -o /usr/local/bin/checkzfs
run chmod +x /usr/local/bin/checkzfs
ok "checkzfs installiert"
}
install_check_snapshot_age() {
info "Installiere check-snapshot-age..."
run curl -fsSL "$CHECK_SNAPSHOT_AGE_URL" -o /usr/local/bin/check-snapshot-age
run chmod +x /usr/local/bin/check-snapshot-age
ok "check-snapshot-age installiert"
}
install_cleansnaps() {
info "Installiere cleansnaps..."
run curl -fsSL "$CLEANSNAPS_URL" -o /usr/local/bin/cleansnaps
run chmod +x /usr/local/bin/cleansnaps
ok "cleansnaps installiert"
}
install_disk_smart_info() {
info "Installiere disk_smart_info..."
run mkdir -p /usr/lib/check_mk_agent/plugins
run curl -fsSL "$DISK_SMART_INFO_URL" -o /usr/lib/check_mk_agent/plugins/disk_smart_info
run chmod +x /usr/lib/check_mk_agent/plugins/disk_smart_info
ok "disk_smart_info installiert"
}
install_ethtool() {
info "Installiere ethtool..."
run apt-get install -y -qq ethtool
ok "ethtool installiert"
}
check_ssh() {
[[ -z "${SOURCEHOST:-}" ]] && warn "Kein SOURCEHOST in Config – SSH-Check übersprungen" && return
info "Teste SSH-Verbindung zu ${SOURCEUSER}@${SOURCEHOST}:${SOURCEPORT}..."
if ssh -p "${SOURCEPORT:-22}" -o BatchMode=yes -o ConnectTimeout=5 \
"${SOURCEUSER}@${SOURCEHOST}" "echo ok" &>/dev/null; then
ok "SSH-Verbindung via Schlüssel funktioniert"
else
warn "Keine schlüsselbasierte SSH-Verbindung möglich"
if confirm "SSH-Schlüssel jetzt auf ${SOURCEHOST} kopieren (ssh-copy-id)?"; then
run ssh-copy-id -p "${SOURCEPORT:-22}" "${SOURCEUSER}@${SOURCEHOST}"
ok "Schlüssel kopiert – bitte Installer erneut starten zum Verifizieren"
exit 0
fi
fi
}
select_zfs_pools() {
[[ -z "${SOURCEHOST:-}" ]] && warn "Kein SOURCEHOST – ZFS-Tagging übersprungen" && return
info "Lade ZFS-Datasets von ${SOURCEHOST}..."
local local_hostname remote_hostname tag_key tag_value
local_hostname="$(hostname -s)"
remote_hostname="$(ssh -p "${SOURCEPORT:-22}" "${SOURCEUSER}@${SOURCEHOST}" "hostname -s" 2>/dev/null)" \
|| { warn "SSH-Verbindung fehlgeschlagen – ZFS-Tagging übersprungen"; return; }
tag_key="bashclub:miyagi-${remote_hostname}-${local_hostname}"
tag_value="subvols"
mapfile -t pools < <(ssh -p "${SOURCEPORT:-22}" "${SOURCEUSER}@${SOURCEHOST}" \
"zfs list -H -o name" 2>/dev/null | \
grep -E '/(vm|subvol)-' | \
grep -viE 'repl|replica' | \
sed 's:/[^/]*$::' | \
sort -u)
if [[ ${#pools[@]} -eq 0 ]]; then
warn "Keine geeigneten ZFS-Datasets gefunden"
return
fi
local options=()
for pool in "${pools[@]}"; do
local tag status
tag="$(ssh -p "${SOURCEPORT:-22}" "${SOURCEUSER}@${SOURCEHOST}" \
zfs get -H -o value "$tag_key" "$pool" 2>/dev/null)"
[[ "$tag" == "$tag_value" ]] && status="on" || status="off"
options+=("$pool" "" "$status")
done
local selected
selected=$(dialog --clear \
--backtitle "miyagi-backup Installer" \
--title "ZFS Pools taggen" \
--checklist "Datasets für Replikation auswählen:" 20 78 12 \
"${options[@]}" \
3>&1 1>&2 2>&3)
clear
[[ $? -ne 0 ]] && info "ZFS-Tagging abgebrochen" && return
local selected_array=()
read -ra selected_array <<< "$selected"
for pool in "${pools[@]}"; do
local is_selected=0
local prev_status="off"
for ((i=0; i<${#options[@]}; i+=3)); do
[[ "${options[i]}" == "$pool" ]] && prev_status="${options[i+2]}" && break
done
for sel in "${selected_array[@]}"; do
[[ "$sel" == "\"$pool\"" || "$sel" == "$pool" ]] && is_selected=1 && break
done
if [[ "$is_selected" -eq 1 && "$prev_status" == "off" ]]; then
info "Tagge $pool"
run ssh -p "${SOURCEPORT:-22}" "${SOURCEUSER}@${SOURCEHOST}" \
zfs set "${tag_key}=${tag_value}" "$pool"
elif [[ "$is_selected" -eq 0 && "$prev_status" == "on" ]]; then
info "Entferne Tag von $pool"
run ssh -p "${SOURCEPORT:-22}" "${SOURCEUSER}@${SOURCEHOST}" \
zfs inherit "$tag_key" "$pool"
fi
done
ok "ZFS-Tagging abgeschlossen"
}
# ── Hauptmenü ─────────────────────────────────────────────────────────────────
main() {
[[ $EUID -ne 0 ]] && die "Bitte als root ausführen"
ensure_dialog
echo ""
echo " ╔══════════════════════════════════════╗"
echo " ║ miyagi-backup Installer v3 ║"
echo " ╚══════════════════════════════════════╝"
echo ""
local choices
choices=$(dialog --clear \
--backtitle "miyagi-backup Installer v3" \
--title "Komponenten auswählen" \
--checklist "Was soll installiert werden?" 22 65 12 \
"miyagi" "miyagi-backup.sh + Verzeichnisse + Symlink" on \
"checkzfs" "checkzfs (ZFS-Replikationsprüfung)" on \
"snapage" "check-snapshot-age" on \
"cleansnaps" "cleansnaps (ZFS-Snapshot-Housekeeping)" on \
"smartinfo" "disk_smart_info (CheckMK Plugin)" on \
"ethtool" "ethtool (WOL-Unterstützung)" off \
"ssh" "SSH-Verbindung prüfen / Schlüssel kopieren" on \
"zfstag" "ZFS-Pools taggen (benötigt Config -c)" off \
3>&1 1>&2 2>&3)
clear
[[ $? -ne 0 ]] && echo "Abgebrochen." && exit 0
[[ -z "$choices" ]] && echo "Nichts ausgewählt." && exit 0
[[ "$choices" == *'"miyagi"'* || "$choices" == *'miyagi'* ]] && install_miyagi
[[ "$choices" == *'"checkzfs"'* || "$choices" == *'checkzfs'* ]] && install_checkzfs
[[ "$choices" == *'"snapage"'* || "$choices" == *'snapage'* ]] && install_check_snapshot_age
[[ "$choices" == *'"cleansnaps"'* || "$choices" == *'cleansnaps'* ]] && install_cleansnaps
[[ "$choices" == *'"smartinfo"'* || "$choices" == *'smartinfo'* ]] && install_disk_smart_info
[[ "$choices" == *'"ethtool"'* || "$choices" == *'ethtool'* ]] && install_ethtool
[[ "$choices" == *'"ssh"'* || "$choices" == *'ssh'* ]] && check_ssh
[[ "$choices" == *'"zfstag"'* || "$choices" == *'zfstag'* ]] && select_zfs_pools
echo ""
ok "Installation abgeschlossen."
echo ""
}
parse_args "$@"
main
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env bash
# miyagi-backup Updater v3
set -uo pipefail
DEV_MODE=0
INSTALL_DIR="/opt/miyagi-backup"
CONFIG_DIR="/etc/miyagi-backup"
HOOKS_DIR="${CONFIG_DIR}/hooks.d/pre-shutdown"
HOOKS_DIR_POST_UPDATE="${CONFIG_DIR}/hooks.d/post-update"
HOOKS_DIR_POST_BACKUP="${CONFIG_DIR}/hooks.d/post-backup"
SYMLINK="/usr/local/bin/miyagi-backup"
GITHUB_BASE="https://raw.githubusercontent.com/bashclub/miyagi-backup/main"
GITEA_BASE="https://gitea.perlbach24.de/scripte/miyagi-backup/raw/branch/main"
CHECKZFS_URL="https://gitea.perlbach24.de/scripte/check-zfs-replication/raw/branch/main/checkzfs.py"
CHECK_SNAPSHOT_AGE_URL="https://gitea.perlbach24.de/scripte/check-zfs-replication/raw/branch/main/check-snapshot-age"
CLEANSNAPS_URL="https://raw.githubusercontent.com/bashclub/zfs-housekeeping/refs/heads/main/cleansnaps.sh"
DISK_SMART_INFO_URL="https://raw.githubusercontent.com/bashclub/checkmk-smart/main/disk_smart_info.py"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
info() { echo -e "${BLUE}[..]${NC} $*"; }
warn() { echo -e "${YELLOW}[!!]${NC} $*"; }
die() { echo -e "${RED}[ERR]${NC} $*" >&2; exit 1; }
usage() {
cat <<EOF
Usage: $0 [--dev]
--dev Update von Gitea (dev) statt GitHub (stable)
--help Diese Hilfe
EOF
exit 0
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--dev) DEV_MODE=1; shift ;;
--help) usage ;;
*) die "Unbekannter Parameter: $1" ;;
esac
done
}
get_base_url() {
[[ "$DEV_MODE" -eq 1 ]] && echo "$GITEA_BASE" || echo "$GITHUB_BASE"
}
# ── Self-Update ───────────────────────────────────────────────────────────────
self_update() {
local base
base="$(get_base_url)"
local self="${INSTALL_DIR}/miyagi-update.sh"
if [[ -f "$self" ]] && command -v curl >/dev/null; then
curl -fsSL "${base}/miyagi-update.sh" -o "${self}.new" 2>/dev/null \
&& mv "${self}.new" "$self" \
&& chmod +x "$self" \
&& info "Updater aktualisiert" \
|| rm -f "${self}.new"
fi
}
# ── miyagi-backup.sh updaten ─────────────────────────────────────────────────
update_miyagi() {
local base
base="$(get_base_url)"
local source_label
[[ "$DEV_MODE" -eq 1 ]] && source_label="Gitea (dev)" || source_label="GitHub (stable)"
info "Aktualisiere miyagi-backup.sh von $source_label..."
if [[ ! -d "$INSTALL_DIR" ]]; then
warn "Installverzeichnis $INSTALL_DIR fehlt – bitte zuerst miyagi-install.sh ausführen"
return 1
fi
curl -fsSL "${base}/miyagi-backup.sh" -o "${INSTALL_DIR}/miyagi-backup.sh.new" || {
warn "Download fehlgeschlagen – miyagi-backup.sh bleibt unverändert"
return 1
}
mv "${INSTALL_DIR}/miyagi-backup.sh.new" "${INSTALL_DIR}/miyagi-backup.sh"
chmod +x "${INSTALL_DIR}/miyagi-backup.sh"
# Symlink sicherstellen
ln -sf "${INSTALL_DIR}/miyagi-backup.sh" "$SYMLINK"
# config.example aktualisieren (nie .conf überschreiben)
curl -fsSL "${base}/config.example" -o "${CONFIG_DIR}/config.example.new" 2>/dev/null \
&& mv "${CONFIG_DIR}/config.example.new" "${CONFIG_DIR}/config.example" \
|| warn "config.example konnte nicht aktualisiert werden"
# Hooks-Verzeichnisse sicherstellen
mkdir -p "$HOOKS_DIR" "$HOOKS_DIR_POST_UPDATE" "$HOOKS_DIR_POST_BACKUP"
# Example-Hook aktualisieren (nicht ausführbar = bleibt inaktiv)
curl -fsSL "${base}/hooks.d/pre-shutdown/example-hook.sh" \
-o "${HOOKS_DIR}/example-hook.sh" 2>/dev/null \
&& info "example-hook.sh aktualisiert" \
|| warn "example-hook.sh konnte nicht aktualisiert werden"
# Standard-Hooks (ehemals fest im Skript, seit dieser Version als Hook
# ausgelagert) einmalig anlegen, damit bestehende Installationen ihr
# bisheriges Verhalten behalten. Bereits vorhandene Dateien (ggf. vom
# User angepasst/deaktiviert) werden nicht überschrieben.
if [[ ! -e "${HOOKS_DIR_POST_UPDATE}/10-disable-pve-notifications.sh" ]]; then
curl -fsSL "${base}/hooks.d/post-update/10-disable-pve-notifications.sh" \
-o "${HOOKS_DIR_POST_UPDATE}/10-disable-pve-notifications.sh" 2>/dev/null \
&& chmod +x "${HOOKS_DIR_POST_UPDATE}/10-disable-pve-notifications.sh" \
&& info "Hook 10-disable-pve-notifications.sh neu angelegt (aktiv)" \
|| warn "Hook 10-disable-pve-notifications.sh konnte nicht angelegt werden"
fi
if [[ ! -e "${HOOKS_DIR_POST_BACKUP}/10-pbs-log-clear.sh" ]]; then
curl -fsSL "${base}/hooks.d/post-backup/10-pbs-log-clear.sh" \
-o "${HOOKS_DIR_POST_BACKUP}/10-pbs-log-clear.sh" 2>/dev/null \
&& chmod +x "${HOOKS_DIR_POST_BACKUP}/10-pbs-log-clear.sh" \
&& info "Hook 10-pbs-log-clear.sh neu angelegt (aktiv)" \
|| warn "Hook 10-pbs-log-clear.sh konnte nicht angelegt werden"
fi
if [[ ! -e "${HOOKS_DIR_POST_BACKUP}/20-pve-log-clear.sh" ]]; then
curl -fsSL "${base}/hooks.d/post-backup/20-pve-log-clear.sh" \
-o "${HOOKS_DIR_POST_BACKUP}/20-pve-log-clear.sh" 2>/dev/null \
&& chmod +x "${HOOKS_DIR_POST_BACKUP}/20-pve-log-clear.sh" \
&& info "Hook 20-pve-log-clear.sh neu angelegt (aktiv)" \
|| warn "Hook 20-pve-log-clear.sh konnte nicht angelegt werden"
fi
ok "miyagi-backup.sh aktualisiert"
}
# ── Abhängigkeiten updaten (nur wenn bereits installiert) ─────────────────────
update_dep() {
local name="$1"
local url="$2"
local target="$3"
if [[ ! -f "$target" ]]; then
info "$name nicht installiert – übersprungen (install mit miyagi-install.sh)"
return 0
fi
info "Aktualisiere $name..."
curl -fsSL "$url" -o "${target}.new" 2>/dev/null || {
warn "$name Download fehlgeschlagen – bleibt unverändert"
rm -f "${target}.new"
return 1
}
mv "${target}.new" "$target"
chmod +x "$target"
ok "$name aktualisiert"
}
# ── Status-Zusammenfassung ────────────────────────────────────────────────────
print_status() {
echo ""
echo " ┌──────────────────────────────────────────┐"
printf " │ %-40s│\n" "miyagi-backup $([ -f "${INSTALL_DIR}/miyagi-backup.sh" ] && echo "✓ installiert" || echo "✗ fehlt")"
printf " │ %-40s│\n" "checkzfs $(command -v checkzfs &>/dev/null && echo "✓" || echo "✗ nicht installiert")"
printf " │ %-40s│\n" "check-snap-age $(command -v check-snapshot-age &>/dev/null && echo "✓" || echo "✗ nicht installiert")"
printf " │ %-40s│\n" "cleansnaps $(command -v cleansnaps &>/dev/null && echo "✓" || echo "✗ nicht installiert")"
printf " │ %-40s│\n" "ethtool $(command -v ethtool &>/dev/null && echo "✓" || echo "✗ nicht installiert")"
echo " └──────────────────────────────────────────┘"
echo ""
}
# ── Main ──────────────────────────────────────────────────────────────────────
main() {
[[ $EUID -ne 0 ]] && die "Bitte als root ausführen"
echo ""
echo " ╔══════════════════════════════════════╗"
echo " ║ miyagi-backup Updater v3 ║"
echo " ╚══════════════════════════════════════╝"
echo ""
self_update
update_miyagi
update_dep "checkzfs" "$CHECKZFS_URL" "/usr/local/bin/checkzfs"
update_dep "check-snapshot-age" "$CHECK_SNAPSHOT_AGE_URL" "/usr/local/bin/check-snapshot-age"
update_dep "cleansnaps" "$CLEANSNAPS_URL" "/usr/local/bin/cleansnaps"
update_dep "disk_smart_info" "$DISK_SMART_INFO_URL" "/usr/lib/check_mk_agent/plugins/disk_smart_info"
print_status
ok "Update abgeschlossen."
}
parse_args "$@"
main