commit 75fba0229a51ccb2db0dae9d24ab8ffb8afae926 Author: duffyduck Date: Thu Aug 20 10:33:59 2026 +0200 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff7fecd --- /dev/null +++ b/README.md @@ -0,0 +1,930 @@ +# Checkmk im Container – Docker (Debian) & Podman (RHEL) mit NGINX Proxy Manager + +Vollständige, produktionstaugliche Container-Installation von **Checkmk** mit +**NGINX Proxy Manager** als HTTPS-Terminierung (Let's Encrypt *oder* selbstsigniert), +inklusive Werkzeugen für **Backup**, **Restore** und die **Migration bestehender +Sites von einer Direktinstallation** – ohne dass die Agents neu eingerichtet +werden müssen. + +--- + +## Inhaltsverzeichnis + +1. [Überblick & Architektur](#1-überblick--architektur) +2. [Verzeichnisstruktur](#2-verzeichnisstruktur) +3. [Voraussetzungen & Ports](#3-voraussetzungen--ports) +4. [Installation auf Debian/Ubuntu (Docker)](#4-installation-auf-debianubuntu-docker) +5. [Installation auf RHEL/Rocky/Alma/Fedora (Podman)](#5-installation-auf-rhelrockyalmafedora-podman) +6. [Autostart nach einem Neustart des Hosts (Podman)](#6-autostart-nach-einem-neustart-des-hosts-podman) +7. [Konfiguration (`.env`)](#7-konfiguration-env) +8. [Erststart](#8-erststart) +9. [HTTPS einrichten – Let's Encrypt oder selbstsigniert](#9-https-einrichten--lets-encrypt-oder-selbstsigniert) +10. [Firewall](#10-firewall) +11. [Verwaltung: `cmk-manage.sh` (CLI und ncurses-Oberfläche)](#11-verwaltung-cmk-managesh) +12. [Backup](#12-backup) +13. [Restore](#13-restore) +14. [Migration einer Direktinstallation (`migrate_creator.sh`)](#14-migration-einer-direktinstallation-migrate_creatorsh) +15. [Mehrere Sites in einem Container](#15-mehrere-sites-in-einem-container) +16. [Checkmk aktualisieren](#16-checkmk-aktualisieren) +17. [Fehlersuche](#17-fehlersuche) +18. [Sicherheitshinweise](#18-sicherheitshinweise) +19. [Deinstallation](#19-deinstallation) + +--- + +## 1. Überblick & Architektur + +``` + Internet / LAN + | + :80 :443 :81 + | + +-----------------------------------+ + | NGINX Proxy Manager (Container) | network_mode: host + | - Let's Encrypt ODER self-signed | + | - Weboberfläche auf Port 81 | + +-----------------------------------+ + | http://127.0.0.1:5000 + v + +-----------------------------------+ + | Checkmk / OMD (Container) | network_mode: host + | :5000 Web-GUI (Apache) | + | :8000 Agent-Receiver | <-- Agents (cmk-agent-ctl) + | :6557 Livestatus (optional) | + +-----------------------------------+ + | + Bind-Mount: ./data/checkmk/sites -> /omd/sites +``` + +**Bewusste Entwurfsentscheidungen** + +| Punkt | Umsetzung | Grund | +|---|---|---| +| Checkmk im Host-Netzwerk | `network_mode: host`, **keine** `ports:`-Angaben | Agents, SNMP-Traps, ICMP und der Agent-Receiver arbeiten ohne NAT; die Quell-IP der überwachten Hosts bleibt erhalten | +| NGINX Proxy Manager ebenfalls im Host-Netzwerk | `network_mode: host` | erreicht das Checkmk-Backend direkt über `127.0.0.1:5000`; kein Bridge-Netz, keine `host-gateway`-Tricks nötig – funktioniert unter Docker **und** Podman identisch | +| Keine Named Volumes | ausschließlich **Bind-Mounts** nach `./data/` | alle Daten liegen sichtbar im Projektverzeichnis, einfach zu sichern, zu kopieren und zu prüfen | +| Eine Compose-Datei für beides | `docker compose` und `podman-compose` | identische Bedienung auf Debian und RHEL | +| `:Z` an den Bind-Mounts | SELinux-Relabeling | nötig unter RHEL; unter Debian/Docker wirkungslos (kein Fehler) | + +--- + +## 2. Verzeichnisstruktur + +``` +. +├── README.md +├── docker-compose.yml # für Docker UND Podman +├── .env.example # Vorlage -> .env +├── install.sh # Installation Docker (Debian) / Podman (RHEL) +│ +├── scripts/ +│ ├── cmk-manage.sh # Stack, Backup, Restore, Import + ncurses-GUI +│ ├── migrate_creator.sh # -> auf den QUELLSERVER kopieren +│ └── lib/ +│ └── common.sh # gemeinsame Funktionen + TUI-Helfer +│ +├── systemd/ +│ ├── checkmk-stack.service.example # Compose-basierte Unit (Autostart) +│ └── quadlet/ +│ ├── checkmk.container # Podman-Quadlet (Alternative) +│ └── nginx-proxy-manager.container +│ +├── data/ # ALLE Nutzdaten (Bind-Mounts) +│ ├── checkmk/sites/ # -> /omd/sites (komplette OMD-Sites) +│ └── npm/ +│ ├── data/ # -> /data (NPM-Konfiguration/DB) +│ ├── letsencrypt/ # -> /etc/letsencrypt (Zertifikate) +│ └── custom-certs/ # selbstsignierte Zertifikate zum Hochladen +│ +└── backups/ # Backups und Migrations-Bundles (*.tar.gz) +``` + +--- + +## 3. Voraussetzungen & Ports + +* 64-Bit-Linux, mind. 2 CPU-Kerne, 4 GB RAM (Empfehlung: 4 Kerne, 8 GB) +* Plattenplatz: Faustregel 1 GB je 100 überwachte Hosts und Jahr, plus Backups +* root-Rechte +* Ausgehender Internetzugang für den Image-Download + +### Belegte Ports (Host-Netzwerk!) + +| Port | Dienst | Erreichbar von | +|---|---|---| +| 80/tcp | NGINX Proxy Manager – HTTP, Let's-Encrypt-Challenge | Internet/LAN | +| 443/tcp | NGINX Proxy Manager – HTTPS | Internet/LAN | +| 81/tcp | NGINX Proxy Manager – **Adminoberfläche** | nur Admin-Netz! | +| 5000/tcp | Checkmk Apache (Backend) | **nur localhost** – nicht freigeben | +| 8000/tcp | Checkmk Agent-Receiver (TLS-registrierte Agents) | überwachte Hosts | +| 6557/tcp | Livestatus (nur bei `CMK_LIVESTATUS_TCP=on`) | verteilte Sites | + +> **Wichtig:** Da beide Container im Host-Netzwerk laufen, dürfen die Ports 80, +> 443 und 81 auf dem Host nicht bereits belegt sein (z. B. durch einen +> installierten Apache/NGINX). Prüfen mit `ss -tlnp | grep -E ':(80|81|443|5000)\b'`. + +--- + +## 4. Installation auf Debian/Ubuntu (Docker) + +```bash +# Projekt an den gewünschten Ort legen +sudo mkdir -p /opt/checkmk-stack +sudo cp -r ./* ./.env.example /opt/checkmk-stack/ +cd /opt/checkmk-stack + +# Docker CE + Compose-Plugin installieren, Projekt vorbereiten +sudo ./install.sh --with-systemd +``` + +`install.sh` erledigt dabei: + +1. Docker-Repository von `download.docker.com` einbinden (GPG-Key + Sources-Liste) +2. `docker-ce`, `docker-ce-cli`, `containerd.io`, `docker-buildx-plugin`, + `docker-compose-plugin` installieren +3. `dialog`, `openssl`, `tar`, `curl` installieren (für die ncurses-Oberfläche) +4. `systemctl enable --now docker` +5. Verzeichnisse unter `data/` und `backups/` anlegen, `.env` aus `.env.example` erzeugen +6. optional die systemd-Unit `checkmk-stack.service` einrichten + +Anschließend: + +```bash +sudo $EDITOR /opt/checkmk-stack/.env # mindestens CMK_PASSWORD und CMK_SITE_ID +sudo ./scripts/cmk-manage.sh up +sudo ./scripts/cmk-manage.sh status +``` + +Unter Docker sorgt bereits `restart: unless-stopped` dafür, dass die Container +nach einem Reboot wieder starten (der Docker-Daemon startet sie). Die +systemd-Unit ist trotzdem sinnvoll, weil sie zusätzlich **alle** Sites startet +(siehe [Kapitel 15](#15-mehrere-sites-in-einem-container)). + +--- + +## 5. Installation auf RHEL/Rocky/Alma/Fedora (Podman) + +Auf Red-Hat-Systemen steht Docker nicht zur Verfügung – hier wird **Podman** +verwendet. Die Compose-Datei ist dieselbe. + +```bash +sudo mkdir -p /opt/checkmk-stack +sudo cp -r ./* ./.env.example /opt/checkmk-stack/ +cd /opt/checkmk-stack + +sudo ./install.sh --with-systemd +``` + +`install.sh` installiert dabei: + +1. `podman`, `tar`, `dialog`, `openssl` +2. `podman-compose` – in dieser Reihenfolge versucht: + Distributions-Repository → EPEL → `pip3 install podman-compose` +3. richtet die systemd-Unit ein (bei Podman **immer**, siehe nächstes Kapitel) + +Manuell entspricht das: + +```bash +sudo dnf install -y podman podman-compose tar dialog openssl +# Falls podman-compose nicht im Repo ist: +sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm +sudo dnf install -y podman-compose +``` + +Starten: + +```bash +sudo $EDITOR /opt/checkmk-stack/.env +sudo ./scripts/cmk-manage.sh up # nutzt automatisch podman-compose +sudo ./scripts/cmk-manage.sh status +``` + +### SELinux + +Die Bind-Mounts sind in `docker-compose.yml` mit `:Z` versehen, Podman setzt +dadurch automatisch den Kontext `container_file_t`. Falls Zugriffe dennoch +scheitern: + +```bash +sudo semanage fcontext -a -t container_file_t "/opt/checkmk-stack/data(/.*)?" +sudo restorecon -Rv /opt/checkmk-stack/data +sudo ausearch -m avc -ts recent # AVC-Meldungen prüfen +``` + +--- + +## 6. Autostart nach einem Neustart des Hosts (Podman) + +> **Das ist bei Podman der wichtigste Unterschied zu Docker:** Podman hat +> *keinen Daemon*. Die Angabe `restart: unless-stopped` in der Compose-Datei +> wirkt deshalb **nicht** über einen Reboot hinweg. Der Autostart muss über +> systemd erfolgen. Es gibt zwei erprobte Wege. + +### Variante A – Compose-basierte systemd-Unit (empfohlen, identisch zu Docker) + +Wird von `./install.sh --with-systemd` automatisch erzeugt und aktiviert. +Manuell: + +```bash +sudo tee /etc/systemd/system/checkmk-stack.service >/dev/null <<'EOF' +[Unit] +Description=Checkmk + NGINX Proxy Manager (Container-Stack) +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +RemainAfterExit=yes +WorkingDirectory=/opt/checkmk-stack +EnvironmentFile=/opt/checkmk-stack/.env +TimeoutStartSec=0 +ExecStart=/bin/sh -c '/usr/bin/podman-compose up -d' +ExecStartPost=/opt/checkmk-stack/scripts/cmk-manage.sh start-all-sites +ExecStop=/bin/sh -c '/usr/bin/podman-compose down' + +[Install] +WantedBy=multi-user.target +EOF + +sudo systemctl daemon-reload +sudo systemctl enable --now checkmk-stack.service +sudo systemctl status checkmk-stack.service +``` + +Prüfen, dass es einen Reboot übersteht: + +```bash +sudo systemctl is-enabled checkmk-stack # -> enabled +sudo reboot +# nach dem Neustart: +sudo podman ps +sudo /opt/checkmk-stack/scripts/cmk-manage.sh status +``` + +`ExecStartPost` ruft `start-all-sites` auf. Das ist nötig, weil der +Checkmk-Entrypoint nur die in `CMK_SITE_ID` eingetragene Site startet – bei +mehreren Sites blieben die übrigen sonst gestoppt. + +### Variante B – Podman-Quadlet (ab Podman 4.4, ohne Compose) + +Quadlet erzeugt aus einfachen Beschreibungsdateien echte systemd-Units. +Vorteil: kein `podman-compose` nötig, saubere Abhängigkeiten, natives Podman. + +```bash +sudo ./install.sh --quadlet +``` + +Das schreibt (mit den Werten aus der `.env`) nach `/etc/containers/systemd/`: + +* `checkmk.container` +* `nginx-proxy-manager.container` + +Danach: + +```bash +sudo systemctl daemon-reload +sudo systemctl start checkmk nginx-proxy-manager +sudo systemctl status checkmk +``` + +Die Units werden durch `WantedBy=multi-user.target default.target` beim +Systemstart automatisch aktiviert – ein separates `systemctl enable` ist bei +Quadlet nicht nötig (und auch nicht möglich, da die Units generiert werden). + +Vorlagen liegen unter [systemd/quadlet/](systemd/quadlet/). + +### Variante C – rootless Podman (optional, erhöhter Aufwand) + +```bash +sudo ./install.sh --rootless +sudo loginctl enable-linger checkmk # Dienstbenutzer +# Unit als Benutzer-Unit: +mkdir -p ~/.config/systemd/user +cp systemd/checkmk-stack.service.example ~/.config/systemd/user/checkmk-stack.service +systemctl --user daemon-reload +systemctl --user enable --now checkmk-stack +``` + +`--rootless` setzt `net.ipv4.ip_unprivileged_port_start=80`, damit die Ports +80/443 ohne root gebunden werden dürfen. `loginctl enable-linger` sorgt dafür, +dass die Benutzer-Units auch ohne Anmeldung nach einem Reboot starten. + +> Hinweis: Im rootless-Betrieb werden die UIDs der Site-Benutzer über +> User-Namespaces abgebildet. Die Dateien unter `data/checkmk/sites` gehören +> dann Subordinate-UIDs (z. B. `165536`). Das ist normal, erschwert aber die +> Arbeit mit den Dateien vom Host aus. Für Migrationen wird der **rootful** +> Betrieb empfohlen. + +--- + +## 7. Konfiguration (`.env`) + +```bash +cp .env.example .env +chmod 600 .env +$EDITOR .env +``` + +Die wichtigsten Schalter: + +| Variable | Bedeutung | +|---|---| +| `CMK_RUNTIME` | leer = automatische Erkennung, sonst `docker` oder `podman` | +| `CMK_IMAGE_REPO` | `checkmk/check-mk-raw` (frei) oder `checkmk/check-mk-enterprise` | +| `CMK_IMAGE_TAG` | **Version** – bei einer Migration muss sie zur Quell-Site passen! | +| `CMK_SITE_ID` | Site, die der Container anlegt bzw. beim Start hochfährt | +| `CMK_PASSWORD` | Startpasswort für den Benutzer `cmkadmin` (nur bei Neuanlage) | +| `CMK_LIVESTATUS_TCP` | `on` öffnet Port 6557 für verteilte Überwachung | +| `MAIL_RELAY_HOST` | SMTP-Relay für Benachrichtigungen | +| `TZ` | Zeitzone, z. B. `Europe/Berlin` | +| `BACKUP_DIR` | Ablage für Backups/Bundles (Vorgabe `./backups`) | +| `CERT_DOMAIN`, `CERT_DAYS` | Vorgaben für selbstsignierte Zertifikate | + +--- + +## 8. Erststart + +```bash +sudo ./scripts/cmk-manage.sh up +sudo ./scripts/cmk-manage.sh status +``` + +Ausgabe (Beispiel): + +``` +Container + checkmk läuft + nginx-proxy-manager läuft + +Sites im Container + SITE VERSION STATUS + cmk 2.3.0p23.cre läuft + +Zugang + Checkmk (direkt) : http://10.0.0.5:5000/cmk/ + Checkmk (über Proxy) : https:///cmk/ + Agent-Receiver : 10.0.0.5:8000 + NGINX Proxy Manager : http://10.0.0.5:81/ (Erstlogin: admin@example.com / changeme) +``` + +Anmeldung an Checkmk: Benutzer `cmkadmin`, Passwort aus `CMK_PASSWORD`. + +--- + +## 9. HTTPS einrichten – Let's Encrypt oder selbstsigniert + +### 9.1 NGINX Proxy Manager vorbereiten + +1. `http://:81` aufrufen +2. Erstanmeldung: **admin@example.com** / **changeme** +3. Sofort E-Mail-Adresse und Passwort ändern (wird beim ersten Login erzwungen) + +### 9.2 Proxy-Host für Checkmk anlegen + +**Hosts → Proxy Hosts → Add Proxy Host** + +| Feld | Wert | +|---|---| +| Domain Names | `checkmk.example.com` (bzw. der interne Name) | +| Scheme | `http` | +| Forward Hostname / IP | `127.0.0.1` | +| Forward Port | `5000` | +| Block Common Exploits | ein | +| Websockets Support | **ein** (für die Livedaten der Oberfläche) | + +Reiter **Advanced** (empfohlen, damit Checkmk die echte Client-IP sieht und +große Agent-Uploads funktionieren): + +```nginx +client_max_body_size 0; +proxy_read_timeout 300s; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +``` + +### 9.3 Variante A – Let's Encrypt (öffentlich erreichbarer Server) + +Voraussetzungen: der DNS-Name zeigt öffentlich auf diesen Server und **Port 80 +ist aus dem Internet erreichbar** (HTTP-01-Challenge). + +Im Proxy-Host, Reiter **SSL**: + +* SSL Certificate → **Request a new SSL Certificate** +* *Force SSL* ein +* *HTTP/2 Support* ein +* *HSTS Enabled* nach Bedarf +* E-Mail-Adresse angeben, Nutzungsbedingungen bestätigen → **Save** + +Die Erneuerung übernimmt der Proxy Manager automatisch; die Zertifikate liegen +im Bind-Mount `data/npm/letsencrypt/` und sind damit im Projektverzeichnis +gesichert. + +Bei interner DNS-Zone ohne öffentlichen Zugang: **DNS-Challenge** verwenden +(SSL → *Use a DNS Challenge* → Provider auswählen, API-Token hinterlegen). + +### 9.4 Variante B – Selbstsigniertes Zertifikat (interne Umgebung) + +Erzeugen: + +```bash +sudo ./scripts/cmk-manage.sh cert \ + --domain checkmk.intern.example \ + --alt 10.0.0.5 --alt checkmk \ + --days 3650 +``` + +Ergebnis in `data/npm/custom-certs/checkmk.intern.example/`: + +* `privkey.pem` – privater Schlüssel +* `fullchain.pem` – Zertifikat (mit SAN für alle angegebenen Namen/IPs) + +Einbinden: + +1. **SSL Certificates → Add SSL Certificate → Custom** +2. Name vergeben, `privkey.pem` als *Certificate Key*, `fullchain.pem` als + *Certificate* hochladen +3. Im Proxy-Host unter **SSL** dieses Zertifikat auswählen, *Force SSL* und + *HTTP/2* aktivieren + +> Damit die Checkmk-Agents und Browser dem Zertifikat vertrauen, muss +> `fullchain.pem` auf den Clients als vertrauenswürdiges Zertifikat installiert +> werden (Windows: Computerzertifikate → Vertrauenswürdige Stammzertifizierungsstellen; +> Linux: `/usr/local/share/ca-certificates/` + `update-ca-certificates`). +> Für die reine Agent-Kommunikation über Port 8000 ist das **nicht** nötig – +> dort verwendet Checkmk eine eigene, site-interne CA. + +Die ncurses-Oberfläche bietet denselben Ablauf unter *„Selbstsigniertes +Zertifikat erzeugen“*. + +--- + +## 10. Firewall + +**firewalld (RHEL/Rocky/Alma):** + +```bash +sudo firewall-cmd --permanent --add-service=http +sudo firewall-cmd --permanent --add-service=https +sudo firewall-cmd --permanent --add-port=8000/tcp # Agent-Receiver +sudo firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.0/24 port port=81 protocol=tcp accept' +sudo firewall-cmd --reload +``` + +**ufw (Debian/Ubuntu):** + +```bash +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw allow 8000/tcp +sudo ufw allow from 10.0.0.0/24 to any port 81 proto tcp +sudo ufw enable +``` + +Port **5000 bleibt geschlossen** – der Zugriff erfolgt ausschließlich über den +Proxy. Port **81** nur aus dem Administrationsnetz freigeben. + +--- + +## 11. Verwaltung: `cmk-manage.sh` + +Das Skript ist gleichzeitig Kommandozeilenwerkzeug **und** ncurses-Oberfläche. + +### 11.1 ncurses-Oberfläche (wie im Midnight Commander) + +```bash +sudo ./scripts/cmk-manage.sh gui +``` + +``` + ┌──────────────── Checkmk Stack Manager ─────────────────┐ + │ Projekt : /opt/checkmk-stack │ + │ Runtime : podman Standard-Site: cmk │ + │ Backups : /opt/checkmk-stack/backups │ + │ │ + │ status Status von Containern und Sites │ + │ stack Stack steuern (Start/Stop/Logs/Update) │ + │ backup Backup erstellen │ + │ restore Backup wiederherstellen │ + │ import Site von Direktinstallation importieren │ + │ manage Backups verwalten (Liste/Details/Löschen) │ + │ sites Sites im Container verwalten │ + │ cert Selbstsigniertes Zertifikat erzeugen │ + │ env Konfiguration (.env) ansehen/bearbeiten │ + │ shell Root-Shell im Checkmk-Container │ + │ quit Beenden │ + │ < OK > │ + └────────────────────────────────────────────────────────┘ +``` + +Bedienung: Pfeiltasten, **Leertaste** markiert in Auswahllisten (z. B. mehrere +Sites für ein Backup), **Enter** bestätigt, **ESC/Cancel** geht zurück. +Es wird `dialog` bevorzugt; ist nur `whiptail` vorhanden, wird dieses genutzt. +Eine Dateiauswahl mit Verzeichnisnavigation ist eingebaut. + +### 11.2 Kommandozeile + +```bash +./scripts/cmk-manage.sh --help +``` + +| Kommando | Wirkung | +|---|---| +| `up` / `down` / `restart` / `pull` | Stack steuern | +| `status` | Container, Sites, Zugangsadressen | +| `logs [-f]` | Container-Logs | +| `sites` | Sites im Container auflisten | +| `site-start SITE` / `site-stop SITE` | einzelne Site steuern | +| `start-all-sites` | alle Sites starten (wird von systemd aufgerufen) | +| `backup …` | Backup als `.tar.gz`-Bundle | +| `list` / `verify` / `prune --keep N` | Backups verwalten | +| `restore …` | Backup zurückspielen | +| `import …` | Migrations-Bundle einspielen | +| `cert …` | selbstsigniertes Zertifikat erzeugen | +| `shell` | Root-Shell im Checkmk-Container | +| `omd …` | beliebiges `omd`-Kommando im Container | + +Globale Optionen: `--env-file DATEI`, `-y/--yes` (keine Rückfragen, z. B. für +Cronjobs), `-v/--verbose`. + +--- + +## 12. Backup + +Backups werden mit `omd backup` erstellt und als **Bundle** verpackt: + +``` +cmkbackup___.tar.gz +└── cmkbundle/ + ├── MANIFEST Site, Version, Quelle, Prüfsumme … + ├── INFO.txt lesbare Zusammenfassung + ├── site.tar.gz das eigentliche omd-Backup + └── site.tar.gz.sha256 +``` + +Dadurch weiß das Restore-/Import-Werkzeug immer, **welche Site** und **welche +Checkmk-Version** im Archiv steckt, und kann die Integrität prüfen. + +```bash +# eine Site +sudo ./scripts/cmk-manage.sh backup --site cmk + +# alle Sites, ohne Logdateien, mit Kennzeichnung +sudo ./scripts/cmk-manage.sh backup --all --no-logs --label vor-update + +# konsistentestes Backup: Site wird kurz gestoppt +sudo ./scripts/cmk-manage.sh backup --site cmk --stop + +# ohne Performance-Daten (deutlich kleiner) +sudo ./scripts/cmk-manage.sh backup --site cmk --no-rrds --dir /mnt/nas/checkmk +``` + +Auflisten und prüfen: + +```bash +./scripts/cmk-manage.sh list +./scripts/cmk-manage.sh verify --file backups/cmkbackup_cmk_2.3.0p23.cre_20260820-030000.tar.gz +./scripts/cmk-manage.sh prune --keep 7 +``` + +### Nächtliches Backup per Cron + +```bash +sudo crontab -e +``` + +```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 +``` + +--- + +## 13. Restore + +```bash +# Einfachster Fall: Site existiert nicht mehr +sudo ./scripts/cmk-manage.sh restore --file backups/cmkbackup_cmk_2.3.0p23.cre_20260820-030000.tar.gz + +# Vorhandene Site überschreiben (laufende Prozesse beenden) +sudo ./scripts/cmk-manage.sh restore --file backups/… --reuse --kill + +# Unter anderem Namen zurückspielen (z. B. für einen Test) +sudo ./scripts/cmk-manage.sh restore --file backups/… --as-site test +``` + +Ablauf des Skripts: + +1. Archivstruktur und SHA256-Prüfsumme kontrollieren +2. prüfen, ob die **Checkmk-Version des Backups im Container vorhanden** ist +3. bei vorhandener Site Rückfrage/`--reuse`, Site stoppen +4. Archiv in den Container kopieren und `omd restore` ausführen +5. `TMPFS` der Site abschalten (im Container nicht nutzbar) +6. Site starten und Hinweise ausgeben + +> **`--as-site` ändert den Site-Namen** – registrierte Agents zeigen dann ins +> Leere und müssten neu registriert werden. Für eine Migration also **nicht** +> verwenden. + +Ein rohes `omd backup`-Archiv (ohne Bundle-Hülle) wird ebenfalls akzeptiert; +der Site-Name wird dann aus dem Archiv gelesen. + +--- + +## 14. Migration einer Direktinstallation (`migrate_creator.sh`) + +Ziel: eine bestehende Checkmk-Installation auf Blech/VM in den Container +übernehmen – **ohne die Agents anzufassen**. + +### 14.1 Warum die Agents weiterlaufen + +Ein `omd backup` enthält die komplette Site inklusive +`etc/ssl` (site-eigene CA und Zertifikate der registrierten Agents), +`etc/check_mk` (Regeln, Hosts, Passwörter) und der Monitoring-Historie. +Wird die Site **unter demselben Namen** wiederhergestellt und ist der Server +unter **derselben IP-Adresse** erreichbar, dann + +* finden Push-/Pull-Agents (`cmk-agent-ctl`) ihre Registrierung wieder, +* bleibt die TLS-Verbindung zum Agent-Receiver auf **Port 8000** gültig, +* stimmen alle URLs (`https:////check_mk/…`) weiterhin, +* bleiben Performance-Daten und Historie erhalten. + +Voraussetzung ist deshalb, dass das Zielsystem die **IP des Quellservers +übernimmt** (bzw. der DNS-Name darauf umgestellt wird) – das ist im geplanten +Vorgehen ohnehin so. + +### 14.2 Schritt 1 – Skript auf den Quellserver kopieren + +```bash +scp scripts/migrate_creator.sh root@alt-server:/root/ +ssh root@alt-server 'chmod +x /root/migrate_creator.sh' +``` + +Das Skript ist **eigenständig** – es braucht auf dem Quellserver nur `bash`, +`tar` und `omd` (für die Oberfläche zusätzlich `dialog` oder `whiptail`). + +### 14.3 Schritt 2 – Bundle auf dem Quellserver erzeugen + +Vorhandene Sites anzeigen: + +```bash +sudo ./migrate_creator.sh --list +``` + +``` +SITE VERSION STATUS GROESSE +prod 2.3.0p23.cre laeuft 4.7 GiB +test 2.3.0p23.cre gestoppt 318.2 MiB +``` + +**Mit Oberfläche** (Sites per Leertaste auswählen): + +```bash +sudo ./migrate_creator.sh --gui +``` + +Der Dialog fragt nacheinander ab: Sites → Optionen (Site stoppen, RRDs/Logs +auslassen) → Zielverzeichnis → Kennzeichnung → optionale Übertragung per `scp`. + +**Mit Parametern:** + +```bash +# eine Site, Site während des Backups stoppen (konsistenteste Variante) +sudo ./migrate_creator.sh --site prod --stop --out /var/tmp + +# alle Sites, ohne Logs, direkt auf das Zielsystem übertragen +sudo ./migrate_creator.sh --all --no-logs \ + --scp root@10.0.0.5:/opt/checkmk-stack/backups/ +``` + +Ergebnis: + +``` +/var/tmp/cmkmigrate_prod_2.3.0p23.cre_20260820-101500.tar.gz +``` + +Das Bundle enthält zusätzlich zum omd-Backup ein `MANIFEST` mit Site-Name, +Checkmk-Version, Quell-Hostname, **Quell-IP-Adressen**, Quell-Betriebssystem +und SHA256-Prüfsumme sowie ein Verzeichnis `siteinfo/` mit `omd config show`, +`omd status` und der IP-Konfiguration des Altsystems zur späteren Kontrolle. + +### 14.4 Schritt 3 – Zielsystem auf die richtige Version bringen + +`omd restore` kann eine Site **nur wiederherstellen, wenn genau deren +Checkmk-Version im Zielsystem installiert ist.** Im Container kommt die Version +aus dem Image – also muss der Image-Tag passen: + +```bash +grep CMK_VERSION <(tar -xzOf backups/cmkmigrate_prod_*.tar.gz cmkbundle/MANIFEST) +# CMK_VERSION=2.3.0p23.cre -> Image-Tag: 2.3.0p23 + +sudo $EDITOR .env +# CMK_IMAGE_TAG=2.3.0p23 +sudo ./scripts/cmk-manage.sh up +``` + +Das Import-Skript prüft das selbst und bricht mit einer entsprechenden Meldung +ab, wenn die Version fehlt. + +> **Edition beachten:** `.cre` = Raw Edition → `checkmk/check-mk-raw`. +> Bei `.cee` / `.cce` / `.cme` wird das entsprechende Enterprise-/Cloud-Image +> benötigt (`CMK_IMAGE_REPO=checkmk/check-mk-enterprise`, Zugang über die +> Checkmk-Subskription). + +### 14.5 Schritt 4 – Bundle importieren + +Mit Oberfläche: + +```bash +sudo ./scripts/cmk-manage.sh gui # -> "import" +``` + +Mit Parametern: + +```bash +sudo ./scripts/cmk-manage.sh import \ + --file backups/cmkmigrate_prod_2.3.0p23.cre_20260820-101500.tar.gz \ + --set-default-site +``` + +Das Skript + +1. erkennt das Migrations-Bundle und zeigt Quell-Host und Quell-IP an, +2. **warnt, wenn die IP dieses Hosts von der Quell-IP abweicht** (dann müssten + die Agents nachgeführt werden), +3. prüft Prüfsumme, Version und Edition, +4. fragt bei bereits vorhandener Site nach (`--reuse` überschreibt), +5. führt `omd restore` unter dem **unveränderten Site-Namen** aus, +6. setzt auf Wunsch `CMK_SITE_ID` in der `.env` (`--set-default-site`), damit + der Container die Site auch nach einem Reboot automatisch startet, +7. startet die Site und gibt die nächsten Schritte aus. + +### 14.6 Schritt 5 – Umschalten und kontrollieren + +```bash +# 1) Alten Server abschalten bzw. dessen Site stoppen (kein Doppel-Monitoring!) +ssh root@alt-server 'omd stop prod' + +# 2) Zielsystem auf die IP/den DNS-Namen des Altsystems umstellen +# (statische IP übernehmen oder DNS-Eintrag umbiegen) + +# 3) Container neu erstellen, falls CMK_SITE_ID geändert wurde +sudo ./scripts/cmk-manage.sh up +sudo ./scripts/cmk-manage.sh status + +# 4) Kontrolle in Checkmk +sudo ./scripts/cmk-manage.sh omd su prod + OMD[prod]:~$ cmk -O # Konfiguration aktivieren + OMD[prod]:~$ omd status + OMD[prod]:~$ exit +``` + +Danach in der Oberfläche prüfen: + +* **Monitor → All hosts** – melden sich die Agents wieder? +* Bei TLS-Agents: der Service *„Check_MK Agent“* darf keine + Registrierungsfehler melden. +* Ein Agent lässt sich vom überwachten Host aus testen mit + `cmk-agent-ctl status` – dort muss die Site `prod` mit dem Zielserver und + „Connection: TLS“ erscheinen. + +### 14.7 Checkliste Migration + +- [ ] `migrate_creator.sh` auf den Quellserver kopiert +- [ ] Bundle erzeugt (möglichst mit `--stop`) +- [ ] Bundle auf das Zielsystem kopiert (`backups/`) +- [ ] `CMK_IMAGE_TAG` auf die Quellversion gesetzt, Stack neu erstellt +- [ ] `import --set-default-site` ausgeführt, Site läuft +- [ ] Alte Site gestoppt / alter Server abgeschaltet +- [ ] IP bzw. DNS auf das Zielsystem umgestellt +- [ ] Ports 80/443/8000 in der Firewall offen +- [ ] Proxy-Host im NGINX Proxy Manager mit Zertifikat eingerichtet +- [ ] Agents melden sich, `cmk -O` ausgeführt +- [ ] Backup des neuen Systems eingerichtet (Cron) + +--- + +## 15. Mehrere Sites in einem Container + +Der Checkmk-Entrypoint legt beim Start nur die Site aus `CMK_SITE_ID` an und +startet auch nur diese. Weitere Sites (z. B. mehrere importierte) existieren im +Bind-Mount, werden aber nach einem Neustart nicht automatisch hochgefahren. + +Deshalb: + +```bash +sudo ./scripts/cmk-manage.sh start-all-sites +``` + +Dieses Kommando wird von der systemd-Unit (`ExecStartPost`) bzw. der +Quadlet-Unit automatisch nach dem Containerstart ausgeführt und startet alle +vorhandenen Sites. + +> **Empfehlung:** Für sauber getrennte Umgebungen lieber je Site ein eigenes +> Projektverzeichnis mit eigener `.env` betreiben. Da beide Container im +> Host-Netzwerk laufen, kann pro Host allerdings nur **ein** Checkmk-Container +> die Ports 5000/8000 belegen – für echte Mehrfachinstanzen dann getrennte +> Hosts oder eine Bridge-Netzwerk-Variante verwenden. + +--- + +## 16. Checkmk aktualisieren + +Da `/omd/sites` ein Bind-Mount ist, überleben die Daten den Austausch des +Images. Die Checkmk-Software selbst steckt im Image unter `/omd/versions`. + +```bash +# 1) IMMER zuerst sichern +sudo ./scripts/cmk-manage.sh backup --all --label vor-update + +# 2) neuen Tag eintragen +sudo $EDITOR .env # z.B. CMK_IMAGE_TAG=2.3.0p28 + +# 3) Image holen und Container neu erstellen +sudo ./scripts/cmk-manage.sh pull +sudo ./scripts/cmk-manage.sh up + +# 4) Site aktualisieren (interaktiv, Konflikte bestätigen) +sudo ./scripts/cmk-manage.sh omd stop cmk +sudo ./scripts/cmk-manage.sh omd update cmk +sudo ./scripts/cmk-manage.sh omd start cmk +``` + +Bei einem Versionssprung (z. B. 2.2 → 2.3) unbedingt vorher die +Checkmk-Werksnotizen (Werks/Release Notes) lesen. + +--- + +## 17. Fehlersuche + +| Symptom | Ursache / Lösung | +|---|---| +| `Port 80/443 already in use` | Auf dem Host läuft bereits ein Webserver: `ss -tlnp \| grep ':80'` → `systemctl disable --now apache2 nginx httpd` | +| Nach Reboot laufen die Container nicht (Podman) | systemd-Unit fehlt: `./install.sh --with-systemd` bzw. `--quadlet`, danach `systemctl is-enabled checkmk-stack` | +| `omd restore`: *version is not installed* | `CMK_IMAGE_TAG` in der `.env` auf die Version aus dem `MANIFEST` setzen und `up` ausführen | +| Import bricht mit Editions-Warnung ab | Enterprise-Backup benötigt das Enterprise-Image (`CMK_IMAGE_REPO`) | +| Proxy zeigt `502 Bad Gateway` | Checkmk-Container läuft nicht oder Weiterleitung nicht auf `127.0.0.1:5000` gesetzt: `./scripts/cmk-manage.sh status`, `curl -I http://127.0.0.1:5000/` | +| Let's Encrypt schlägt fehl | Port 80 aus dem Internet nicht erreichbar oder DNS falsch; Alternative: DNS-Challenge oder selbstsigniertes Zertifikat | +| Agents melden sich nach der Migration nicht | Site-Name geändert (`--as-site`), IP nicht übernommen oder Port 8000 blockiert; auf dem Agent prüfen: `cmk-agent-ctl status` | +| Permission denied auf `data/` unter RHEL | SELinux: `restorecon -Rv /opt/checkmk-stack/data`, `:Z` an den Mounts prüfen | +| `podman-compose` nicht gefunden | EPEL aktivieren oder auf Quadlet umstellen (`./install.sh --quadlet`) | +| 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 | + +Nützliche Kommandos: + +```bash +./scripts/cmk-manage.sh logs -f # Container-Logs +./scripts/cmk-manage.sh omd status cmk # Dienste der Site +./scripts/cmk-manage.sh shell # Root-Shell im Container +docker exec -it checkmk omd su cmk # bzw. podman exec … +``` + +--- + +## 18. Sicherheitshinweise + +* `CMK_PASSWORD` sofort nach der Installation in der Oberfläche ändern und aus + der `.env` entfernen (wird nur bei der Neuanlage ausgewertet); + `chmod 600 .env` ist gesetzt. +* Standardzugang des Proxy Managers (`admin@example.com` / `changeme`) beim + ersten Login zwingend ändern. +* Port **81** (Adminoberfläche) nur aus dem Administrationsnetz erreichbar + machen, Port **5000** gar nicht nach außen öffnen. +* Backups enthalten **alle** Passwörter und Zertifikate der Site – verschlüsselt + ablegen bzw. Zugriffsrechte eng fassen (`chmod 700 backups`). +* Regelmäßig `./scripts/cmk-manage.sh pull` + `up` für Sicherheitsupdates der + Images einplanen. +* Host-Netzwerk bedeutet: der Container teilt sich den Netzwerk-Stack des Hosts. + Es gibt keine zusätzliche Netzwerkisolierung – die Absicherung erfolgt über + die Firewall des Hosts. + +--- + +## 19. Deinstallation + +```bash +sudo ./scripts/cmk-manage.sh down + +# systemd +sudo systemctl disable --now checkmk-stack.service +sudo rm -f /etc/systemd/system/checkmk-stack.service +# bzw. Quadlet +sudo rm -f /etc/containers/systemd/checkmk.container \ + /etc/containers/systemd/nginx-proxy-manager.container +sudo systemctl daemon-reload + +# Images entfernen +docker image rm checkmk/check-mk-raw:2.3.0-latest jc21/nginx-proxy-manager:2 +# podman image rm … + +# Daten (ACHTUNG: löscht die komplette Überwachung!) +sudo rm -rf /opt/checkmk-stack/data +``` + +--- + +## Lizenz / Hinweise + +Dieses Projekt enthält lediglich Konfiguration und Hilfsskripte. +Checkmk (Raw Edition, GPLv2) und NGINX Proxy Manager (MIT) unterliegen den +Lizenzbedingungen der jeweiligen Hersteller. diff --git a/backups/.gitkeep b/backups/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/checkmk/sites/.gitkeep b/data/checkmk/sites/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/npm/custom-certs/.gitkeep b/data/npm/custom-certs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/npm/data/.gitkeep b/data/npm/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/npm/letsencrypt/.gitkeep b/data/npm/letsencrypt/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0422741 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,65 @@ +# ===================================================================== +# Checkmk + NGINX Proxy Manager +# Funktioniert identisch mit: docker compose | podman-compose +# +# - Beide Dienste laufen im Host-Netzwerk -> keine "ports:"-Definition +# - Ausschliesslich Bind-Mounts nach ./data -> keine named volumes +# ===================================================================== + +services: + + # ------------------------------------------------------------------- + # Checkmk (OMD) - im Host-Netzwerk + # Port 5000 = Web-GUI (wird vom NGINX Proxy Manager als Backend genutzt) + # Port 8000 = Agent-Receiver (cmk-agent-ctl / TLS-registrierte Agents) + # Port 6557 = Livestatus (nur wenn CMK_LIVESTATUS_TCP=on) + # ------------------------------------------------------------------- + checkmk: + image: ${CMK_IMAGE_REPO:-checkmk/check-mk-raw}:${CMK_IMAGE_TAG:-2.3.0-latest} + container_name: ${CMK_CONTAINER_NAME:-checkmk} + hostname: ${CMK_HOSTNAME:-checkmk} + network_mode: host + restart: unless-stopped + stop_grace_period: 120s + environment: + CMK_SITE_ID: ${CMK_SITE_ID:-cmk} + CMK_PASSWORD: ${CMK_PASSWORD:-} + CMK_LIVESTATUS_TCP: ${CMK_LIVESTATUS_TCP:-off} + MAIL_RELAY_HOST: ${MAIL_RELAY_HOST:-} + TZ: ${TZ:-Europe/Berlin} + volumes: + # Kompletter OMD-Datenbestand als Bind-Mount im Projektverzeichnis + - ${CMK_DATA_DIR:-./data/checkmk/sites}:/omd/sites:Z + - /etc/localtime:/etc/localtime:ro + ulimits: + nofile: + soft: 65536 + hard: 65536 + # --------------------------------------------------------------- + # Optional: tmpfs fuer das Site-tmp-Verzeichnis (Performance). + # Bewusst deaktiviert, weil uid/gid der Site nach einem Import + # abweichen koennen und das tmpfs dann falsch gemountet waere. + # Bei einer frisch angelegten Site ist uid/gid=1000 korrekt: + # --------------------------------------------------------------- + # tmpfs: + # - /opt/omd/sites/${CMK_SITE_ID:-cmk}/tmp:uid=1000,gid=1000 + + # ------------------------------------------------------------------- + # NGINX Proxy Manager - ebenfalls Host-Netzwerk + # Port 80 = HTTP + Let's-Encrypt HTTP-01 Challenge + # Port 443 = HTTPS + # Port 81 = Admin-Oberflaeche (unbedingt per Firewall einschraenken!) + # ------------------------------------------------------------------- + nginx-proxy-manager: + image: ${NPM_IMAGE:-jc21/nginx-proxy-manager:2} + container_name: ${NPM_CONTAINER_NAME:-nginx-proxy-manager} + network_mode: host + restart: unless-stopped + environment: + DISABLE_IPV6: ${NPM_DISABLE_IPV6:-true} + TZ: ${TZ:-Europe/Berlin} + volumes: + - ${NPM_DATA_DIR:-./data/npm/data}:/data:Z + - ${NPM_LE_DIR:-./data/npm/letsencrypt}:/etc/letsencrypt:Z + depends_on: + - checkmk diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..267ac54 --- /dev/null +++ b/install.sh @@ -0,0 +1,307 @@ +#!/usr/bin/env bash +# ===================================================================== +# install.sh - Installation des Checkmk-/NGINX-Proxy-Manager-Stacks +# +# Debian/Ubuntu : Docker CE + Compose-Plugin +# RHEL/Rocky/Alma/CentOS/Fedora : Podman + podman-compose +# +# Zusaetzlich: dialog (ncurses-Oberflaeche), openssl, tar, curl +# ===================================================================== +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$PROJECT_DIR/scripts/lib/common.sh" + +WITH_SYSTEMD="" +WITH_QUADLET="" +SKIP_RUNTIME="" +ROOTLESS="" + +usage() { +cat <<'EOF' +install.sh - Installation des Checkmk-Container-Stacks + +OPTIONEN + --with-systemd systemd-Unit einrichten, damit der Stack nach einem + Neustart des Hosts automatisch wieder startet + (Vorgabe bei Podman) + --quadlet statt der Compose-Unit Podman-Quadlet-Dateien + (/etc/containers/systemd/) einrichten + --rootless Podman rootless vorbereiten (linger + Ports ab 80) + --skip-runtime Docker/Podman nicht installieren (nur Projekt einrichten) + -h, --help Hilfe +EOF +} + +while [ $# -gt 0 ]; do + case "$1" in + --with-systemd) WITH_SYSTEMD=1; shift ;; + --quadlet) WITH_QUADLET=1; WITH_SYSTEMD=1; shift ;; + --rootless) ROOTLESS=1; shift ;; + --skip-runtime) SKIP_RUNTIME=1; shift ;; + -h|--help) usage; exit 0 ;; + *) die "Unbekannte Option: $1" ;; + esac +done + +[ "$(id -u)" -eq 0 ] || die "Bitte als root ausfuehren: sudo ./install.sh" + +# --------------------------------------------------------------------- +# Distribution erkennen +# --------------------------------------------------------------------- +[ -r /etc/os-release ] || die "/etc/os-release nicht lesbar - Distribution unbekannt." +# shellcheck disable=SC1091 +. /etc/os-release +OS_ID="${ID:-unknown}" +OS_LIKE="${ID_LIKE:-}" +OS_NAME="${PRETTY_NAME:-$OS_ID}" + +FAMILY="" +case "$OS_ID" in + debian|ubuntu|linuxmint|raspbian|pop) FAMILY=debian ;; + rhel|centos|rocky|almalinux|fedora|ol|oracle|scientific) FAMILY=rhel ;; + *) + case " $OS_LIKE " in + *debian*) FAMILY=debian ;; + *rhel*|*fedora*|*centos*) FAMILY=rhel ;; + esac + ;; +esac +[ -n "$FAMILY" ] || die "Nicht unterstuetzte Distribution: $OS_NAME" + +printf '\n%s=== Checkmk Container-Stack - Installation ===%s\n\n' "$C_BLD" "$C_RST" +log "System : $OS_NAME" +log "Familie : $FAMILY" +log "Projektpfad : $PROJECT_DIR" + +# --------------------------------------------------------------------- +# Runtime installieren +# --------------------------------------------------------------------- +install_debian() { + log "Installiere Docker CE aus dem offiziellen Docker-Repository ..." + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y ca-certificates curl gnupg tar dialog openssl + + if [ "$OS_ID" = debian ] || [ "$OS_ID" = ubuntu ]; then + install -m 0755 -d /etc/apt/keyrings + if [ ! -f /etc/apt/keyrings/docker.asc ]; then + curl -fsSL "https://download.docker.com/linux/$OS_ID/gpg" -o /etc/apt/keyrings/docker.asc + chmod a+r /etc/apt/keyrings/docker.asc + fi + local codename="${VERSION_CODENAME:-}" + [ -n "$codename" ] || codename="$(. /etc/os-release && echo "${UBUNTU_CODENAME:-stable}")" + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$OS_ID $codename stable" \ + >/etc/apt/sources.list.d/docker.list + apt-get update -qq + apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + else + warn "Abgeleitete Distribution ($OS_NAME) - verwende die Distributionspakete." + apt-get install -y docker.io docker-compose + fi + + systemctl enable --now docker + ok "Docker installiert: $(docker --version)" +} + +install_rhel() { + log "Installiere Podman ..." + local PM=dnf + command -v dnf >/dev/null 2>&1 || PM=yum + $PM install -y podman tar dialog openssl || die "Podman-Installation fehlgeschlagen." + + # podman-compose besorgen (Reihenfolge: Distro-Repo -> EPEL -> pip) + if ! command -v podman-compose >/dev/null 2>&1; then + log "Suche podman-compose ..." + if ! $PM install -y podman-compose 2>/dev/null; then + if [ "$OS_ID" != fedora ]; then + local rel; rel="${VERSION_ID%%.*}" + log "Aktiviere EPEL ${rel} ..." + $PM install -y "https://dl.fedoraproject.org/pub/epel/epel-release-latest-${rel}.noarch.rpm" 2>/dev/null || true + $PM install -y podman-compose 2>/dev/null || true + fi + fi + fi + if ! command -v podman-compose >/dev/null 2>&1; then + log "Versuche podman-compose per pip ..." + $PM install -y python3-pip 2>/dev/null || true + pip3 install --quiet podman-compose 2>/dev/null || true + fi + + if command -v podman-compose >/dev/null 2>&1; then + ok "podman-compose vorhanden: $(podman-compose --version 2>&1 | head -n1)" + else + warn "podman-compose konnte nicht installiert werden. + Alternative: Installation ueber Quadlet ( ./install.sh --quadlet ), + dafuer wird kein Compose benoetigt." + fi + ok "Podman installiert: $(podman --version)" + + # SELinux: Bind-Mounts brauchen den passenden Kontext (:Z ist im Compose gesetzt) + if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" = "Enforcing" ]; then + log "SELinux ist aktiv - die Bind-Mounts sind im Compose mit ':Z' versehen." + fi +} + +if [ -z "$SKIP_RUNTIME" ]; then + case "$FAMILY" in + debian) install_debian ;; + rhel) install_rhel ;; + esac +else + log "Runtime-Installation uebersprungen (--skip-runtime)." +fi + +# --------------------------------------------------------------------- +# Projekt vorbereiten +# --------------------------------------------------------------------- +log "Bereite Projektverzeichnis vor ..." +mkdir -p "$PROJECT_DIR/data/checkmk/sites" \ + "$PROJECT_DIR/data/npm/data" \ + "$PROJECT_DIR/data/npm/letsencrypt" \ + "$PROJECT_DIR/data/npm/custom-certs" \ + "$PROJECT_DIR/backups" +chmod +x "$PROJECT_DIR"/scripts/*.sh "$PROJECT_DIR/install.sh" 2>/dev/null || true + +if [ ! -f "$PROJECT_DIR/.env" ]; then + cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env" + chmod 600 "$PROJECT_DIR/.env" + ok ".env aus .env.example erzeugt - bitte Passwort und Site-Namen anpassen!" +else + log ".env ist bereits vorhanden - bleibt unveraendert." +fi + +load_env "$PROJECT_DIR/.env" +detect_runtime +ok "Verwendete Runtime: $RUNTIME" + +# Podman: Autostart nach Reboot ohne systemd-Unit funktioniert nicht -> +# Unit standardmaessig einrichten. +if [ "$RUNTIME" = podman ] && [ -z "$WITH_SYSTEMD" ]; then + warn "Podman hat keinen Daemon: ohne systemd-Unit starten die Container + nach einem Reboot NICHT automatisch. Die Unit wird jetzt eingerichtet." + WITH_SYSTEMD=1 +fi + +# --------------------------------------------------------------------- +# Rootless-Vorbereitung (optional) +# --------------------------------------------------------------------- +if [ -n "$ROOTLESS" ]; then + log "Bereite rootless-Betrieb vor ..." + echo 'net.ipv4.ip_unprivileged_port_start=80' >/etc/sysctl.d/99-checkmk-rootless.conf + sysctl -q --system || true + ok "Unprivilegierte Ports ab 80 erlaubt." + warn "Zusaetzlich noetig: loginctl enable-linger + und die Unit als '--user'-Unit einrichten (siehe README)." +fi + +# --------------------------------------------------------------------- +# systemd +# --------------------------------------------------------------------- +install_compose_unit() { + local compose_bin + detect_compose + compose_bin="${COMPOSE_CMD[*]}" + + log "Richte systemd-Unit 'checkmk-stack.service' ein ..." + cat >/etc/systemd/system/checkmk-stack.service <"/etc/containers/systemd/$(basename "$f")" + done + systemctl daemon-reload + ok "Quadlet-Units geschrieben. Start: systemctl start checkmk nginx-proxy-manager" + log "Die Units starten durch 'WantedBy=multi-user.target' automatisch nach einem Reboot." +} + +if [ -n "$WITH_SYSTEMD" ]; then + if [ -n "$WITH_QUADLET" ]; then + [ "$RUNTIME" = podman ] || die "--quadlet ist nur mit Podman moeglich." + install_quadlet_units + else + install_compose_unit + fi +fi + +# --------------------------------------------------------------------- +# Firewall-Hinweise +# --------------------------------------------------------------------- +print_firewall_hint() { + if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then +cat <<'EOF' + +Firewall (firewalld) - empfohlene Regeln: + firewall-cmd --permanent --add-service=http + firewall-cmd --permanent --add-service=https + firewall-cmd --permanent --add-port=8000/tcp # Agent-Receiver + # Admin-Oberflaeche des Proxy Managers nur aus dem Admin-Netz: + firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.0/24 port port=81 protocol=tcp accept' + # Checkmk-Port 5000 NICHT nach aussen oeffnen (nur ueber den Proxy) + firewall-cmd --reload +EOF + elif command -v ufw >/dev/null 2>&1; then +cat <<'EOF' + +Firewall (ufw) - empfohlene Regeln: + ufw allow 80/tcp + ufw allow 443/tcp + ufw allow 8000/tcp # Agent-Receiver + ufw allow from 10.0.0.0/24 to any port 81 proto tcp # Proxy-Manager-Admin + # Port 5000 (Checkmk direkt) bleibt zu - Zugriff nur ueber den Proxy +EOF + fi +} +print_firewall_hint + +cat < [Optionen] + +GLOBALE OPTIONEN + --env-file DATEI abweichende .env verwenden + -y, --yes alle Rueckfragen automatisch mit "ja" beantworten + -v, --verbose ausfuehrliche Ausgabe + -h, --help diese Hilfe + +KOMMANDOS - Oberflaeche + gui | menu ncurses-Oberflaeche starten (dialog/whiptail) + +KOMMANDOS - Stack + up Stack starten (compose up -d) + down Stack stoppen und Container entfernen + restart Stack neu starten + pull Images aktualisieren + status Status von Containern und Sites anzeigen + logs [-f] [DIENST] Logs anzeigen + shell Root-Shell im Checkmk-Container + omd ARG... beliebiges omd-Kommando im Container ausfuehren + +KOMMANDOS - Sites + sites Sites im Container auflisten + site-start SITE Site starten + site-stop SITE Site stoppen + start-all-sites alle vorhandenen Sites starten (fuer Autostart nach Reboot) + +KOMMANDOS - Backup / Restore / Migration + backup [Optionen] Backup einer/aller Sites als .tar.gz-Bundle erzeugen + --site SITE Site (mehrfach moeglich) + --all alle Sites sichern + --dir VERZ Zielverzeichnis (Vorgabe: $BACKUP_DIR) + --label TEXT Freitext-Kennzeichnung im Dateinamen + --no-rrds Performance-Daten (RRDs) auslassen + --no-logs Logdateien auslassen + --stop Site waehrend des Backups stoppen (konsistenter) + + list vorhandene Backups/Bundles auflisten + verify --file DATEI Bundle pruefen (Manifest + SHA256) + + restore [Optionen] Backup wiederherstellen + --file DATEI Bundle oder rohes omd-Backup (.tar.gz) + --as-site NAME unter anderem Site-Namen wiederherstellen + (ACHTUNG: Agents muessen dann neu registriert werden!) + --reuse vorhandene Site ueberschreiben + --kill laufende Prozesse der Site beenden + --no-start Site nach dem Restore nicht starten + + import [Optionen] Site von einer Direktinstallation uebernehmen + (Bundle aus migrate_creator.sh) + --file DATEI Migrations-Bundle (.tar.gz) + --reuse vorhandene Site ueberschreiben + --kill laufende Prozesse der Site beenden + --set-default-site CMK_SITE_ID in der .env auf die importierte Site setzen + --no-start Site nach dem Import nicht starten + + prune --keep N nur die N neuesten Backups behalten + +KOMMANDOS - Zertifikat + cert [Optionen] selbstsigniertes Zertifikat fuer den Proxy erzeugen + --domain FQDN Common Name / SAN + --alt NAME zusaetzlicher SAN (mehrfach; DNS-Name oder IP) + --days N Gueltigkeit in Tagen (Vorgabe 3650) + +BEISPIELE + ./scripts/cmk-manage.sh up + ./scripts/cmk-manage.sh backup --all --no-logs + ./scripts/cmk-manage.sh import --file backups/cmkmigrate_prod_2.3.0p23_*.tar.gz --set-default-site + ./scripts/cmk-manage.sh gui +EOF +} + +# ===================================================================== +# Bundle-Format +# +# .tar.gz +# cmkbundle/MANIFEST key=value, per "source" lesbar +# cmkbundle/INFO.txt menschenlesbare Zusammenfassung +# cmkbundle/site.tar.gz das eigentliche "omd backup"-Archiv +# cmkbundle/site.tar.gz.sha256 +# ===================================================================== + +edition_from_version() { + case "$1" in + *.cre) echo "Raw Edition" ;; + *.cee) echo "Enterprise Edition" ;; + *.cme) echo "Managed Services Edition" ;; + *.cce) echo "Cloud Edition" ;; + *) echo "unbekannt" ;; + esac +} + +# Manifest eines Bundles einlesen -> Variablen B_* +bundle_read() { + local file="$1" + B_SITE=""; B_CMK_VERSION=""; B_SOURCE_KIND=""; B_SOURCE_HOST="" + B_SOURCE_IP=""; B_CREATED=""; B_ARCHIVE_SHA256=""; B_LABEL="" + B_BUNDLE_TYPE=""; B_ARCHIVE_SIZE=""; B_OMD_BACKUP_OPTS=""; B_SOURCE_OS="" + B_IS_BUNDLE=0 + + if tar -tzf "$file" 2>/dev/null | grep -qx "$BUNDLE_DIRNAME/MANIFEST"; then + B_IS_BUNDLE=1 + local mf; mf="$(mktemp)" + tar -xzOf "$file" "$BUNDLE_DIRNAME/MANIFEST" >"$mf" + # nur bekannte Schluessel uebernehmen (kein blindes eval) + local k v + while IFS='=' read -r k v; do + case "$k" in + SITE) B_SITE="$v" ;; + CMK_VERSION) B_CMK_VERSION="$v" ;; + SOURCE_KIND) B_SOURCE_KIND="$v" ;; + SOURCE_HOST) B_SOURCE_HOST="$v" ;; + SOURCE_IP) B_SOURCE_IP="$v" ;; + SOURCE_OS) B_SOURCE_OS="$v" ;; + CREATED) B_CREATED="$v" ;; + ARCHIVE_SHA256) B_ARCHIVE_SHA256="$v" ;; + ARCHIVE_SIZE) B_ARCHIVE_SIZE="$v" ;; + LABEL) B_LABEL="$v" ;; + BUNDLE_TYPE) B_BUNDLE_TYPE="$v" ;; + OMD_BACKUP_OPTS) B_OMD_BACKUP_OPTS="$v" ;; + esac + done <"$mf" + rm -f "$mf" + else + # rohes omd-Backup: Site-Name ist die erste Pfadkomponente. + # Ein omd-Archiv beginnt immer mit "/..." - alles andere ist + # kein Checkmk-Backup und wird abgewiesen. + local first + first="$(tar -tzf "$file" 2>/dev/null | head -n1)" + case "$first" in + */*) B_SITE="${first%%/*}" ;; + *) return 1 ;; + esac + B_BUNDLE_TYPE="raw" + B_SOURCE_KIND="unbekannt" + fi + [ -n "$B_SITE" ] || return 1 +} + +bundle_show() { + local file="$1" + bundle_read "$file" || { echo "Kein gueltiges Checkmk-Backup: $file"; return 1; } + cat </dev/null || die "Archiv ist beschaedigt oder kein gueltiges tar.gz." + bundle_read "$file" || die "Im Archiv wurde keine Checkmk-Site gefunden." + if [ "$B_IS_BUNDLE" = 1 ] && [ -n "$B_ARCHIVE_SHA256" ]; then + log "Pruefe SHA256 der Nutzdaten ..." + local sum + sum="$(tar -xzOf "$file" "$BUNDLE_DIRNAME/site.tar.gz" | sha256sum | awk '{print $1}')" + [ "$sum" = "$B_ARCHIVE_SHA256" ] || die "Pruefsumme falsch! + erwartet : $B_ARCHIVE_SHA256 + ermittelt: $sum" + ok "Pruefsumme korrekt." + else + warn "Kein Manifest/keine Pruefsumme vorhanden - nur Strukturpruefung." + fi + bundle_show "$file" + ok "Bundle ist in Ordnung." +} + +# Nutzdaten (omd-Archiv) aus einem Bundle nach stdout schreiben +bundle_extract_payload() { + local file="$1" + if tar -tzf "$file" 2>/dev/null | grep -qx "$BUNDLE_DIRNAME/site.tar.gz"; then + tar -xzOf "$file" "$BUNDLE_DIRNAME/site.tar.gz" + else + cat "$file" + fi +} + +# ===================================================================== +# Stack-Kommandos +# ===================================================================== +cmd_up() { + [ -f "$PROJECT_DIR/.env" ] || die "Keine .env vorhanden. Bitte: cp .env.example .env && \$EDITOR .env" + log "Starte Stack (${RUNTIME}) ..." + compose up -d + ok "Stack gestartet." + cmd_status +} + +cmd_down() { log "Stoppe Stack ..."; compose down; ok "Stack gestoppt."; } +cmd_restart() { compose down || true; compose up -d; ok "Stack neu gestartet."; } +cmd_pull() { compose pull; ok "Images aktualisiert. Danach: $0 up"; } +cmd_logs() { compose logs "$@"; } +cmd_shell() { require_cmk_running; $RUNTIME exec -it "$CMK_CONTAINER_NAME" bash; } +cmd_omd() { require_cmk_running; $RUNTIME exec -it "$CMK_CONTAINER_NAME" omd "$@"; } + +cmd_status() { + printf '\n%sContainer%s\n' "$C_BLD" "$C_RST" + local c + for c in "$CMK_CONTAINER_NAME" "$NPM_CONTAINER_NAME"; do + if container_exists "$c"; then + printf ' %-24s %s\n' "$c" \ + "$(container_running "$c" && echo "${C_GRN}laeuft${C_RST}" || echo "${C_RED}gestoppt${C_RST}")" + else + printf ' %-24s %s\n' "$c" "${C_YEL}nicht vorhanden${C_RST}" + fi + done + + if container_running "$CMK_CONTAINER_NAME"; then + printf '\n%sSites im Container%s\n' "$C_BLD" "$C_RST" + printf ' %-20s %-18s %s\n' "SITE" "VERSION" "STATUS" + local s + while IFS= read -r s; do + [ -n "$s" ] || continue + printf ' %-20s %-18s %s\n' "$s" "$(cmk_site_version "$s")" "$(cmk_site_state_text "$s")" + done < <(cmk_sites) + printf '\n Standard-Site laut .env : %s\n' "$CMK_SITE_ID" + fi + + printf '\n%sZugang%s\n' "$C_BLD" "$C_RST" + local ip; ip="$(primary_ip)" + printf ' Checkmk (direkt) : http://%s:5000/%s/\n' "$ip" "$CMK_SITE_ID" + printf ' Checkmk (ueber Proxy) : https:///%s/\n' "$CMK_SITE_ID" + printf ' Agent-Receiver : %s:8000\n' "$ip" + printf ' NGINX Proxy Manager : http://%s:81/ (Erstlogin: admin@example.com / changeme)\n\n' "$ip" +} + +# ===================================================================== +# Site-Kommandos +# ===================================================================== +cmd_sites() { + require_cmk_running + printf '%-20s %-18s %s\n' "SITE" "VERSION" "STATUS" + local s + while IFS= read -r s; do + [ -n "$s" ] || continue + printf '%-20s %-18s %s\n' "$s" "$(cmk_site_version "$s")" "$(cmk_site_state_text "$s")" + done < <(cmk_sites) +} + +cmd_site_start() { + require_cmk_running + [ $# -ge 1 ] || die "Site-Name fehlt." + ct omd start "$1" && ok "Site '$1' gestartet." +} + +cmd_site_stop() { + require_cmk_running + [ $# -ge 1 ] || die "Site-Name fehlt." + ct omd stop "$1" && ok "Site '$1' gestoppt." +} + +# Wird vom systemd-Unit nach dem Start aufgerufen: alle Sites hochfahren. +# (Der Container-Entrypoint startet nur CMK_SITE_ID.) +cmd_start_all_sites() { + local tries=0 + while ! container_running "$CMK_CONTAINER_NAME"; do + tries=$((tries+1)); [ "$tries" -gt 60 ] && die "Container '$CMK_CONTAINER_NAME' startet nicht." + sleep 2 + done + # kurz warten, bis der Entrypoint durch ist + sleep 5 + local s + while IFS= read -r s; do + [ -n "$s" ] || continue + if [ "$(cmk_site_state "$s")" != "0" ]; then + log "Starte Site '$s' ..." + ct omd start "$s" || warn "Site '$s' konnte nicht gestartet werden." + else + debug "Site '$s' laeuft bereits." + fi + done < <(cmk_sites) + ok "Alle Sites geprueft/gestartet." +} + +# ===================================================================== +# Backup +# ===================================================================== +cmd_backup() { + local -a sites=() opts=() + local all="" dir="" label="" stop_site="" + while [ $# -gt 0 ]; do + case "$1" in + --site) sites+=("$2"); shift 2 ;; + --all) all=1; shift ;; + --dir) dir="$2"; shift 2 ;; + --label) label="$2"; shift 2 ;; + --no-rrds) opts+=(--no-rrds); shift ;; + --no-logs) opts+=(--no-logs); shift ;; + --stop) stop_site=1; shift ;; + *) die "Unbekannte Option fuer backup: $1" ;; + esac + done + require_cmk_running + dir="${dir:-$BACKUP_DIR}" + mkdir -p "$dir" + + if [ -n "$all" ]; then + mapfile -t sites < <(cmk_sites) + fi + [ ${#sites[@]} -gt 0 ] || die "Keine Site angegeben (--site SITE oder --all)." + + local s + for s in "${sites[@]}"; do + cmk_site_exists "$s" || die "Site '$s' existiert im Container nicht." + backup_one_site "$s" "$dir" "$label" "$stop_site" "${opts[@]+"${opts[@]}"}" + done +} + +backup_one_site() { + local site="$1" dir="$2" label="$3" stop_site="$4"; shift 4 + local -a opts=("$@") + local ts version outfile tmp restarted="" + ts="$(date +%Y%m%d-%H%M%S)" + version="$(cmk_site_version "$site")" + outfile="$dir/cmkbackup_${site}_${version:-unknown}${label:+_$label}_${ts}.tar.gz" + + if [ -n "$stop_site" ] && [ "$(cmk_site_state "$site")" = "0" ]; then + log "Stoppe Site '$site' fuer ein konsistentes Backup ..." + ct omd stop "$site" >/dev/null + restarted=1 + fi + + tmp="$(mktemp -d)" + mkdir -p "$tmp/$BUNDLE_DIRNAME" + + log "Erzeuge omd-Backup der Site '$site' ${opts[*]+(${opts[*]})} ..." + if ! $RUNTIME exec "$CMK_CONTAINER_NAME" omd backup ${opts[@]+"${opts[@]}"} "$site" - \ + >"$tmp/$BUNDLE_DIRNAME/site.tar.gz" 2>"$tmp/err.log" \ + || [ ! -s "$tmp/$BUNDLE_DIRNAME/site.tar.gz" ]; then + warn "Streaming-Backup nicht moeglich - weiche auf Zwischendatei im Container aus." + debug "$(cat "$tmp/err.log" 2>/dev/null)" + ct sh -c "rm -f /var/tmp/cmkbackup.tar.gz" + ct omd backup ${opts[@]+"${opts[@]}"} "$site" /var/tmp/cmkbackup.tar.gz \ + || { rm -rf "$tmp"; die "omd backup fehlgeschlagen."; } + $RUNTIME cp "$CMK_CONTAINER_NAME:/var/tmp/cmkbackup.tar.gz" "$tmp/$BUNDLE_DIRNAME/site.tar.gz" + ct sh -c "rm -f /var/tmp/cmkbackup.tar.gz" + fi + + [ -n "$restarted" ] && { log "Starte Site '$site' wieder ..."; ct omd start "$site" >/dev/null; } + + local sum size + 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_OS=$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || echo unbekannt) +CREATED=$(date -Is) +CREATED_BY=cmk-manage.sh +ARCHIVE=site.tar.gz +ARCHIVE_SHA256=$sum +ARCHIVE_SIZE=$size +OMD_BACKUP_OPTS=${opts[*]-} +LABEL=$label +AGENT_RECEIVER_PORT=8000 +EOF + + cat >"$tmp/$BUNDLE_DIRNAME/INFO.txt" < --reuse +EOF + + log "Verpacke Bundle ..." + tar -czf "$outfile" -C "$tmp" "$BUNDLE_DIRNAME" + rm -rf "$tmp" + ok "Backup erstellt: $outfile ($(human_size "$(file_size "$outfile")"))" + LAST_BACKUP_FILE="$outfile" +} + +# ===================================================================== +# Auflisten / Aufraeumen +# ===================================================================== +list_backup_files() { + find "$BACKUP_DIR" -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.tgz' \) -printf '%T@ %p\n' 2>/dev/null \ + | sort -rn | cut -d' ' -f2- +} + +cmd_list() { + local f n=0 + printf '%-58s %10s %s\n' "DATEI" "GROESSE" "SITE / VERSION" + while IFS= read -r f; do + [ -n "$f" ] || continue + bundle_read "$f" 2>/dev/null || continue + printf '%-58s %10s %s / %s\n' "$(basename "$f")" \ + "$(human_size "$(file_size "$f")")" "$B_SITE" "${B_CMK_VERSION:-?}" + n=$((n+1)) + done < <(list_backup_files) + [ "$n" -eq 0 ] && echo "(keine Backups in $BACKUP_DIR)" + return 0 +} + +cmd_prune() { + local keep=5 + while [ $# -gt 0 ]; do + case "$1" in + --keep) keep="$2"; shift 2 ;; + *) die "Unbekannte Option fuer prune: $1" ;; + esac + done + local -a files=(); mapfile -t files < <(list_backup_files) + if [ ${#files[@]} -le "$keep" ]; then + ok "Nichts zu loeschen (${#files[@]} Backups, behalte $keep)." + return 0 + fi + local i + for ((i=keep; i<${#files[@]}; i++)); do + if confirm "Backup '$(basename "${files[$i]}")' loeschen?"; then + rm -f "${files[$i]}" && log "geloescht: $(basename "${files[$i]}")" + fi + done + ok "Aufraeumen beendet." +} + +# ===================================================================== +# Restore / Import (gemeinsame Implementierung) +# ===================================================================== +do_restore() { + local file="$1" as_site="$2" reuse="$3" kill_opt="$4" nostart="$5" mode="$6" + + [ -f "$file" ] || die "Datei nicht gefunden: $file" + require_cmk_running + bundle_read "$file" || die "'$file' ist kein Checkmk-Backup (weder Bundle noch omd-Archiv)." + + local target="${as_site:-$B_SITE}" + + echo + bundle_show "$file" + echo + printf '%sZiel-Site im Container : %s%s\n' "$C_BLD" "$target" "$C_RST" + + # --- Pruefsumme ------------------------------------------------- + if [ "$B_IS_BUNDLE" = 1 ] && [ -n "$B_ARCHIVE_SHA256" ]; then + log "Pruefe Pruefsumme ..." + local sum + sum="$(bundle_extract_payload "$file" | sha256sum | awk '{print $1}')" + [ "$sum" = "$B_ARCHIVE_SHA256" ] || die "Pruefsumme stimmt nicht - Bundle beschaedigt." + ok "Pruefsumme korrekt." + fi + + # --- Versionspruefung ------------------------------------------- + if [ -n "$B_CMK_VERSION" ]; then + local installed + installed="$(cmk_versions_installed)" + if ! printf '%s\n' "$installed" | grep -qxF "$B_CMK_VERSION"; then + err "Die Checkmk-Version des Backups ist im Container nicht installiert." + printf ' benoetigt : %s\n' "$B_CMK_VERSION" + printf ' installiert : %s\n' "$(printf '%s' "$installed" | tr '\n' ' ')" + cat </dev/null 2>&1 || true + fi + else + confirm "Site '$target' jetzt aus dem Backup wiederherstellen?" || die "Abgebrochen." + fi + + if [ -n "$as_site" ] && [ "$as_site" != "$B_SITE" ]; then + warn "Die Site wird von '$B_SITE' nach '$as_site' umbenannt. + Registrierte Agents verweisen auf '$B_SITE' und muessen neu registriert werden!" + confirm "Trotzdem umbenennen?" || die "Abgebrochen." + rargs+=("$as_site") + fi + + # --- Nutzdaten in den Container schieben ------------------------ + local tmp; tmp="$(mktemp -d)" + log "Bereite Nutzdaten vor ..." + bundle_extract_payload "$file" >"$tmp/payload.tar.gz" + log "Kopiere Archiv in den Container ($(human_size "$(file_size "$tmp/payload.tar.gz")")) ..." + ct sh -c 'rm -f /var/tmp/cmkrestore.tar.gz' + $RUNTIME cp "$tmp/payload.tar.gz" "$CMK_CONTAINER_NAME:/var/tmp/cmkrestore.tar.gz" + rm -rf "$tmp" + + log "Fuehre 'omd restore ${rargs[*]-} ' aus ..." + if ! ct omd restore ${rargs[@]+"${rargs[@]}"} /var/tmp/cmkrestore.tar.gz; then + ct sh -c 'rm -f /var/tmp/cmkrestore.tar.gz' || true + die "omd restore ist fehlgeschlagen." + fi + ct sh -c 'rm -f /var/tmp/cmkrestore.tar.gz' || true + ok "Site '$target' wurde wiederhergestellt." + + # --- Nachbereitung ---------------------------------------------- + # TMPFS im Container abschalten (Mount im Container nicht moeglich/noetig) + ct omd config "$target" set TMPFS off >/dev/null 2>&1 || true + + if [ -z "$nostart" ]; then + log "Starte Site '$target' ..." + ct omd start "$target" || warn "Site konnte nicht vollstaendig gestartet werden - bitte 'omd status $target' pruefen." + fi + + post_restore_hints "$target" "$mode" +} + +post_restore_hints() { + local target="$1" mode="$2" ip; ip="$(primary_ip)" + cat < Forward to http://127.0.0.1:5000 + 4) Agents: solange Site-Name (${target}) und IP-Adresse gleich bleiben und + Port 8000 (Agent-Receiver) offen ist, muessen die Agents NICHT neu + registriert werden - die Site-Zertifikate sind im Backup enthalten. + Pruefen: $0 omd su $target -> cmk -O ; danach im GUI die + Host-Status/Agent-Verbindungen kontrollieren. + +EOF +} + +cmd_restore() { + local file="" as_site="" reuse="" kill_opt="" nostart="" + while [ $# -gt 0 ]; do + case "$1" in + --file) file="$2"; shift 2 ;; + --as-site) as_site="$2"; shift 2 ;; + --reuse) reuse=1; shift ;; + --kill) kill_opt=1; shift ;; + --no-start) nostart=1; shift ;; + *) die "Unbekannte Option fuer restore: $1" ;; + esac + done + [ -n "$file" ] || die "--file fehlt." + do_restore "$file" "$as_site" "$reuse" "$kill_opt" "$nostart" "restore" +} + +cmd_import() { + local file="" reuse="" kill_opt="" nostart="" setdefault="" + while [ $# -gt 0 ]; do + case "$1" in + --file) file="$2"; shift 2 ;; + --reuse) reuse=1; shift ;; + --kill) kill_opt=1; shift ;; + --no-start) nostart=1; shift ;; + --set-default-site) setdefault=1; shift ;; + *) die "Unbekannte Option fuer import: $1" ;; + esac + done + [ -n "$file" ] || die "--file fehlt." + + bundle_read "$file" || die "'$file' ist kein gueltiges Migrations-Bundle." + if [ "$B_IS_BUNDLE" = 1 ] && [ "$B_BUNDLE_TYPE" = "migrate" ]; then + log "Migrations-Bundle erkannt (Quelle: ${B_SOURCE_HOST:-?} / ${B_SOURCE_IP:-?})." + local myip; myip="$(primary_ip)" + if [ -n "$B_SOURCE_IP" ] && [ "$B_SOURCE_IP" != "$myip" ]; then + warn "Die IP dieses Hosts ($myip) weicht von der Quell-IP ($B_SOURCE_IP) ab. + Damit registrierte Agents ohne Neuregistrierung weiterarbeiten, muss dieser + Host die IP $B_SOURCE_IP uebernehmen (bzw. der DNS-Name darauf zeigen)." + confirm "Trotzdem importieren?" || die "Abgebrochen." + fi + fi + + do_restore "$file" "" "$reuse" "$kill_opt" "$nostart" "import" + + if [ -n "$setdefault" ]; then + env_set CMK_SITE_ID "$B_SITE" + ok "CMK_SITE_ID=$B_SITE in der .env gesetzt." + warn "Damit die Aenderung wirkt, den Container neu erstellen: $0 up" + fi +} + +# ===================================================================== +# Selbstsigniertes Zertifikat fuer den NGINX Proxy Manager +# ===================================================================== +cmd_cert() { + local domain="${CERT_DOMAIN:-checkmk.example.local}" days="${CERT_DAYS:-3650}" + local -a alts=() + while [ $# -gt 0 ]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + --alt) alts+=("$2"); shift 2 ;; + --days) days="$2"; shift 2 ;; + *) die "Unbekannte Option fuer cert: $1" ;; + esac + done + command -v openssl >/dev/null 2>&1 || die "openssl ist nicht installiert." + + local outdir="$PROJECT_DIR/data/npm/custom-certs/$domain" + mkdir -p "$outdir" + + local san="DNS:$domain" + local a + for a in ${alts[@]+"${alts[@]}"}; do + case "$a" in + [0-9]*.[0-9]*.[0-9]*.[0-9]*) san="$san,IP:$a" ;; + *) san="$san,DNS:$a" ;; + esac + done + local myip; myip="$(primary_ip)" + [ -n "$myip" ] && san="$san,IP:$myip" + + log "Erzeuge selbstsigniertes Zertifikat fuer '$domain' ($days Tage) ..." + openssl req -x509 -nodes -newkey rsa:4096 -sha256 -days "$days" \ + -keyout "$outdir/privkey.pem" -out "$outdir/fullchain.pem" \ + -subj "/C=${CERT_COUNTRY:-DE}/O=${CERT_ORG:-Selfsigned}/CN=$domain" \ + -addext "subjectAltName=$san" \ + -addext "basicConstraints=critical,CA:FALSE" \ + -addext "keyUsage=critical,digitalSignature,keyEncipherment" \ + -addext "extendedKeyUsage=serverAuth" 2>/dev/null \ + || die "Zertifikatserstellung fehlgeschlagen." + chmod 600 "$outdir/privkey.pem" + + ok "Zertifikat erstellt:" + printf ' Schluessel : %s\n' "$outdir/privkey.pem" + printf ' Zertifikat : %s\n' "$outdir/fullchain.pem" + printf ' SAN : %s\n\n' "$san" + cat < SSL Certificates -> Add SSL Certificate -> Custom + 2) Name z.B. "$domain" + Certificate Key : privkey.pem + Certificate : fullchain.pem + 3) Im Proxy-Host unter "SSL" dieses Zertifikat auswaehlen, + "Force SSL" und "HTTP/2 Support" aktivieren. +EOF +} + +# ===================================================================== +# ncurses-Oberflaeche +# ===================================================================== +gui_pick_site() { # -> Site auf stdout + local -a items=() s + while IFS= read -r s; do + [ -n "$s" ] || continue + items+=("$s" "$(cmk_site_version "$s") / $(cmk_site_state_text "$s")") + done < <(cmk_sites) + [ ${#items[@]} -gt 0 ] || { ui_msg "Sites" "Im Container ist keine Site vorhanden."; return 1; } + ui_menu "Site waehlen" "Vorhandene Sites im Container:" "${items[@]}" +} + +gui_pick_backup() { # -> Datei auf stdout + local -a items=() f + while IFS= read -r f; do + [ -n "$f" ] || continue + bundle_read "$f" 2>/dev/null || continue + items+=("$f" "$B_SITE ${B_CMK_VERSION:-?} $(human_size "$(file_size "$f")")") + done < <(list_backup_files) + if [ ${#items[@]} -eq 0 ]; then + ui_msg "Backups" "In $BACKUP_DIR wurde kein Backup gefunden.\n\nAlternativ kann im naechsten Schritt frei im Dateisystem gesucht werden." + ui_pick_file "$PROJECT_DIR" '*.tar.gz' + return + fi + items+=("__browse__" "... anderes Verzeichnis durchsuchen ...") + local sel + sel="$(ui_menu "Backup waehlen" "Backups in $BACKUP_DIR:" "${items[@]}")" || return 1 + if [ "$sel" = "__browse__" ]; then + ui_pick_file "$PROJECT_DIR" '*.tar.gz' + else + printf '%s\n' "$sel" + fi +} + +gui_status() { + local f; f="$(mktemp)" + NO_COLOR=1 cmd_status >"$f" 2>&1 || true + ui_text "Status" "$f" + rm -f "$f" +} + +gui_stack_menu() { + while :; do + local sel + sel="$(ui_menu "Stack steuern" "Container-Runtime: $RUNTIME" \ + up "Stack starten (compose up -d)" \ + restart "Stack neu starten" \ + down "Stack stoppen und entfernen" \ + pull "Images aktualisieren" \ + logs "Letzte 200 Logzeilen ansehen" \ + back "zurueck")" || return 0 + case "$sel" in + up) ui_run "Stack starten" bash -c "cd '$PROJECT_DIR' && '$SELF' up" ;; + restart) ui_run "Neu starten" bash -c "cd '$PROJECT_DIR' && '$SELF' restart" ;; + down) ui_yesno "Stack stoppen" "Container wirklich stoppen und entfernen?\n(Die Daten unter ./data bleiben erhalten.)" \ + && ui_run "Stack stoppen" bash -c "cd '$PROJECT_DIR' && '$SELF' down" ;; + pull) ui_run "Images laden" bash -c "cd '$PROJECT_DIR' && '$SELF' pull" ;; + logs) local lf; lf="$(mktemp)"; compose logs --tail=200 >"$lf" 2>&1 || true + ui_text "Logs (letzte 200 Zeilen)" "$lf"; rm -f "$lf" ;; + back) return 0 ;; + esac + done +} + +gui_backup() { + require_cmk_running + local -a items=() s + while IFS= read -r s; do + [ -n "$s" ] || continue + items+=("$s" "$(cmk_site_version "$s")" off) + done < <(cmk_sites) + [ ${#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 + [ -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 + + local label + label="$(ui_input "Kennzeichnung" "Optionaler Text fuer den Dateinamen (leer = keiner):" "")" || label="" + label="$(printf '%s' "$label" | tr -cd 'A-Za-z0-9._-')" + + local -a args=(backup --dir "$BACKUP_DIR") + [ -n "$label" ] && args+=(--label "$label") + case "$optsel" in *no-rrds*) args+=(--no-rrds);; esac + case "$optsel" in *no-logs*) args+=(--no-logs);; esac + case "$optsel" in *stop*) args+=(--stop);; esac + local sitename + for sitename in $(printf '%s' "$chosen" | tr -d '"'); do + args+=(--site "$sitename") + done + + ui_run "Backup laeuft" bash -c "cd '$PROJECT_DIR' && '$SELF' -y ${args[*]}" +} + +gui_restore() { + require_cmk_running + local file; file="$(gui_pick_backup)" || return + [ -n "$file" ] || return + + local f; f="$(mktemp)" + bundle_show "$file" >"$f" 2>&1 || true + ui_text "Backup-Informationen" "$f"; rm -f "$f" + + 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 + reuse="--reuse --kill" + fi + ui_yesno "Restore starten" "Site '$B_SITE' aus\n$(basename "$file")\nwiederherstellen?" || return + ui_run "Restore laeuft" bash -c "cd '$PROJECT_DIR' && '$SELF' -y restore --file '$file' $reuse" +} + +gui_import() { + require_cmk_running + ui_msg "Site importieren" \ +"Import einer Site von einer Direktinstallation (Bare-Metal/VM). + +Voraussetzung: auf dem Quellserver wurde mit 'migrate_creator.sh' +ein Migrations-Bundle (.tar.gz) erzeugt und hierher kopiert. + +Wichtig fuer die Agents: + - Site-Name bleibt unveraendert + - dieser Host uebernimmt die IP/den DNS-Namen des Quellservers + - 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 f; f="$(mktemp)" + bundle_show "$file" >"$f" 2>&1 || true + ui_text "Bundle-Informationen" "$f"; rm -f "$f" + + 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 + reuse="--reuse --kill" + fi + local setdef="" + ui_yesno "Standard-Site" "Soll CMK_SITE_ID in der .env auf '$B_SITE' gesetzt werden?\n\n(Empfohlen - sonst startet der Container nach einem Reboot\ndie importierte Site nicht automatisch.)" \ + && setdef="--set-default-site" + + ui_run "Import laeuft" bash -c "cd '$PROJECT_DIR' && '$SELF' -y import --file '$file' $reuse $setdef" +} + +gui_backups_menu() { + while :; do + local sel + sel="$(ui_menu "Backups verwalten" "Verzeichnis: $BACKUP_DIR" \ + list "Backups auflisten" \ + info "Details eines Backups anzeigen" \ + verify "Backup pruefen (Struktur + Pruefsumme)" \ + del "Backup loeschen" \ + back "zurueck")" || return 0 + case "$sel" in + list) local f; f="$(mktemp)"; NO_COLOR=1 cmd_list >"$f" 2>&1 || true + ui_text "Backups" "$f"; rm -f "$f" ;; + info) local b; b="$(gui_pick_backup)" || continue; [ -n "$b" ] || continue + local f2; f2="$(mktemp)"; bundle_show "$b" >"$f2" 2>&1 || true + ui_text "Details" "$f2"; rm -f "$f2" ;; + verify) local b2; b2="$(gui_pick_backup)" || continue; [ -n "$b2" ] || continue + ui_run "Pruefe Bundle" bash -c "cd '$PROJECT_DIR' && '$SELF' verify --file '$b2'" ;; + del) local b3; b3="$(gui_pick_backup)" || continue; [ -n "$b3" ] || continue + ui_yesno "Loeschen" "Datei wirklich loeschen?\n\n$b3" && rm -f "$b3" \ + && ui_msg "Geloescht" "Die Datei wurde entfernt." ;; + back) return 0 ;; + esac + done +} + +gui_sites_menu() { + require_cmk_running + while :; do + local sel + sel="$(ui_menu "Sites" "Sites im Container '$CMK_CONTAINER_NAME'" \ + list "Sites anzeigen" \ + start "Site starten" \ + stop "Site stoppen" \ + all "alle Sites starten" \ + back "zurueck")" || return 0 + case "$sel" in + list) local f; f="$(mktemp)"; cmd_sites >"$f" 2>&1 || true; ui_text "Sites" "$f"; rm -f "$f" ;; + start) local s; s="$(gui_pick_site)" || continue; [ -n "$s" ] || continue + ui_run "Site starten" bash -c "cd '$PROJECT_DIR' && '$SELF' site-start '$s'" ;; + stop) local s2; s2="$(gui_pick_site)" || continue; [ -n "$s2" ] || continue + ui_run "Site stoppen" bash -c "cd '$PROJECT_DIR' && '$SELF' site-stop '$s2'" ;; + all) ui_run "Alle Sites starten" bash -c "cd '$PROJECT_DIR' && '$SELF' start-all-sites" ;; + back) return 0 ;; + esac + done +} + +gui_cert() { + local domain alt days + domain="$(ui_input "Zertifikat" "FQDN / Common Name:" "${CERT_DOMAIN:-checkmk.example.local}")" || return + [ -n "$domain" ] || return + 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 + for x in $alt; do a+=(--alt "$x"); done + ui_run "Zertifikat erzeugen" bash -c "cd '$PROJECT_DIR' && '$SELF' cert --domain '$domain' --days '$days' ${a[*]+${a[*]}}" +} + +gui_env() { + local f="${ENV_FILE:-$PROJECT_DIR/.env}" + [ -f "$f" ] || { ui_msg ".env" "Keine .env vorhanden."; return; } + ui_text "Konfiguration ($f)" "$f" + if ui_yesno "Bearbeiten" "Die .env jetzt im Editor oeffnen?"; then + "${EDITOR:-nano}" "$f" + load_env "$f" + fi +} + +gui_main() { + ui_require + while :; do + local sel + sel="$(ui_menu "Checkmk Stack Manager" \ +"Projekt : $PROJECT_DIR +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" \ + restore "Backup wiederherstellen" \ + import "Site von Direktinstallation importieren" \ + manage "Backups verwalten (Liste/Details/Loeschen)" \ + sites "Sites im Container verwalten" \ + cert "Selbstsigniertes Zertifikat erzeugen" \ + env "Konfiguration (.env) ansehen/bearbeiten" \ + 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 ;; + cert) gui_cert ;; + env) gui_env ;; + shell) clear; cmd_shell || true ;; + quit) break ;; + esac + done + clear + ok "Beendet." +} + +# ===================================================================== +# Einstiegspunkt +# ===================================================================== +main() { + while [ $# -gt 0 ]; do + case "$1" in + --env-file) ENVFILE_OPT="$2"; shift 2 ;; + -y|--yes) ASSUME_YES=1; shift ;; + -v|--verbose) VERBOSE=1; shift ;; + -h|--help) usage; exit 0 ;; + *) break ;; + esac + done + + load_env "${ENVFILE_OPT:-$PROJECT_DIR/.env}" + detect_runtime + UI_BACKTITLE="Checkmk Stack Manager - Runtime: $RUNTIME" + + local cmd="${1:-gui}"; [ $# -gt 0 ] && shift || true + case "$cmd" in + 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 ;; + 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 "$@" ;; + 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 "$@" ;; + help|-h|--help) usage ;; + *) err "Unbekanntes Kommando: $cmd"; echo; usage; exit 1 ;; + esac +} + +main "$@" diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh new file mode 100644 index 0000000..0c7b021 --- /dev/null +++ b/scripts/lib/common.sh @@ -0,0 +1,290 @@ +#!/usr/bin/env bash +# ===================================================================== +# common.sh - gemeinsame Funktionen fuer cmk-manage.sh +# Wird per "source" eingebunden, nicht direkt ausgefuehrt. +# ===================================================================== + +# --------------------------------------------------------------------- +# Ausgabe / Logging +# --------------------------------------------------------------------- +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[ %s ]%s %s\n' "$C_BLU" "INFO" "$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; } +debug() { [ -n "${VERBOSE:-}" ] && printf ' > %s\n' "$*" >&2 || true; } + +# --------------------------------------------------------------------- +# .env laden +# --------------------------------------------------------------------- +load_env() { + local envfile="${1:-$PROJECT_DIR/.env}" + if [ -f "$envfile" ]; then + set -a + # shellcheck disable=SC1090 + . "$envfile" + set +a + ENV_FILE="$envfile" + else + warn "Keine .env gefunden ($envfile) - es gelten die Standardwerte." + ENV_FILE="" + fi + # Defaults + CMK_CONTAINER_NAME="${CMK_CONTAINER_NAME:-checkmk}" + NPM_CONTAINER_NAME="${NPM_CONTAINER_NAME:-nginx-proxy-manager}" + CMK_SITE_ID="${CMK_SITE_ID:-cmk}" + BACKUP_DIR="${BACKUP_DIR:-$PROJECT_DIR/backups}" + case "$BACKUP_DIR" in + /*) : ;; + *) BACKUP_DIR="$PROJECT_DIR/${BACKUP_DIR#./}" ;; + esac +} + +# .env-Wert dauerhaft setzen +env_set() { + local key="$1" val="$2" f="${ENV_FILE:-$PROJECT_DIR/.env}" + [ -f "$f" ] || die "Keine .env vorhanden - kann '$key' nicht setzen." + if grep -qE "^[#[:space:]]*${key}=" "$f"; then + sed -i -E "s|^[#[:space:]]*${key}=.*|${key}=${val}|" "$f" + else + printf '%s=%s\n' "$key" "$val" >>"$f" + fi + export "$key=$val" +} + +# --------------------------------------------------------------------- +# Container-Runtime erkennen (docker / podman) +# --------------------------------------------------------------------- +detect_runtime() { + if [ -n "${CMK_RUNTIME:-}" ]; then + command -v "$CMK_RUNTIME" >/dev/null 2>&1 \ + || die "CMK_RUNTIME='$CMK_RUNTIME' ist nicht installiert." + RUNTIME="$CMK_RUNTIME" + elif command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then + RUNTIME=docker + elif command -v podman >/dev/null 2>&1; then + RUNTIME=podman + elif command -v docker >/dev/null 2>&1; then + RUNTIME=docker # vorhanden, aber Daemon evtl. gestoppt + else + die "Weder docker noch podman gefunden. Bitte zuerst ./install.sh ausfuehren." + fi + debug "Runtime: $RUNTIME" +} + +# Compose-Kommando ermitteln (als Array COMPOSE_CMD) +detect_compose() { + COMPOSE_CMD=() + if [ "$RUNTIME" = docker ]; then + if docker compose version >/dev/null 2>&1; then + COMPOSE_CMD=(docker compose) + elif command -v docker-compose >/dev/null 2>&1; then + COMPOSE_CMD=(docker-compose) + fi + else + if command -v podman-compose >/dev/null 2>&1; then + COMPOSE_CMD=(podman-compose) + elif podman compose version >/dev/null 2>&1; then + COMPOSE_CMD=(podman compose) + fi + fi + [ ${#COMPOSE_CMD[@]} -gt 0 ] || die "Kein Compose-Kommando gefunden (docker compose / podman-compose)." + debug "Compose: ${COMPOSE_CMD[*]}" +} + +compose() { + detect_compose + ( cd "$PROJECT_DIR" && "${COMPOSE_CMD[@]}" "$@" ) +} + +# --------------------------------------------------------------------- +# Container-Helfer +# --------------------------------------------------------------------- +container_exists() { $RUNTIME container inspect "$1" >/dev/null 2>&1; } +container_running() { [ "$($RUNTIME container inspect -f '{{.State.Running}}' "$1" 2>/dev/null)" = "true" ]; } + +require_cmk_running() { + container_exists "$CMK_CONTAINER_NAME" \ + || die "Container '$CMK_CONTAINER_NAME' existiert nicht. Zuerst: $0 up" + container_running "$CMK_CONTAINER_NAME" \ + || die "Container '$CMK_CONTAINER_NAME' laeuft nicht. Zuerst: $0 up" +} + +# Kommando im Checkmk-Container als root ausfuehren +ct() { $RUNTIME exec "$CMK_CONTAINER_NAME" "$@"; } +# ... mit stdin +cti() { $RUNTIME exec -i "$CMK_CONTAINER_NAME" "$@"; } + +# --------------------------------------------------------------------- +# OMD-Helfer (im Container) +# --------------------------------------------------------------------- +cmk_sites() { + ct sh -c 'ls -1 /omd/sites 2>/dev/null' | sed '/^$/d' || true +} + +cmk_site_exists() { + cmk_sites | grep -qxF "$1" +} + +cmk_site_version() { + ct omd version -b "$1" 2>/dev/null | tr -d '\r' || true +} + +# 0 = laeuft komplett, 1 = teilweise, 2 = gestoppt +cmk_site_state() { + local rc=0 + ct omd status --bare "$1" >/dev/null 2>&1 || rc=$? + echo "$rc" +} + +cmk_site_state_text() { + case "$(cmk_site_state "$1")" in + 0) echo "laeuft" ;; + 1) echo "teilweise" ;; + *) echo "gestoppt" ;; + esac +} + +cmk_versions_installed() { + ct omd versions -b 2>/dev/null | tr -d '\r' || ct sh -c 'ls -1 /omd/versions | grep -v "^default$"' 2>/dev/null || true +} + +# --------------------------------------------------------------------- +# Diverses +# --------------------------------------------------------------------- +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 prompt="${1:-Fortfahren?} [j/N] " a + read -r -p "$prompt" 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}' +} + +# ===================================================================== +# TUI-Helfer (dialog bevorzugt, sonst whiptail) - ncurses wie im mc +# ===================================================================== +UI_BACKTITLE="Checkmk Stack Manager - Container-Runtime: ${RUNTIME:-?}" + +ui_init() { + [ -n "${UI_BIN:-}" ] && return 0 + 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_H="${UI_H:-22}"; UI_W="${UI_W:-78}"; UI_MH="${UI_MH:-13}" + export DIALOGRC="${DIALOGRC:-}" +} + +ui_available() { ui_init; [ -n "$UI_BIN" ]; } + +ui_require() { + ui_available || die "Fuer die TUI wird 'dialog' oder 'whiptail' benoetigt. + Debian/Ubuntu : apt-get install -y dialog + RHEL/Rocky : dnf install -y dialog" + UI_BACKTITLE="Checkmk Stack Manager - Runtime: $RUNTIME" +} + +ui_menu() { # title text tag item [tag item ...] -> gewaehlter tag auf stdout + local title="$1" text="$2"; shift 2 + "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$title" \ + --menu "$text" "$UI_H" "$UI_W" "$UI_MH" "$@" 3>&1 1>&2 2>&3 +} + +ui_checklist() { # title text tag item status ... + local title="$1" text="$2"; shift 2 + "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$title" \ + --checklist "$text" "$UI_H" "$UI_W" "$UI_MH" "$@" 3>&1 1>&2 2>&3 +} + +ui_radiolist() { + local title="$1" text="$2"; shift 2 + "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$title" \ + --radiolist "$text" "$UI_H" "$UI_W" "$UI_MH" "$@" 3>&1 1>&2 2>&3 +} + +ui_input() { # title text default + "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" \ + --inputbox "$2" 10 "$UI_W" "${3:-}" 3>&1 1>&2 2>&3 +} + +ui_password() { + "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" \ + --passwordbox "$2" 10 "$UI_W" 3>&1 1>&2 2>&3 +} + +ui_msg() { # title text + "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --msgbox "$2" 16 "$UI_W" +} + +ui_yesno() { # title text -> exit 0 = ja + "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --yesno "$2" 14 "$UI_W" +} + +ui_text() { # title file + "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --textbox "$2" "$UI_H" "$UI_W" +} + +ui_info() { # title text (ohne OK-Button) + "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --infobox "$2" 8 "$UI_W" +} + +# Kommando ausfuehren, Ausgabe live in Datei, danach in Textbox anzeigen +ui_run() { # title cmd... + local title="$1"; shift + local logf; logf="$(mktemp)" + ui_info "$title" "Bitte warten ...\n\n$*" + if "$@" >"$logf" 2>&1; then + ui_text "$title - erfolgreich" "$logf" + rm -f "$logf"; return 0 + else + ui_text "$title - FEHLGESCHLAGEN" "$logf" + rm -f "$logf"; return 1 + fi +} + +# --------------------------------------------------------------------- +# Dateiauswahl im Midnight-Commander-Stil (funktioniert auch mit whiptail) +# --------------------------------------------------------------------- +ui_pick_file() { # startdir [glob] -> Dateipfad auf stdout + local dir="${1:-$PWD}" glob="${2:-*}" sel + dir="$(cd "$dir" 2>/dev/null && pwd || echo "$PWD")" + while :; do + local -a items=() + items+=(".." "") + local d f + while IFS= read -r d; do + [ -n "$d" ] && items+=("$d/" "") + done < <(find "$dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' 2>/dev/null | sort) + while IFS= read -r f; do + [ -n "$f" ] || continue + items+=("$f" "$(human_size "$(file_size "$dir/$f")")") + done < <(find "$dir" -mindepth 1 -maxdepth 1 -type f -name "$glob" -printf '%f\n' 2>/dev/null | sort) + + sel="$(ui_menu "Datei waehlen" "Verzeichnis: $dir\nFilter: $glob" "${items[@]}")" || return 1 + case "$sel" in + "..") dir="$(dirname "$dir")" ;; + */) dir="$dir/${sel%/}" ;; + *) printf '%s\n' "$dir/$sel"; return 0 ;; + esac + done +} diff --git a/scripts/migrate_creator.sh b/scripts/migrate_creator.sh new file mode 100755 index 0000000..4e536a0 --- /dev/null +++ b/scripts/migrate_creator.sh @@ -0,0 +1,378 @@ +#!/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)" +} +ui_menu() { local t="$1" x="$2"; shift 2; "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$t" --menu "$x" 20 76 11 "$@" 3>&1 1>&2 2>&3; } +ui_checklist() { local t="$1" x="$2"; shift 2; "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$t" --checklist "$x" 20 76 11 "$@" 3>&1 1>&2 2>&3; } +ui_input() { "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --inputbox "$2" 10 76 "${3:-}" 3>&1 1>&2 2>&3; } +ui_msg() { "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --msgbox "$2" 18 76; } +ui_yesno() { "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --yesno "$2" 14 76; } +ui_text() { "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --textbox "$2" 22 78; } +ui_info() { "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --infobox "$2" 8 76; } + +# --------------------------------------------------------------------- +# 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) In der .env die passende Checkmk-Version eintragen: + CMK_IMAGE_TAG=${version%.*} + und den Stack neu erstellen: ./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 + 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 diff --git a/systemd/checkmk-stack.service.example b/systemd/checkmk-stack.service.example new file mode 100644 index 0000000..3d9061f --- /dev/null +++ b/systemd/checkmk-stack.service.example @@ -0,0 +1,33 @@ +# ===================================================================== +# Beispiel-Unit (wird von install.sh --with-systemd automatisch +# mit den korrekten Pfaden erzeugt und nach +# /etc/systemd/system/checkmk-stack.service geschrieben). +# +# Manuell: +# cp systemd/checkmk-stack.service.example \ +# /etc/systemd/system/checkmk-stack.service +# # Pfade und Compose-Kommando anpassen +# systemctl daemon-reload && systemctl enable --now checkmk-stack +# ===================================================================== +[Unit] +Description=Checkmk + NGINX Proxy Manager (Container-Stack) +Wants=network-online.target +After=network-online.target +# Nur bei Docker zusaetzlich: +#Requires=docker.service +#After=docker.service + +[Service] +Type=oneshot +RemainAfterExit=yes +WorkingDirectory=/opt/checkmk-stack +EnvironmentFile=/opt/checkmk-stack/.env +TimeoutStartSec=0 +# Docker: /usr/bin/docker compose up -d +# Podman: /usr/bin/podman-compose up -d +ExecStart=/bin/sh -c '/usr/bin/podman-compose up -d' +ExecStartPost=/opt/checkmk-stack/scripts/cmk-manage.sh start-all-sites +ExecStop=/bin/sh -c '/usr/bin/podman-compose down' + +[Install] +WantedBy=multi-user.target diff --git a/systemd/quadlet/checkmk.container b/systemd/quadlet/checkmk.container new file mode 100644 index 0000000..4f21410 --- /dev/null +++ b/systemd/quadlet/checkmk.container @@ -0,0 +1,30 @@ +# ===================================================================== +# Podman Quadlet Unit - Checkmk +# Ziel: /etc/containers/systemd/checkmk.container +# Wird von install.sh --quadlet mit den Werten aus der .env befuellt. +# Danach: systemctl daemon-reload && systemctl start checkmk +# Der Autostart nach einem Reboot ergibt sich aus [Install]. +# ===================================================================== +[Unit] +Description=Checkmk (OMD) im Container +Wants=network-online.target +After=network-online.target + +[Container] +ContainerName=__CMK_CONTAINER_NAME__ +Image=docker.io/__CMK_IMAGE__ +Network=host +EnvironmentFile=__PROJECT_DIR__/.env +Environment=CMK_SITE_ID=__CMK_SITE_ID__ +Volume=__PROJECT_DIR__/data/checkmk/sites:/omd/sites:Z +Volume=/etc/localtime:/etc/localtime:ro +PodmanArgs=--ulimit nofile=65536:65536 + +[Service] +Restart=always +TimeoutStartSec=300 +# Alle vorhandenen Sites starten (der Entrypoint startet nur CMK_SITE_ID) +ExecStartPost=/bin/sh -c 'sleep 20; __PROJECT_DIR__/scripts/cmk-manage.sh start-all-sites || true' + +[Install] +WantedBy=multi-user.target default.target diff --git a/systemd/quadlet/nginx-proxy-manager.container b/systemd/quadlet/nginx-proxy-manager.container new file mode 100644 index 0000000..439c5f1 --- /dev/null +++ b/systemd/quadlet/nginx-proxy-manager.container @@ -0,0 +1,24 @@ +# ===================================================================== +# Podman Quadlet Unit - NGINX Proxy Manager +# Ziel: /etc/containers/systemd/nginx-proxy-manager.container +# ===================================================================== +[Unit] +Description=NGINX Proxy Manager (HTTPS-Terminierung fuer Checkmk) +Wants=network-online.target +After=network-online.target checkmk.service + +[Container] +ContainerName=__NPM_CONTAINER_NAME__ +Image=docker.io/__NPM_IMAGE__ +Network=host +EnvironmentFile=__PROJECT_DIR__/.env +Environment=DISABLE_IPV6=true +Volume=__PROJECT_DIR__/data/npm/data:/data:Z +Volume=__PROJECT_DIR__/data/npm/letsencrypt:/etc/letsencrypt:Z + +[Service] +Restart=always +TimeoutStartSec=180 + +[Install] +WantedBy=multi-user.target default.target