diff --git a/CHANGELOG.md b/CHANGELOG.md index a68fcf0..9842d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,13 +11,15 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). - Betrieb per Docker Compose (postgres, migrate, app, worker) mit einem Image für alles, non-root (#019) - LXC-Installer `deploy/install-lxc.sh`: legt auf dem Proxmox-Host einen Debian-13-Container an, installiert Node.js und PostgreSQL, richtet systemd-Dienste ein; mit Update-Pfad (#019) - Migrationen im Betrieb ohne Prisma-CLI, kompatibel zu Prisma (#019) -- Übersicht zeigt Nodes, CPUs und freien RAM je Server sowie die letzten Deployments (#018) +- Übersicht zeigt Nodes, CPUs und freien RAM je Server (#018) +- README mit Banner, Screenshots, Installation, ersten Schritten und Katalog-Anleitungen (#018) - Marken-Dateien nach Vorgabe unter `public/brand/`: Symbol (dunkel/hell), kleines Icon, Avatar, Logo (dunkel/hell), Banner 1280×640, Favicons, Apple-Touch- und Manifest-Icons; erzeugt mit `pnpm marke` (#017) - Web-App-Manifest; Favicon passt sich hellem und dunklem Browser an (#017) ### Geändert ### Behoben +- Die Übersicht zeigte statt der letzten Deployments nur einen Platzhalter (#018) ### Entfernt diff --git a/README.md b/README.md index 09df022..dfeea8c 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,80 @@ -# ProxDeploy +

ProxDeploy – VMs und LXC-Container auf Proxmox VE ausrollen, per Web oder CLI.

-**VMs und LXC-Container auf Proxmox VE ausrollen, per Web oder CLI.** +**ProxDeploy** ist eine selbst gehostete Webanwendung mit einer CLI (`proxdeploy`). Damit wählt man +einen von mehreren Proxmox-VE-Servern (8.x/9.x, Einzelhost oder Cluster) und rollt darauf vollständig +unattended eine VM oder einen LXC-Container mit Debian oder Ubuntu aus – mit Profilen wie `base`, +`docker` oder `zabbix-agent`. Distributionen und Profile sind YAML-Dateien im Katalog, kein Code. -ProxDeploy ist eine selbst gehostete Webanwendung mit einer CLI (`proxdeploy`). Damit wählt man einen von -mehreren Proxmox-VE-Servern (8.x/9.x, Einzelhost oder Cluster) und rollt darauf vollständig unattended -eine VM oder einen LXC-Container mit Debian oder Ubuntu aus, inklusive Profilen wie `base`, `docker` -oder `zabbix-agent`. Weitere Distributionen und Profile sind YAML-Dateien im Katalog, kein Code. - -> ProxDeploy ist ein unabhängiges Werkzeug und stammt nicht von Proxmox Server Solutions GmbH. +> ProxDeploy ist ein unabhängiges Werkzeug und stammt nicht von der Proxmox Server Solutions GmbH. > „Proxmox“ ist eine Marke der Proxmox Server Solutions GmbH. -## Stand +

Live-Ansicht eines Deployments: Phasen mit Status, Abschlussbox mit IP und SSH-Befehl

-In Entwicklung. Version und Fortschritt stehen im [CHANGELOG](CHANGELOG.md), der Plan in -[docs/architektur.md](docs/architektur.md), die Aufgaben in [docs/issues/](docs/issues/README.md). +## Was es kann -| Etappe | Inhalt | Stand | +- **Assistent in neun Schritten** – Server, Typ, Distribution, Name, Ressourcen, Netzwerk (DHCP/statisch, + VLAN), Zugang (SSH-Schlüssel), Profile mit Variablen – und am Ende ein **Dry-Run** mit allen geplanten + Proxmox-Aufrufen und dem passenden CLI-Befehl. +- **Live-Fortschritt** je Phase (Server-Sent Events), in Web und CLI mit denselben Texten; bei Fehlern + Ursache und Lösungshinweis, danach auf Wunsch **Aufräumen** des halb angelegten Gasts. +- **Templates** aus offiziellen Cloud-Images (Prüfsumme geprüft, UEFI, Guest-Agent eingebaut), auf + Wunsch mit eingebackenen Profilen – ein Docker-Host ist dann in Sekunden statt Minuten fertig. +- **Ohne SSH zum Node** per API und Guest-Agent; mit SSH zusätzlich LXC-Provisionierung und + cloud-init-Snippets. Was ein Server kann, zeigt der Verbindungstest. +- **Verlauf** mit Filtern und Wiederholen, **Vorlagen** („Docker-Host klein“), hinterlegte + **SSH-Schlüssel**, **Rollen** Admin/Operator, **Audit-Log**, Anmeldung lokal oder per **OIDC**. +- **CLI** als einzelnes Binary für Linux, macOS und Windows – mit Batch-Dateien für viele Gäste. + +| Übersicht | Assistent mit Dry-Run | Am Handy | |---|---|---| -| 1 · v0.1.0 | Server-Verwaltung, Fingerprint-Pinning, Verbindungstest mit Fähigkeitserkennung | ✓ fertig, auf PVE 9.2 geprüft | -| 2 · v0.2.0 | Katalog, Template-Bau, Debian-13-VM, Live-Fortschritt, Assistent | ✓ fertig, auf PVE 9.2 geprüft | -| 3 · v0.3.0 | LXC, Ubuntu, Profile, Vorlagen, Profile im Template, SSH-Schlüssel | ✓ fertig, auf PVE 9.2 geprüft (Bau-Adresse #021: Abnahme offen) | -| 4 · v0.4.0 | CLI `proxdeploy` (Go), API-Tokens | ✓ fertig, auf PVE 9.2 geprüft | -| 5 · v0.5.0 | Verlauf, Aufräumen, Audit-Log, Benutzer, OIDC | ✓ fertig (Aufräumen auf PVE 9.2 geprüft, OIDC gegen Mock-IdP) | -| 6 · v0.6.0 | Branding, README mit Screenshots, Installer | ◐ als Nächstes | +| ![Übersicht mit Servern und letzten Deployments](docs/bilder/uebersicht.png) | ![Zusammenfassung im Assistenten mit geplanten Proxmox-Aufrufen](docs/bilder/assistent-zusammenfassung.png) | ![Live-Ansicht bei 390 px](docs/bilder/handy-live-ansicht.png) | + +Weitere Ansichten: [Verlauf](docs/bilder/verlauf.png) · [Server mit Verbindungstest](docs/bilder/server.png) · [Katalog](docs/bilder/katalog.png). +Erzeugt mit `scripts/screenshots.ts` aus dem gebauten Stand. + +## Installation + +ProxDeploy braucht PostgreSQL und läuft als zwei Prozesse (app und worker). Es gehört hinter einen +Reverse-Proxy mit TLS; dann `TRUST_PROXY=true` und `APP_URL` auf die öffentliche Adresse setzen. + +### Docker Compose + +```bash +git clone https://git.wilde-server.de/florian.wilde/proxdeploy.git && cd proxdeploy +cp .env.example .env +# POSTGRES_PASSWORT=$(openssl rand -hex 24) +# PROXDEPLOY_SCHLUESSEL=$(openssl rand -base64 32) ← sichern! ohne ihn sind Tokens und Schlüssel verloren +# SITZUNG_GEHEIMNIS=$(openssl rand -base64 48) +# APP_URL=https://proxdeploy.example +# ADMIN_PASSWORT=… ← erster Admin, nur beim allerersten Start +docker compose up -d --build +``` + +Compose startet `postgres`, spielt mit `migrate` die Datenbank ein und startet dann `app` (Port 3000 auf +127.0.0.1) und `worker`. Aktualisieren: `git pull && docker compose up -d --build`. + +### In einem Debian-LXC auf dem Proxmox-Host + +```bash +scripts/paket-bauen.sh 0.6.0 --tar # im Repository: proxdeploy-0.6.0.tar.gz +scp proxdeploy-0.6.0.tar.gz deploy/install-lxc.sh deploy/im-container.sh root@pve01:/root/ +ssh root@pve01 ./install-lxc.sh --paket proxdeploy-0.6.0.tar.gz --url https://proxdeploy.example +``` + +Der Installer legt einen unprivilegierten Debian-13-Container an (DHCP auf `vmbr0`; `--ip`, `--gw`, +`--vlan`, `--storage`, `--ctid` u. a. siehe `--help`), installiert Node.js 24 (Prüfsumme geprüft) und +PostgreSQL, richtet systemd-Dienste ein und zeigt am Ende Adresse und das Passwort des ersten Admins. +Aktualisieren mit `./install-lxc.sh --aktualisieren --paket proxdeploy-0.6.1.tar.gz` – die +letzten drei Fassungen bleiben unter `/opt/proxdeploy/versionen/`. + +## Erste Schritte + +1. **Token in Proxmox:** eigenen Benutzer und die Rolle `ProxDeploy` anlegen, Token ohne + Privilege Separation – die Befehle für 8.x und 9.x stehen in [docs/proxmox-rechte.md](docs/proxmox-rechte.md). +2. **Server anlegen:** URL, Token, Zertifikat bestätigen (Fingerprint-Pinning für selbst signierte + Zertifikate), „Verbindung testen“. Optional SSH-Zugang zum Node hinterlegen. +3. **SSH-Schlüssel** unter Einstellungen hinterlegen – sie kommen automatisch in jeden Gast. +4. **Ausrollen:** fehlt das Template, baut ProxDeploy es vorher (oder unter „Templates“). ## Was ProxDeploy auf einem Server kann @@ -129,6 +181,28 @@ OIDC_GRUPPE_OPERATOR=proxdeploy-operators - Gesperrte Konten bleiben gesperrt, auch per SSO. Getestet gegen einen Mock-IdP (`tests/oidc.test.ts`); ein echter IdP folgt beim Feldtest. +## Katalog erweitern + +- **Neue Distribution:** eine YAML-Datei unter `catalog/distros/` – Image-URL, Prüfsummen-Quelle, + Standardbenutzer, LXC-Template-Muster. Für eine neue Familie (z. B. `rhel` mit dnf) kommt ein + Adapter unter `src/engine/adapter/` dazu. → [docs/katalog.md](docs/katalog.md#neue-distribution-hinzufügen) +- **Eigenes Profil:** in der Web-GUI unter Katalog → „Profil anlegen“ (YAML mit Live-Prüfung und + Skriptvorschau) oder als Datei unter `catalog/profiles/`. → [docs/katalog.md](docs/katalog.md#eigenes-profil-anlegen) + +## Stand + +Version und Änderungen stehen im [CHANGELOG](CHANGELOG.md), der Plan in +[docs/architektur.md](docs/architektur.md), die Aufgaben in [docs/issues/](docs/issues/README.md). + +| Etappe | Inhalt | Stand | +|---|---|---| +| 1 · v0.1.0 | Server-Verwaltung, Fingerprint-Pinning, Verbindungstest mit Fähigkeitserkennung | ✓ fertig, auf PVE 9.2 geprüft | +| 2 · v0.2.0 | Katalog, Template-Bau, Debian-13-VM, Live-Fortschritt, Assistent | ✓ fertig, auf PVE 9.2 geprüft | +| 3 · v0.3.0 | LXC, Ubuntu, Profile, Vorlagen, Profile im Template, SSH-Schlüssel | ✓ fertig, auf PVE 9.2 geprüft (Bau-Adresse #021: Abnahme offen) | +| 4 · v0.4.0 | CLI `proxdeploy` (Go), API-Tokens | ✓ fertig, auf PVE 9.2 geprüft | +| 5 · v0.5.0 | Verlauf, Aufräumen, Audit-Log, Benutzer, OIDC | ✓ fertig (Aufräumen auf PVE 9.2 geprüft, OIDC gegen Mock-IdP) | +| 6 · v0.6.0 | Branding, README mit Screenshots, Installer | ◐ als Nächstes | + ## Entwicklung Voraussetzungen: Node 24 (z. B. über nvm), pnpm 12 (corepack), Docker für PostgreSQL. @@ -154,12 +228,6 @@ Go-Client nutzt die aus `cli/api/openapi.json` erzeugten Typen. Konventionen (deutsche Bezeichner, Schichtung, Versionen) stehen in [CLAUDE.md](CLAUDE.md). -## Betrieb - -Docker Compose (app, worker, postgres) und ein Installer für einen Debian-LXC auf Proxmox folgen mit -Etappe 6. ProxDeploy ist für den Betrieb hinter einem Reverse-Proxy mit TLS gedacht -(`TRUST_PROXY=true`, `APP_URL` auf die öffentliche Adresse). - ## Lizenz Noch nicht festgelegt. diff --git a/docs/bilder/assistent-zusammenfassung.png b/docs/bilder/assistent-zusammenfassung.png new file mode 100644 index 0000000..cb5f5f2 Binary files /dev/null and b/docs/bilder/assistent-zusammenfassung.png differ diff --git a/docs/bilder/handy-live-ansicht.png b/docs/bilder/handy-live-ansicht.png new file mode 100644 index 0000000..d69ff32 Binary files /dev/null and b/docs/bilder/handy-live-ansicht.png differ diff --git a/docs/bilder/katalog.png b/docs/bilder/katalog.png new file mode 100644 index 0000000..ef1db94 Binary files /dev/null and b/docs/bilder/katalog.png differ diff --git a/docs/bilder/live-ansicht.png b/docs/bilder/live-ansicht.png new file mode 100644 index 0000000..0f5f206 Binary files /dev/null and b/docs/bilder/live-ansicht.png differ diff --git a/docs/bilder/server.png b/docs/bilder/server.png new file mode 100644 index 0000000..df37bb2 Binary files /dev/null and b/docs/bilder/server.png differ diff --git a/docs/bilder/uebersicht.png b/docs/bilder/uebersicht.png new file mode 100644 index 0000000..fbadc2c Binary files /dev/null and b/docs/bilder/uebersicht.png differ diff --git a/docs/bilder/verlauf.png b/docs/bilder/verlauf.png new file mode 100644 index 0000000..b2cd052 Binary files /dev/null and b/docs/bilder/verlauf.png differ diff --git a/docs/issues/018-readme.md b/docs/issues/018-readme.md index 54cc162..c15ec5c 100644 --- a/docs/issues/018-readme.md +++ b/docs/issues/018-readme.md @@ -1,5 +1,5 @@ # #018 README und Dokumentation -**Status:** offen +**Status:** erledigt **Meilenstein:** v0.6.0 **Labels:** doku **Geschätzt:** 4 h **Tatsächlich:** – @@ -8,14 +8,16 @@ README mit Banner, Screenshots, Installation, CLI, Proxmox-Rechten, neuer Distribution, eigenem Profil. ## Aufgaben -- [ ] Screenshots aus dem gebauten Stand -- [ ] Hinweis: unabhängiges Tool, nicht von Proxmox Server Solutions +- [x] Screenshots aus dem gebauten Stand (`scripts/screenshots.ts`, dunkel, 1440 px und 390 px, echte Daten von pve-demo) +- [x] Hinweis: unabhängiges Tool, nicht von Proxmox Server Solutions +- [x] Banner oben, Funktionen, Installation (Compose, LXC), Erste Schritte, CLI, OIDC, Katalog erweitern ## Akzeptanzkriterien -- [ ] Anleitungen einmal Schritt für Schritt nachvollzogen +- [x] Anleitungen einmal Schritt für Schritt nachvollzogen: Compose (eigene Env-Datei, frische DB), LXC-Installer auf pve-demo, „Erste Schritte“ in der frisch installierten Instanz (Server anlegen, testen, Token, CLI, Dry-Run, echter LXC). „Neue Distribution“ ist der Weg, auf dem Ubuntu 26.04 dazukam (#010); „Eigenes Profil“ prüft der Playwright-Ablauf `profil-editor`. ## Abhängigkeiten #013 ## Notizen / Entscheidungen -– +- Beim Erstellen der Screenshots fiel auf: die Übersicht zeigte seit Etappe 1 nur einen Platzhalter + („Noch keine Deployments“) – jetzt Server mit Nodes, CPUs und freiem RAM sowie die letzten Aufträge. diff --git a/docs/issues/README.md b/docs/issues/README.md index 674a35f..8ac4cdb 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -21,7 +21,7 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a | [#015](015-rollen-audit.md) | Rollen, Benutzer, Tokens, Audit-Log | erledigt | v0.5.0 | 6 h | 0 h 10 min | | [#016](016-oidc.md) | Anmeldung per OIDC | erledigt | v0.5.0 | 4 h | 0 h 08 min | | [#017](017-branding.md) | Branding-Assets | erledigt | v0.6.0 | 4 h | – | -| [#018](018-readme.md) | README und Dokumentation | offen | v0.6.0 | 4 h | – | +| [#018](018-readme.md) | README und Dokumentation | erledigt | v0.6.0 | 4 h | – | | [#019](019-installer.md) | Docker-Image und LXC-Installer | erledigt | v0.6.0 | 6 h | – | | [#020](020-feldtest.md) | Feldtest 8.x und 9.x, Version 1.0.0 | offen | v1.0.0 | 6 h | – | | [#021](021-template-bau-ohne-ssh.md) | Template-Bau ohne SSH (Bau-Adresse) | umgesetzt, Abnahme offen | v0.3.0 | 3 h | 0 h 19 min | diff --git a/scripts/screenshots.ts b/scripts/screenshots.ts new file mode 100644 index 0000000..5156093 --- /dev/null +++ b/scripts/screenshots.ts @@ -0,0 +1,91 @@ +/** + * Screenshots für das README aus dem gebauten Stand (`scripts/neu-starten.sh`, Port 3310), dunkles + * Theme, 1440 px und 390 px. Ergebnis: docs/bilder/*.png + * + * pnpm exec tsx scripts/screenshots.ts [auftrag-kurzid] + */ +import { mkdirSync } from "node:fs"; +import { chromium, type Page } from "@playwright/test"; + +const BASIS = process.env["SCREENSHOT_URL"] ?? "http://127.0.0.1:3310"; +const BENUTZER = process.env["E2E_BENUTZER"] ?? "admin"; +const PASSWORT = process.env["E2E_PASSWORT"] ?? "proxdeploy-dev-admin"; +const ZIEL = "docs/bilder"; + +async function anmelden(seite: Page) { + await seite.goto(`${BASIS}/anmelden`); + await seite.getByLabel("Benutzername").fill(BENUTZER); + await seite.getByLabel("Passwort").fill(PASSWORT); + await seite.getByRole("button", { name: "Anmelden" }).click(); + await seite.waitForURL((url) => !url.pathname.startsWith("/anmelden")); +} + +async function bild(seite: Page, name: string, ganz = false) { + await seite.waitForLoadState("networkidle"); + // Blinkender Cursor und Spinner sollen nicht zufällig halb im Bild stehen. + await seite.addStyleTag({ content: "*{animation:none!important;transition:none!important}" }); + await seite.screenshot({ path: `${ZIEL}/${name}.png`, fullPage: ganz }); + console.log(`${ZIEL}/${name}.png`); +} + +async function main() { + const auftrag = process.argv[2]; + if (!auftrag) throw new Error("Kurz-ID eines erfolgreichen Auftrags angeben (für die Live-Ansicht)."); + mkdirSync(ZIEL, { recursive: true }); + const browser = await chromium.launch(); + try { + const kontext = await browser.newContext({ + viewport: { width: 1440, height: 900 }, + deviceScaleFactor: 1, + }); + await kontext.addCookies([{ name: "pd_theme", value: "dunkel", url: BASIS }]); + const seite = await kontext.newPage(); + await anmelden(seite); + + await seite.goto(`${BASIS}/`); + await bild(seite, "uebersicht"); + + await seite.goto(`${BASIS}/auftraege/${auftrag}`); + await bild(seite, "live-ansicht"); + + await seite.goto(`${BASIS}/auftraege`); + await bild(seite, "verlauf"); + + await seite.goto(`${BASIS}/server/demo`); + await bild(seite, "server", true); + + await seite.goto(`${BASIS}/katalog`); + await bild(seite, "katalog"); + + // Assistent bis zur Zusammenfassung – mit echtem Dry-Run gegen den Server. + await seite.goto(`${BASIS}/ausrollen`); + await seite.waitForLoadState("networkidle"); + const weiter = async (bis: number) => { + await seite.getByRole("button", { name: "Weiter" }).click(); + await seite.getByText(`Schritt ${bis} von 9`).waitFor(); + }; + for (let s = 2; s <= 4; s++) await weiter(s); + await seite.getByLabel("Name (Hostname)").fill("web01"); + for (let s = 5; s <= 7; s++) await weiter(s); + // Ohne hinterlegten Schlüssel einen angeben (öffentlicher Beispielschlüssel). + const feld = seite.locator("textarea#schluessel"); + if ((await feld.inputValue()) === "") + await feld.fill( + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEFTdkCuqQ6mF7I8QzmkNaQpKj7S/I2+yuxWeYasJjLd florian@laptop", + ); + for (let s = 8; s <= 9; s++) await weiter(s); + await seite.getByText("Geplante Proxmox-Aufrufe").waitFor({ timeout: 60_000 }); + await bild(seite, "assistent-zusammenfassung"); + + const handy = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2 }); + await handy.addCookies([{ name: "pd_theme", value: "dunkel", url: BASIS }]); + const klein = await handy.newPage(); + await anmelden(klein); + await klein.goto(`${BASIS}/auftraege/${auftrag}`); + await bild(klein, "handy-live-ansicht"); + } finally { + await browser.close(); + } +} + +await main(); diff --git a/src/app/(app)/page.tsx b/src/app/(app)/page.tsx index 42fdbfd..9f8a3a6 100644 --- a/src/app/(app)/page.tsx +++ b/src/app/(app)/page.tsx @@ -1,16 +1,21 @@ import type { Metadata } from "next"; +import Link from "next/link"; import { Karte } from "@/components/karte"; import { Status } from "@/components/status"; -import Link from "next/link"; +import { ART_TEXT, STATUS_ANZEIGE } from "@/lib/auftrag-text"; +import { bytes, zeitpunkt } from "@/lib/formate"; import { seiteSchuetzen } from "@/server/auth/zugang"; +import { auftraegeSuchen } from "@/server/services/auftraege"; import { serverListe } from "@/server/services/server"; import { ServerStatus } from "./server/server-status"; export const metadata: Metadata = { title: "Übersicht" }; +/** Server mit Status und freien Ressourcen (aus dem letzten Verbindungstest), letzte Aufträge. */ export default async function Uebersicht() { const benutzer = await seiteSchuetzen("lesen", "/"); - const server = await serverListe(); + const [server, { eintraege }] = await Promise.all([serverListe(), auftraegeSuchen({ seite: 1 })]); + const letzte = eintraege.slice(0, 8); return (
@@ -32,23 +37,73 @@ export default async function Uebersicht() { Noch kein Proxmox-Server angelegt. ) : ( )} - - Noch keine Deployments. + + Verlauf + + } + > + {letzte.length === 0 ? ( + Noch keine Deployments. + ) : ( +
    + {letzte.map((a) => { + const [art, text] = STATUS_ANZEIGE[a.status]; + return ( +
  • + + + {a.name ?? ART_TEXT[a.art]} + + {" "} + {a.art !== "AUSROLLEN" && a.name ? `· ${ART_TEXT[a.art]} ` : ""}· {a.server.name} + {a.vmid ? ` · ${a.vmid}` : ""} · {zeitpunkt(a.erstelltAm)} + + + + {text} + + +
  • + ); + })} +
+ )}