diff --git a/.env.example b/.env.example index bf86d42..43657f2 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,15 @@ # ===================================================================== # Checkmk + NGINX Proxy Manager - zentrale Konfiguration # Kopieren nach ".env" und anpassen: cp .env.example .env +# +# Diese Datei ist die EINZIGE Quelle der Wahrheit. Sie wird gelesen von +# * docker-compose.yml / podman-compose (Variablen-Ersetzung) +# * der systemd-Unit (EnvironmentFile=) +# * scripts/cmk-manage.sh (source) +# Das Verwaltungsskript SCHREIBT hier nur hinein (z.B. CMK_IMAGE), +# es haelt keine zweite Konfiguration. +# +# Werte mit Leerzeichen bitte in Anfuehrungszeichen setzen. # ===================================================================== # --------------------------------------------------------------------- @@ -8,17 +17,60 @@ # --------------------------------------------------------------------- CMK_RUNTIME= -# --------------------------------------------------------------------- -# Checkmk -# --------------------------------------------------------------------- -# Raw Edition (frei): checkmk/check-mk-raw -# Enterprise (Subskription): checkmk/check-mk-enterprise -CMK_IMAGE_REPO=checkmk/check-mk-raw +# ===================================================================== +# CHECKMK-IMAGE +# ===================================================================== +# Das ist der einzige Wert, den compose/systemd tatsaechlich verwendet. +# +# IMMER VOLLQUALIFIZIERT ANGEBEN (mit Registry-Praefix)! +# Podman loest kurze Namen ueber "unqualified-search-registries" auf - +# auf RHEL steht dort registry.access.redhat.com an erster Stelle. Ein +# kurzer Name wie "checkmk/check-mk-enterprise" wird deshalb dort gesucht +# und nicht gefunden ("image not known") oder es erscheint eine Abfrage. +# +# Beispiele: +# docker.io/checkmk/check-mk-raw:2.3.0-latest +# registry.checkmk.com/enterprise/check-mk-enterprise:2.3.0p23 +# localhost/checkmk/check-mk-enterprise:2.3.0p23 (aus Tarball geladen) +CMK_IMAGE=docker.io/checkmk/check-mk-raw:2.3.0-latest -# WICHTIG bei Migration: Die Version MUSS zur Version der Quell-Site passen, -# sonst schlaegt "omd restore" fehl. Beispiele: 2.3.0-latest, 2.3.0p23, 2.2.0p39 -CMK_IMAGE_TAG=2.3.0-latest +# --------------------------------------------------------------------- +# Nur fuer den Image-Downloader: scripts/cmk-manage.sh image ... +# Daraus wird CMK_IMAGE ermittelt und oben automatisch eingetragen. +# --------------------------------------------------------------------- +# Edition: +# raw Docker Hub, frei (bis 2.4) +# community Docker Hub, frei (ab 2.5, Nachfolger von "raw") +# enterprise NUR registry.checkmk.com oder Tarball - NICHT auf Docker Hub! +# pro Docker Hub (ab 2.5, Nachfolger von "enterprise") +# cloud Docker Hub +# managed Docker Hub (bis 2.4) +# ultimate Docker Hub (ab 2.5) +# ultimatemt Docker Hub (ab 2.5, Nachfolger von "managed") +CMK_EDITION=raw +# Konkrete Version. Fuer den Tarball-Download muss eine exakte Version +# angegeben werden (z.B. 2.3.0p23), "-latest" funktioniert dort nicht. +CMK_VERSION=2.3.0-latest + +# --- Bezugsweg 1: private Registry (Enterprise) ---------------------- +# Zugangsdaten aus dem Checkmk-Kundenportal (portal.checkmk.com) +CMK_REGISTRY=registry.checkmk.com +CMK_REGISTRY_USER= +CMK_REGISTRY_PASSWORD= + +# --- Bezugsweg 2: Image-Tarball vom Download-Portal ------------------ +# Zugangsdaten der Checkmk-Downloadseite. Der Downloader probiert die +# ueblichen Dateinamen unter // durch; mit +# CMK_DOWNLOAD_URL laesst sich die vollstaendige URL fest vorgeben. +CMK_DOWNLOAD_BASEURL=https://download.checkmk.com/checkmk +CMK_DOWNLOAD_URL= +CMK_DOWNLOAD_USER= +CMK_DOWNLOAD_PASSWORD= + +# ===================================================================== +# CHECKMK-LAUFZEIT +# ===================================================================== CMK_CONTAINER_NAME=checkmk CMK_HOSTNAME=checkmk @@ -38,28 +90,59 @@ MAIL_RELAY_HOST= # Zeitzone TZ=Europe/Berlin -# --------------------------------------------------------------------- -# NGINX Proxy Manager (HTTPS-Terminierung, Let's Encrypt ODER self-signed) -# --------------------------------------------------------------------- -NPM_IMAGE=jc21/nginx-proxy-manager:2 +# ===================================================================== +# NGINX PROXY MANAGER (HTTPS: Let's Encrypt ODER selbstsigniert) +# ===================================================================== +# Ebenfalls vollqualifiziert - siehe Hinweis oben zu Podman! +NPM_IMAGE=docker.io/jc21/nginx-proxy-manager:2 NPM_CONTAINER_NAME=nginx-proxy-manager NPM_DISABLE_IPV6=true -# --------------------------------------------------------------------- -# Pfade (relativ zum Projektverzeichnis) - ausschliesslich Bind-Mounts, -# keine Docker/Podman-Volumes! -# --------------------------------------------------------------------- +# Port der Adminoberflaeche. Vorgabe im Image ist fest 81; ein abweichender +# Port wird ueber einen zusaetzlichen nginx-Server bereitgestellt. +# NICHT von Hand aendern - dabei muss die nginx-Zusatzdatei mitgezogen werden: +# ./scripts/cmk-manage.sh npm-port 10443 +# oder im Menue unter "Einstellungen". +NPM_ADMIN_PORT=81 + +# ===================================================================== +# TERMINAL-BROWSER (lokale Bedienung des Proxy Managers) +# Fuer den Fall, dass Port 81 von aussen nicht erreichbar ist. +# Aufruf: ./scripts/cmk-manage.sh browser +# ===================================================================== +# Startseite des Browsers. Leer = automatisch aus NPM_ADMIN_PORT abgeleitet. +NPM_ADMIN_URL= + +# Vorgabe-Engine: browsh | carbonyl | links | w3m | lynx | shell +# ACHTUNG: nur browsh und carbonyl koennen JavaScript und damit die +# Oberflaeche des Proxy Managers darstellen. +BROWSER_ENGINE=browsh + +# Images der Engines (vollqualifiziert - siehe Podman-Hinweis oben) +BROWSH_IMAGE=docker.io/browsh/browsh:v1.8.3 +CARBONYL_IMAGE=docker.io/fathyb/carbonyl:latest + +# Selbst gebautes Werkzeug-Image (Textbrowser, curl, jq, openssl). +# Leer lassen - das Skript waehlt automatisch den zur Runtime passenden +# Namen (Podman braucht das Praefix "localhost/", Docker darf es nicht haben). +BROWSER_TOOLS_IMAGE= + +# ===================================================================== +# PFADE (relativ zum Projektverzeichnis) +# Ausschliesslich Bind-Mounts - keine Docker/Podman-Volumes! +# ===================================================================== CMK_DATA_DIR=./data/checkmk/sites NPM_DATA_DIR=./data/npm/data NPM_LE_DIR=./data/npm/letsencrypt -# Ablage fuer Backups / Migrations-Bundles (Host-Pfad) +# Ablage fuer Backups / Migrations-Bundles und heruntergeladene Images BACKUP_DIR=./backups +IMAGE_DIR=./images -# --------------------------------------------------------------------- -# Self-signed Zertifikat (Vorgaben fuer scripts/cmk-manage.sh cert) -# --------------------------------------------------------------------- +# ===================================================================== +# SELBSTSIGNIERTES ZERTIFIKAT (Vorgaben fuer "cmk-manage.sh cert") +# ===================================================================== CERT_DOMAIN=checkmk.example.local CERT_DAYS=3650 CERT_COUNTRY=DE -CERT_ORG="Beispiel GmbH" # Werte mit Leerzeichen bitte in Anfuehrungszeichen +CERT_ORG="Beispiel GmbH" diff --git a/.gitignore b/.gitignore index 93c84b6..54d9531 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ backups/* !backups/.gitkeep *.tar.gz *.log +images/* +!images/.gitkeep diff --git a/README.md b/README.md index ff7fecd..b99461f 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,21 @@ werden müssen. 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) +8. [Checkmk-Image beschaffen (Raw, Enterprise & Co.)](#8-checkmk-image-beschaffen-raw-enterprise--co) +9. [Erststart](#9-erststart) +10. [HTTPS einrichten – Let's Encrypt oder selbstsigniert](#10-https-einrichten--lets-encrypt-oder-selbstsigniert) +11. [Proxy Manager lokal einrichten (Terminal-Browser)](#11-proxy-manager-lokal-einrichten-terminal-browser) +12. [Adminoberfläche auf einen anderen Port legen](#12-adminoberfläche-auf-einen-anderen-port-legen) +13. [Firewall](#13-firewall) +14. [Verwaltung: `cmk-manage.sh` (CLI und ncurses-Oberfläche)](#14-verwaltung-cmk-managesh) +15. [Backup](#15-backup) +16. [Restore](#16-restore) +17. [Migration einer Direktinstallation (`migrate_creator.sh`)](#17-migration-einer-direktinstallation-migrate_creatorsh) +18. [Mehrere Sites in einem Container](#18-mehrere-sites-in-einem-container) +19. [Checkmk aktualisieren](#19-checkmk-aktualisieren) +20. [Fehlersuche](#20-fehlersuche) +21. [Sicherheitshinweise](#21-sicherheitshinweise) +22. [Deinstallation](#22-deinstallation) --- @@ -83,6 +86,10 @@ werden müssen. │ └── lib/ │ └── common.sh # gemeinsame Funktionen + TUI-Helfer │ +├── containers/ +│ └── browser/ +│ └── Containerfile # Werkzeug-Image: Textbrowser, curl, jq, openssl +│ ├── systemd/ │ ├── checkmk-stack.service.example # Compose-basierte Unit (Autostart) │ └── quadlet/ @@ -96,7 +103,8 @@ werden müssen. │ ├── letsencrypt/ # -> /etc/letsencrypt (Zertifikate) │ └── custom-certs/ # selbstsignierte Zertifikate zum Hochladen │ -└── backups/ # Backups und Migrations-Bundles (*.tar.gz) +├── backups/ # Backups und Migrations-Bundles (*.tar.gz) +└── images/ # heruntergeladene Checkmk-Image-Tarballs ``` --- @@ -114,7 +122,7 @@ werden müssen. |---|---|---| | 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! | +| 81/tcp | NGINX Proxy Manager – **Adminoberfläche** (Vorgabe; per [`npm-port`](#12-adminoberfläche-auf-einen-anderen-port-legen) änderbar) | 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 | @@ -151,6 +159,10 @@ Anschließend: ```bash sudo $EDITOR /opt/checkmk-stack/.env # mindestens CMK_PASSWORD und CMK_SITE_ID + +# Image festlegen (Raw geht ohne Anmeldung, Enterprise siehe Kapitel 8) +sudo ./scripts/cmk-manage.sh image pull --edition raw --version 2.3.0-latest + sudo ./scripts/cmk-manage.sh up sudo ./scripts/cmk-manage.sh status ``` @@ -158,7 +170,7 @@ 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)). +(siehe [Kapitel 18](#18-mehrere-sites-in-einem-container)). --- @@ -195,10 +207,19 @@ Starten: ```bash sudo $EDITOR /opt/checkmk-stack/.env + +# Image beschaffen - bei Enterprise mit Zugangsdaten, siehe Kapitel 8 +sudo ./scripts/cmk-manage.sh image pull --edition enterprise --version 2.3.0p23 \ + --user --password + sudo ./scripts/cmk-manage.sh up # nutzt automatisch podman-compose sudo ./scripts/cmk-manage.sh status ``` +> Auf RHEL unbedingt [Kapitel 8.2](#8-checkmk-image-beschaffen-raw-enterprise--co) +> lesen: kurze Image-Namen und `podman import` statt `podman load` sind die +> beiden Ursachen fuer „Image unbekannt“. + ### SELinux Die Bind-Mounts sind in `docker-compose.yml` mit `:Z` versehen, Podman setzt @@ -330,19 +351,204 @@ 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_IMAGE` | **das tatsaechlich verwendete Image**, immer vollqualifiziert – siehe [Kapitel 8](#8-checkmk-image-beschaffen-raw-enterprise--co) | +| `CMK_EDITION`, `CMK_VERSION` | nur Vorgaben fuer den Image-Downloader | +| `CMK_REGISTRY_USER/-PASSWORD` | Anmeldung an `registry.checkmk.com` (Enterprise) | +| `CMK_DOWNLOAD_USER/-PASSWORD` | Anmeldung am Checkmk-Downloadportal | | `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` | +| `NPM_ADMIN_PORT` | Port der Proxy-Manager-Oberfläche (Vorgabe 81) – nur über `npm-port` bzw. das Menü ändern | +| `NPM_ADMIN_URL` | Startseite des Terminal-Browsers; leer = automatisch aus `NPM_ADMIN_PORT` | | `BACKUP_DIR` | Ablage für Backups/Bundles (Vorgabe `./backups`) | | `CERT_DOMAIN`, `CERT_DAYS` | Vorgaben für selbstsignierte Zertifikate | +### 7.1 Wer liest die `.env` – und warum gibt es sie zusätzlich zum Skript? + +Die `.env` ist die **einzige** Konfigurationsdatei. Es gibt bewusst keine zweite +Konfiguration im Verwaltungsskript: + +``` + .env (einzige Quelle der Wahrheit) + | + +---------------+----------------+------------------+ + | | | | +docker-compose systemd-Unit cmk-manage.sh migrate_creator.sh +(Variablen- (EnvironmentFile) (source) (unabhaengig, laeuft + ersetzung) auf dem Quellserver) +``` + +* **Gelesen** wird sie von compose, von der systemd-Unit und vom Skript. +* **Geschrieben** wird sie durch das Skript – entweder gezielt + (`image …` setzt `CMK_IMAGE`, `import --set-default-site` setzt + `CMK_SITE_ID`, `npm-port` setzt `NPM_ADMIN_PORT`) oder über das + Einstellungsmenü bzw. `config set` (siehe [Kapitel 14](#14-verwaltung-cmk-managesh)). + Ein Editor ist damit nicht mehr nötig, bleibt aber als Menüpunkt erhalten. +* Das Skript selbst haelt nur **Rueckfallwerte** fuer den Fall, dass ein + Schluessel in der `.env` fehlt (in `scripts/lib/common.sh`, Funktion + `load_env`). Es ueberschreibt niemals einen gesetzten Wert. + +Kurz: die `.env` sagt *was* laeuft, das Skript *macht* etwas damit. Doppelt +gepflegt werden muss nichts. + --- -## 8. Erststart +## 8. Checkmk-Image beschaffen (Raw, Enterprise & Co.) + +### 8.1 Welche Edition liegt wo? + +Das ist die haeufigste Stolperfalle: **die Enterprise-Edition liegt nicht auf +Docker Hub.** Im Namensraum `checkmk` auf Docker Hub gibt es ausschliesslich: + +| Edition (`CMK_EDITION`) | Bezugsquelle | Anmeldung | +|---|---|---| +| `raw` (bis 2.4) | `docker.io/checkmk/check-mk-raw` | nein | +| `community` (ab 2.5, Nachfolger von raw) | `docker.io/checkmk/check-mk-community` | nein | +| `cloud` | `docker.io/checkmk/check-mk-cloud` | nein | +| `managed` (bis 2.4) | `docker.io/checkmk/check-mk-managed` | nein | +| `pro` (ab 2.5, Nachfolger von enterprise) | `docker.io/checkmk/check-mk-pro` | nein | +| `ultimate`, `ultimatemt` (ab 2.5) | `docker.io/checkmk/check-mk-ultimate[mt]` | nein | +| **`enterprise` (2.0 – 2.4)** | **`registry.checkmk.com/enterprise/check-mk-enterprise`** oder Image-Tarball aus dem Kundenportal | **ja** | +| `free` (historisch, bis 2.1) | `docker.io/checkmk/check-mk-free` | nein | + +Ab Checkmk 2.5 wurden die Editionen umbenannt (Raw → Community, +Enterprise → Pro, Managed → Ultimate MT); diese Images liegen wieder offen auf +Docker Hub. Wer also eine **2.5 oder neuer** einsetzt, nimmt einfach `pro`. +Fuer **2.2/2.3/2.4-Enterprise** fuehrt nur der Weg ueber `registry.checkmk.com` +oder den Tarball. + +> Frei herunterladbar heisst nicht lizenzfrei – der Lizenzschluessel wird +> anschliessend in Checkmk selbst hinterlegt. + +### 8.2 Zwei Podman-Fallstricke (genau die aus der Praxis) + +**1. Kurze Image-Namen.** Podman kennt keine Vorgabe-Registry. Kurze Namen wie +`checkmk/check-mk-enterprise:2.3.0p23` werden ueber +`unqualified-search-registries` aus `/etc/containers/registries.conf` aufgeloest +– auf RHEL steht dort `registry.access.redhat.com` an **erster** Stelle. Podman +sucht das Image also bei Red Hat, findet es nicht und meldet +`image not known` bzw. fragt interaktiv nach. Deshalb steht in `.env` und +`docker-compose.yml` konsequent der **vollqualifizierte** Name: + +``` +CMK_IMAGE=registry.checkmk.com/enterprise/check-mk-enterprise:2.3.0p23 +``` + +Aktuelle Auflösungsreihenfolge ansehen: + +```bash +grep -A3 unqualified-search-registries /etc/containers/registries.conf +``` + +**2. `import` statt `load`.** Ein heruntergeladener Image-Tarball wird mit +**`load`** eingespielt: + +| Kommando | Erwartet | Ergebnis | +|---|---|---| +| `podman import datei.tar.gz` | ein **Dateisystem**-Archiv (rootfs) | Image **ohne** Entrypoint, ohne Umgebung, ohne Tag → unbrauchbar, „Image unbekannt“ | +| `podman load -i datei.tar.gz` | ein **Image**-Archiv (`docker save`/OCI) | vollstaendiges Image inkl. Metadaten und Tag ✔ | + +Ein mit `import` erzeugtes Fehl-Image wieder loswerden: + +```bash +podman images | grep '' +podman rmi +``` + +`cmk-manage.sh image load` prueft das Archiv vorher auf `manifest.json` bzw. +`oci-layout` und weist ein falsches Archiv mit genau diesem Hinweis ab. + +### 8.3 Zugangsdaten in der `.env` hinterlegen + +```bash +CMK_EDITION=enterprise +CMK_VERSION=2.3.0p23 + +# Bezugsweg 1 - private Registry +CMK_REGISTRY=registry.checkmk.com +CMK_REGISTRY_USER= +CMK_REGISTRY_PASSWORD= + +# Bezugsweg 2 - Tarball vom Downloadportal +CMK_DOWNLOAD_BASEURL=https://download.checkmk.com/checkmk +CMK_DOWNLOAD_USER= +CMK_DOWNLOAD_PASSWORD= +``` + +Die Zugangsdaten stammen aus dem Checkmk-Kundenportal (portal.checkmk.com). +Die `.env` sollte deshalb `chmod 600` haben – `install.sh` setzt das. + +### 8.4 Image holen – drei Wege + +**a) Aus einer Registry (empfohlen)** + +```bash +# frei verfuegbar, ohne Anmeldung +sudo ./scripts/cmk-manage.sh image pull --edition raw --version 2.3.0-latest + +# Enterprise mit Anmeldung an registry.checkmk.com +sudo ./scripts/cmk-manage.sh image pull --edition enterprise --version 2.3.0p23 \ + --user --password +``` + +**b) Image-Tarball vom Downloadportal (mit Zugangsdaten)** + +```bash +sudo ./scripts/cmk-manage.sh image download --edition enterprise --version 2.3.0p23 \ + --user --password +``` + +Der Downloader probiert die ueblichen Dateinamen unter +`//` durch +(`check-mk-enterprise-.docker.tar.gz` und +`check-mk-enterprise-docker-.tar.gz`). Falls Checkmk das Schema +geaendert hat, laesst sich die vollstaendige URL direkt vorgeben: + +```bash +sudo ./scripts/cmk-manage.sh image download \ + --url 'https://download.checkmk.com/checkmk/2.3.0p23/check-mk-enterprise-2.3.0p23.docker.tar.gz' \ + --user --password +``` + +Die Zugangsdaten werden per temporaerer `.netrc`-Datei an `curl` uebergeben und +tauchen damit **nicht** in der Prozessliste auf. + +**c) Bereits heruntergeladene Datei einspielen** + +Wer die Datei im Browser aus dem Kundenportal geladen hat: + +```bash +sudo ./scripts/cmk-manage.sh image load --file /root/check-mk-enterprise-2.3.0p23.docker.tar.gz +``` + +In allen drei Faellen ermittelt das Skript den **tatsaechlichen** Image-Namen +(bei `load` z. B. `localhost/checkmk/check-mk-enterprise:2.3.0p23`) und traegt +ihn als `CMK_IMAGE` in die `.env` ein. Danach: + +```bash +sudo ./scripts/cmk-manage.sh up +``` + +### 8.5 Nachsehen, was gilt + +```bash +./scripts/cmk-manage.sh image status # konfiguriertes + lokale Images +./scripts/cmk-manage.sh image list # nur lokale Checkmk-Images +./scripts/cmk-manage.sh image use localhost/checkmk/check-mk-enterprise:2.3.0p23 +``` + +`image status` warnt ausdruecklich, wenn `CMK_IMAGE` ein kurzer Name ohne +Registry ist. + +In der ncurses-Oberflaeche liegt derselbe Ablauf unter dem Menuepunkt +**„Checkmk-Image beschaffen/auswaehlen“** (Edition auswaehlen → Version → +Zugangsdaten → holen). + +--- + +## 9. Erststart ```bash sudo ./scripts/cmk-manage.sh up @@ -367,15 +573,24 @@ Zugang NGINX Proxy Manager : http://10.0.0.5:81/ (Erstlogin: admin@example.com / changeme) ``` +Der Port der Adminoberfläche folgt `NPM_ADMIN_PORT` aus der `.env`; die Ausgabe +zeigt immer den tatsächlich eingestellten Wert. + +``` +``` + Anmeldung an Checkmk: Benutzer `cmkadmin`, Passwort aus `CMK_PASSWORD`. --- -## 9. HTTPS einrichten – Let's Encrypt oder selbstsigniert +## 10. HTTPS einrichten – Let's Encrypt oder selbstsigniert ### 9.1 NGINX Proxy Manager vorbereiten -1. `http://:81` aufrufen +1. `http://:81` aufrufen – bzw. den in `NPM_ADMIN_PORT` gesetzten Port + (siehe [Kapitel 12](#12-adminoberfläche-auf-einen-anderen-port-legen)); + ist der Port im Netz gesperrt, hilft der Terminal-Browser aus + [Kapitel 11](#11-proxy-manager-lokal-einrichten-terminal-browser) 2. Erstanmeldung: **admin@example.com** / **changeme** 3. Sofort E-Mail-Adresse und Passwort ändern (wird beim ersten Login erzwungen) @@ -458,7 +673,243 @@ Zertifikat erzeugen“*. --- -## 10. Firewall +## 11. Proxy Manager lokal einrichten (Terminal-Browser) + +Wenn Port 81 aus dem Netz nicht erreichbar ist, muss die Adminoberfläche des +NGINX Proxy Managers **auf der Maschine selbst** bedient werden. Dafür startet +`cmk-manage.sh browser` einen weiteren Container im Host-Netzwerk – der erreicht +`127.0.0.1:81` genauso wie der Proxy Manager selbst. + +```bash +sudo ./scripts/cmk-manage.sh browser +``` + +Das ist bewusst **kein** Dienst in der `docker-compose.yml`: ein Browser ist ein +interaktives Programm mit Terminal, kein Hintergrunddienst. Er wird mit +`run --rm -it` gestartet und verschwindet beim Beenden wieder rückstandslos. + +### 11.1 Warum kein `lynx` + +Die Adminoberfläche des Proxy Managers ist eine JavaScript-Anwendung. Klassische +Textbrowser können sie **nicht** darstellen – man sieht eine leere Seite. Es +gibt deshalb zwei Engines, die einen vollwertigen Browser im Terminal rendern: + +| Engine | Basis | JavaScript | Proxy Manager bedienbar | +|---|---|---|---| +| `browsh` | Firefox | ja | **ja** – Vorgabe, gepflegt | +| `carbonyl` | Chromium | ja | **ja** – schönere Darstellung, Stand 2023 | +| `links` (links2) | Textbrowser | nein | nein | +| `w3m` | Textbrowser | nein | nein | +| `lynx` | Textbrowser | nein | nein | +| `shell` | Bash + curl/jq/openssl | – | für Diagnose und die API | + +Die Textbrowser sind trotzdem eingebaut, weil sie für schnelle Prüfungen +praktisch sind. Wählt man sie, warnt das Skript ausdrücklich. + +### 11.2 Bedienung + +```bash +# Vorgabe: browsh auf http://127.0.0.1:81 +sudo ./scripts/cmk-manage.sh browser + +# andere Engine oder andere Seite +sudo ./scripts/cmk-manage.sh browser --engine carbonyl +sudo ./scripts/cmk-manage.sh browser --engine links --url http://127.0.0.1:5000/cmk/ + +# Shell mit Netzwerkwerkzeugen im Host-Netzwerk +sudo ./scripts/cmk-manage.sh browser --engine shell +``` + +Tastenbelegung in **browsh**: + +| Taste | Wirkung | +|---|---| +| `Strg+L` | Adresszeile | +| `Strg+T` | neuer Tab | +| `Strg+W` | Tab schließen | +| `Strg+Q` | beenden | + +Beim ersten Aufruf einer Textbrowser- oder `shell`-Engine baut das Skript ein +kleines Werkzeug-Image (Debian slim mit links2, w3m, lynx, curl, jq, openssl, +rund 170 MB). Vorab bauen – auch ohne Terminal, etwa im Installationsskript: + +```bash +sudo ./scripts/cmk-manage.sh browser --build-only +``` + +`browsh` und `carbonyl` werden dagegen als fertige Images geladen; die Namen +stehen in der `.env` (`BROWSH_IMAGE`, `CARBONYL_IMAGE`) und sind +vollqualifiziert, damit Podman sie nicht bei `registry.access.redhat.com` sucht. + +In der ncurses-Oberfläche liegt alles unter **„Terminal-Browser (Proxy Manager +lokal bedienen)"**. + +### 11.3 Grenze: Datei-Upload + +Ein Terminal-Browser kann keinen Dateiauswahl-Dialog des Betriebssystems +öffnen. Für **Let's Encrypt** ist das egal (dort werden nur Formularfelder +ausgefüllt), aber das Hochladen eines **selbstsignierten Zertifikats** +(*SSL Certificates → Custom*) funktioniert so nicht zuverlässig. + +Für genau diesen Fall führt der Weg über die REST-Schnittstelle des Proxy +Managers. Zertifikat wie in [Kapitel 10.4](#10-https-einrichten--lets-encrypt-oder-selbstsigniert) +erzeugen, dann: + +```bash +NPM=http://127.0.0.1:81 +CERTDIR=./data/npm/custom-certs/checkmk.intern.example + +# 1) Anmelden und Token holen +TOKEN=$(curl -s -X POST "$NPM/api/tokens" \ + -H 'Content-Type: application/json' \ + -d '{"identity":"admin@example.com","secret":""}' | jq -r .token) + +# 2) Zertifikatseintrag anlegen (provider "other" = eigenes Zertifikat) +CERT_ID=$(curl -s -X POST "$NPM/api/nginx/certificates" \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d '{"provider":"other","nice_name":"checkmk-selfsigned","domain_names":["checkmk.intern.example"]}' \ + | jq -r .id) + +# 3) Schlüssel und Zertifikat hochladen +curl -s -X POST "$NPM/api/nginx/certificates/$CERT_ID/upload" \ + -H "Authorization: Bearer $TOKEN" \ + -F "certificate=@$CERTDIR/fullchain.pem" \ + -F "certificate_key=@$CERTDIR/privkey.pem" + +# 4) Proxy-Host für Checkmk anlegen +curl -s -X POST "$NPM/api/nginx/proxy-hosts" \ + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ + -d "{\"domain_names\":[\"checkmk.intern.example\"], + \"forward_scheme\":\"http\", + \"forward_host\":\"127.0.0.1\", + \"forward_port\":5000, + \"certificate_id\":$CERT_ID, + \"ssl_forced\":true, + \"http2_support\":true, + \"block_exploits\":true, + \"allow_websocket_upgrade\":true, + \"advanced_config\":\"client_max_body_size 0;\", + \"enabled\":true, + \"meta\":{}}" +``` + +`curl` und `jq` sind im Werkzeug-Image enthalten, falls der Host sie nicht hat: + +```bash +sudo ./scripts/cmk-manage.sh browser --engine shell +``` + +> Beim allerersten Login erzwingt der Proxy Manager das Ändern von +> E-Mail-Adresse und Kennwort. Das geht am einfachsten im Browser +> (`cmk-manage.sh browser`); danach funktioniert die API mit den neuen Daten. + +### 11.4 Alternative: SSH-Tunnel + +Falls SSH auf die Maschine möglich ist, ist der Tunnel der bequemste Weg – +dann bedient man die Oberfläche im gewohnten Browser: + +```bash +ssh -N -L 8081:127.0.0.1:81 benutzer@checkmk-server +# danach lokal: http://127.0.0.1:8081 +``` + +Das ändert nichts an der Firewall: Port 81 bleibt von außen dicht, der Zugriff +läuft durch die bestehende SSH-Verbindung. + +--- + +## 12. Adminoberfläche auf einen anderen Port legen + +Die Weboberfläche des NGINX Proxy Managers hört im Image **fest auf Port 81** +(`listen 81 default;` in `/etc/nginx/conf.d/production.conf`). Eine +Umgebungsvariable dafür gibt es in den veröffentlichten 2.x-Versionen noch +nicht – im Entwicklungszweig ist `NPM_ADMIN_PORT` bereits vorbereitet, in +Version 2.15.1 aber noch nicht enthalten. Und weil beide Container im +Host-Netzwerk laufen, lässt sich der Port auch nicht per Port-Mapping +verschieben. + +Ist Port 81 im Netz gesperrt, hilft deshalb: + +```bash +sudo ./scripts/cmk-manage.sh npm-port 10443 +``` + +### 12.1 Was dabei passiert + +nginx bindet innerhalb seines `http{}`-Blocks einen offiziellen +Erweiterungspunkt ein: + +``` +include /data/nginx/custom/http.conf; +``` + +`/data` ist unser Bind-Mount `./data/npm/data`. Dort legt das Kommando einen +zusätzlichen Server an, der auf dem gewünschten Port lauscht und an die +Oberfläche auf `127.0.0.1:81` weiterreicht: + +``` +./data/npm/data/nginx/custom/http.conf + +# >>> cmk-manage.sh: Admin-Port - nicht von Hand aendern >>> +server { + listen 10443; + listen [::]:10443; + server_name _; + client_max_body_size 0; + location / { + proxy_pass http://127.0.0.1:81; + ... + } +} +# <<< cmk-manage.sh: Admin-Port <<< +``` + +Nur der Bereich zwischen den beiden Markierungen wird verwaltet – eigene +Ergänzungen in derselben Datei bleiben unangetastet. Anschließend startet das +Skript den Proxy-Manager-Container neu, damit nginx die Datei liest, und trägt +`NPM_ADMIN_PORT` sowie `NPM_ADMIN_URL` in die `.env` ein. Der Terminal-Browser +aus [Kapitel 11](#11-proxy-manager-lokal-einrichten-terminal-browser) startet +dann automatisch auf dem neuen Port. + +### 12.2 Wichtig: Port 81 bleibt lokal bestehen + +Der eingebaute Server auf Port 81 lässt sich so **nicht** abschalten – er +lauscht weiter auf allen Adressen. Das ist unkritisch, solange die Firewall ihn +sperrt, und genau so ist die Empfehlung: + +```bash +# firewalld +sudo firewall-cmd --permanent --add-port=10443/tcp +sudo firewall-cmd --permanent --remove-port=81/tcp 2>/dev/null +sudo firewall-cmd --reload + +# ufw +sudo ufw allow 10443/tcp +sudo ufw delete allow 81/tcp 2>/dev/null +``` + +### 12.3 Stand ansehen und zurücksetzen + +```bash +./scripts/cmk-manage.sh npm-port # aktuellen Stand anzeigen +sudo ./scripts/cmk-manage.sh npm-port 81 # zurück auf die Vorgabe +``` + +Bei `81` wird der verwaltete Block wieder entfernt; ist die Datei danach leer, +verschwindet sie ganz. Es gilt dann wieder ausschließlich die Konfiguration aus +dem Image. + +Das Kommando lehnt Ports ab, die im Stack schon vergeben sind (80, 443, 5000, +8000, 6557), prüft den Zahlenbereich und warnt, wenn auf dem Wunschport bereits +ein anderer Dienst lauscht. + +> Sobald eine NPM-Version mit `NPM_ADMIN_PORT` erscheint, wird dieser Umweg +> überflüssig – dann genügt die Umgebungsvariable. Bis dahin ist der +> Erweiterungspunkt der Weg, der ohne Änderung am Image auskommt. + +--- + +## 13. Firewall **firewalld (RHEL/Rocky/Alma):** @@ -468,6 +919,9 @@ 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 + +# Bei abweichendem Admin-Port (z.B. 10443) stattdessen dessen Nummer freigeben - +# das passende Kommando gibt "cmk-manage.sh npm-port " direkt aus. ``` **ufw (Debian/Ubuntu):** @@ -485,7 +939,7 @@ Proxy. Port **81** nur aus dem Administrationsnetz freigeben. --- -## 11. Verwaltung: `cmk-manage.sh` +## 14. Verwaltung: `cmk-manage.sh` Das Skript ist gleichzeitig Kommandozeilenwerkzeug **und** ncurses-Oberfläche. @@ -508,8 +962,10 @@ sudo ./scripts/cmk-manage.sh gui │ import Site von Direktinstallation importieren │ │ manage Backups verwalten (Liste/Details/Löschen) │ │ sites Sites im Container verwalten │ + │ image Checkmk-Image beschaffen/auswählen │ + │ browser Terminal-Browser (Proxy Manager lokal) │ │ cert Selbstsigniertes Zertifikat erzeugen │ - │ env Konfiguration (.env) ansehen/bearbeiten │ + │ config Einstellungen (.env) - menügeführt │ │ shell Root-Shell im Checkmk-Container │ │ quit Beenden │ │ < OK > │ @@ -539,6 +995,10 @@ Eine Dateiauswahl mit Verzeichnisnavigation ist eingebaut. | `list` / `verify` / `prune --keep N` | Backups verwalten | | `restore …` | Backup zurückspielen | | `import …` | Migrations-Bundle einspielen | +| `image status\|list\|pull\|download\|load\|use` | Checkmk-Image beschaffen und auswaehlen | +| `browser [--engine …]` | Terminal-Browser im Container – Proxy Manager lokal bedienen | +| `config list\|get\|set\|edit` | Einstellungen der `.env` anzeigen und ändern | +| `npm-port [PORT]` | Port der Proxy-Manager-Oberfläche setzen/anzeigen | | `cert …` | selbstsigniertes Zertifikat erzeugen | | `shell` | Root-Shell im Checkmk-Container | | `omd …` | beliebiges `omd`-Kommando im Container | @@ -546,9 +1006,66 @@ Eine Dateiauswahl mit Verzeichnisnavigation ist eingebaut. Globale Optionen: `--env-file DATEI`, `-y/--yes` (keine Rückfragen, z. B. für Cronjobs), `-v/--verbose`. +### 14.3 Einstellungen ändern – ohne Editor + +Sämtliche Werte der `.env` lassen sich über das Menü pflegen. In der +Oberfläche: **Einstellungen (.env) – menügeführt**. Die Werte sind nach +Bereichen gruppiert: + +``` + ┌──────────────────── Einstellungen ─────────────────────┐ + │ Alle Werte der Datei .env - direkt hier änderbar. │ + │ Datei: /opt/checkmk-stack/.env │ + │ │ + │ runtime Runtime und Zeitzone │ + │ image Checkmk-Image und Zugangsdaten │ + │ checkmk Checkmk-Site und Container │ + │ npm NGINX Proxy Manager │ + │ browser Terminal-Browser │ + │ paths Verzeichnisse │ + │ cert Selbstsignierte Zertifikate │ + │ show Alle Werte auflisten │ + │ editor Datei .env im Editor öffnen │ + │ back zurück │ + └────────────────────────────────────────────────────────┘ +``` + +Jeder Eintrag zeigt seinen aktuellen Wert und öffnet beim Bestätigen die +passende Eingabe: Freitext, Zahl, Auswahlliste oder Kennwortfeld (Kennwörter +erscheinen in der Übersicht als `********` und bleiben unverändert, wenn man +das Feld leer lässt). Der Editor bleibt als Menüpunkt erhalten. + +Dieselben Werte über die Kommandozeile: + +```bash +./scripts/cmk-manage.sh config list # alles mit aktuellen Werten +./scripts/cmk-manage.sh config list --category npm # nur ein Bereich +./scripts/cmk-manage.sh config get CMK_SITE_ID +./scripts/cmk-manage.sh config set CMK_SITE_ID prod +./scripts/cmk-manage.sh config set NPM_ADMIN_PORT 10443 +``` + +**Geprüft wird beim Setzen**, nicht erst beim nächsten Start: + +| Schlüssel | Prüfung | +|---|---| +| `NPM_ADMIN_PORT` | Zahl 1–65535, nicht 80/443/5000/8000/6557, Warnung wenn belegt | +| `CMK_SITE_ID` | nur Buchstaben, Ziffern, `_`; höchstens 16 Zeichen | +| `CMK_IMAGE`, `NPM_IMAGE`, … | muss vollqualifiziert sein (Podman-Fallstrick) | +| `CMK_RUNTIME` | muss auf diesem System installiert sein | +| `CERT_DAYS` u. a. | Zahlenfelder | +| URL-Felder | müssen mit `http://` oder `https://` beginnen | + +Werte mit Leerzeichen werden automatisch in Anführungszeichen gesetzt, damit +die `.env` weiterhin von compose, systemd **und** der Shell gelesen werden +kann. Ändert man `NPM_ADMIN_PORT`, zieht das Skript die nginx-Zusatzdatei +automatisch mit (siehe [Kapitel 12](#12-adminoberfläche-auf-einen-anderen-port-legen)). +Bei Werten, die erst nach dem Neuerstellen der Container greifen, weist das +Menü ausdrücklich darauf hin. + --- -## 12. Backup +## 15. Backup Backups werden mit `omd backup` erstellt und als **Bundle** verpackt: @@ -599,7 +1116,7 @@ sudo crontab -e --- -## 13. Restore +## 16. Restore ```bash # Einfachster Fall: Site existiert nicht mehr @@ -630,7 +1147,7 @@ der Site-Name wird dann aus dem Archiv gelesen. --- -## 14. Migration einer Direktinstallation (`migrate_creator.sh`) +## 17. Migration einer Direktinstallation (`migrate_creator.sh`) Ziel: eine bestehende Checkmk-Installation auf Blech/VM in den Container übernehmen – **ohne die Agents anzufassen**. @@ -714,21 +1231,32 @@ 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 +tar -xzOf backups/cmkmigrate_prod_*.tar.gz cmkbundle/MANIFEST | grep CMK_VERSION +# CMK_VERSION=2.3.0p23.cre -> Version 2.3.0p23, Endung .cre = Raw Edition -sudo $EDITOR .env -# CMK_IMAGE_TAG=2.3.0p23 +# passendes Image holen und als CMK_IMAGE eintragen lassen +sudo ./scripts/cmk-manage.sh image pull --edition raw --version 2.3.0p23 sudo ./scripts/cmk-manage.sh up ``` +Bei einer Enterprise-Quelle (`.cee`) entsprechend: + +```bash +sudo ./scripts/cmk-manage.sh image pull --edition enterprise --version 2.3.0p23 \ + --user --password +# oder, wenn die Registry nicht erreichbar ist: +sudo ./scripts/cmk-manage.sh image download --edition enterprise --version 2.3.0p23 \ + --user --password +``` + 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). +> **Edition beachten:** die Endung der Version verrät sie – +> `.cre` = Raw, `.cee` = Enterprise, `.cce` = Cloud, `.cme` = Managed. +> Das Import-Skript liest sie aus dem Manifest und nennt im Fehlerfall direkt +> das passende `image pull`-Kommando. Enterprise-Images gibt es **nicht** auf +> Docker Hub – siehe [Kapitel 8](#8-checkmk-image-beschaffen-raw-enterprise--co). ### 14.5 Schritt 4 – Bundle importieren @@ -792,7 +1320,7 @@ Danach in der Oberfläche prüfen: - [ ] `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 +- [ ] passendes Image geholt (`image pull`/`image download`), `CMK_IMAGE` 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 @@ -803,7 +1331,7 @@ Danach in der Oberfläche prüfen: --- -## 15. Mehrere Sites in einem Container +## 18. 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 @@ -827,7 +1355,7 @@ vorhandenen Sites. --- -## 16. Checkmk aktualisieren +## 19. 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`. @@ -836,11 +1364,10 @@ Images. Die Checkmk-Software selbst steckt im Image unter `/omd/versions`. # 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 +# 2) neues Image holen (traegt CMK_IMAGE automatisch in die .env ein) +sudo ./scripts/cmk-manage.sh image pull --edition raw --version 2.3.0p28 -# 3) Image holen und Container neu erstellen -sudo ./scripts/cmk-manage.sh pull +# 3) Container neu erstellen sudo ./scripts/cmk-manage.sh up # 4) Site aktualisieren (interaktiv, Konflikte bestätigen) @@ -854,19 +1381,29 @@ Checkmk-Werksnotizen (Werks/Release Notes) lesen. --- -## 17. Fehlersuche +## 20. 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`) | +| `omd restore`: *version is not installed* | passendes Image holen: `cmk-manage.sh image pull --edition --version `, dann `up` | +| Import bricht mit Editions-Warnung ab | Enterprise-Backup benötigt ein Enterprise-Image, siehe [Kapitel 8](#8-checkmk-image-beschaffen-raw-enterprise--co) | +| `image not known` / Podman sucht bei `registry.access.redhat.com` | kurzer Image-Name. `CMK_IMAGE` vollqualifiziert setzen: `cmk-manage.sh image use docker.io/…` bzw. `registry.checkmk.com/enterprise/…` | +| `checkmk/check-mk-enterprise` nicht auf Docker Hub gefunden | richtig so – Enterprise liegt nur auf `registry.checkmk.com` oder als Tarball vor; ab 2.5 heißt die Edition `pro` und liegt frei auf Docker Hub | +| Tarball mit `podman import` eingespielt, Image „unbekannt“ | `import` erzeugt ein Image ohne Metadaten. Fehl-Image mit `podman rmi ` entfernen und `cmk-manage.sh image load --file …` verwenden (nutzt `load`) | +| Download des Image-Tarballs schlägt fehl (401/404) | Zugangsdaten des Kundenportals prüfen, exakte Version angeben (kein `-latest`), notfalls `image download --url ''` | | 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`) | +| Adminoberfläche aus dem LAN nicht erreichbar | Port 81 gesperrt. Auf einen freien Port legen: `cmk-manage.sh npm-port 10443` und in der Firewall freigeben | +| Nach `npm-port` antwortet der neue Port nicht | Proxy Manager wurde nicht neu gestartet: `cmk-manage.sh restart`; Datei prüfen: `data/npm/data/nginx/custom/http.conf` | +| `cmk-manage.sh` bricht mit „CMK_RUNTIME … ist nicht installiert" ab | wird nur noch gewarnt und selbst gesucht; sauber setzen mit `config set CMK_RUNTIME auto` | +| Proxy-Manager-Oberfläche im Browser leer / nur Textfragmente | Textbrowser ohne JavaScript verwendet. `cmk-manage.sh browser --engine browsh` (bzw. `carbonyl`) nehmen | +| Selbstsigniertes Zertifikat lässt sich im Terminal-Browser nicht hochladen | Datei-Dialoge gibt es dort nicht – Upload über die API, siehe [Kapitel 11.3](#11-proxy-manager-lokal-einrichten-terminal-browser) | +| `browser` meldet „braucht ein echtes Terminal" | Aufruf aus Pipe/Skript. Direkt auf der Konsole oder per SSH aufrufen; nur der Bau geht headless mit `--build-only` | | 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 | @@ -881,7 +1418,7 @@ docker exec -it checkmk omd su cmk # bzw. podman exec … --- -## 18. Sicherheitshinweise +## 21. Sicherheitshinweise * `CMK_PASSWORD` sofort nach der Installation in der Oberfläche ändern und aus der `.env` entfernen (wird nur bei der Neuanlage ausgewertet); @@ -900,7 +1437,7 @@ docker exec -it checkmk omd su cmk # bzw. podman exec … --- -## 19. Deinstallation +## 22. Deinstallation ```bash sudo ./scripts/cmk-manage.sh down @@ -913,10 +1450,13 @@ 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 +# Images entfernen (Namen aus "cmk-manage.sh image list") +docker image rm docker.io/checkmk/check-mk-raw:2.3.0-latest docker.io/jc21/nginx-proxy-manager:2 # podman image rm … +# ggf. gespeicherte Registry-Anmeldung entfernen +podman logout registry.checkmk.com # bzw. docker logout + # Daten (ACHTUNG: löscht die komplette Überwachung!) sudo rm -rf /opt/checkmk-stack/data ``` diff --git a/containers/browser/Containerfile b/containers/browser/Containerfile new file mode 100644 index 0000000..44e6ffb --- /dev/null +++ b/containers/browser/Containerfile @@ -0,0 +1,37 @@ +# ===================================================================== +# Kleines Werkzeug-Image fuer die lokale Bedienung/Diagnose des Stacks. +# +# Enthaelt Textbrowser und Netzwerkwerkzeuge. ACHTUNG: Textbrowser +# koennen KEIN JavaScript - die Admin-Oberflaeche des NGINX Proxy +# Managers laesst sich damit NICHT bedienen. Dafuer sind die Engines +# "browsh" oder "carbonyl" vorgesehen (siehe cmk-manage.sh browser). +# +# Bauen: ./scripts/cmk-manage.sh browser --build +# Nutzen: ./scripts/cmk-manage.sh browser --engine links +# ./scripts/cmk-manage.sh browser --engine shell +# ===================================================================== +FROM docker.io/library/debian:stable-slim + +LABEL org.opencontainers.image.title="checkmk-stack browser tools" \ + org.opencontainers.image.description="Textbrowser und Netzwerkwerkzeuge fuer die lokale Bedienung" + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + links2 \ + w3m \ + lynx \ + curl \ + ca-certificates \ + openssl \ + jq \ + less \ + iproute2 \ + iputils-ping \ + procps \ + && rm -rf /var/lib/apt/lists/* + +ENV TERM=xterm-256color \ + LANG=C.UTF-8 + +WORKDIR /root +CMD ["/bin/bash"] diff --git a/docker-compose.yml b/docker-compose.yml index 0422741..29d9d94 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,11 @@ services: # 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} + # Vollqualifizierter Name! Wird aus der .env genommen und ggf. von + # "cmk-manage.sh image ..." dort eingetragen. Kurze Namen wuerde Podman + # ueber unqualified-search-registries aufloesen (auf RHEL zuerst + # registry.access.redhat.com) und das Image nicht finden. + image: ${CMK_IMAGE:-docker.io/checkmk/check-mk-raw:2.3.0-latest} container_name: ${CMK_CONTAINER_NAME:-checkmk} hostname: ${CMK_HOSTNAME:-checkmk} network_mode: host @@ -51,7 +55,7 @@ services: # Port 81 = Admin-Oberflaeche (unbedingt per Firewall einschraenken!) # ------------------------------------------------------------------- nginx-proxy-manager: - image: ${NPM_IMAGE:-jc21/nginx-proxy-manager:2} + image: ${NPM_IMAGE:-docker.io/jc21/nginx-proxy-manager:2} container_name: ${NPM_CONTAINER_NAME:-nginx-proxy-manager} network_mode: host restart: unless-stopped diff --git a/images/.gitkeep b/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/install.sh b/install.sh index 267ac54..7199c46 100755 --- a/install.sh +++ b/install.sh @@ -159,8 +159,10 @@ 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/data/nginx/custom" \ "$PROJECT_DIR/data/npm/custom-certs" \ - "$PROJECT_DIR/backups" + "$PROJECT_DIR/backups" \ + "$PROJECT_DIR/images" chmod +x "$PROJECT_DIR"/scripts/*.sh "$PROJECT_DIR/install.sh" 2>/dev/null || true if [ ! -f "$PROJECT_DIR/.env" ]; then @@ -238,8 +240,8 @@ install_quadlet_units() { local f for f in "$PROJECT_DIR"/systemd/quadlet/*.container; do sed -e "s|__PROJECT_DIR__|$PROJECT_DIR|g" \ - -e "s|__CMK_IMAGE__|${CMK_IMAGE_REPO:-checkmk/check-mk-raw}:${CMK_IMAGE_TAG:-2.3.0-latest}|g" \ - -e "s|__NPM_IMAGE__|${NPM_IMAGE:-jc21/nginx-proxy-manager:2}|g" \ + -e "s|__CMK_IMAGE__|${CMK_IMAGE:-docker.io/checkmk/check-mk-raw:2.3.0-latest}|g" \ + -e "s|__NPM_IMAGE__|${NPM_IMAGE:-docker.io/jc21/nginx-proxy-manager:2}|g" \ -e "s|__CMK_SITE_ID__|${CMK_SITE_ID:-cmk}|g" \ -e "s|__CMK_CONTAINER_NAME__|${CMK_CONTAINER_NAME:-checkmk}|g" \ -e "s|__NPM_CONTAINER_NAME__|${NPM_CONTAINER_NAME:-nginx-proxy-manager}|g" \ @@ -300,7 +302,12 @@ Naechste Schritte: $PROJECT_DIR/scripts/cmk-manage.sh up 3) NGINX Proxy Manager oeffnen: http://$(primary_ip):81 Erstlogin: admin@example.com / changeme - 4) Verwaltung / Backup / Migration - ncurses-Oberflaeche: + 4) Ist Port 81 im Netz gesperrt, laesst sich die Adminoberflaeche auf einen + anderen Port legen: + $PROJECT_DIR/scripts/cmk-manage.sh npm-port 10443 + Oder ganz ohne Netzzugriff per Terminal-Browser auf dieser Maschine: + $PROJECT_DIR/scripts/cmk-manage.sh browser + 5) Verwaltung / Backup / Migration - ncurses-Oberflaeche: $PROJECT_DIR/scripts/cmk-manage.sh gui Alles Weitere steht in der README.md. diff --git a/scripts/cmk-manage.sh b/scripts/cmk-manage.sh index e6b524e..89f62c3 100755 --- a/scripts/cmk-manage.sh +++ b/scripts/cmk-manage.sh @@ -89,6 +89,44 @@ KOMMANDOS - Backup / Restore / Migration prune --keep N nur die N neuesten Backups behalten +KOMMANDOS - Checkmk-Image beschaffen + image status konfiguriertes und lokal vorhandene Images anzeigen + image list lokal vorhandene Checkmk-Images + image pull [Opt.] Image aus einer Registry holen + --edition E raw|community|free|cloud|managed|pro|ultimate| + ultimatemt|enterprise (Vorgabe: CMK_EDITION) + --version V z.B. 2.3.0p23 (Vorgabe: CMK_VERSION) + --registry R abweichende Registry + --user U --password P Zugangsdaten (Enterprise: Kundenportal) + --image REF fertige Referenz statt Edition/Version + --no-set .env nicht veraendern + image download [Opt.] Image-Tarball vom Downloadportal holen und einspielen + --edition/--version/--user/--password wie oben + --url URL vollstaendige URL fest vorgeben + --dir VERZ Ablage (Vorgabe: $IMAGE_DIR) + --keep Tarball nach dem Einspielen behalten + image load --file F bereits heruntergeladenen Tarball einspielen + image use REF vorhandenes Image als CMK_IMAGE in die .env eintragen + +KOMMANDOS - Konfiguration + config list [--category KAT] alle Einstellungen mit aktuellen Werten + config get KEY einzelnen Wert ausgeben + config set KEY WERT Wert setzen (mit Pruefung) + config edit .env im Editor oeffnen + npm-port [PORT] Port der Proxy-Manager-Oberflaeche + (ohne PORT: aktuellen Stand anzeigen) + +KOMMANDOS - Lokale Bedienung ohne externen Zugriff + browser [Optionen] Terminal-Browser im Container starten, um den + NGINX Proxy Manager direkt auf der Maschine + einzurichten (Host-Netzwerk, erreicht 127.0.0.1:81) + -e, --engine E browsh (Vorgabe) | carbonyl | links | w3m | lynx | shell + browsh/carbonyl koennen JavaScript - die Textbrowser + NICHT, damit laesst sich der Proxy Manager nicht bedienen + -u, --url URL Startseite (Vorgabe: http://127.0.0.1:81) + --build Werkzeug-Image neu bauen + --pull Engine-Image neu laden + KOMMANDOS - Zertifikat cert [Optionen] selbstsigniertes Zertifikat fuer den Proxy erzeugen --domain FQDN Common Name / SAN @@ -99,6 +137,13 @@ 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 image pull --edition enterprise --version 2.3.0p23 \ + --user --password + ./scripts/cmk-manage.sh image download --edition enterprise --version 2.3.0p23 + ./scripts/cmk-manage.sh image load --file images/check-mk-enterprise-2.3.0p23.docker.tar.gz + ./scripts/cmk-manage.sh npm-port 10443 # Adminoberflaeche auf Port 10443 + ./scripts/cmk-manage.sh config set CMK_SITE_ID prod + ./scripts/cmk-manage.sh browser # Proxy Manager lokal bedienen ./scripts/cmk-manage.sh gui EOF } @@ -218,6 +263,406 @@ bundle_extract_payload() { fi } +# ===================================================================== +# Checkmk-Image beschaffen +# +# Hintergrund: +# * Die Enterprise-Edition liegt NICHT auf Docker Hub. Dort gibt es nur +# raw / community / cloud / managed / pro / ultimate / ultimatemt. +# Enterprise kommt aus registry.checkmk.com (Login) oder als Tarball +# vom Checkmk-Downloadportal (HTTP-Basic-Auth). +# * Ein heruntergeladener Image-Tarball wird mit "load" eingespielt, +# NICHT mit "import". "import" erwartet ein Dateisystem-Archiv und +# erzeugt ein Image ohne Metadaten (ohne Entrypoint, ohne Tag) - +# danach "kennt" die Runtime das Image scheinbar nicht. +# * Nach dem Laden traegt dieses Skript den TATSAECHLICHEN, voll- +# qualifizierten Namen in CMK_IMAGE der .env ein. +# ===================================================================== + +edition_repo() { # Edition -> vollqualifiziertes Repository + case "$1" in + raw) echo "docker.io/checkmk/check-mk-raw" ;; + community) echo "docker.io/checkmk/check-mk-community" ;; + free) echo "docker.io/checkmk/check-mk-free" ;; + cloud) echo "docker.io/checkmk/check-mk-cloud" ;; + managed) echo "docker.io/checkmk/check-mk-managed" ;; + pro) echo "docker.io/checkmk/check-mk-pro" ;; + ultimate) echo "docker.io/checkmk/check-mk-ultimate" ;; + ultimatemt) echo "docker.io/checkmk/check-mk-ultimatemt" ;; + enterprise) echo "${CMK_REGISTRY:-registry.checkmk.com}/enterprise/check-mk-enterprise" ;; + *) return 1 ;; + esac +} + +edition_known() { edition_repo "$1" >/dev/null 2>&1; } + +# Braucht diese Edition eine Anmeldung an der Registry? +edition_needs_login() { [ "$1" = enterprise ]; } + +edition_pkgname() { echo "check-mk-$1"; } + +# Lokal vorhandene Checkmk-Images +image_local_list() { + $RUNTIME images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null \ + | grep -Ei 'check-?mk' | grep -v '' | sort -u || true +} + +image_exists_local() { + $RUNTIME image inspect "$1" >/dev/null 2>&1 +} + +image_is_short_name() { + # Kurzer Name = ohne Registry-Anteil. Podman muesste ihn dann ueber + # unqualified-search-registries erraten. + case "$1" in + */*) : ;; + *) return 0 ;; # gar kein "/" -> immer kurz + esac + case "${1%%/*}" in + *.*|*:*|localhost) return 1 ;; # Registry mit Punkt, Port oder localhost + *) return 0 ;; + esac +} + +image_set_env() { + local ref="$1" + env_set CMK_IMAGE "$ref" + ok "CMK_IMAGE=$ref in der .env eingetragen." + if image_is_short_name "$ref"; then + warn "'$ref' ist ein KURZER Name ohne Registry. Podman wuerde ihn ueber + unqualified-search-registries aufloesen (auf RHEL zuerst + registry.access.redhat.com). Besser vollqualifiziert angeben." + fi + log "Danach den Stack neu erstellen: $SELF up" +} + +# Vor/Nachher-Vergleich, um den Namen eines geladenen Images sicher zu finden +image_snapshot() { $RUNTIME images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | sort -u || true; } + +# Tarball einspielen -> gibt den geladenen Image-Namen auf stdout aus +image_load_file() { + local file="$1" + [ -f "$file" ] || die "Datei nicht gefunden: $file" + + # Ist das ueberhaupt ein Container-Image-Archiv (docker-archive/OCI)? + if ! tar -tzf "$file" 2>/dev/null | grep -qE '^(manifest\.json|oci-layout|index\.json)$'; then + if tar -tf "$file" 2>/dev/null | grep -qE '^(manifest\.json|oci-layout|index\.json)$'; then + : + else + err "'$file' enthaelt kein manifest.json / oci-layout." + cat >&2 <<'EOF' + Das ist damit KEIN Container-Image-Archiv, sondern vermutlich ein + normales tar-Archiv. Nur "docker save"/"podman save"-Archive bzw. die + Image-Tarballs von Checkmk lassen sich mit "load" einspielen. + + Haeufige Verwechslung: + podman import -> erwartet ein Dateisystem-Archiv (rootfs), + erzeugt ein Image OHNE Entrypoint und ohne Tag + podman load -> spielt ein echtes Image inkl. Metadaten ein <-- richtig +EOF + return 1 + fi + fi + + local before after loaded + before="$(image_snapshot)" + log "Spiele Image ein: $RUNTIME load -i $file (das dauert einen Moment) ..." >&2 + local out + out="$($RUNTIME load -i "$file" 2>&1)" || { printf '%s\n' "$out" >&2; return 1; } + printf '%s\n' "$out" >&2 + + # 1. Versuch: Ausgabe auswerten + # docker: "Loaded image: repo:tag" podman: "Loaded image(s): repo:tag" + loaded="$(printf '%s\n' "$out" | sed -nE 's/^Loaded image(\(s\))?:[[:space:]]*//p' | head -n1 | tr -d '\r')" + + # 2. Versuch: Differenz der Image-Liste (bevorzugt ein Checkmk-Image) + if [ -z "$loaded" ] || [ "${loaded#sha256:}" != "$loaded" ]; then + after="$(image_snapshot)" + local diff_new + diff_new="$(comm -13 <(printf '%s\n' "$before") <(printf '%s\n' "$after") | grep -v '')" + local cand + cand="$(printf '%s\n' "$diff_new" | grep -Ei 'check-?mk' | head -n1)" + [ -n "$cand" ] || cand="$(printf '%s\n' "$diff_new" | sed '/^$/d' | head -n1)" + [ -n "$cand" ] && loaded="$cand" + fi + + if [ -z "$loaded" ]; then + err "Der Name des geladenen Images konnte nicht ermittelt werden." + err "Bitte manuell nachsehen: $RUNTIME images" + return 1 + fi + if [ "${loaded#sha256:}" != "$loaded" ]; then + err "Das Archiv enthaelt keinen Image-Namen (nur die ID $loaded)." + err "Manuell benennen: $RUNTIME tag $loaded localhost/checkmk/check-mk-enterprise:" + return 1 + fi + printf '%s\n' "$loaded" +} + +# Datei mit HTTP-Basic-Auth holen (Zugangsdaten nicht in der Prozessliste) +image_fetch_url() { + local url="$1" out="$2" user="$3" pass="$4" + local host; host="$(printf '%s' "$url" | sed -E 's|^[a-z]+://([^/]+).*|\1|')" + + if command -v curl >/dev/null 2>&1; then + local netrc="" + local -a auth=() + if [ -n "$user" ]; then + netrc="$(mktemp)"; chmod 600 "$netrc" + printf 'machine %s login %s password %s\n' "$host" "$user" "$pass" >"$netrc" + auth=(--netrc-file "$netrc") + fi + log "Lade $url" + if curl -fL --progress-bar ${auth[@]+"${auth[@]}"} -o "$out" "$url"; then + [ -n "$netrc" ] && rm -f "$netrc" + return 0 + fi + [ -n "$netrc" ] && rm -f "$netrc" + return 1 + elif command -v wget >/dev/null 2>&1; then + warn "curl fehlt - verwende wget (die Zugangsdaten sind dabei kurzzeitig + in der Prozessliste sichtbar)." + local -a wa=() + [ -n "$user" ] && wa=(--user="$user" --password="$pass") + wget ${wa[@]+"${wa[@]}"} -O "$out" "$url" + return $? + else + die "Weder curl noch wget vorhanden." + fi +} + +# --------------------------------------------------------------------- +# image pull - aus einer Registry holen +# --------------------------------------------------------------------- +image_cmd_pull() { + local edition="${CMK_EDITION:-raw}" version="${CMK_VERSION:-}" registry="${CMK_REGISTRY:-}" + local user="${CMK_REGISTRY_USER:-}" pass="${CMK_REGISTRY_PASSWORD:-}" ref="" setenv=1 + while [ $# -gt 0 ]; do + case "$1" in + --edition) edition="$2"; shift 2 ;; + --version) version="$2"; shift 2 ;; + --registry) registry="$2"; shift 2 ;; + --user) user="$2"; shift 2 ;; + --password) pass="$2"; shift 2 ;; + --image) ref="$2"; shift 2 ;; + --no-set) setenv=""; shift ;; + *) die "Unbekannte Option fuer 'image pull': $1" ;; + esac + done + + if [ -z "$ref" ]; then + edition_known "$edition" || die "Unbekannte Edition '$edition'. + Moeglich: raw community free cloud managed pro ultimate ultimatemt enterprise" + [ -n "$version" ] || die "Keine Version angegeben (--version oder CMK_VERSION)." + CMK_REGISTRY="${registry:-${CMK_REGISTRY:-registry.checkmk.com}}" + ref="$(edition_repo "$edition"):$version" + fi + + local reg="${ref%%/*}" + if edition_needs_login "$edition" || [ -n "$user" ]; then + if [ -z "$user" ]; then + err "Fuer '$edition' ist eine Anmeldung an $reg noetig." + cat >&2 < --password +EOF + return 1 + fi + log "Melde an $reg an ..." + printf '%s' "$pass" | $RUNTIME login --username "$user" --password-stdin "$reg" \ + || die "Anmeldung an $reg fehlgeschlagen." + ok "Anmeldung erfolgreich." + fi + + log "Hole Image: $ref" + if ! $RUNTIME pull "$ref"; then + err "Das Image '$ref' konnte nicht geholt werden." + if [ "$edition" = enterprise ]; then + cat >&2 <<'EOF' + + Hinweis: die Enterprise-Edition gibt es NICHT auf Docker Hub. + Zwei Wege stehen zur Verfuegung: + a) private Registry: + cmk-manage.sh image pull --edition enterprise --version 2.3.0p23 \ + --user --password + b) Tarball vom Downloadportal: + cmk-manage.sh image download --edition enterprise --version 2.3.0p23 \ + --user --password + Ab Checkmk 2.5 heisst diese Edition "pro" und liegt frei auf Docker Hub: + cmk-manage.sh image pull --edition pro --version 2.5.0p11 +EOF + fi + return 1 + fi + ok "Image vorhanden: $ref" + [ -n "$setenv" ] && image_set_env "$ref" + return 0 +} + +# --------------------------------------------------------------------- +# image download - Tarball vom Downloadportal holen und einspielen +# --------------------------------------------------------------------- +image_cmd_download() { + local edition="${CMK_EDITION:-raw}" version="${CMK_VERSION:-}" + local user="${CMK_DOWNLOAD_USER:-}" pass="${CMK_DOWNLOAD_PASSWORD:-}" + local url="${CMK_DOWNLOAD_URL:-}" dir="" keep="" setenv=1 + while [ $# -gt 0 ]; do + case "$1" in + --edition) edition="$2"; shift 2 ;; + --version) version="$2"; shift 2 ;; + --url) url="$2"; shift 2 ;; + --user) user="$2"; shift 2 ;; + --password) pass="$2"; shift 2 ;; + --dir) dir="$2"; shift 2 ;; + --keep) keep=1; shift ;; + --no-set) setenv=""; shift ;; + *) die "Unbekannte Option fuer 'image download': $1" ;; + esac + done + + dir="${dir:-${IMAGE_DIR:-$PROJECT_DIR/images}}" + case "$dir" in /*) : ;; *) dir="$PROJECT_DIR/${dir#./}" ;; esac + mkdir -p "$dir" + + local -a urls=() + if [ -n "$url" ]; then + urls=("$url") + else + [ -n "$version" ] || die "Keine Version angegeben (--version oder CMK_VERSION)." + case "$version" in + *-latest|latest) die "Fuer den Tarball-Download wird eine exakte Version benoetigt, + z.B. 2.3.0p23 - '$version' funktioniert dort nicht." ;; + esac + edition_known "$edition" || die "Unbekannte Edition '$edition'." + local base="${CMK_DOWNLOAD_BASEURL:-https://download.checkmk.com/checkmk}" + local pkg; pkg="$(edition_pkgname "$edition")" + # Checkmk hat den Dateinamen ueber die Jahre geaendert - beide probieren + urls=("$base/$version/$pkg-$version.docker.tar.gz" + "$base/$version/$pkg-docker-$version.tar.gz") + fi + + local target="" u + for u in "${urls[@]}"; do + target="$dir/$(basename "$u")" + if image_fetch_url "$u" "$target" "$user" "$pass"; then + ok "Heruntergeladen: $target ($(human_size "$(file_size "$target")"))" + break + fi + rm -f "$target"; target="" + warn "Nicht gefunden/abgelehnt: $u" + done + + if [ -z "$target" ]; then + err "Der Image-Tarball konnte nicht geladen werden." + cat >&2 </' \\ + --user --password + * oder die Datei manuell im Kundenportal herunterladen und einspielen: + $SELF image load --file /pfad/zur/datei.tar.gz +EOF + return 1 + fi + + local loaded + loaded="$(image_load_file "$target")" || return 1 + ok "Image eingespielt: $loaded" + [ -z "$keep" ] && { rm -f "$target"; log "Tarball entfernt (mit --keep behalten)."; } + [ -n "$setenv" ] && image_set_env "$loaded" + return 0 +} + +# --------------------------------------------------------------------- +# image load - bereits heruntergeladenen Tarball einspielen +# --------------------------------------------------------------------- +image_cmd_load() { + local file="" setenv=1 + while [ $# -gt 0 ]; do + case "$1" in + --file) file="$2"; shift 2 ;; + --no-set) setenv=""; shift ;; + *) [ -z "$file" ] && { file="$1"; shift; } || die "Unbekannte Option: $1" ;; + esac + done + [ -n "$file" ] || die "--file fehlt." + local loaded + loaded="$(image_load_file "$file")" || return 1 + ok "Image eingespielt: $loaded" + [ -n "$setenv" ] && image_set_env "$loaded" +} + +# --------------------------------------------------------------------- +# image use / list / status +# --------------------------------------------------------------------- +image_cmd_use() { + [ $# -ge 1 ] || die "Image-Referenz fehlt, z.B.: + $SELF image use localhost/checkmk/check-mk-enterprise:2.3.0p23" + local ref="$1" + image_exists_local "$ref" || warn "'$ref' ist lokal (noch) nicht vorhanden." + image_set_env "$ref" +} + +image_cmd_list() { + printf '%sLokal vorhandene Checkmk-Images%s\n' "$C_BLD" "$C_RST" + local l n=0 + while IFS= read -r l; do + [ -n "$l" ] || continue + if [ "$l" = "${CMK_IMAGE:-}" ]; then + printf ' %s %s<- laut .env in Verwendung%s\n' "$l" "$C_GRN" "$C_RST" + else + printf ' %s\n' "$l" + fi + n=$((n+1)) + done < <(image_local_list) + [ "$n" -eq 0 ] && echo " (keines)" + return 0 +} + +image_cmd_status() { + printf '\n%sKonfiguriertes Image (.env: CMK_IMAGE)%s\n' "$C_BLD" "$C_RST" + printf ' %s\n' "${CMK_IMAGE:-}" + if [ -n "${CMK_IMAGE:-}" ]; then + if image_exists_local "$CMK_IMAGE"; then + printf ' Status : %slokal vorhanden%s\n' "$C_GRN" "$C_RST" + else + printf ' Status : %snicht lokal vorhanden%s (wird beim Start geholt)\n' "$C_YEL" "$C_RST" + fi + if image_is_short_name "$CMK_IMAGE"; then + printf ' %sACHTUNG%s: kurzer Name ohne Registry - unter Podman fehleranfaellig.\n' "$C_RED" "$C_RST" + fi + fi + printf '\n%sVorgaben fuer den Downloader%s\n' "$C_BLD" "$C_RST" + printf ' Edition : %s -> %s\n' "${CMK_EDITION:-raw}" "$(edition_repo "${CMK_EDITION:-raw}" 2>/dev/null || echo '?')" + printf ' Version : %s\n' "${CMK_VERSION:-}" + printf ' Registry : %s (Benutzer: %s)\n' "${CMK_REGISTRY:-registry.checkmk.com}" "${CMK_REGISTRY_USER:-}" + printf ' Download : %s (Benutzer: %s)\n' "${CMK_DOWNLOAD_BASEURL:-https://download.checkmk.com/checkmk}" "${CMK_DOWNLOAD_USER:-}" + echo + image_cmd_list + echo +} + +cmd_image() { + local sub="${1:-status}"; [ $# -gt 0 ] && shift || true + case "$sub" in + status) image_cmd_status ;; + list|ls) image_cmd_list ;; + pull) image_cmd_pull "$@" ;; + download) image_cmd_download "$@" ;; + load) image_cmd_load "$@" ;; + use|set) image_cmd_use "$@" ;; + *) die "Unbekanntes Unterkommando 'image $sub'. + Moeglich: status | list | pull | download | load | use" ;; + esac +} + # ===================================================================== # Stack-Kommandos # ===================================================================== @@ -237,6 +682,10 @@ cmd_shell() { require_cmk_running; $RUNTIME exec -it "$CMK_CONTAINER_NAME" bas cmd_omd() { require_cmk_running; $RUNTIME exec -it "$CMK_CONTAINER_NAME" omd "$@"; } cmd_status() { + printf '\n%sImage%s\n' "$C_BLD" "$C_RST" + printf ' %-24s %s\n' "${CMK_IMAGE:-}" \ + "$(image_exists_local "${CMK_IMAGE:-}" && echo "lokal vorhanden" || echo "${C_YEL}nicht lokal vorhanden${C_RST}")" + printf '\n%sContainer%s\n' "$C_BLD" "$C_RST" local c for c in "$CMK_CONTAINER_NAME" "$NPM_CONTAINER_NAME"; do @@ -264,7 +713,7 @@ cmd_status() { 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" + printf ' NGINX Proxy Manager : http://%s:%s/ (Erstlogin: admin@example.com / changeme)\n\n' "$ip" "${NPM_ADMIN_PORT:-81}" } # ===================================================================== @@ -503,17 +952,27 @@ do_restore() { 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' ' ')" + local want_ver want_ed + want_ver="${B_CMK_VERSION%.*}" + case "$B_CMK_VERSION" in + *.cre) want_ed=raw ;; + *.cee) want_ed=enterprise ;; + *.cce) want_ed=cloud ;; + *.cme) want_ed=managed ;; + *) want_ed="${CMK_EDITION:-raw}" ;; + esac cat < --password EOF die "Abbruch wegen Versionskonflikt." fi @@ -524,8 +983,8 @@ EOF case "$B_CMK_VERSION" in *.cee|*.cme|*.cce) warn "Das Backup stammt aus einer $(edition_from_version "$B_CMK_VERSION"). - Dafuer wird das passende Enterprise-/Cloud-Image benoetigt - (CMK_IMAGE_REPO=checkmk/check-mk-enterprise)." ;; + Dafuer wird das passende Image benoetigt - die Raw-Edition genuegt nicht. + Aktuell konfiguriert: ${CMK_IMAGE:-}" ;; esac # --- vorhandene Site -------------------------------------------- @@ -655,6 +1114,509 @@ cmd_import() { fi } +# ===================================================================== +# Admin-Port des NGINX Proxy Managers +# +# Die Adminoberflaeche des Proxy Managers lauscht im Image fest auf Port 81 +# ("listen 81 default;" in /etc/nginx/conf.d/production.conf). Eine +# Umgebungsvariable dafuer gibt es in den veroeffentlichten 2.x-Versionen +# noch nicht. Da beide Container im Host-Netzwerk laufen, laesst sich der +# Port auch nicht per Port-Mapping verschieben. +# +# Loesung: nginx bindet in der Datei +# /data/nginx/custom/http.conf (= ./data/npm/data/nginx/custom/http.conf) +# einen offiziellen Erweiterungspunkt innerhalb des http{}-Blocks ein. +# Dort legen wir einen zusaetzlichen Server an, der auf dem gewuenschten +# Port lauscht und an die Oberflaeche auf 127.0.0.1:81 weiterreicht. +# +# Port 81 bleibt dabei lokal bestehen - er wird per Firewall dichtgehalten. +# ===================================================================== + +NPM_CUSTOM_CONF_REL="nginx/custom/http.conf" +NPM_BLOCK_START="# >>> cmk-manage.sh: Admin-Port - nicht von Hand aendern >>>" +NPM_BLOCK_END="# <<< cmk-manage.sh: Admin-Port <<<" + +npm_custom_conf_path() { + local base="${NPM_DATA_DIR:-$PROJECT_DIR/data/npm/data}" + case "$base" in /*) : ;; *) base="$PROJECT_DIR/${base#./}" ;; esac + printf '%s/%s\n' "$base" "$NPM_CUSTOM_CONF_REL" +} + +# Vorhandenen verwalteten Block entfernen, Fremdinhalte bleiben erhalten +npm_conf_strip_block() { + local f="$1" + [ -f "$f" ] || return 0 + awk -v s="$NPM_BLOCK_START" -v e="$NPM_BLOCK_END" ' + $0 == s { skip=1; next } + $0 == e { skip=0; next } + !skip { print } + ' "$f" > "$f.tmp" && mv "$f.tmp" "$f" +} + +npm_conf_write_block() { + local f="$1" port="$2" + mkdir -p "$(dirname "$f")" + touch "$f" + npm_conf_strip_block "$f" + { + printf '%s\n' "$NPM_BLOCK_START" + cat <> "$f" + # Leerzeilen am Anfang aufraeumen + sed -i '/./,$!d' "$f" +} + +port_in_use() { + command -v ss >/dev/null 2>&1 || return 1 + ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$1\$" +} + +valid_port() { + case "$1" in + ''|*[!0-9]*) return 1 ;; + esac + [ "$1" -ge 1 ] && [ "$1" -le 65535 ] +} + +cmd_npm_port() { + local port="" restart=1 + while [ $# -gt 0 ]; do + case "$1" in + --no-restart) restart=""; shift ;; + -h|--help) + cat <<'EOF' +cmk-manage.sh npm-port [PORT] [--no-restart] + + Legt fest, auf welchem Port die Adminoberflaeche des NGINX Proxy Managers + erreichbar ist. Ohne PORT wird der aktuelle Stand angezeigt. + + 81 = Vorgabe, kein Zusatzeintrag noetig + sonst = zusaetzlicher nginx-Server auf diesem Port (Port 81 bleibt lokal + bestehen und sollte per Firewall dichtgehalten werden) +EOF + return 0 ;; + *) port="$1"; shift ;; + esac + done + + local conf; conf="$(npm_custom_conf_path)" + + if [ -z "$port" ]; then + printf '\n%sAdminoberflaeche des NGINX Proxy Managers%s\n' "$C_BLD" "$C_RST" + printf ' Konfigurierter Port : %s\n' "${NPM_ADMIN_PORT:-81}" + printf ' Adresse : %s\n' "${NPM_ADMIN_URL:-http://127.0.0.1:81}" + printf ' Zusatzkonfiguration : %s\n' \ + "$(grep -qsF "$NPM_BLOCK_START" "$conf" && echo "$conf" || echo 'keine (Vorgabe 81)')" + if [ "${NPM_ADMIN_PORT:-81}" != 81 ]; then + printf ' Port 81 lokal : %s\n' "$(port_in_use 81 && echo 'lauscht (per Firewall sperren)' || echo 'lauscht nicht')" + fi + echo + return 0 + fi + + valid_port "$port" || die "'$port' ist keine gueltige Portnummer (1-65535)." + + case "$port" in + 80|443) die "Port $port wird vom Proxy Manager bereits fuer HTTP/HTTPS genutzt." ;; + 5000) die "Port 5000 gehoert der Checkmk-Oberflaeche." ;; + 8000) die "Port 8000 gehoert dem Checkmk-Agent-Receiver." ;; + 6557) die "Port 6557 gehoert Livestatus." ;; + esac + + if [ "$port" != 81 ] && port_in_use "$port"; then + warn "Auf Port $port lauscht bereits ein Dienst." + confirm "Trotzdem fortfahren?" || die "Abgebrochen." + fi + + if [ "$port" = 81 ]; then + npm_conf_strip_block "$conf" + [ -s "$conf" ] || rm -f "$conf" + ok "Zusatzkonfiguration entfernt - es gilt wieder die Vorgabe (Port 81)." + else + npm_conf_write_block "$conf" "$port" + ok "Zusatzkonfiguration geschrieben: $conf" + fi + + env_set NPM_ADMIN_PORT "$port" + env_set NPM_ADMIN_URL "http://127.0.0.1:$port" + NPM_ADMIN_PORT="$port"; NPM_ADMIN_URL="http://127.0.0.1:$port" + + if [ -n "$restart" ] && container_running "$NPM_CONTAINER_NAME"; then + log "Starte '$NPM_CONTAINER_NAME' neu, damit nginx die Konfiguration liest ..." + if $RUNTIME restart "$NPM_CONTAINER_NAME" >/dev/null; then + ok "Proxy Manager neu gestartet." + else + warn "Neustart fehlgeschlagen - bitte manuell: $SELF restart" + fi + else + log "Wirksam nach: $SELF up (bzw. Neustart des Proxy Managers)" + fi + + local ip; ip="$(primary_ip)" + cat < "kategorie|key|typ|label|auswahl|leer" + settings_defs | awk -F'|' -v k="$1" '$2==k {print; exit}' +} + +settings_value() { # KEY -> aktueller Wert + eval "printf '%s' \"\${$1-}\"" +} + +settings_display() { # KEY TYP -> Anzeigewert (Kennwoerter maskiert) + local v; v="$(settings_value "$1")" + if [ "$2" = password ] && [ -n "$v" ]; then + printf '********' + elif [ -z "$v" ]; then + printf '' + else + printf '%s' "$v" + fi +} + +settings_validate() { # TYP WERT LEER_ERLAUBT -> 0/1, Meldung auf stdout + local typ="$1" val="$2" opt="$3" + if [ -z "$val" ]; then + [ -n "$opt" ] && return 0 + echo "Der Wert darf nicht leer sein."; return 1 + fi + case "$typ" in + port) valid_port "$val" || { echo "Keine gueltige Portnummer (1-65535)."; return 1; } ;; + int) case "$val" in ''|*[!0-9]*) echo "Bitte eine Zahl angeben."; return 1 ;; esac ;; + url) case "$val" in http://*|https://*) : ;; *) echo "Bitte mit http:// oder https:// beginnen."; return 1 ;; esac ;; + esac + return 0 +} + +# Zusaetzliche, schluesselbezogene Pruefung (ueber den Typ hinaus) +settings_validate_key() { + local key="$1" val="$2" + case "$key" in + CMK_RUNTIME) + [ -z "$val" ] && return 0 + command -v "$val" >/dev/null 2>&1 \ + || { echo "'$val' ist auf diesem System nicht installiert."; return 1; } ;; + CMK_SITE_ID) + case "$val" in + *[!a-zA-Z0-9_]*) echo "Ein Site-Name darf nur Buchstaben, Ziffern und _ enthalten."; return 1 ;; + esac + [ "${#val}" -le 16 ] || { echo "Ein Site-Name darf hoechstens 16 Zeichen lang sein."; return 1; } ;; + CMK_IMAGE|NPM_IMAGE|BROWSH_IMAGE|CARBONYL_IMAGE) + if [ -n "$val" ] && image_is_short_name "$val"; then + echo "Bitte vollqualifiziert angeben (z.B. docker.io/...) - Podman +findet kurze Namen sonst nicht."; return 1 + fi ;; + esac + return 0 +} + +# Nacharbeit nach dem Setzen bestimmter Schluessel +settings_post_set() { + local key="$1" val="$2" + case "$key" in + NPM_ADMIN_PORT) + # Der nginx-Zusatzblock muss mitgezogen werden + cmd_npm_port "$val" ;; + CMK_IMAGE|CMK_SITE_ID|CMK_CONTAINER_NAME|NPM_IMAGE|NPM_CONTAINER_NAME|TZ|CMK_LIVESTATUS_TCP|NPM_DISABLE_IPV6|CMK_DATA_DIR|NPM_DATA_DIR|NPM_LE_DIR) + log "Wirksam nach: $SELF up (Container werden neu erstellt)" ;; + esac +} + +cmd_config() { + local sub="${1:-list}"; [ $# -gt 0 ] && shift || true + case "$sub" in + list|ls) + local cat_filter="" + [ "${1:-}" = "--category" ] && cat_filter="$2" + local line c k t l + while IFS='|' read -r c k t l _ _; do + [ -n "$k" ] || continue + [ -n "$cat_filter" ] && [ "$c" != "$cat_filter" ] && continue + printf '%-24s %-10s %s\n' "$k" "$c" "$(settings_display "$k" "$t")" + done < <(settings_defs) + ;; + get) + [ $# -ge 1 ] || die "Schluessel fehlt: $SELF config get KEY" + [ -n "$(settings_def_for "$1")" ] || die "Unbekannter Schluessel '$1'. Uebersicht: $SELF config list" + settings_value "$1"; echo + ;; + set) + [ $# -ge 2 ] || die "Aufruf: $SELF config set KEY WERT" + local key="$1" val="$2" def typ opt msg + def="$(settings_def_for "$key")" + [ -n "$def" ] || die "Unbekannter Schluessel '$key'. Uebersicht: $SELF config list" + typ="$(printf '%s' "$def" | cut -d'|' -f3)" + opt="$(printf '%s' "$def" | cut -d'|' -f6)" + [ "$val" = "auto" ] && [ "$key" = CMK_RUNTIME ] && val="" + if ! msg="$(settings_validate "$typ" "$val" "$opt")"; then + die "$msg" + fi + if ! msg="$(settings_validate_key "$key" "$val")"; then + die "$msg" + fi + if [ "$key" = NPM_ADMIN_PORT ]; then + cmd_npm_port "$val" + else + env_set "$key" "$val" "$opt" + ok "$key=$val" + settings_post_set "$key" "$val" + fi + ;; + edit) + local f="${ENV_FILE:-$PROJECT_DIR/.env}" + "${EDITOR:-nano}" "$f"; load_env "$f"; ok "Konfiguration neu eingelesen." ;; + *) die "Unbekanntes Unterkommando 'config $sub'. + Moeglich: list [--category KAT] | get KEY | set KEY WERT | edit" ;; + esac +} + +# ===================================================================== +# Terminal-Browser im Container +# +# Zweck: den NGINX Proxy Manager direkt auf der Maschine einrichten, +# wenn dessen Adminoberflaeche (Port 81) von aussen nicht erreichbar ist. +# +# WICHTIG: Die Adminoberflaeche des Proxy Managers ist eine JavaScript- +# Anwendung. Klassische Textbrowser (links2, w3m, lynx) koennen sie NICHT +# darstellen. Dafuer gibt es zwei Engines, die einen echten Browser im +# Terminal rendern: +# browsh - Firefox im Terminal (gepflegt) +# carbonyl - Chromium im Terminal (schoenere Darstellung, Stand 2023) +# Die Textbrowser bleiben fuer schnelle Pruefungen nuetzlich. +# +# Der Container laeuft im Host-Netzwerk und erreicht damit 127.0.0.1:81 +# genau wie der Proxy Manager selbst. +# ===================================================================== + +browser_tools_image() { + if [ -n "${BROWSER_TOOLS_IMAGE:-}" ]; then + printf '%s\n' "$BROWSER_TOOLS_IMAGE" + elif [ "$RUNTIME" = podman ]; then + # Podman legt selbst gebaute Images unter localhost/ ab + printf '%s\n' "localhost/checkmk-stack/browser-tools:1" + else + # Docker wuerde ein "localhost/"-Praefix als echte Registry deuten + printf '%s\n' "checkmk-stack/browser-tools:1" + fi +} + +browser_build_tools() { + local img; img="$(browser_tools_image)" + local ctx="$PROJECT_DIR/containers/browser" + [ -f "$ctx/Containerfile" ] || die "Containerfile fehlt: $ctx/Containerfile" + log "Baue Werkzeug-Image '$img' ..." + # -f explizit: Docker sucht sonst nach "Dockerfile", Podman nimmt beides + $RUNTIME build -f "$ctx/Containerfile" -t "$img" "$ctx" \ + || die "Bau des Werkzeug-Images fehlgeschlagen." + ok "Image gebaut: $img" +} + +browser_ensure_tools() { + local img; img="$(browser_tools_image)" + image_exists_local "$img" || browser_build_tools +} + +cmd_browser() { + local engine="${BROWSER_ENGINE:-browsh}" + local url="" build="" build_only="" pull="" mono="" + while [ $# -gt 0 ]; do + case "$1" in + -e|--engine) engine="$2"; shift 2 ;; + -u|--url) url="$2"; shift 2 ;; + --build) build=1; shift ;; + --build-only) build=1; build_only=1; shift ;; + --pull) pull=1; shift ;; + --monochrome) mono=1; shift ;; + -h|--help) + cat <<'EOF' +cmk-manage.sh browser - Terminal-Browser im Container + + -e, --engine ENGINE browsh Firefox im Terminal (Vorgabe, kann JavaScript) + carbonyl Chromium im Terminal (kann JavaScript) + links links2 (Textbrowser, KEIN JavaScript) + w3m w3m (Textbrowser, KEIN JavaScript) + lynx lynx (Textbrowser, KEIN JavaScript) + shell Bash mit curl/jq/openssl im Host-Netzwerk + -u, --url URL Startseite (Vorgabe: $NPM_ADMIN_URL, sonst + http://127.0.0.1:81 = Proxy-Manager-Oberflaeche) + --build Werkzeug-Image neu bauen und danach starten + --build-only Werkzeug-Image nur bauen (ohne Terminal, z.B. im Skript) + --pull Image der gewaehlten Engine neu laden + --monochrome nur bei browsh: einfarbige Darstellung + +Bedienung browsh: Strg+L Adresszeile, Strg+T neuer Tab, + Strg+W Tab schliessen, Strg+Q beenden +EOF + return 0 ;; + *) die "Unbekannte Option fuer browser: $1" ;; + esac + done + + url="${url:-${NPM_ADMIN_URL:-http://127.0.0.1:81}}" + + # Engine-Namen zuerst pruefen, damit ein Tippfehler auch ohne + # Terminal eine brauchbare Meldung ergibt + case "$engine" in + browsh|carbonyl|links|links2|w3m|lynx|shell) : ;; + *) die "Unbekannte Engine '$engine'. + Moeglich: browsh | carbonyl | links | w3m | lynx | shell" ;; + esac + + # Bauen braucht kein Terminal - deshalb vor der TTY-Pruefung + if [ -n "$build" ]; then + browser_build_tools + [ -n "$build_only" ] && return 0 + fi + + if [ ! -t 0 ] || [ ! -t 1 ]; then + die "Ein Terminal-Browser braucht ein echtes Terminal (TTY). + Bitte direkt auf der Konsole bzw. in einer SSH-Sitzung aufrufen, + nicht in einer Pipe oder aus einem Skript heraus." + fi + + local image + local -a args=() runopts=() + case "$engine" in + browsh) + image="${BROWSH_IMAGE:-docker.io/browsh/browsh:v1.8.3}" + args=(--startup-url "$url") + [ -n "$mono" ] && args+=(--monochrome) + ;; + carbonyl) + image="${CARBONYL_IMAGE:-docker.io/fathyb/carbonyl:latest}" + args=("$url") + ;; + links|links2) + browser_ensure_tools; image="$(browser_tools_image)"; args=(links2 "$url") ;; + w3m) + browser_ensure_tools; image="$(browser_tools_image)"; args=(w3m "$url") ;; + lynx) + browser_ensure_tools; image="$(browser_tools_image)"; args=(lynx "$url") ;; + shell) + browser_ensure_tools; image="$(browser_tools_image)"; args=(/bin/bash) ;; + esac + + if [ -n "$pull" ]; then + case "$engine" in + browsh|carbonyl) log "Lade $image ..."; $RUNTIME pull "$image" || die "Konnte '$image' nicht laden." ;; + *) browser_build_tools ;; + esac + fi + + case "$engine" in + links|w3m|lynx) + warn "Textbrowser koennen kein JavaScript - die Oberflaeche des + NGINX Proxy Managers laesst sich damit NICHT bedienen. + Dafuer: $SELF browser --engine browsh" ;; + esac + + log "Starte $engine im Host-Netzwerk -> $url" + [ "$engine" = browsh ] && log "Beenden mit Strg+Q, Adresszeile mit Strg+L." + + runopts=(--rm -it --network host + -e "TERM=${TERM:-xterm-256color}" + -e "COLORTERM=${COLORTERM:-truecolor}") + + $RUNTIME run "${runopts[@]}" "$image" "${args[@]}" +} + # ===================================================================== # Selbstsigniertes Zertifikat fuer den NGINX Proxy Manager # ===================================================================== @@ -912,6 +1874,226 @@ gui_sites_menu() { done } +gui_image_menu() { + while :; do + local sel + sel="$(ui_menu "Checkmk-Image" \ +"Aktuell (.env): ${CMK_IMAGE:-} +Edition/Version: ${CMK_EDITION:-raw} / ${CMK_VERSION:-?}" \ + status "Status und lokale Images anzeigen" \ + pull "Aus Registry holen (Docker Hub / registry.checkmk.com)" \ + download "Tarball vom Downloadportal holen (mit Zugangsdaten)" \ + load "Heruntergeladenen Tarball einspielen (load)" \ + use "Vorhandenes Image auswaehlen" \ + back "zurueck")" || return 0 + case "$sel" in + status) local f; f="$(mktemp)"; NO_COLOR=1 cmd_image status >"$f" 2>&1 || true + ui_text "Image-Status" "$f"; rm -f "$f" ;; + pull) gui_image_pull ;; + download) gui_image_download ;; + load) local lf; lf="$(ui_pick_file "${IMAGE_DIR:-$PROJECT_DIR/images}" '*.tar.gz')" || continue + [ -n "$lf" ] || continue + ui_run "Image einspielen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y image load --file '$lf'" + load_env "${ENV_FILE:-$PROJECT_DIR/.env}" ;; + use) gui_image_use ;; + back) return 0 ;; + esac + done +} + +gui_image_pick_edition() { + ui_menu "Edition" "Welche Checkmk-Edition?" \ + raw "Raw (frei, Docker Hub, bis 2.4)" \ + community "Community (frei, Docker Hub, ab 2.5)" \ + enterprise "Enterprise (NICHT auf Docker Hub - Login noetig)" \ + pro "Pro (ab 2.5, Nachfolger von Enterprise)" \ + cloud "Cloud (Docker Hub)" \ + managed "Managed (Docker Hub, bis 2.4)" \ + ultimate "Ultimate (ab 2.5)" \ + ultimatemt "Ultimate Multi-Tenancy (ab 2.5)" +} + +gui_image_pull() { + local ed ver user pass + ed="$(gui_image_pick_edition)" || return + ver="$(ui_input "Version" "Version bzw. Tag (z.B. 2.3.0p23 oder 2.3.0-latest):" "${CMK_VERSION:-}")" || return + [ -n "$ver" ] || return + local -a extra=() + if [ "$ed" = enterprise ] || [ -n "${CMK_REGISTRY_USER:-}" ]; then + user="$(ui_input "Registry-Anmeldung" "Benutzer (Checkmk-Kundenportal):" "${CMK_REGISTRY_USER:-}")" || return + pass="$(ui_password "Registry-Anmeldung" "Kennwort:")" || return + [ -n "$user" ] && extra+=(--user "$user" --password "$pass") + fi + ui_run "Image holen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y image pull --edition '$ed' --version '$ver' ${extra[*]+${extra[*]}}" + load_env "${ENV_FILE:-$PROJECT_DIR/.env}" +} + +gui_image_download() { + local ed ver user pass + ed="$(gui_image_pick_edition)" || return + ver="$(ui_input "Version" "EXAKTE Version (z.B. 2.3.0p23 - kein '-latest'):" "${CMK_VERSION:-}")" || return + [ -n "$ver" ] || return + user="$(ui_input "Downloadportal" "Benutzer:" "${CMK_DOWNLOAD_USER:-}")" || return + pass="$(ui_password "Downloadportal" "Kennwort:")" || return + ui_run "Image herunterladen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y image download --edition '$ed' --version '$ver' --user '$user' --password '$pass'" + load_env "${ENV_FILE:-$PROJECT_DIR/.env}" +} + +gui_image_use() { + local -a items=() l + while IFS= read -r l; do + [ -n "$l" ] || continue + items+=("$l" "$([ "$l" = "${CMK_IMAGE:-}" ] && echo '(aktuell)' || echo ' ')") + done < <(image_local_list) + [ ${#items[@]} -gt 0 ] || { ui_msg "Images" "Es ist kein Checkmk-Image lokal vorhanden.\n\nZuerst 'pull', 'download' oder 'load' verwenden."; return; } + local sel + sel="$(ui_menu "Image auswaehlen" "Lokal vorhandene Checkmk-Images:" "${items[@]}")" || return + ui_run "Image setzen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y image use '$sel'" + load_env "${ENV_FILE:-$PROJECT_DIR/.env}" +} + +# --------------------------------------------------------------------- +# Einstellungen im Menue +# --------------------------------------------------------------------- +settings_needs_restart() { + case "$1" in + CMK_IMAGE|CMK_SITE_ID|CMK_CONTAINER_NAME|CMK_HOSTNAME|CMK_PASSWORD|\ + CMK_LIVESTATUS_TCP|MAIL_RELAY_HOST|TZ|NPM_IMAGE|NPM_CONTAINER_NAME|\ + NPM_DISABLE_IPV6|CMK_DATA_DIR|NPM_DATA_DIR|NPM_LE_DIR) return 0 ;; + *) return 1 ;; + esac +} + +gui_settings_edit() { + local key="$1" def typ label choices opt cur new msg + def="$(settings_def_for "$key")" + [ -n "$def" ] || { ui_msg "Fehler" "Unbekannter Schluessel '$key'."; return; } + typ="$(printf '%s' "$def" | cut -d'|' -f3)" + label="$(printf '%s' "$def" | cut -d'|' -f4)" + choices="$(printf '%s' "$def"| cut -d'|' -f5)" + opt="$(printf '%s' "$def" | cut -d'|' -f6)" + cur="$(settings_value "$key")" + + while :; do + case "$typ" in + choice) + local -a ropts=() c + [ -z "$cur" ] && case ":$choices:" in *:auto:*) cur=auto ;; esac + local IFS_BAK="$IFS"; IFS=':' + for c in $choices; do + IFS="$IFS_BAK" + ropts+=("$c" " " "$([ "$c" = "$cur" ] && echo on || echo off)") + IFS=':' + done + IFS="$IFS_BAK" + new="$(ui_radiolist "$key" "$label\n\nAktuell: ${cur:-}" "${ropts[@]}")" || return + [ "$new" = auto ] && new="" + ;; + password) + new="$(ui_password "$key" "$label\n\n(leer lassen = unveraendert)")" || return + [ -z "$new" ] && return + ;; + *) + new="$(ui_input "$key" "$label\n\nTyp: $typ$([ -n "$opt" ] && echo ' (darf leer bleiben)')" "$cur")" || return + ;; + esac + + if msg="$(settings_validate "$typ" "$new" "$opt")" \ + && msg="$(settings_validate_key "$key" "$new")"; then + break + else + ui_msg "Ungueltige Eingabe" "$msg" + cur="$new" + fi + done + + if [ "$key" = NPM_ADMIN_PORT ]; then + ui_run "Admin-Port setzen" bash -c "cd '$PROJECT_DIR' && '$SELF' -y npm-port '$new'" + else + env_set "$key" "$new" "$opt" + local hint="" + settings_needs_restart "$key" && hint="\n\nWirksam nach 'Stack steuern -> Stack starten'\n(die Container werden dabei neu erstellt)." + ui_msg "Gespeichert" "$key = $([ "$typ" = password ] && echo '********' || echo "${new:-}")$hint" + fi + load_env "${ENV_FILE:-$PROJECT_DIR/.env}" +} + +gui_settings_category() { + local cat="$1" title="$2" + while :; do + local -a items=() c k t l ch o + while IFS='|' read -r c k t l ch o; do + [ "$c" = "$cat" ] || continue + items+=("$k" "$l = $(settings_display "$k" "$t")") + done < <(settings_defs) + items+=("back" "zurueck") + local sel + sel="$(ui_menu "$title" "Eintrag waehlen und mit ENTER aendern:" "${items[@]}")" || return 0 + [ "$sel" = back ] && return 0 + gui_settings_edit "$sel" + done +} + +gui_settings_menu() { + while :; do + local -a items=() c l + while IFS='|' read -r c l; do + [ -n "$c" ] && items+=("$c" "$l") + done < <(settings_categories) + items+=("show" "Alle Werte auflisten") + items+=("editor" "Datei .env im Editor oeffnen") + items+=("back" "zurueck") + local sel + sel="$(ui_menu "Einstellungen" \ +"Alle Werte der Datei .env - direkt hier aenderbar. +Datei: ${ENV_FILE:-$PROJECT_DIR/.env}" "${items[@]}")" || return 0 + case "$sel" in + back) return 0 ;; + show) local f; f="$(mktemp)" + { printf '%-24s %-10s %s\n' "SCHLUESSEL" "BEREICH" "WERT" + NO_COLOR=1 cmd_config list; } >"$f" 2>&1 + ui_text "Alle Einstellungen" "$f"; rm -f "$f" ;; + editor) local ef="${ENV_FILE:-$PROJECT_DIR/.env}" + clear; "${EDITOR:-nano}" "$ef" + load_env "$ef"; ui_msg "Konfiguration" "Die .env wurde neu eingelesen." ;; + *) local title + title="$(settings_categories | awk -F'|' -v c="$sel" '$1==c{print $2}')" + gui_settings_category "$sel" "${title:-$sel}" ;; + esac + done +} + +gui_browser_menu() { + while :; do + local sel + sel="$(ui_menu "Terminal-Browser" \ +"Bedienung des NGINX Proxy Managers direkt auf dieser Maschine. +Startseite: ${NPM_ADMIN_URL:-http://127.0.0.1:81} + +Nur browsh und carbonyl koennen JavaScript und damit die +Oberflaeche des Proxy Managers darstellen." \ + browsh "Firefox im Terminal (empfohlen)" \ + carbonyl "Chromium im Terminal" \ + links "links2 - Textbrowser, KEIN JavaScript" \ + w3m "w3m - Textbrowser, KEIN JavaScript" \ + lynx "lynx - Textbrowser, KEIN JavaScript" \ + shell "Shell mit curl/jq/openssl im Host-Netzwerk" \ + url "Andere Startseite eingeben" \ + build "Werkzeug-Image neu bauen" \ + back "zurueck")" || return 0 + case "$sel" in + url) local u + u="$(ui_input "Startseite" "URL:" "${NPM_ADMIN_URL:-http://127.0.0.1:81}")" || continue + [ -n "$u" ] && NPM_ADMIN_URL="$u" ;; + build) ui_run "Werkzeug-Image bauen" bash -c "cd '$PROJECT_DIR' && '$SELF' browser --build-only" || true ;; + back) return 0 ;; + *) clear + cmd_browser --engine "$sel" --url "${NPM_ADMIN_URL:-http://127.0.0.1:81}" || true + ;; + esac + done +} + gui_cert() { local domain alt days domain="$(ui_input "Zertifikat" "FQDN / Common Name:" "${CERT_DOMAIN:-checkmk.example.local}")" || return @@ -923,16 +2105,6 @@ gui_cert() { 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 @@ -948,8 +2120,10 @@ Backups : $BACKUP_DIR" \ import "Site von Direktinstallation importieren" \ manage "Backups verwalten (Liste/Details/Loeschen)" \ sites "Sites im Container verwalten" \ + image "Checkmk-Image beschaffen/auswaehlen" \ + browser "Terminal-Browser (Proxy Manager lokal bedienen)" \ cert "Selbstsigniertes Zertifikat erzeugen" \ - env "Konfiguration (.env) ansehen/bearbeiten" \ + config "Einstellungen (.env) - menuegefuehrt" \ shell "Root-Shell im Checkmk-Container" \ quit "Beenden")" || break case "$sel" in @@ -960,8 +2134,10 @@ Backups : $BACKUP_DIR" \ import) gui_import ;; manage) gui_backups_menu ;; sites) gui_sites_menu ;; + image) gui_image_menu ;; + browser) gui_browser_menu ;; cert) gui_cert ;; - env) gui_env ;; + config) gui_settings_menu ;; shell) clear; cmd_shell || true ;; quit) break ;; esac @@ -1011,6 +2187,10 @@ main() { restore) cmd_restore "$@" ;; import) cmd_import "$@" ;; cert) cmd_cert "$@" ;; + image) cmd_image "$@" ;; + browser) cmd_browser "$@" ;; + npm-port) cmd_npm_port "$@" ;; + config) cmd_config "$@" ;; help|-h|--help) usage ;; *) err "Unbekanntes Kommando: $cmd"; echo; usage; exit 1 ;; esac diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh index 0c7b021..f7979e5 100644 --- a/scripts/lib/common.sh +++ b/scripts/lib/common.sh @@ -36,21 +36,42 @@ load_env() { warn "Keine .env gefunden ($envfile) - es gelten die Standardwerte." ENV_FILE="" fi - # Defaults + # Rueckfallwerte, falls ein Schluessel in der .env fehlt. + # Die .env bleibt die massgebliche Konfiguration - hier steht nur, + # was gilt, wenn dort nichts eingetragen ist. 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#./}" ;; + CMK_IMAGE="${CMK_IMAGE:-docker.io/checkmk/check-mk-raw:2.3.0-latest}" + NPM_ADMIN_PORT="${NPM_ADMIN_PORT:-81}" + # Leer = automatisch aus dem Admin-Port ableiten + NPM_ADMIN_URL="${NPM_ADMIN_URL:-http://127.0.0.1:$NPM_ADMIN_PORT}" + BACKUP_DIR="$(abs_path "${BACKUP_DIR:-$PROJECT_DIR/backups}")" + IMAGE_DIR="$(abs_path "${IMAGE_DIR:-$PROJECT_DIR/images}")" +} + +# relativen Pfad (./foo) am Projektverzeichnis ausrichten +abs_path() { + case "$1" in + /*) printf '%s\n' "$1" ;; + *) printf '%s\n' "$PROJECT_DIR/${1#./}" ;; esac } # .env-Wert dauerhaft setzen +# env_set KEY WERT [allow_empty] env_set() { - local key="$1" val="$2" f="${ENV_FILE:-$PROJECT_DIR/.env}" + local key="$1" val="$2" allow_empty="${3:-}" f="${ENV_FILE:-$PROJECT_DIR/.env}" [ -f "$f" ] || die "Keine .env vorhanden - kann '$key' nicht setzen." + # mehrzeilige Werte wuerden die .env zerstoeren + case "$val" in + *$'\n'*) die "Ungueltiger Wert fuer '$key' (mehrzeilig)." ;; + esac + [ -n "$val" ] || [ -n "$allow_empty" ] || die "Leerer Wert fuer '$key' - abgebrochen." + # Werte mit Leerzeichen in Anfuehrungszeichen setzen + case "$val" in + *" "*) case "$val" in \"*\") : ;; *) val="\"$val\"" ;; esac ;; + esac if grep -qE "^[#[:space:]]*${key}=" "$f"; then sed -i -E "s|^[#[:space:]]*${key}=.*|${key}=${val}|" "$f" else @@ -63,10 +84,15 @@ env_set() { # 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." + if [ -n "${CMK_RUNTIME:-}" ] && command -v "$CMK_RUNTIME" >/dev/null 2>&1; then RUNTIME="$CMK_RUNTIME" + elif [ -n "${CMK_RUNTIME:-}" ]; then + # Falsch gesetzter Wert darf das Werkzeug nicht unbenutzbar machen + warn "CMK_RUNTIME='$CMK_RUNTIME' ist nicht installiert - suche selbst. + Korrigieren mit: cmk-manage.sh config set CMK_RUNTIME auto" + CMK_RUNTIME="" + detect_runtime + return 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 @@ -187,11 +213,13 @@ primary_ip() { UI_BACKTITLE="Checkmk Stack Manager - Container-Runtime: ${RUNTIME:-?}" ui_init() { + # Geometrie IMMER setzen - sonst laeuft das Skript unter "set -u" auf + # eine unbound variable, wenn UI_BIN von aussen vorgegeben wurde. + UI_H="${UI_H:-22}"; UI_W="${UI_W:-78}"; UI_MH="${UI_MH:-13}" [ -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:-}" } @@ -205,51 +233,61 @@ ui_require() { } ui_menu() { # title text tag item [tag item ...] -> gewaehlter tag auf stdout + ui_init 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 ... + ui_init 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() { + ui_init 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_init "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" \ --inputbox "$2" 10 "$UI_W" "${3:-}" 3>&1 1>&2 2>&3 } ui_password() { + ui_init "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" \ --passwordbox "$2" 10 "$UI_W" 3>&1 1>&2 2>&3 } ui_msg() { # title text + ui_init "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --msgbox "$2" 16 "$UI_W" } ui_yesno() { # title text -> exit 0 = ja + ui_init "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --yesno "$2" 14 "$UI_W" } ui_text() { # title file + ui_init "$UI_BIN" --backtitle "$UI_BACKTITLE" --title "$1" --textbox "$2" "$UI_H" "$UI_W" } ui_info() { # title text (ohne OK-Button) + ui_init "$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... + ui_init local title="$1"; shift local logf; logf="$(mktemp)" ui_info "$title" "Bitte warten ...\n\n$*" @@ -266,6 +304,7 @@ ui_run() { # title cmd... # Dateiauswahl im Midnight-Commander-Stil (funktioniert auch mit whiptail) # --------------------------------------------------------------------- ui_pick_file() { # startdir [glob] -> Dateipfad auf stdout + ui_init local dir="${1:-$PWD}" glob="${2:-*}" sel dir="$(cd "$dir" 2>/dev/null && pwd || echo "$PWD")" while :; do diff --git a/scripts/migrate_creator.sh b/scripts/migrate_creator.sh index 4e536a0..51302c4 100755 --- a/scripts/migrate_creator.sh +++ b/scripts/migrate_creator.sh @@ -206,9 +206,19 @@ 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 + 2) Passendes Image beschaffen (Version MUSS uebereinstimmen): + ./scripts/cmk-manage.sh image pull --edition $( + case "$version" in + *.cre) echo raw ;; + *.cee) echo enterprise ;; + *.cce) echo cloud ;; + *.cme) echo managed ;; + *) echo raw ;; + esac) --version ${version%.*} + Die Enterprise-Edition liegt NICHT auf Docker Hub - dort zusaetzlich + --user/--password (Checkmk-Kundenportal) angeben oder + "image download" verwenden. + Danach: ./scripts/cmk-manage.sh up 3) Import ausfuehren: ./scripts/cmk-manage.sh import --file backups/$(basename "$outfile") --set-default-site diff --git a/systemd/quadlet/checkmk.container b/systemd/quadlet/checkmk.container index 4f21410..24221c6 100644 --- a/systemd/quadlet/checkmk.container +++ b/systemd/quadlet/checkmk.container @@ -12,7 +12,7 @@ After=network-online.target [Container] ContainerName=__CMK_CONTAINER_NAME__ -Image=docker.io/__CMK_IMAGE__ +Image=__CMK_IMAGE__ Network=host EnvironmentFile=__PROJECT_DIR__/.env Environment=CMK_SITE_ID=__CMK_SITE_ID__ diff --git a/systemd/quadlet/nginx-proxy-manager.container b/systemd/quadlet/nginx-proxy-manager.container index 439c5f1..10a725d 100644 --- a/systemd/quadlet/nginx-proxy-manager.container +++ b/systemd/quadlet/nginx-proxy-manager.container @@ -9,7 +9,7 @@ After=network-online.target checkmk.service [Container] ContainerName=__NPM_CONTAINER_NAME__ -Image=docker.io/__NPM_IMAGE__ +Image=__NPM_IMAGE__ Network=host EnvironmentFile=__PROJECT_DIR__/.env Environment=DISABLE_IPV6=true