diff --git a/.env.example b/.env.example index 594c98e..9cd669c 100644 --- a/.env.example +++ b/.env.example @@ -144,8 +144,13 @@ CMK_DATA_DIR=./data/checkmk/sites NPM_DATA_DIR=./data/npm/data NPM_LE_DIR=./data/npm/letsencrypt -# Ablage fuer Backups / Migrations-Bundles und heruntergeladene Images +# Ablage fuer MANUELLE Backups und Migrations-Bundles. +# Geplante Jobs bringen ihr eigenes Zielverzeichnis mit (backup-jobs.conf). BACKUP_DIR=./backups + +# Sollen die Backup-Jobs automatisch per cron laufen? +# Umschalten ueber das Menue oder: ./scripts/cmk-manage.sh job auto on +BACKUP_AUTO=off IMAGE_DIR=./images # ===================================================================== diff --git a/.gitignore b/.gitignore index 54d9531..c4e7a0b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ backups/* *.log images/* !images/.gitkeep +backup-jobs.conf diff --git a/README.md b/README.md index 0353d13..6598f72 100644 --- a/README.md +++ b/README.md @@ -553,7 +553,8 @@ Die wichtigsten Schalter: | `NPM_ENABLED` | `on`/`off` – soll der Proxy-Manager-Container überhaupt laufen? Siehe [12.4](#124-proxy-manager-ganz-abschalten-externer-nginx) | | `NPM_ADMIN_PORT` | Port der Proxy-Manager-Oberfläche (Vorgabe 81) – nur über `npm-port` bzw. das Menü ändern | | `NPM_ADMIN_URL` | Startseite des Terminal-Browsers; leer = automatisch aus `NPM_ADMIN_PORT` | -| `BACKUP_DIR` | Ablage für Backups/Bundles (Vorgabe `./backups`) | +| `BACKUP_DIR` | Ablage für **manuelle** Backups (Vorgabe `./backups`); geplante Jobs haben eigene Pfade | +| `BACKUP_AUTO` | `on`/`off` – sollen die Backup-Jobs per cron laufen? | | `CERT_DOMAIN`, `CERT_DAYS` | Vorgaben für selbstsignierte Zertifikate | ### 7.1 Wer liest die `.env` – und warum gibt es sie zusätzlich zum Skript? @@ -1324,6 +1325,7 @@ Eine Dateiauswahl mit Verzeichnisnavigation ist eingebaut. | `start-all-sites` | alle Sites starten (wird von systemd aufgerufen) | | `backup …` | Backup als `.tar.gz`-Bundle | | `list` / `verify` / `prune --keep N` | Backups verwalten | +| `job list\|add\|set\|del\|run\|auto` | geplante Backups (eigene Zielpfade je Job) | | `restore …` | Backup zurückspielen | | `import …` | Migrations-Bundle einspielen | | `image status\|list\|pull\|download\|load\|use` | Checkmk-Image beschaffen und auswaehlen | @@ -1435,17 +1437,112 @@ Auflisten und prüfen: ./scripts/cmk-manage.sh prune --keep 7 ``` -### Nächtliches Backup per Cron +### 15.1 Geplante Backups (Backup-Jobs) + +Manuelle Backups gehen immer nach `BACKUP_DIR` aus der `.env`. Geplante Jobs +bringen dagegen **ihr eigenes Zielverzeichnis** mit – so lässt sich täglich auf +eine NAS-Freigabe und wöchentlich woanders hin sichern. + +Im Menü: **backup → „Backup erstellen / planen"** + +``` + ┌──────────── Backup erstellen / planen ─────────────────┐ + │ Manuelle Backups gehen immer nach: │ + │ /opt/checkmk-stack/backups (Einstellung BACKUP_DIR)│ + │ │ + │ Geplante Jobs haben ihr eigenes Zielverzeichnis. │ + │ Automatische Ausfuehrung: aus │ + │ │ + │ manuell Backup jetzt erstellen │ + │ jobs Backup-Jobs verwalten │ + │ auto Automatische Ausfuehrung ein/aus │ + │ status Zeitplan und Jobs anzeigen │ + │ jetzt Einen Job sofort ausfuehren │ + │ back zurueck │ + └────────────────────────────────────────────────────────┘ +``` + +Dieselben Jobs liegen auch in den **Einstellungen → „Backup-Jobs"**. + +#### Job anlegen + +Im Menü führt ein Dialog durch Sites, Zeitplan (täglich / wöchentlich / +monatlich / freier Cron-Ausdruck), Zielverzeichnis, Aufbewahrung und Optionen. +Dasselbe per Parameter: ```bash -sudo crontab -e +# täglich 02:15, alle Sites, ohne Logs, 14 Backups aufbewahren +sudo ./scripts/cmk-manage.sh job add --name nachts \ + --cron "15 2 * * *" --no-logs --keep 14 + +# sonntags 03:30, nur zwei Sites, auf die NAS, ohne RRDs, 8 aufbewahren +sudo ./scripts/cmk-manage.sh job add --name nas-woche \ + --cron "30 3 * * 0" --sites prod,test \ + --dir /mnt/nas/checkmk --no-rrds --no-logs --keep 8 ``` -```cron -15 2 * * * /opt/checkmk-stack/scripts/cmk-manage.sh -y backup --all --no-logs >>/var/log/cmk-backup.log 2>&1 -30 3 * * 0 /opt/checkmk-stack/scripts/cmk-manage.sh -y prune --keep 14 >>/var/log/cmk-backup.log 2>&1 +Alle Kommandos: + +| Kommando | Wirkung | +|---|---| +| `job list` | angelegte Jobs anzeigen | +| `job status` | Zeitplan, Cron-Datei und Schalter | +| `job add --name N --cron "…"` | Job anlegen | +| `job set NAME …` | Job ändern (nicht genannte Felder bleiben) | +| `job del NAME` | Job löschen (die Backups bleiben) | +| `job enable\|disable NAME` | Job ein-/ausschalten | +| `job run NAME` | Job sofort ausführen | +| `job run-all` | alle aktiven Jobs ausführen | +| `job auto on\|off` | automatische Ausführung ein/aus | + +Optionen von `job add` / `job set`: + +| Option | Bedeutung | +|---|---| +| `--sites ALL\|a,b` | zu sichernde Sites (Vorgabe `ALL`) | +| `--cron "15 2 * * *"` | Zeitplan, fünf Cron-Felder | +| `--dir VERZ` | Zielverzeichnis dieses Jobs (Vorgabe `BACKUP_DIR`) | +| `--keep N` | wie viele Backups im Ziel bleiben (`0` = alle) | +| `--no-rrds`, `--no-logs`, `--stop` | wie bei `backup` | +| `--disabled` | anlegen, aber noch nicht einplanen | + +#### Automatik ein- und ausschalten + +```bash +sudo ./scripts/cmk-manage.sh job auto on # einplanen +sudo ./scripts/cmk-manage.sh job auto off # Zeitplan entfernen ``` +`on` schreibt aus allen **aktiven** Jobs die Datei `/etc/cron.d/checkmk-backup` +– je Job eine Zeile mit seinem eigenen Zeitplan: + +```cron +15 2 * * * root /opt/checkmk-stack/scripts/cmk-manage.sh -y job run nachts >>/var/log/cmk-backup.log 2>&1 +30 3 * * 0 root /opt/checkmk-stack/scripts/cmk-manage.sh -y job run nas-woche >>/var/log/cmk-backup.log 2>&1 +``` + +Dafür werden root-Rechte gebraucht. Ohne root gibt das Kommando die Zeilen +aus, damit man sie selbst übernehmen kann. `off` entfernt die Datei wieder; die +Jobs selbst bleiben erhalten. Der Schalter steht als `BACKUP_AUTO` in der +`.env`. + +Nach jedem Lauf räumt der Job sein eigenes Zielverzeichnis auf `--keep` auf – +ein separater `prune`-Eintrag ist nicht nötig. + +#### Wo die Jobs liegen + +In `backup-jobs.conf` im Projektverzeichnis, eine Zeile je Job: + +``` +name|sites|cron|verzeichnis|optionen|aufbewahren|aktiv +nachts|ALL|15 2 * * *|/opt/checkmk-stack/backups|--no-logs|14|on +nas-woche|prod,test|30 3 * * 0|/mnt/nas/checkmk|--no-rrds --no-logs|8|on +``` + +Die Datei ist bewusst lesbar und versionierbar, gehört aber nicht ins +Repository (sie steht in `.gitignore`, weil sie systemspezifische Pfade +enthält). + --- ## 16. Restore @@ -1739,6 +1836,8 @@ Checkmk-Werksnotizen (Werks/Release Notes) lesen. | Proxy-Manager-Oberfläche im Browser leer / nur Textfragmente | Textbrowser ohne JavaScript verwendet. `cmk-manage.sh browser --engine browsh` (bzw. `carbonyl`) nehmen | | Selbstsigniertes Zertifikat lässt sich im Terminal-Browser nicht hochladen | Datei-Dialoge gibt es dort nicht – Upload über die API, siehe [Kapitel 11.3](#113-grenze-datei-upload) | | `browser` meldet „braucht ein echtes Terminal" | Aufruf aus Pipe/Skript. Direkt auf der Konsole oder per SSH aufrufen; nur der Bau geht headless mit `--build-only` | +| Menüpunkt bricht mit einer Fehlermeldung in die Shell ab | behoben: fehlende Container, Sites oder Backups werden jetzt als Dialog gemeldet, die Oberfläche bleibt offen | +| `job auto on` meldet fehlende Rechte | `/etc/cron.d/` braucht root: mit `sudo` aufrufen, oder die ausgegebenen Zeilen selbst übernehmen | | TUI startet nicht | `dialog` installieren: `apt-get install -y dialog` bzw. `dnf install -y dialog` | | Site startet nicht, `tmp`-Fehler | `./scripts/cmk-manage.sh omd config set TMPFS off`, danach Site starten | diff --git a/README_en.md b/README_en.md index 2673aa6..5f3a5ac 100644 --- a/README_en.md +++ b/README_en.md @@ -556,7 +556,8 @@ The most important settings: | `NPM_ENABLED` | `on`/`off` – should the Proxy Manager container run at all? See [12.4](#124-turning-the-proxy-manager-off-entirely-external-nginx) | | `NPM_ADMIN_PORT` | port of the Proxy Manager interface (default 81) – change only via `npm-port` or the menu | | `NPM_ADMIN_URL` | start page of the terminal browser; empty = derived from `NPM_ADMIN_PORT` | -| `BACKUP_DIR` | location for backups/bundles (default `./backups`) | +| `BACKUP_DIR` | location for **manual** backups (default `./backups`); scheduled jobs have their own paths | +| `BACKUP_AUTO` | `on`/`off` – should the backup jobs run via cron? | | `CERT_DOMAIN`, `CERT_DAYS` | defaults for self-signed certificates | ### 7.1 Who reads `.env` – and why does it exist alongside the script? @@ -1331,6 +1332,7 @@ A file chooser with directory navigation is built in. | `start-all-sites` | start all sites (invoked by systemd) | | `backup …` | backup as a `.tar.gz` bundle | | `list` / `verify` / `prune --keep N` | manage backups | +| `job list\|add\|set\|del\|run\|auto` | scheduled backups (own target path per job) | | `restore …` | restore a backup | | `import …` | import a migration bundle | | `image status\|list\|pull\|download\|load\|use` | obtain and select the Checkmk image | @@ -1445,17 +1447,112 @@ Listing and verifying: ./scripts/cmk-manage.sh prune --keep 7 ``` -### Nightly backup via cron +### 15.1 Scheduled backups (backup jobs) + +Manual backups always go to `BACKUP_DIR` from `.env`. Scheduled jobs, by +contrast, bring **their own target directory** – so you can back up to a NAS +share daily and somewhere else weekly. + +In the menu: **backup → "Backup erstellen / planen"** (create / schedule) + +``` + ┌──────────── Backup erstellen / planen ─────────────────┐ + │ Manuelle Backups gehen immer nach: │ + │ /opt/checkmk-stack/backups (Einstellung BACKUP_DIR)│ + │ │ + │ Geplante Jobs haben ihr eigenes Zielverzeichnis. │ + │ Automatische Ausfuehrung: aus │ + │ │ + │ manuell Backup jetzt erstellen │ + │ jobs Backup-Jobs verwalten │ + │ auto Automatische Ausfuehrung ein/aus │ + │ status Zeitplan und Jobs anzeigen │ + │ jetzt Einen Job sofort ausfuehren │ + │ back zurueck │ + └────────────────────────────────────────────────────────┘ +``` + +(create now / manage jobs / automatic execution on-off / show schedule / run a +job now / back.) The same jobs are also reachable from +**Einstellungen → "Backup-Jobs"**. + +#### Creating a job + +In the menu a dialog walks through sites, schedule (daily / weekly / monthly / +free cron expression), target directory, retention and options. The same with +parameters: ```bash -sudo crontab -e +# daily at 02:15, all sites, without logs, keep 14 backups +sudo ./scripts/cmk-manage.sh job add --name nachts \ + --cron "15 2 * * *" --no-logs --keep 14 + +# Sundays at 03:30, two sites only, to the NAS, without RRDs, keep 8 +sudo ./scripts/cmk-manage.sh job add --name nas-woche \ + --cron "30 3 * * 0" --sites prod,test \ + --dir /mnt/nas/checkmk --no-rrds --no-logs --keep 8 ``` -```cron -15 2 * * * /opt/checkmk-stack/scripts/cmk-manage.sh -y backup --all --no-logs >>/var/log/cmk-backup.log 2>&1 -30 3 * * 0 /opt/checkmk-stack/scripts/cmk-manage.sh -y prune --keep 14 >>/var/log/cmk-backup.log 2>&1 +All commands: + +| Command | Effect | +|---|---| +| `job list` | show the configured jobs | +| `job status` | schedule, cron file and switch | +| `job add --name N --cron "…"` | create a job | +| `job set NAME …` | modify a job (fields not given stay as they are) | +| `job del NAME` | delete a job (the backups remain) | +| `job enable\|disable NAME` | turn a job on/off | +| `job run NAME` | run a job right now | +| `job run-all` | run every active job | +| `job auto on\|off` | turn automatic execution on/off | + +Options for `job add` / `job set`: + +| Option | Meaning | +|---|---| +| `--sites ALL\|a,b` | sites to back up (default `ALL`) | +| `--cron "15 2 * * *"` | schedule, five cron fields | +| `--dir DIR` | this job's target directory (default `BACKUP_DIR`) | +| `--keep N` | how many backups stay in the target (`0` = all) | +| `--no-rrds`, `--no-logs`, `--stop` | as for `backup` | +| `--disabled` | create it but do not schedule it yet | + +#### Turning automation on and off + +```bash +sudo ./scripts/cmk-manage.sh job auto on # schedule +sudo ./scripts/cmk-manage.sh job auto off # remove the schedule ``` +`on` writes `/etc/cron.d/checkmk-backup` from all **active** jobs – one line +per job with its own schedule: + +```cron +15 2 * * * root /opt/checkmk-stack/scripts/cmk-manage.sh -y job run nachts >>/var/log/cmk-backup.log 2>&1 +30 3 * * 0 root /opt/checkmk-stack/scripts/cmk-manage.sh -y job run nas-woche >>/var/log/cmk-backup.log 2>&1 +``` + +This needs root. Without root the command prints the lines so you can add them +yourself. `off` removes the file again; the jobs themselves are kept. The +switch is stored as `BACKUP_AUTO` in `.env`. + +After each run a job prunes its own target directory down to `--keep` – a +separate `prune` entry is not needed. + +#### Where the jobs live + +In `backup-jobs.conf` in the project directory, one line per job: + +``` +name|sites|cron|directory|options|keep|enabled +nachts|ALL|15 2 * * *|/opt/checkmk-stack/backups|--no-logs|14|on +nas-woche|prod,test|30 3 * * 0|/mnt/nas/checkmk|--no-rrds --no-logs|8|on +``` + +The file is deliberately readable and diffable, but does not belong in the +repository (it is in `.gitignore` because it contains system-specific paths). + --- ## 16. Restore @@ -1749,6 +1846,8 @@ release notes beforehand. | Proxy Manager interface blank / only text fragments | a text browser without JavaScript was used. Use `cmk-manage.sh browser --engine browsh` (or `carbonyl`) | | Self-signed certificate cannot be uploaded in the terminal browser | there are no file dialogs there – upload via the API, see [chapter 11.3](#113-limitation-file-upload) | | `browser` reports "braucht ein echtes Terminal" (needs a real terminal) | called from a pipe/script. Call it on the console or over SSH; only the build runs headless, with `--build-only` | +| A menu entry drops out to the shell with an error | fixed: missing containers, sites or backups are now reported in a dialog and the interface stays open | +| `job auto on` reports missing permissions | `/etc/cron.d/` needs root: call it with `sudo`, or add the printed lines yourself | | The TUI does not start | install `dialog`: `apt-get install -y dialog` or `dnf install -y dialog` | | Site does not start, `tmp` errors | `./scripts/cmk-manage.sh omd config set TMPFS off`, then start the site | diff --git a/scripts/cmk-manage.sh b/scripts/cmk-manage.sh index c19cad9..598815e 100755 --- a/scripts/cmk-manage.sh +++ b/scripts/cmk-manage.sh @@ -89,6 +89,21 @@ KOMMANDOS - Backup / Restore / Migration prune --keep N nur die N neuesten Backups behalten + job list angelegte Backup-Jobs anzeigen + job status Zeitplan, Cron-Datei und Schalter anzeigen + job add --name N --cron "15 2 * * *" [Optionen] Job anlegen + --sites ALL|a,b zu sichernde Sites (Vorgabe ALL) + --dir VERZ Zielverzeichnis des Jobs (Vorgabe: $BACKUP_DIR) + --keep N Anzahl Backups, die im Ziel bleiben (0 = alle) + --no-rrds --no-logs --stop wie bei "backup" + --disabled angelegt, aber noch nicht eingeplant + job set NAME [Optionen] vorhandenen Job aendern + job del NAME Job loeschen + job enable|disable NAME Job ein-/ausschalten + job run NAME Job sofort ausfuehren + job run-all alle aktiven Jobs ausfuehren + job auto [on|off] automatische Ausfuehrung per cron ein/aus + KOMMANDOS - Checkmk-Image beschaffen image status konfiguriertes und lokal vorhandene Images anzeigen image list lokal vorhandene Checkmk-Images @@ -1217,6 +1232,334 @@ EOF LAST_BACKUP_FILE="$outfile" } +# ===================================================================== +# Backup-Jobs (geplante Backups) +# +# Manuelle Backups nehmen immer BACKUP_DIR aus der .env. Geplante Jobs +# bringen ihr eigenes Zielverzeichnis mit - so kann z.B. taeglich auf +# eine NAS-Freigabe und woechentlich woanders hin gesichert werden. +# +# Ablage: backup-jobs.conf im Projektverzeichnis, eine Zeile je Job: +# name|sites|cron|verzeichnis|optionen|aufbewahren|aktiv +# Ausgefuehrt werden sie ueber /etc/cron.d/checkmk-backup, das aus +# dieser Datei erzeugt wird ("job auto on"). +# ===================================================================== + +JOBS_FILE_NAME="backup-jobs.conf" +CRON_FILE="/etc/cron.d/checkmk-backup" + +jobs_file() { printf '%s/%s\n' "$PROJECT_DIR" "$JOBS_FILE_NAME"; } + +jobs_init() { + local f; f="$(jobs_file)" + [ -f "$f" ] && return 0 + cat >"$f" <<'EOF' +# Backup-Jobs fuer cmk-manage.sh - eine Zeile je Job: +# name|sites|cron|verzeichnis|optionen|aufbewahren|aktiv +# +# name nur Buchstaben, Ziffern, _ und - +# sites ALL oder Site-Namen durch Komma getrennt +# cron fuenf Cron-Felder, z.B. "15 2 * * *" +# verzeichnis Zielverzeichnis (absolut oder relativ zum Projekt) +# optionen leer oder --no-rrds / --no-logs / --stop (mit Leerzeichen) +# aufbewahren Anzahl der Backups, die im Zielverzeichnis bleiben (0 = alle) +# aktiv on oder off +# +# Bearbeiten moeglichst ueber: cmk-manage.sh job ... bzw. das Menue. +EOF + ok "Neue Jobdatei angelegt: $f" +} + +jobs_read() { # nur Datenzeilen + local f; f="$(jobs_file)" + [ -f "$f" ] || return 0 + grep -vE '^[[:space:]]*(#|$)' "$f" || true +} + +job_line() { jobs_read | awk -F'|' -v n="$1" '$1==n {print; exit}'; } +job_exists() { [ -n "$(job_line "$1")" ]; } +job_f() { printf '%s' "$1" | cut -d'|' -f"$2"; } + +job_dir_abs() { # Verzeichnis eines Jobs absolut + local d="$1" + case "$d" in /*) printf '%s\n' "$d" ;; *) printf '%s\n' "$PROJECT_DIR/${d#./}" ;; esac +} + +job_validate_name() { + case "$1" in + ''|*[!A-Za-z0-9_-]*) echo "Der Name darf nur Buchstaben, Ziffern, _ und - enthalten."; return 1 ;; + esac + return 0 +} + +job_validate_cron() { + local n; n="$(printf '%s' "$1" | awk '{print NF}')" + [ "$n" = 5 ] || { echo "Ein Cron-Ausdruck besteht aus genau fuenf Feldern, z.B. '15 2 * * *'."; return 1; } + return 0 +} + +job_validate_field() { # keine Trennzeichen in Feldern + case "$1" in *'|'*) echo "Das Zeichen | ist in diesem Feld nicht erlaubt."; return 1 ;; esac + return 0 +} + +# Job schreiben (anlegen oder ersetzen) +job_put() { # name sites cron dir opts keep enabled + jobs_init + local f tmp; f="$(jobs_file)" + tmp="$(mktemp)" + awk -F'|' -v n="$1" '$1!=n' "$f" >"$tmp" + printf '%s|%s|%s|%s|%s|%s|%s\n' "$1" "$2" "$3" "$4" "$5" "$6" "$7" >>"$tmp" + # Kommentarkopf erhalten + { grep -E '^[[:space:]]*#' "$f" 2>/dev/null || true; grep -vE '^[[:space:]]*(#|$)' "$tmp" ; } >"$f" + rm -f "$tmp" +} + +job_remove() { + local f tmp; f="$(jobs_file)" + [ -f "$f" ] || return 0 + tmp="$(mktemp)" + awk -F'|' -v n="$1" '$1!=n' "$f" >"$tmp" && mv "$tmp" "$f" +} + +# --- Aufraeumen im Zielverzeichnis eines Jobs ------------------------- +prune_in_dir() { # verzeichnis anzahl + local dir="$1" keep="$2" i=0 + [ -n "$keep" ] && [ "$keep" -gt 0 ] 2>/dev/null || return 0 + local -a files=() + mapfile -t files < <(find "$dir" -maxdepth 1 -type f -name '*.tar.gz' -printf '%T@ %p\n' 2>/dev/null \ + | sort -rn | cut -d' ' -f2-) + [ ${#files[@]} -le "$keep" ] && return 0 + for ((i=keep; i<${#files[@]}; i++)); do + log "Aufbewahrung: entferne $(basename "${files[$i]}")" + rm -f "${files[$i]}" + done +} + +# --- Job ausfuehren --------------------------------------------------- +job_run_one() { + local name="$1" line + line="$(job_line "$name")" + [ -n "$line" ] || die "Job '$name' ist nicht vorhanden. Uebersicht: $SELF job list" + + local sites cron dir opts keep enabled + sites="$(job_f "$line" 2)"; cron="$(job_f "$line" 3)"; dir="$(job_f "$line" 4)" + opts="$(job_f "$line" 5)"; keep="$(job_f "$line" 6)"; enabled="$(job_f "$line" 7)" + + dir="$(job_dir_abs "$dir")" + mkdir -p "$dir" || die "Zielverzeichnis nicht anlegbar: $dir" + + log "Job '$name' - Ziel $dir, Sites ${sites}${opts:+, Optionen $opts}" + require_cmk_running + + local -a args=(--dir "$dir" --label "$name") + local o + for o in $opts; do + case "$o" in + --no-rrds|--no-logs|--stop) args+=("$o") ;; + *) warn "Unbekannte Option '$o' im Job '$name' - wird uebergangen." ;; + esac + done + if [ "$sites" = ALL ]; then + args+=(--all) + else + local sname + for sname in $(printf '%s' "$sites" | tr ',' ' '); do args+=(--site "$sname"); done + fi + + ASSUME_YES=1 cmd_backup "${args[@]}" + prune_in_dir "$dir" "$keep" + ok "Job '$name' abgeschlossen." +} + +# --- Cron ------------------------------------------------------------ +cron_write() { + jobs_init + local n=0 line name cron + local tmp; tmp="$(mktemp)" + { + printf '# Automatische Checkmk-Backups - erzeugt von cmk-manage.sh\n' + printf '# Nicht von Hand aendern, Quelle ist %s\n' "$(jobs_file)" + printf 'SHELL=/bin/sh\n' + printf 'PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n' + while IFS= read -r line; do + [ -n "$line" ] || continue + [ "$(job_f "$line" 7)" = on ] || continue + name="$(job_f "$line" 1)"; cron="$(job_f "$line" 3)" + printf '%s root %s -y job run %s >>/var/log/cmk-backup.log 2>&1\n' \ + "$cron" "$SELF" "$name" + n=$((n+1)) + done < <(jobs_read) + } >"$tmp" + + if [ "$n" -eq 0 ]; then + rm -f "$tmp" + warn "Kein aktiver Job vorhanden - es wird nichts eingeplant." + [ -f "$CRON_FILE" ] && { rm -f "$CRON_FILE"; log "Vorhandene Cron-Datei entfernt."; } + return 0 + fi + + if [ "$(id -u)" -ne 0 ]; then + err "Zum Schreiben von $CRON_FILE werden root-Rechte benoetigt." + echo + cat "$tmp" + echo + log "Die Zeilen oben von Hand nach $CRON_FILE uebernehmen, oder erneut mit sudo aufrufen." + rm -f "$tmp" + return 1 + fi + install -m 0644 "$tmp" "$CRON_FILE" + rm -f "$tmp" + ok "$n Job(s) eingeplant in $CRON_FILE" +} + +cron_remove_file() { + if [ -f "$CRON_FILE" ]; then + [ "$(id -u)" -eq 0 ] || die "Zum Entfernen von $CRON_FILE werden root-Rechte benoetigt." + rm -f "$CRON_FILE" + ok "Automatische Backups abgeschaltet ($CRON_FILE entfernt)." + else + ok "Es war nichts eingeplant." + fi +} + +backup_auto_enabled() { + case "$(printf '%s' "${BACKUP_AUTO:-off}" | tr 'A-Z' 'a-z')" in + on|true|1|yes|ja) return 0 ;; *) return 1 ;; + esac +} + +# --- Anzeige ---------------------------------------------------------- +jobs_list_table() { + printf '%-14s %-12s %-14s %-28s %-18s %5s %s\n' \ + "NAME" "SITES" "ZEITPLAN" "ZIEL" "OPTIONEN" "HALT" "AKTIV" + local line n=0 + while IFS= read -r line; do + [ -n "$line" ] || continue + printf '%-14s %-12s %-14s %-28s %-18s %5s %s\n' \ + "$(job_f "$line" 1)" "$(job_f "$line" 2)" "$(job_f "$line" 3)" \ + "$(job_f "$line" 4)" "$(job_f "$line" 5)" "$(job_f "$line" 6)" "$(job_f "$line" 7)" + n=$((n+1)) + done < <(jobs_read) + [ "$n" -eq 0 ] && echo "(noch keine Jobs angelegt)" + return 0 +} + +cmd_job() { + local sub="${1:-list}"; [ $# -gt 0 ] && shift || true + case "$sub" in + list|ls) jobs_list_table ;; + + status) + printf '\n%sAutomatische Backups%s\n' "$C_BLD" "$C_RST" + printf ' Schalter : %s\n' "$(backup_auto_enabled && echo 'ein (BACKUP_AUTO=on)' || echo 'aus (BACKUP_AUTO=off)')" + printf ' Cron-Datei : %s\n' "$([ -f "$CRON_FILE" ] && echo "$CRON_FILE" || echo 'nicht vorhanden')" + printf ' Jobdatei : %s\n' "$(jobs_file)" + printf ' Manuell nach: %s (BACKUP_DIR)\n' "$BACKUP_DIR" + printf '\n%sJobs%s\n' "$C_BLD" "$C_RST" + jobs_list_table + if [ -f "$CRON_FILE" ]; then + printf '\n%sEingeplant%s\n' "$C_BLD" "$C_RST" + grep -vE '^[[:space:]]*(#|SHELL|PATH|$)' "$CRON_FILE" | sed 's/^/ /' + fi + echo ;; + + add|set) + local name="" sites="ALL" cron="" dir="" keep="7" enabled="on" opts="" + local is_set=""; [ "$sub" = set ] && is_set=1 + while [ $# -gt 0 ]; do + case "$1" in + --name) name="$2"; shift 2 ;; + --sites) sites="$2"; shift 2 ;; + --cron) cron="$2"; shift 2 ;; + --dir) dir="$2"; shift 2 ;; + --keep) keep="$2"; shift 2 ;; + --no-rrds) opts="$opts --no-rrds"; shift ;; + --no-logs) opts="$opts --no-logs"; shift ;; + --stop) opts="$opts --stop"; shift ;; + --enabled) enabled=on; shift ;; + --disabled) enabled=off; shift ;; + *) [ -z "$name" ] && { name="$1"; shift; } || die "Unbekannte Option fuer 'job $sub': $1" ;; + esac + done + [ -n "$name" ] || die "--name fehlt." + local msg + msg="$(job_validate_name "$name")" || die "$msg" + + local old=""; old="$(job_line "$name")" + if [ -n "$is_set" ]; then + [ -n "$old" ] || die "Job '$name' ist nicht vorhanden." + # nicht angegebene Felder aus dem Bestand uebernehmen + [ "$sites" = ALL ] && sites="$(job_f "$old" 2)" + [ -z "$cron" ] && cron="$(job_f "$old" 3)" + [ -z "$dir" ] && dir="$(job_f "$old" 4)" + [ -z "$opts" ] && opts="$(job_f "$old" 5)" + [ "$keep" = 7 ] && keep="$(job_f "$old" 6)" + enabled="$(job_f "$old" 7)" + else + [ -z "$old" ] || die "Job '$name' existiert bereits. Aendern mit: $SELF job set $name ..." + fi + dir="${dir:-$BACKUP_DIR}" + [ -n "$cron" ] || die "--cron fehlt, z.B. --cron '15 2 * * *'." + msg="$(job_validate_cron "$cron")" || die "$msg" + msg="$(job_validate_field "$dir")" || die "$msg" + case "$keep" in ''|*[!0-9]*) die "--keep braucht eine Zahl (0 = alles behalten)." ;; esac + + opts="$(printf '%s' "$opts" | sed 's/^ *//')" + job_put "$name" "$sites" "$cron" "$dir" "$opts" "$keep" "$enabled" + ok "Job '$name' gespeichert." + jobs_list_table | sed -n "1p;/^$name /p" + backup_auto_enabled && cron_write || log "Einplanen mit: $SELF job auto on" + ;; + + del|rm|delete) + [ $# -ge 1 ] || die "Name fehlt: $SELF job del NAME" + job_exists "$1" || die "Job '$1' ist nicht vorhanden." + confirm "Job '$1' wirklich loeschen?" || die "Abgebrochen." + job_remove "$1"; ok "Job '$1' geloescht." + backup_auto_enabled && cron_write || true + ;; + + enable|disable) + [ $# -ge 1 ] || die "Name fehlt: $SELF job $sub NAME" + local l; l="$(job_line "$1")" + [ -n "$l" ] || die "Job '$1' ist nicht vorhanden." + local st=on; [ "$sub" = disable ] && st=off + job_put "$1" "$(job_f "$l" 2)" "$(job_f "$l" 3)" "$(job_f "$l" 4)" \ + "$(job_f "$l" 5)" "$(job_f "$l" 6)" "$st" + ok "Job '$1' ist jetzt $st." + backup_auto_enabled && cron_write || true + ;; + + run) + [ $# -ge 1 ] || die "Name fehlt: $SELF job run NAME" + job_run_one "$1" ;; + + run-all) + local line n=0 + while IFS= read -r line; do + [ -n "$line" ] || continue + [ "$(job_f "$line" 7)" = on ] || continue + job_run_one "$(job_f "$line" 1)" || warn "Job fehlgeschlagen: $(job_f "$line" 1)" + n=$((n+1)) + done < <(jobs_read) + [ "$n" -eq 0 ] && warn "Kein aktiver Job vorhanden." + return 0 ;; + + auto) + case "${1:-}" in + on) env_set BACKUP_AUTO on; BACKUP_AUTO=on; cron_write ;; + off) env_set BACKUP_AUTO off; BACKUP_AUTO=off; cron_remove_file ;; + '') printf ' Automatische Backups: %s\n' \ + "$(backup_auto_enabled && echo ein || echo aus)" ;; + *) die "Aufruf: $SELF job auto [on|off]" ;; + esac ;; + + *) die "Unbekanntes Unterkommando 'job $sub'. + Moeglich: list | status | add | set | del | enable | disable | run | run-all | auto" ;; + esac +} + # ===================================================================== # Auflisten / Aufraeumen # ===================================================================== @@ -1807,7 +2150,8 @@ browser|BROWSER_TOOLS_IMAGE|text|Werkzeug-Image (leer = automatisch)||1 paths|CMK_DATA_DIR|path|Checkmk-Sites (Bind-Mount)|| paths|NPM_DATA_DIR|path|Proxy-Manager-Daten|| paths|NPM_LE_DIR|path|Let's-Encrypt-Zertifikate|| -paths|BACKUP_DIR|path|Backups und Bundles|| +paths|BACKUP_DIR|path|Backups und Bundles (manuell)|| +backup|BACKUP_AUTO|choice|Automatische Backups per cron|off:on| paths|IMAGE_DIR|path|Heruntergeladene Image-Tarballs|| cert|CERT_DOMAIN|text|FQDN fuer selbstsignierte Zertifikate|| cert|CERT_DAYS|int|Gueltigkeit in Tagen|| @@ -1825,6 +2169,7 @@ npm|NGINX Proxy Manager browser|Terminal-Browser paths|Verzeichnisse cert|Selbstsignierte Zertifikate +backup|Backup-Automatik EOF } @@ -1893,6 +2238,8 @@ settings_post_set() { NPM_ENABLED) # Container entsprechend entfernen bzw. starten cmd_npm_enable "$val" ;; + BACKUP_AUTO) + cmd_job auto "$val" ;; CMK_IMAGE|CMK_SITE_ID|CMK_CONTAINER_NAME|NPM_IMAGE|NPM_CONTAINER_NAME|TZ|CMK_LIVESTATUS_TCP|NPM_DISABLE_IPV6|CMK_DATA_DIR|NPM_DATA_DIR|NPM_LE_DIR) log "Wirksam nach: $SELF up (Container werden neu erstellt)" ;; esac @@ -1934,6 +2281,8 @@ cmd_config() { cmd_npm_port "$val" elif [ "$key" = NPM_ENABLED ]; then cmd_npm_enable "$val" + elif [ "$key" = BACKUP_AUTO ]; then + env_set "$key" "$val"; BACKUP_AUTO="$val"; cmd_job auto "$val" else env_set "$key" "$val" "$opt" ok "$key=$val" @@ -2157,7 +2506,61 @@ EOF # ===================================================================== # ncurses-Oberflaeche # ===================================================================== +# --------------------------------------------------------------------- +# GUI-sichere Vorbedingungen +# +# In der Oberflaeche darf nichts "die" aufrufen - das wuerde die ganze TUI +# beenden. Diese Helfer melden das Problem im Dialog und geben 1 zurueck. +# --------------------------------------------------------------------- +gui_need_container() { + if ! container_exists "$CMK_CONTAINER_NAME"; then + ui_msg "Checkmk-Container fehlt" \ +"Der Container '$CMK_CONTAINER_NAME' existiert nicht. + +Dafuer wird er zuerst gebraucht. Im Hauptmenue: + stack -> Stack starten + +Auf der Kommandozeile: + $SELF up" + return 1 + fi + if ! container_running "$CMK_CONTAINER_NAME"; then + ui_msg "Checkmk-Container gestoppt" \ +"Der Container '$CMK_CONTAINER_NAME' existiert, laeuft aber nicht. + +Im Hauptmenue: stack -> Stack starten +Kommandozeile: $SELF up" + return 1 + fi + return 0 +} + +gui_need_sites() { + gui_need_container || return 1 + local n; n="$(cmk_sites | sed '/^$/d' | wc -l)" + if [ "$n" -eq 0 ]; then + ui_msg "Keine Sites" \ +"Im Container ist noch keine Site vorhanden. + +Der Container legt die Site aus CMK_SITE_ID beim ersten Start selbst an. +Alternativ eine Site importieren: Hauptmenue -> import" + return 1 + fi + return 0 +} + +# Interaktives Kommando ausfuehren, ohne dass ein Fehler die TUI beendet +gui_run_interactive() { + clear 2>/dev/null || true + if ! ( "$@" ); then + printf '\n%sDas Kommando wurde mit einem Fehler beendet.%s\n' "$C_YEL" "$C_RST" + fi + printf '\nWeiter mit ENTER ...' + read -r _ Site auf stdout + gui_need_container || return 1 local -a items=() s while IFS= read -r s; do [ -n "$s" ] || continue @@ -2220,7 +2623,7 @@ gui_stack_menu() { } gui_backup() { - require_cmk_running + gui_need_sites || return 0 local -a items=() s while IFS= read -r s; do [ -n "$s" ] || continue @@ -2229,14 +2632,14 @@ gui_backup() { [ ${#items[@]} -gt 0 ] || { ui_msg "Backup" "Keine Sites vorhanden."; return; } local chosen - chosen="$(ui_checklist "Backup" "Zu sichernde Site(s) mit LEERTASTE markieren:" "${items[@]}")" || return + chosen="$(ui_checklist "Backup" "Zu sichernde Site(s) mit LEERTASTE markieren:" "${items[@]}")" || return 0 [ -n "$chosen" ] || { ui_msg "Backup" "Keine Site ausgewaehlt."; return; } local optsel optsel="$(ui_checklist "Backup-Optionen" "Optionen waehlen:" \ no-rrds "Performance-Daten (RRD) auslassen - kleineres Archiv" off \ no-logs "Logdateien auslassen" off \ - stop "Site waehrend des Backups stoppen (konsistenter)" off)" || return + stop "Site waehrend des Backups stoppen (konsistenter)" off)" || return 0 local label label="$(ui_input "Kennzeichnung" "Optionaler Text fuer den Dateinamen (leer = keiner):" "")" || label="" @@ -2255,10 +2658,217 @@ gui_backup() { ui_run "Backup laeuft" bash -c "cd '$PROJECT_DIR' && '$SELF' -y ${args[*]}" } +# --------------------------------------------------------------------- +# Backup: erstellen und planen +# --------------------------------------------------------------------- +gui_backup_menu() { + while :; do + local auto sel + auto="$(backup_auto_enabled && echo 'ein' || echo 'aus')" + sel="$(ui_menu "Backup erstellen / planen" \ +"Manuelle Backups gehen immer nach: + $BACKUP_DIR (Einstellung BACKUP_DIR) + +Geplante Jobs haben ihr eigenes Zielverzeichnis. +Automatische Ausfuehrung: $auto" \ + manuell "Backup jetzt erstellen" \ + jobs "Backup-Jobs verwalten (anlegen/aendern/loeschen)" \ + auto "Automatische Ausfuehrung ein/aus (derzeit: $auto)" \ + status "Zeitplan und Jobs anzeigen" \ + jetzt "Einen Job sofort ausfuehren" \ + back "zurueck")" || return 0 + case "$sel" in + manuell) gui_backup || true ;; + jobs) gui_jobs_menu || true ;; + auto) gui_backup_auto || true ;; + status) local f; f="$(mktemp)"; NO_COLOR=1 cmd_job status >"$f" 2>&1 || true + ui_text "Backup-Zeitplan" "$f"; rm -f "$f" ;; + jetzt) gui_job_run_now || true ;; + back) return 0 ;; + esac + done +} + +gui_backup_auto() { + if backup_auto_enabled; then + ui_yesno "Automatische Backups" \ +"Automatische Backups sind eingeschaltet. + +Abschalten? Die Jobs bleiben erhalten, es wird nur +nichts mehr automatisch ausgefuehrt." || return 0 + ui_run "Automatik abschalten" bash -c "cd '$PROJECT_DIR' && '$SELF' -y job auto off" + else + local n; n="$(jobs_read | awk -F'|' '$7=="on"' | wc -l)" + if [ "$n" -eq 0 ]; then + ui_msg "Keine aktiven Jobs" \ +"Es ist noch kein aktiver Backup-Job angelegt. + +Zuerst unter 'Backup-Jobs verwalten' einen Job anlegen - +dort wird auch der Zeitplan festgelegt." + return + fi + ui_yesno "Automatische Backups" \ +"$n aktive(r) Job(s) werden in die System-Cron eingetragen +($CRON_FILE). + +Dafuer werden root-Rechte benoetigt. Einschalten?" || return 0 + ui_run "Automatik einschalten" bash -c "cd '$PROJECT_DIR' && '$SELF' -y job auto on" + fi + load_env "${ENV_FILE:-$PROJECT_DIR/.env}" +} + +gui_pick_job() { # -> Jobname auf stdout + local -a items=() line + while IFS= read -r line; do + [ -n "$line" ] || continue + items+=("$(job_f "$line" 1)" \ + "$(job_f "$line" 3) -> $(job_f "$line" 4) [$(job_f "$line" 7)]") + done < <(jobs_read) + if [ ${#items[@]} -eq 0 ]; then + ui_msg "Keine Jobs" "Es ist noch kein Backup-Job angelegt." + return 1 + fi + ui_menu "Job waehlen" "Vorhandene Backup-Jobs:" "${items[@]}" +} + +gui_job_run_now() { + gui_need_sites || return 0 + local j; j="$(gui_pick_job)" || return 0 + [ -n "$j" ] || return 0 + ui_yesno "Job ausfuehren" "Job '$j' jetzt ausfuehren?" || return 0 + ui_run "Job '$j'" bash -c "cd '$PROJECT_DIR' && '$SELF' -y job run '$j'" +} + +gui_jobs_menu() { + while :; do + local sel + sel="$(ui_menu "Backup-Jobs" \ +"Jeder Job hat eigenen Zeitplan, eigenes Zielverzeichnis +und eigene Aufbewahrung. + +Jobdatei: $(jobs_file)" \ + list "Jobs anzeigen" \ + neu "Neuen Job anlegen" \ + edit "Job aendern" \ + toggle "Job ein-/ausschalten" \ + del "Job loeschen" \ + back "zurueck")" || return 0 + case "$sel" in + list) local f; f="$(mktemp)"; NO_COLOR=1 jobs_list_table >"$f" 2>&1 || true + ui_text "Backup-Jobs" "$f"; rm -f "$f" ;; + neu) gui_job_edit "" || true ;; + edit) local j; j="$(gui_pick_job)" || continue; [ -n "$j" ] && gui_job_edit "$j" ;; + toggle) local j2 l st + j2="$(gui_pick_job)" || continue; [ -n "$j2" ] || continue + l="$(job_line "$j2")"; st=disable + [ "$(job_f "$l" 7)" = off ] && st=enable + ui_run "Job $st" bash -c "cd '$PROJECT_DIR' && '$SELF' -y job $st '$j2'" ;; + del) local j3; j3="$(gui_pick_job)" || continue; [ -n "$j3" ] || continue + ui_yesno "Loeschen" "Job '$j3' wirklich loeschen?\n\n(Bereits erstellte Backups bleiben erhalten.)" \ + && ui_run "Job loeschen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y job del '$j3'" ;; + back) return 0 ;; + esac + done +} + +# Zeitplan bequem zusammenklicken +gui_pick_cron() { # vorgabe -> cron-ausdruck auf stdout + local cur="${1:-15 2 * * *}" kind time hh mm dow dom + kind="$(ui_menu "Zeitplan" "Wie oft soll gesichert werden?" \ + taeglich "jeden Tag" \ + woechentlich "einmal pro Woche" \ + monatlich "einmal pro Monat" \ + frei "Cron-Ausdruck selbst eingeben")" || return 1 + if [ "$kind" = frei ]; then + ui_input "Cron-Ausdruck" "Fuenf Felder: Minute Stunde Tag Monat Wochentag" "$cur" + return + fi + time="$(ui_input "Uhrzeit" "Uhrzeit im Format HH:MM" "02:15")" || return 1 + case "$time" in + [0-9][0-9]:[0-9][0-9]) : ;; + *) ui_msg "Ungueltig" "Bitte im Format HH:MM angeben, z.B. 02:15."; return 1 ;; + esac + hh="${time%%:*}"; mm="${time##*:}" + hh="$((10#$hh))"; mm="$((10#$mm))" + [ "$hh" -le 23 ] && [ "$mm" -le 59 ] || { ui_msg "Ungueltig" "Stunde 0-23, Minute 0-59."; return 1; } + case "$kind" in + taeglich) printf '%s %s * * *\n' "$mm" "$hh" ;; + woechentlich) dow="$(ui_menu "Wochentag" "An welchem Tag?" \ + 1 Montag 2 Dienstag 3 Mittwoch 4 Donnerstag \ + 5 Freitag 6 Samstag 0 Sonntag)" || return 1 + printf '%s %s * * %s\n' "$mm" "$hh" "$dow" ;; + monatlich) dom="$(ui_input "Tag im Monat" "Welcher Tag (1-28)?" "1")" || return 1 + case "$dom" in ''|*[!0-9]*) ui_msg "Ungueltig" "Bitte eine Zahl."; return 1 ;; esac + printf '%s %s %s * *\n' "$mm" "$hh" "$dom" ;; + esac +} + +gui_job_edit() { # name (leer = neu) + local name="$1" line="" sites="ALL" cron="15 2 * * *" dir="$BACKUP_DIR" opts="" keep="7" + if [ -n "$name" ]; then + line="$(job_line "$name")" + sites="$(job_f "$line" 2)"; cron="$(job_f "$line" 3)"; dir="$(job_f "$line" 4)" + opts="$(job_f "$line" 5)"; keep="$(job_f "$line" 6)" + else + name="$(ui_input "Neuer Job" "Name des Jobs (Buchstaben, Ziffern, _ und -):" "nachts")" || return 0 + local msg + if ! msg="$(job_validate_name "$name")"; then ui_msg "Ungueltiger Name" "$msg"; return; fi + if job_exists "$name"; then ui_msg "Schon vorhanden" "Ein Job namens '$name' existiert bereits."; return; fi + fi + + # Sites auswaehlen + if gui_need_sites; then + local -a items=() sname + items+=("ALL" "alle Sites (auch spaeter neu angelegte)" "$([ "$sites" = ALL ] && echo on || echo off)") + while IFS= read -r sname; do + [ -n "$sname" ] || continue + items+=("$sname" "einzelne Site" \ + "$(printf '%s' ",$sites," | grep -q ",$sname," && echo on || echo off)") + done < <(cmk_sites) + local chosen + chosen="$(ui_checklist "Sites" "Was soll dieser Job sichern?\n(ALL schliesst alle anderen ein)" "${items[@]}")" || return 0 + chosen="$(printf '%s' "$chosen" | tr -d '"')" + [ -n "$chosen" ] || { ui_msg "Keine Auswahl" "Es wurde keine Site gewaehlt."; return; } + case " $chosen " in + *" ALL "*) sites=ALL ;; + *) sites="$(printf '%s' "$chosen" | tr ' ' ',')" ;; + esac + fi + + cron="$(gui_pick_cron "$cron")" || return 0 + [ -n "$cron" ] || return 0 + + dir="$(ui_input "Zielverzeichnis" \ +"Wohin soll dieser Job sichern? +(absolut oder relativ zum Projekt; wird bei Bedarf angelegt)" "$dir")" || return 0 + [ -n "$dir" ] || return 0 + + keep="$(ui_input "Aufbewahrung" \ +"Wie viele Backups sollen in diesem Verzeichnis bleiben? +0 = alle behalten" "$keep")" || return 0 + case "$keep" in ''|*[!0-9]*) ui_msg "Ungueltig" "Bitte eine Zahl angeben."; return ;; esac + + local optsel + optsel="$(ui_checklist "Optionen" "Optionen fuer diesen Job:" \ + no-rrds "Performance-Daten (RRD) auslassen" "$(case "$opts" in *no-rrds*) echo on ;; *) echo off ;; esac)" \ + no-logs "Logdateien auslassen" "$(case "$opts" in *no-logs*) echo on ;; *) echo off ;; esac)" \ + stop "Site waehrend des Backups stoppen" "$(case "$opts" in *stop*) echo on ;; *) echo off ;; esac)")" || return 0 + opts="" + case "$optsel" in *no-rrds*) opts="$opts --no-rrds" ;; esac + case "$optsel" in *no-logs*) opts="$opts --no-logs" ;; esac + case "$optsel" in *stop*) opts="$opts --stop" ;; esac + opts="$(printf '%s' "$opts" | sed 's/^ *//')" + + local action=add; job_exists "$name" && action=set + ui_run "Job speichern" bash -c \ + "cd '$PROJECT_DIR' && '$SELF' -y job $action --name '$name' --sites '$sites' \ + --cron '$cron' --dir '$dir' --keep '$keep' $opts" +} + gui_restore() { - require_cmk_running - local file; file="$(gui_pick_backup)" || return - [ -n "$file" ] || return + gui_need_container || return 0 + local file; file="$(gui_pick_backup)" || return 0 + [ -n "$file" ] || return 0 local f; f="$(mktemp)" bundle_show "$file" >"$f" 2>&1 || true @@ -2267,15 +2877,15 @@ gui_restore() { bundle_read "$file" || { ui_msg "Fehler" "Diese Datei ist kein Checkmk-Backup."; return; } local reuse="" if cmk_site_exists "$B_SITE"; then - ui_yesno "Site existiert" "Die Site '$B_SITE' ist bereits vorhanden.\n\nSoll sie UEBERSCHRIEBEN werden?\nAlle aktuellen Daten dieser Site gehen verloren!" || return + ui_yesno "Site existiert" "Die Site '$B_SITE' ist bereits vorhanden.\n\nSoll sie UEBERSCHRIEBEN werden?\nAlle aktuellen Daten dieser Site gehen verloren!" || return 0 reuse="--reuse --kill" fi - ui_yesno "Restore starten" "Site '$B_SITE' aus\n$(basename "$file")\nwiederherstellen?" || return + ui_yesno "Restore starten" "Site '$B_SITE' aus\n$(basename "$file")\nwiederherstellen?" || return 0 ui_run "Restore laeuft" bash -c "cd '$PROJECT_DIR' && '$SELF' -y restore --file '$file' $reuse" } gui_import() { - require_cmk_running + gui_need_container || return 0 ui_msg "Site importieren" \ "Import einer Site von einer Direktinstallation (Bare-Metal/VM). @@ -2288,8 +2898,8 @@ Wichtig fuer die Agents: - Port 8000 (Agent-Receiver) ist erreichbar Dann muessen die Agents NICHT neu eingerichtet werden." - local file; file="$(gui_pick_backup)" || return - [ -n "$file" ] || return + local file; file="$(gui_pick_backup)" || return 0 + [ -n "$file" ] || return 0 local f; f="$(mktemp)" bundle_show "$file" >"$f" 2>&1 || true @@ -2298,7 +2908,7 @@ Dann muessen die Agents NICHT neu eingerichtet werden." bundle_read "$file" || { ui_msg "Fehler" "Diese Datei ist kein Migrations-Bundle."; return; } local reuse="" if cmk_site_exists "$B_SITE"; then - ui_yesno "Site existiert" "Die Site '$B_SITE' existiert bereits im Container.\n\nUEBERSCHREIBEN?" || return + ui_yesno "Site existiert" "Die Site '$B_SITE' existiert bereits im Container.\n\nUEBERSCHREIBEN?" || return 0 reuse="--reuse --kill" fi local setdef="" @@ -2334,7 +2944,7 @@ gui_backups_menu() { } gui_sites_menu() { - require_cmk_running + gui_need_container || return 0 while :; do local sel sel="$(ui_menu "Sites" "Sites im Container '$CMK_CONTAINER_NAME'" \ @@ -2371,14 +2981,14 @@ Edition/Version: ${CMK_EDITION:-raw} / ${CMK_VERSION:-?}" \ case "$sel" in status) local f; f="$(mktemp)"; NO_COLOR=1 cmd_image status >"$f" 2>&1 || true ui_text "Image-Status" "$f"; rm -f "$f" ;; - test) gui_image_test ;; - pull) gui_image_pull ;; - download) gui_image_download ;; + test) gui_image_test || true ;; + pull) gui_image_pull || true ;; + download) gui_image_download || true ;; load) local lf; lf="$(ui_pick_file "${IMAGE_DIR:-$PROJECT_DIR/images}" '*.tar.gz')" || continue [ -n "$lf" ] || continue ui_run "Image einspielen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y image load --file '$lf'" load_env "${ENV_FILE:-$PROJECT_DIR/.env}" ;; - use) gui_image_use ;; + use) gui_image_use || true ;; back) return 0 ;; esac done @@ -2398,14 +3008,14 @@ gui_image_pick_edition() { gui_image_test() { local ed ver user pass - ed="$(gui_image_pick_edition)" || return - ver="$(ui_input "Version" "Version (fuer die Pruefung des Downloadportals):" "${CMK_VERSION:-}")" || return + ed="$(gui_image_pick_edition)" || return 0 + ver="$(ui_input "Version" "Version (fuer die Pruefung des Downloadportals):" "${CMK_VERSION:-}")" || return 0 local -a extra=() if ui_yesno "Zugangsdaten" "Zugangsdaten aus der .env verwenden?\n\nRegistry : ${CMK_REGISTRY_USER:-}\nDownload : ${CMK_DOWNLOAD_USER:-}\n\n = jetzt eingeben"; then : else - user="$(ui_input "Zugangsdaten" "Benutzer (Checkmk-Kundenportal):" "${CMK_REGISTRY_USER:-}")" || return - pass="$(ui_password "Zugangsdaten" "Kennwort:")" || return + user="$(ui_input "Zugangsdaten" "Benutzer (Checkmk-Kundenportal):" "${CMK_REGISTRY_USER:-}")" || return 0 + pass="$(ui_password "Zugangsdaten" "Kennwort:")" || return 0 [ -n "$user" ] && extra+=(--user "$user" --password "$pass") fi ui_run "Zugangsdaten pruefen" bash -c \ @@ -2414,13 +3024,13 @@ gui_image_test() { gui_image_pull() { local ed ver user pass - ed="$(gui_image_pick_edition)" || return - ver="$(ui_input "Version" "Version bzw. Tag (z.B. 2.3.0p23 oder 2.3.0-latest):" "${CMK_VERSION:-}")" || return - [ -n "$ver" ] || return + ed="$(gui_image_pick_edition)" || return 0 + ver="$(ui_input "Version" "Version bzw. Tag (z.B. 2.3.0p23 oder 2.3.0-latest):" "${CMK_VERSION:-}")" || return 0 + [ -n "$ver" ] || return 0 local -a extra=() if [ "$ed" = enterprise ] || [ -n "${CMK_REGISTRY_USER:-}" ]; then - user="$(ui_input "Registry-Anmeldung" "Benutzer (Checkmk-Kundenportal):" "${CMK_REGISTRY_USER:-}")" || return - pass="$(ui_password "Registry-Anmeldung" "Kennwort:")" || return + user="$(ui_input "Registry-Anmeldung" "Benutzer (Checkmk-Kundenportal):" "${CMK_REGISTRY_USER:-}")" || return 0 + pass="$(ui_password "Registry-Anmeldung" "Kennwort:")" || return 0 [ -n "$user" ] && extra+=(--user "$user" --password "$pass") fi ui_run "Image holen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y image pull --edition '$ed' --version '$ver' ${extra[*]+${extra[*]}}" @@ -2429,11 +3039,11 @@ gui_image_pull() { gui_image_download() { local ed ver user pass - ed="$(gui_image_pick_edition)" || return - ver="$(ui_input "Version" "EXAKTE Version (z.B. 2.3.0p23 - kein '-latest'):" "${CMK_VERSION:-}")" || return - [ -n "$ver" ] || return - user="$(ui_input "Downloadportal" "Benutzer:" "${CMK_DOWNLOAD_USER:-}")" || return - pass="$(ui_password "Downloadportal" "Kennwort:")" || return + ed="$(gui_image_pick_edition)" || return 0 + ver="$(ui_input "Version" "EXAKTE Version (z.B. 2.3.0p23 - kein '-latest'):" "${CMK_VERSION:-}")" || return 0 + [ -n "$ver" ] || return 0 + user="$(ui_input "Downloadportal" "Benutzer:" "${CMK_DOWNLOAD_USER:-}")" || return 0 + pass="$(ui_password "Downloadportal" "Kennwort:")" || return 0 ui_run "Image herunterladen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y image download --edition '$ed' --version '$ver' --user '$user' --password '$pass'" load_env "${ENV_FILE:-$PROJECT_DIR/.env}" } @@ -2446,7 +3056,7 @@ gui_image_use() { done < <(image_local_list) [ ${#items[@]} -gt 0 ] || { ui_msg "Images" "Es ist kein Checkmk-Image lokal vorhanden.\n\nZuerst 'pull', 'download' oder 'load' verwenden."; return; } local sel - sel="$(ui_menu "Image auswaehlen" "Lokal vorhandene Checkmk-Images:" "${items[@]}")" || return + sel="$(ui_menu "Image auswaehlen" "Lokal vorhandene Checkmk-Images:" "${items[@]}")" || return 0 ui_run "Image setzen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y image use '$sel'" load_env "${ENV_FILE:-$PROJECT_DIR/.env}" } @@ -2485,15 +3095,15 @@ gui_settings_edit() { IFS=':' done IFS="$IFS_BAK" - new="$(ui_radiolist "$key" "$label\n\nAktuell: ${cur:-}" "${ropts[@]}")" || return + new="$(ui_radiolist "$key" "$label\n\nAktuell: ${cur:-}" "${ropts[@]}")" || return 0 [ "$new" = auto ] && new="" ;; password) - new="$(ui_password "$key" "$label\n\n(leer lassen = unveraendert)")" || return + new="$(ui_password "$key" "$label\n\n(leer lassen = unveraendert)")" || return 0 [ -z "$new" ] && return ;; *) - new="$(ui_input "$key" "$label\n\nTyp: $typ$([ -n "$opt" ] && echo ' (darf leer bleiben)')" "$cur")" || return + new="$(ui_input "$key" "$label\n\nTyp: $typ$([ -n "$opt" ] && echo ' (darf leer bleiben)')" "$cur")" || return 0 ;; esac @@ -2541,6 +3151,7 @@ gui_settings_menu() { while IFS='|' read -r c l; do [ -n "$c" ] && items+=("$c" "$l") done < <(settings_categories) + items+=("jobs" "Backup-Jobs (Zeitplan, Zielpfade, Aufbewahrung)") items+=("show" "Alle Werte auflisten") items+=("editor" "Datei .env im Editor oeffnen") items+=("back" "zurueck") @@ -2550,12 +3161,14 @@ gui_settings_menu() { Datei: ${ENV_FILE:-$PROJECT_DIR/.env}" "${items[@]}")" || return 0 case "$sel" in back) return 0 ;; + jobs) gui_jobs_menu || true ;; show) local f; f="$(mktemp)" { printf '%-24s %-10s %s\n' "SCHLUESSEL" "BEREICH" "WERT" NO_COLOR=1 cmd_config list; } >"$f" 2>&1 ui_text "Alle Einstellungen" "$f"; rm -f "$f" ;; editor) local ef="${ENV_FILE:-$PROJECT_DIR/.env}" - clear; "${EDITOR:-nano}" "$ef" + if [ ! -f "$ef" ]; then ui_msg "Keine .env" "Die Datei $ef existiert nicht.\n\nAnlegen mit: cp .env.example .env"; continue; fi + clear 2>/dev/null || true; "${EDITOR:-nano}" "$ef" load_env "$ef"; ui_msg "Konfiguration" "Die .env wurde neu eingelesen." ;; *) local title title="$(settings_categories | awk -F'|' -v c="$sel" '$1==c{print $2}')" @@ -2588,17 +3201,16 @@ Oberflaeche des Proxy Managers darstellen." \ [ -n "$u" ] && NPM_ADMIN_URL="$u" ;; build) ui_run "Werkzeug-Image bauen" bash -c "cd '$PROJECT_DIR' && '$SELF' browser --build-only" || true ;; back) return 0 ;; - *) clear - cmd_browser --engine "$sel" --url "${NPM_ADMIN_URL:-http://127.0.0.1:81}" || true - ;; + *) gui_run_interactive cmd_browser --engine "$sel" \ + --url "${NPM_ADMIN_URL:-http://127.0.0.1:81}" ;; esac done } gui_cert() { local domain alt days - domain="$(ui_input "Zertifikat" "FQDN / Common Name:" "${CERT_DOMAIN:-checkmk.example.local}")" || return - [ -n "$domain" ] || return + domain="$(ui_input "Zertifikat" "FQDN / Common Name:" "${CERT_DOMAIN:-checkmk.example.local}")" || return 0 + [ -n "$domain" ] || return 0 alt="$(ui_input "Zertifikat" "Weitere Namen/IPs (durch Leerzeichen getrennt, optional):" "$(primary_ip)")" || alt="" days="$(ui_input "Zertifikat" "Gueltigkeit in Tagen:" "${CERT_DAYS:-3650}")" || days=3650 local -a a=(); local x @@ -2616,7 +3228,7 @@ Runtime : $RUNTIME Standard-Site: $CMK_SITE_ID Backups : $BACKUP_DIR" \ status "Status von Containern und Sites" \ stack "Stack steuern (Start/Stop/Logs/Update)" \ - backup "Backup erstellen" \ + backup "Backup erstellen / planen" \ restore "Backup wiederherstellen" \ import "Site von Direktinstallation importieren" \ manage "Backups verwalten (Liste/Details/Loeschen)" \ @@ -2628,22 +3240,23 @@ Backups : $BACKUP_DIR" \ shell "Root-Shell im Checkmk-Container" \ quit "Beenden")" || break case "$sel" in - status) gui_status ;; - stack) gui_stack_menu ;; - backup) gui_backup ;; - restore) gui_restore ;; - import) gui_import ;; - manage) gui_backups_menu ;; - sites) gui_sites_menu ;; - image) gui_image_menu ;; - browser) gui_browser_menu ;; - cert) gui_cert ;; - config) gui_settings_menu ;; - shell) clear; cmd_shell || true ;; + status) gui_status || true ;; + stack) gui_stack_menu || true ;; + backup) gui_backup_menu || true ;; + restore) gui_restore || true ;; + import) gui_import || true ;; + manage) gui_backups_menu || true ;; + sites) gui_sites_menu || true ;; + image) gui_image_menu || true ;; + browser) gui_browser_menu || true ;; + cert) gui_cert || true ;; + config) gui_settings_menu || true ;; + shell) gui_need_container || continue + gui_run_interactive cmd_shell ;; quit) break ;; esac done - clear + clear 2>/dev/null || true ok "Beendet." } @@ -2670,29 +3283,30 @@ main() { gui|menu) gui_main ;; up|start) cmd_up ;; down|stop) cmd_down ;; - restart) cmd_restart ;; - pull) cmd_pull ;; - status) cmd_status ;; - logs) cmd_logs "$@" ;; - shell) cmd_shell ;; - omd) cmd_omd "$@" ;; - sites) cmd_sites ;; + restart) cmd_restart || true ;; + pull) cmd_pull || true ;; + status) cmd_status || true ;; + logs) cmd_logs "$@" || true ;; + shell) cmd_shell || true ;; + omd) cmd_omd "$@" || true ;; + sites) cmd_sites || true ;; site-start) cmd_site_start "$@" ;; site-stop) cmd_site_stop "$@" ;; start-all-sites) cmd_start_all_sites ;; - backup) cmd_backup "$@" ;; - list) cmd_list ;; - prune) cmd_prune "$@" ;; + backup) cmd_backup "$@" || true ;; + list) cmd_list || true ;; + prune) cmd_prune "$@" || true ;; + job|jobs) cmd_job "$@" ;; verify) local vf=""; [ "${1:-}" = "--file" ] && vf="$2"; [ -n "$vf" ] || vf="${1:-}" [ -n "$vf" ] || die "--file fehlt."; bundle_verify "$vf" ;; - restore) cmd_restore "$@" ;; - import) cmd_import "$@" ;; - cert) cmd_cert "$@" ;; - image) cmd_image "$@" ;; - browser) cmd_browser "$@" ;; + restore) cmd_restore "$@" || true ;; + import) cmd_import "$@" || true ;; + cert) cmd_cert "$@" || true ;; + image) cmd_image "$@" || true ;; + browser) cmd_browser "$@" || true ;; npm-port) cmd_npm_port "$@" ;; npm-enable) cmd_npm_enable "$@" ;; - config) cmd_config "$@" ;; + config) cmd_config "$@" || true ;; help|-h|--help) usage ;; *) err "Unbekanntes Kommando: $cmd"; echo; usage; exit 1 ;; esac diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh index 791426f..910fd0e 100644 --- a/scripts/lib/common.sh +++ b/scripts/lib/common.sh @@ -44,6 +44,7 @@ load_env() { CMK_SITE_ID="${CMK_SITE_ID:-cmk}" CMK_IMAGE="${CMK_IMAGE:-docker.io/checkmk/check-mk-raw:2.3.0-latest}" NPM_ENABLED="${NPM_ENABLED:-on}" + BACKUP_AUTO="${BACKUP_AUTO:-off}" NPM_ADMIN_PORT="${NPM_ADMIN_PORT:-81}" # Leer = automatisch aus dem Admin-Port ableiten NPM_ADMIN_URL="${NPM_ADMIN_URL:-http://127.0.0.1:$NPM_ADMIN_PORT}"