#!/usr/bin/env bash # ===================================================================== # migrate_creator.sh # # Wird auf den QUELLSERVER kopiert (klassische Checkmk-/OMD-Direkt- # installation auf Bare-Metal oder VM) und dort als root ausgefuehrt. # # Aufgabe: eine oder mehrere Sites auswaehlen und daraus ein Migrations- # Bundle (.tar.gz) erzeugen, das auf dem Zielsystem mit # ./scripts/cmk-manage.sh import --file # in den Checkmk-Container uebernommen wird. # # Die Agents muessen dabei NICHT neu eingerichtet werden, solange # * der Site-Name unveraendert bleibt und # * das Zielsystem dieselbe IP-Adresse / denselben DNS-Namen bekommt. # Die Site-Zertifikate (Agent-Registrierung) sind im omd-Backup enthalten. # # Dieses Skript ist bewusst eigenstaendig - es hat ausser bash, tar und # omd keine Abhaengigkeiten. Fuer die Oberflaeche wird dialog oder # whiptail genutzt, falls vorhanden. # ===================================================================== set -euo pipefail VERSION="1.0" BUNDLE_DIRNAME="cmkbundle" OUT_DIR="${PWD}" declare -a SITES=() ALL=""; STOP_SITE=""; ASSUME_YES=""; USE_GUI=""; SCP_TARGET=""; LABEL="" declare -a OMD_OPTS=() if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m' C_BLU=$'\033[34m'; C_BLD=$'\033[1m'; C_RST=$'\033[0m' else C_RED=; C_GRN=; C_YEL=; C_BLU=; C_BLD=; C_RST= fi log() { printf '%s[ INFO ]%s %s\n' "$C_BLU" "$C_RST" "$*"; } ok() { printf '%s[ OK ]%s %s\n' "$C_GRN" "$C_RST" "$*"; } warn() { printf '%s[ WARN ]%s %s\n' "$C_YEL" "$C_RST" "$*" >&2; } err() { printf '%s[FEHLER]%s %s\n' "$C_RED" "$C_RST" "$*" >&2; } die() { err "$*"; exit 1; } usage() { cat <<'EOF' migrate_creator.sh - Migrations-Bundle einer Checkmk-Direktinstallation erzeugen AUFRUF migrate_creator.sh [Optionen] OPTIONEN -l, --list vorhandene Sites anzeigen und beenden -s, --site SITE Site auswaehlen (mehrfach angebbar) -a, --all alle Sites sichern -o, --out VERZEICHNIS Zielverzeichnis fuer die .tar.gz-Bundles (Vorgabe: aktuelles) --label TEXT Freitext-Kennzeichnung im Dateinamen --no-rrds Performance-Daten (RRDs) auslassen -> kleineres Archiv --no-logs Logdateien auslassen --stop Site waehrend des Backups stoppen (konsistenteste Variante) --scp ZIEL Bundle anschliessend per scp uebertragen, z.B. --scp root@10.0.0.5:/opt/checkmk-stack/backups/ -g, --gui ncurses-Oberflaeche (dialog/whiptail) -y, --yes keine Rueckfragen -h, --help diese Hilfe BEISPIELE ./migrate_creator.sh --list ./migrate_creator.sh --gui ./migrate_creator.sh --site prod --stop --out /var/tmp ./migrate_creator.sh --all --no-logs --scp root@10.0.0.5:/opt/checkmk-stack/backups/ EOF } # --------------------------------------------------------------------- # Hilfsfunktionen # --------------------------------------------------------------------- human_size() { local b="${1:-0}" if [ "$b" -ge 1073741824 ] 2>/dev/null; then awk -v b="$b" 'BEGIN{printf "%.1f GiB", b/1073741824}' elif [ "$b" -ge 1048576 ] 2>/dev/null; then awk -v b="$b" 'BEGIN{printf "%.1f MiB", b/1048576}' elif [ "$b" -ge 1024 ] 2>/dev/null; then awk -v b="$b" 'BEGIN{printf "%.1f KiB", b/1024}' else echo "${b} B"; fi } file_size() { stat -c %s "$1" 2>/dev/null || echo 0; } confirm() { [ -n "$ASSUME_YES" ] && return 0 local a; read -r -p "${1:-Fortfahren?} [j/N] " a /dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1);exit}}' \ || hostname -I 2>/dev/null | awk '{print $1}' } all_ips() { hostname -I 2>/dev/null | tr ' ' '\n' | sed '/^$/d' | paste -sd, - ; } list_sites() { ls -1 /omd/sites 2>/dev/null | sed '/^$/d'; } site_version(){ omd version -b "$1" 2>/dev/null | tr -d '\r'; } site_state() { local rc=0; omd status --bare "$1" >/dev/null 2>&1 || rc=$? case "$rc" in 0) echo laeuft ;; 1) echo teilweise ;; *) echo gestoppt ;; esac; } site_size() { du -sb "/omd/sites/$1" 2>/dev/null | awk '{print $1}'; } # --------------------------------------------------------------------- # TUI-Helfer # --------------------------------------------------------------------- ui_init() { if command -v dialog >/dev/null 2>&1; then UI_BIN=dialog elif command -v whiptail >/dev/null 2>&1; then UI_BIN=whiptail else UI_BIN=""; fi UI_BACKTITLE="migrate_creator $VERSION - $(hostname)" } # --- Fenstergroesse berechnen ---------------------------------------- # Wie im Hauptskript: die Groesse ergibt sich aus Terminal, Textlaenge # und Anzahl der Eintraege. Feste Werte fuehrten bei langem Text dazu, # dass von der Auswahlliste nur noch eine Zeile sichtbar war. # Hier bewusst mit if-Bloecken statt "[ ... ] && VAR=..." - letzteres # liefert 1, wenn die Bedingung nicht zutrifft, und beendet unter # "set -e" die Funktion. ui_term_size() { UI_ROWS="$(tput lines 2>/dev/null || true)" UI_COLS="$(tput cols 2>/dev/null || true)" case "$UI_ROWS" in ''|*[!0-9]*) UI_ROWS=24 ;; esac case "$UI_COLS" in ''|*[!0-9]*) UI_COLS=80 ;; esac if [ "$UI_ROWS" -lt 10 ]; then UI_ROWS=24; fi if [ "$UI_COLS" -lt 40 ]; then UI_COLS=80; fi return 0 } ui_calc() { # text felder_je_eintrag eintraege... local text="$1" per="$2"; shift 2 ui_term_size local tl maxw tl="$(printf '%s\n' "$text" | wc -l)" maxw="$(printf '%s\n' "$text" | awk '{ if (length($0) > m) m = length($0) } END { print m+0 }')" local -a args=("$@") local i=0 w itemw=0 n=0 while [ "$i" -lt "${#args[@]}" ]; do w=$(( ${#args[$i]} + 4 )) if [ $((i+1)) -lt "${#args[@]}" ]; then w=$(( w + ${#args[$((i+1))]} )); fi if [ "$w" -gt "$itemw" ]; then itemw="$w"; fi i=$(( i + per )); n=$(( n + 1 )) done if [ "$maxw" -gt "$itemw" ]; then UI_W=$(( maxw + 10 )); else UI_W=$(( itemw + 10 )); fi if [ "$UI_W" -lt 60 ]; then UI_W=60; fi if [ "$UI_W" -gt $(( UI_COLS - 4 )) ]; then UI_W=$(( UI_COLS - 4 )); fi local avail=$(( UI_ROWS - tl - 9 )) if [ "$avail" -lt 3 ]; then avail=3; fi if [ "$n" -lt "$avail" ]; then UI_MH="$n"; else UI_MH="$avail"; fi if [ "$UI_MH" -lt 1 ]; then UI_MH=1; fi UI_H=$(( tl + UI_MH + 8 )) if [ "$UI_H" -gt $(( UI_ROWS - 2 )) ]; then UI_H=$(( UI_ROWS - 2 )); fi if [ "$UI_H" -lt 10 ]; then UI_H=10; fi return 0 } ui_calc_text() { # text zusatzzeilen local text="$1" extra="${2:-7}" ui_term_size local tl maxw tl="$(printf '%s\n' "$text" | wc -l)" maxw="$(printf '%s\n' "$text" | awk '{ if (length($0) > m) m = length($0) } END { print m+0 }')" UI_W=$(( maxw + 10 )) if [ "$UI_W" -lt 60 ]; then UI_W=60; fi if [ "$UI_W" -gt $(( UI_COLS - 4 )) ]; then UI_W=$(( UI_COLS - 4 )); fi UI_H=$(( tl + extra )) if [ "$UI_H" -gt $(( UI_ROWS - 2 )) ]; then UI_H=$(( UI_ROWS - 2 )); fi if [ "$UI_H" -lt 8 ]; then UI_H=8; fi return 0 } # --- Engstellen: hierueber laufen ALLE Dialoge ------------------------ _ui_list() { # widget titel text felder_je_eintrag eintraege... local widget="$1" t="$2" x="$3" per="$4"; shift 4 ui_calc "$x" "$per" "$@" "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$t" \ "$widget" "$x" "$UI_H" "$UI_W" "$UI_MH" "$@" 3>&1 1>&2 2>&3 } _ui_box() { # widget titel text zusatzzeilen [weitere] local widget="$1" t="$2" x="$3" extra="$4"; shift 4 ui_calc_text "$x" "$extra" case "$widget" in --inputbox|--passwordbox) "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$t" \ "$widget" "$x" "$UI_H" "$UI_W" "$@" 3>&1 1>&2 2>&3 ;; *) "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$t" \ "$widget" "$x" "$UI_H" "$UI_W" "$@" ;; esac } _ui_file() { # titel datei ui_term_size "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --textbox "$2" \ $(( UI_ROWS - 2 )) $(( UI_COLS - 4 )) } ui_menu() { local t="$1" x="$2"; shift 2; _ui_list --menu "$t" "$x" 2 "$@"; } ui_checklist() { local t="$1" x="$2"; shift 2; _ui_list --checklist "$t" "$x" 3 "$@"; } ui_input() { _ui_box --inputbox "$1" "$2" 10 "${3:-}"; } ui_msg() { _ui_box --msgbox "$1" "$2" 7; } ui_yesno() { _ui_box --yesno "$1" "$2" 8; } ui_info() { _ui_box --infobox "$1" "$2" 5; } ui_text() { _ui_file "$1" "$2"; } # --------------------------------------------------------------------- # Bundle erzeugen # --------------------------------------------------------------------- create_bundle() { local site="$1" local version state ts outfile tmp restarted="" sum size version="$(site_version "$site")" [ -n "$version" ] || die "Konnte die Version der Site '$site' nicht ermitteln." state="$(site_state "$site")" ts="$(date +%Y%m%d-%H%M%S)" outfile="$OUT_DIR/cmkmigrate_${site}_${version}${LABEL:+_$LABEL}_${ts}.tar.gz" log "Site '$site' Version $version Status $state Groesse $(human_size "$(site_size "$site")")" if [ -n "$STOP_SITE" ] && [ "$state" != "gestoppt" ]; then log "Stoppe Site '$site' ..." omd stop "$site" >/dev/null restarted=1 fi tmp="$(mktemp -d)" mkdir -p "$tmp/$BUNDLE_DIRNAME/siteinfo" log "Erzeuge omd-Backup ${OMD_OPTS[*]+(${OMD_OPTS[*]})} - das kann dauern ..." if ! omd backup ${OMD_OPTS[@]+"${OMD_OPTS[@]}"} "$site" "$tmp/$BUNDLE_DIRNAME/site.tar.gz"; then [ -n "$restarted" ] && omd start "$site" >/dev/null || true rm -rf "$tmp" die "omd backup der Site '$site' ist fehlgeschlagen." fi if [ -n "$restarted" ]; then log "Starte Site '$site' wieder ..." omd start "$site" >/dev/null || warn "Site '$site' konnte nicht wieder gestartet werden." fi # --- Zusatzinformationen (rein informativ, fuer die Nachkontrolle) --- omd config "$site" show >"$tmp/$BUNDLE_DIRNAME/siteinfo/omd-config.txt" 2>/dev/null || true omd status --bare "$site" >"$tmp/$BUNDLE_DIRNAME/siteinfo/omd-status.txt" 2>/dev/null || true ip -4 addr show >"$tmp/$BUNDLE_DIRNAME/siteinfo/ip-addr.txt" 2>/dev/null || true { echo "hostname : $(hostname -f 2>/dev/null || hostname)" echo "ips : $(all_ips)" echo "kernel : $(uname -r)" [ -r /etc/os-release ] && (. /etc/os-release && echo "os : $PRETTY_NAME") echo "omd sites:"; omd sites 2>/dev/null || true } >"$tmp/$BUNDLE_DIRNAME/siteinfo/system.txt" # Anzahl registrierter Agent-Verbindungen (nur zur Kontrolle nach dem Import) if [ -d "/omd/sites/$site/var/agent-receiver" ]; then find "/omd/sites/$site/var/agent-receiver" -maxdepth 2 -type d 2>/dev/null \ | wc -l >"$tmp/$BUNDLE_DIRNAME/siteinfo/agent-receiver-entries.txt" || true fi sum="$(sha256sum "$tmp/$BUNDLE_DIRNAME/site.tar.gz" | awk '{print $1}')" size="$(file_size "$tmp/$BUNDLE_DIRNAME/site.tar.gz")" printf '%s site.tar.gz\n' "$sum" >"$tmp/$BUNDLE_DIRNAME/site.tar.gz.sha256" cat >"$tmp/$BUNDLE_DIRNAME/MANIFEST" </dev/null || hostname) SOURCE_IP=$(primary_ip) SOURCE_IPS=$(all_ips) SOURCE_OS=$( [ -r /etc/os-release ] && (. /etc/os-release && echo "$PRETTY_NAME") || echo unbekannt ) CREATED=$(date -Is) CREATED_BY=migrate_creator.sh/$VERSION ARCHIVE=site.tar.gz ARCHIVE_SHA256=$sum ARCHIVE_SIZE=$size OMD_BACKUP_OPTS=${OMD_OPTS[*]-} LABEL=$LABEL SITE_STOPPED_DURING_BACKUP=$([ -n "$STOP_SITE" ] && echo yes || echo no) AGENT_RECEIVER_PORT=8000 EOF cat >"$tmp/$BUNDLE_DIRNAME/INFO.txt" </dev/null || hostname) Quell-IP(s) : $(all_ips) Erstellt : $(date -Is) Nutzdaten : site.tar.gz ($(human_size "$size")) SHA256 : $sum Import auf dem Zielsystem (Container-Installation): 1) Bundle in das Verzeichnis ./backups des Projekts kopieren 2) Passendes Image beschaffen (Version MUSS uebereinstimmen): ./scripts/cmk-manage.sh image pull --edition $( case "$version" in *.cre) echo raw ;; *.cee) echo enterprise ;; *.cce) echo cloud ;; *.cme) echo managed ;; *) echo raw ;; esac) --version ${version%.*} Die Enterprise-Edition liegt NICHT auf Docker Hub - dort zusaetzlich --user/--password (Checkmk-Kundenportal) angeben oder "image download" verwenden. Danach: ./scripts/cmk-manage.sh up 3) Import ausfuehren: ./scripts/cmk-manage.sh import --file backups/$(basename "$outfile") --set-default-site Damit die vorhandenen Agents ohne Neuinstallation weiterarbeiten: * Der Site-Name muss "$site" bleiben (kein --as-site verwenden). * Das Zielsystem muss die IP/den DNS-Namen des Quellservers uebernehmen. * Port 8000 (Agent-Receiver) und 443/80 (Web) muessen erreichbar sein. * Der alte Server sollte danach abgeschaltet bzw. dessen Site gestoppt werden. EOF log "Verpacke Bundle ..." mkdir -p "$OUT_DIR" tar -czf "$outfile" -C "$tmp" "$BUNDLE_DIRNAME" rm -rf "$tmp" ok "Bundle erstellt: $outfile ($(human_size "$(file_size "$outfile")"))" LAST_BUNDLE="$outfile" if [ -n "$SCP_TARGET" ]; then log "Uebertrage per scp nach $SCP_TARGET ..." scp "$outfile" "$SCP_TARGET" && ok "Uebertragung abgeschlossen." \ || warn "scp fehlgeschlagen - bitte manuell kopieren." fi } print_summary() { cat <} root@:/opt/checkmk-stack/backups/ 2) Dort importieren: cd /opt/checkmk-stack ./scripts/cmk-manage.sh import --file backups/$(basename "${LAST_BUNDLE:-}") --set-default-site 3) Quellserver: Site stoppen bzw. Server abschalten, damit es keine doppelte Ueberwachung und keinen IP-Konflikt gibt: omd stop && omd disable ... / systemctl poweroff 4) Zielsystem auf die IP/den DNS-Namen des Quellservers umstellen - dann melden sich die vorhandenen Agents unveraendert weiter. EOF } # --------------------------------------------------------------------- # GUI-Ablauf # --------------------------------------------------------------------- gui_main() { ui_init [ -n "$UI_BIN" ] || die "Fuer --gui wird 'dialog' oder 'whiptail' benoetigt. Debian/Ubuntu : apt-get install -y dialog RHEL/Rocky : dnf install -y dialog" local -a items=() s while IFS= read -r s; do [ -n "$s" ] || continue items+=("$s" "$(site_version "$s") | $(site_state "$s") | $(human_size "$(site_size "$s")")" off) done < <(list_sites) [ ${#items[@]} -gt 0 ] || die "Auf diesem System wurde keine OMD-Site gefunden." ui_msg "Checkmk Migration" \ "Dieses Skript erzeugt aus einer Direktinstallation ein Migrations-Bundle (.tar.gz) fuer den Import in eine Checkmk-Container-Installation. Server : $(hostname -f 2>/dev/null || hostname) IP(s) : $(all_ips) Sites : $(list_sites | tr '\n' ' ') Die Agents muessen spaeter NICHT neu eingerichtet werden, solange der Site-Name gleich bleibt und das Zielsystem dieselbe IP uebernimmt." local chosen chosen="$(ui_checklist "Sites auswaehlen" "Mit LEERTASTE markieren, mit ENTER bestaetigen:" "${items[@]}")" || exit 0 [ -n "$chosen" ] || { ui_msg "Abbruch" "Es wurde keine Site ausgewaehlt."; exit 0; } SITES=() local x for x in $(printf '%s' "$chosen" | tr -d '"'); do SITES+=("$x"); done local opt opt="$(ui_checklist "Optionen" "Optionen fuer das Backup:" \ stop "Site waehrend des Backups stoppen (empfohlen)" on \ no-rrds "Performance-Daten (RRD) auslassen" off \ no-logs "Logdateien auslassen" off)" || exit 0 case "$opt" in *stop*) STOP_SITE=1 ;; esac case "$opt" in *no-rrds*) OMD_OPTS+=(--no-rrds) ;; esac case "$opt" in *no-logs*) OMD_OPTS+=(--no-logs) ;; esac OUT_DIR="$(ui_input "Zielverzeichnis" "Wohin sollen die Bundles geschrieben werden?" "${OUT_DIR}")" || exit 0 LABEL="$(ui_input "Kennzeichnung" "Optionaler Text fuer den Dateinamen (leer = keiner):" "")" || LABEL="" LABEL="$(printf '%s' "$LABEL" | tr -cd 'A-Za-z0-9._-')" if ui_yesno "Direkt uebertragen?" "Sollen die Bundles anschliessend per scp auf das Zielsystem kopiert werden?"; then SCP_TARGET="$(ui_input "scp-Ziel" "Ziel angeben (Benutzer@Host:/Pfad/):" "root@zielserver:/opt/checkmk-stack/backups/")" || SCP_TARGET="" fi ui_yesno "Starten" "Jetzt Bundle(s) erzeugen fuer:\n\n ${SITES[*]}\n\nZiel: $OUT_DIR\n$([ -n "$STOP_SITE" ] && echo 'Die Site(s) werden dabei kurz gestoppt!')" || exit 0 local logf; logf="$(mktemp)" clear ASSUME_YES=1 local site for site in "${SITES[@]}"; do create_bundle "$site" 2>&1 | tee -a "$logf" done print_summary | tee -a "$logf" >/dev/null ui_text "Fertig" "$logf" rm -f "$logf" print_summary } # --------------------------------------------------------------------- # Argumente # --------------------------------------------------------------------- while [ $# -gt 0 ]; do case "$1" in -l|--list) LIST_ONLY=1; shift ;; -s|--site) SITES+=("$2"); shift 2 ;; -a|--all) ALL=1; shift ;; -o|--out) OUT_DIR="$2"; shift 2 ;; --label) LABEL="$2"; shift 2 ;; --no-rrds) OMD_OPTS+=(--no-rrds); shift ;; --no-logs) OMD_OPTS+=(--no-logs); shift ;; --stop) STOP_SITE=1; shift ;; --scp) SCP_TARGET="$2"; shift 2 ;; -g|--gui) USE_GUI=1; shift ;; -y|--yes) ASSUME_YES=1; shift ;; -h|--help) usage; exit 0 ;; *) die "Unbekannte Option: $1 (--help fuer Hilfe)" ;; esac done # --------------------------------------------------------------------- # Vorbedingungen # --------------------------------------------------------------------- command -v omd >/dev/null 2>&1 \ || die "Auf diesem System ist keine OMD-/Checkmk-Direktinstallation gefunden worden ('omd' fehlt)." [ "$(id -u)" -eq 0 ] || die "Bitte als root ausfuehren (sudo $0 ...)." if [ -n "${LIST_ONLY:-}" ]; then printf '%-20s %-18s %-12s %s\n' "SITE" "VERSION" "STATUS" "GROESSE" while IFS= read -r s; do [ -n "$s" ] || continue printf '%-20s %-18s %-12s %s\n' "$s" "$(site_version "$s")" "$(site_state "$s")" "$(human_size "$(site_size "$s")")" done < <(list_sites) exit 0 fi if [ -n "$USE_GUI" ] || { [ ${#SITES[@]} -eq 0 ] && [ -z "$ALL" ] && [ -t 0 ]; }; then gui_main exit 0 fi [ -n "$ALL" ] && mapfile -t SITES < <(list_sites) [ ${#SITES[@]} -gt 0 ] || die "Keine Site angegeben. --site SITE, --all oder --gui verwenden." for s in "${SITES[@]}"; do ( set +o pipefail; list_sites | grep -qxF "$s" ) || die "Site '$s' existiert auf diesem Server nicht." done printf '\n%sZu sichernde Sites:%s %s\n' "$C_BLD" "$C_RST" "${SITES[*]}" printf '%sZielverzeichnis :%s %s\n\n' "$C_BLD" "$C_RST" "$OUT_DIR" [ -n "$STOP_SITE" ] && warn "Die Site(s) werden waehrend des Backups gestoppt!" confirm "Jetzt Migrations-Bundle(s) erzeugen?" || die "Abgebrochen." for s in "${SITES[@]}"; do create_bundle "$s" done print_summary