316 lines
11 KiB
Bash
316 lines
11 KiB
Bash
#!/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
|