feat: add Proxmox provisioning service with CI and deployment tooling
CI / javascript-check (push) Successful in 51s
CI / container-policy (push) Successful in 2s
CI / container-verify (push) Canceled after 0s
CI / container-publish (push) Canceled after 0s
CI / python-tests (push) Canceled after 6m59s

This commit is contained in:
BartelLuis
2026-09-14 20:09:12 +02:00
commit 06c3474636
52 changed files with 9779 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
# API-Ablauf im Labor
Die API verwendet Sessioncookies und für alle Verwaltungsänderungen
`X-CSRF-Token`. Die Beispiele benötigen Bash, curl und jq. Für PowerShell steht
die gleiche API über `Invoke-RestMethod` zur Verfügung. Das vollständige Schema
ist nach Anmeldung unter `/openapi.json` und als generierter Repositorystand in
[openapi.json](openapi.json) verfügbar.
## Anmelden und Daten lesen
```bash
export AIS_URL=https://provision.example.net
umask 077
read -rsp 'Administrator-Passwort: ' AIS_PASSWORD
printf '\n'
printf '%s' "$AIS_PASSWORD" | curl --fail --silent --show-error \
--cookie-jar session.cookies --data-urlencode username=admin \
--data-urlencode password@- "$AIS_URL/auth/login" --output /dev/null
unset AIS_PASSWORD
AIS_CSRF=$(curl --fail --silent --show-error --cookie session.cookies \
"$AIS_URL/api/v1/me" | jq -r .csrf_token)
curl --fail --silent --show-error --cookie session.cookies "$AIS_URL/api/v1/hosts"
```
Cookies wie Zugangsdaten behandeln und nach Ende der Sitzung löschen.
Der Server liefert bei nicht angemeldeten API-Aufrufen HTTP 401 und bei
unzulässigen Rollen oder fehlendem CSRF-Token HTTP 403.
## Profilversion anlegen
Zuerst einen Root-Passwort-Hash als Geheimnis in der Weboberfläche speichern.
Unter Linux kann ein SHA-512-crypt-Hash etwa mit `openssl passwd -6` interaktiv
erzeugt werden. Den Hash nicht als Klartextfeld in ein Profil aufnehmen.
[sample-profile.json](sample-profile.json) kopieren und alle Platzhalter anpassen:
Secret-ID, konkret geprüfter Build, Management-Interface, Datenträgerseriennummer
und Inventarisierungsnachweis. `9.1-1` dient lediglich als Formatbeispiel und ist
keine Kompatibilitätsfreigabe. FQDN und Management-IP kommen aus dem Hostinventar.
```bash
curl --fail --silent --show-error --cookie session.cookies \
--header "X-CSRF-Token: $AIS_CSRF" \
--json @my-installation-profile.json "$AIS_URL/api/v1/profiles"
```
Die Antwort enthält `id`, `version`, `digest` und `status=draft`. Ein weiterer
POST mit demselben Namen und Typ erstellt eine neue unveränderliche Version.
Bereits vorbereitete Läufe behalten die zuvor aufgelösten Werte und Versionen.
Veröffentlichung erfolgt in einer Sitzung einer anderen berechtigten Person:
```bash
curl --fail --silent --show-error --cookie reviewer.cookies \
--header "X-CSRF-Token: $REVIEWER_CSRF" \
--json '{"test_evidence":"lab-report-2026-09-13","reason":"Labortest erfolgreich geprüft"}' \
"$AIS_URL/api/v1/profiles/$PROFILE_ID/publish"
```
Module werden analog über `POST /api/v1/modules` und
`POST /api/v1/modules/{id}/publish` versioniert und veröffentlicht. Ausgangsvorlagen
stehen unter `GET /api/v1/modules/builtin`. Sie enthalten keinen ausgefüllten
Testnachweis und benötigen Anpassung sowie Prüfung auf dem Zielhost.
Ein Postinstallationsprofil verweist auf unveränderliche Modul-IDs:
```json
{
"name": "lab-postinstall",
"kind": "postinstall",
"target_builds": ["9.1-1"],
"steps": [
{
"id": "final-check",
"module_id": "REPLACE_WITH_PUBLISHED_MODULE_ID",
"parameters": {},
"required": true
}
],
"reboot_budget": 1
}
```
Modulabhängigkeiten müssen bereits in vorherigen Schritten enthalten sein.
Mindestens eine Pflichtprüfung wird verlangt. Modulparameter werden gegen das
JSON-Schema der gewählten Modulversion validiert.
## Host prüfen und Installation freigeben
Gruppentoken, geprüftes Medium und beide veröffentlichten Profile müssen bereits
existieren. Der Host verweist über `installation_profile_id`,
`postinstall_profile_id` und `iso_id` auf diese Objekte.
```bash
curl --fail --silent --show-error --cookie session.cookies \
"$AIS_URL/api/v1/hosts/$HOST_ID/preview"
curl --fail --silent --show-error --cookie session.cookies \
--header "X-CSRF-Token: $AIS_CSRF" \
--json '{"expected_version":1,"valid_minutes":30,"confirmation":"pve01.lab.example.net","disks_confirmed":true,"reason":"Freigegebener dedizierter Labortest"}' \
"$AIS_URL/api/v1/hosts/$HOST_ID/approve-install"
```
Vor dem zweiten Aufruf tatsächlichen FQDN, aktuelle Hostversion und dargestellte
Zielgeräte prüfen. Die Aktion erzeugt einen vorbereiteten Lauf und bindet die
Konfiguration. Ein zweiter gleichzeitiger aktiver Lauf für denselben Host ist gesperrt.
Der Proxmox-Installer sendet seinen nativen Request selbst. Für einen isolierten
API-Test zeigt dieses Beispiel die verwendete Payload-Struktur:
```json
{
"$schema": {"version": "1.0"},
"product": {"product": "pve"},
"iso": {"release": "9.1", "build": "1"},
"dmi": {
"system": {
"uuid": "d2e59b03-13cf-4ac9-a390-78c55f6a36d3",
"serial": "LAB-HOST-001"
}
},
"network-interfaces": [{"mac": "02:00:00:00:00:01"}]
}
```
Der echte Installer darf weitere Hardwarefelder senden. Registrierung und
Antwortschema stammen aus den
[offiziellen Installer-Typen](https://github.com/proxmox/proxmox-rs/blob/master/proxmox-installer-types/src/answer.rs).
Ein unbekannter Host erhält keine TOML-Antwort. Der Gruppentoken muss zum Standort
und zum freigegebenen Medium des Hosts passen.
```bash
curl --fail --silent --show-error \
--header "Authorization: Bearer $INSTALLER_GROUP_TOKEN" \
--json @installer-info.json "$AIS_URL/installer/v1/answer" \
--output answer.toml
```
Dieser Abruf verbraucht eine Freigabe. `answer.toml` enthält den Root-Hash und
runbezogene Downloadberechtigungen; wie Zugangsdaten schützen. Innerhalb des
kurzen Auslieferungsfensters liefern Wiederholungen dieselbe Antwort.
## Runner und Status
Der Starthelfer richtet den Runner automatisch ein. Das Gerät erzeugt einen
eigenen Ed25519-Schlüssel, registriert dessen öffentlichen Anteil und signiert
weitere Requests einschließlich Methode, Pfad, Zeitstempel, Nonce und Body-Digest.
Ein gewöhnlicher curl-Aufruf mit Installer-Gruppentoken berechtigt nicht zum
Manifest-, Artefakt- oder Secret-Abruf.
```bash
curl --fail --silent --show-error --cookie session.cookies \
"$AIS_URL/api/v1/runs/$RUN_ID"
```
Die Laufansicht enthält Schrittzustände, redigierte Logs und die fixierte
Konfiguration. Fortsetzung und Abbruch benötigen `expected_version` und einen
Begründungstext über `/api/v1/runs/{id}/resume` bzw. `/cancel`. Eine Fortsetzung
erst nach Prüfung des lokalen Zustands anfordern. Ein bereits laufender
Paketmanager wird beim Abbruch am nächsten sicheren Übergang angehalten.
Für einen dauerhaft verlassenen Lauf steht `/api/v1/runs/{id}/reconcile` zur
Verfügung. Zuerst den Abbruch anfordern, ausgestellte Leases und das
Antwortauslieferungsfenster ablaufen lassen sowie Installer und Runner lokal
stoppen und prüfen. Danach `expected_version`, `reason`, `confirmation` mit dem
Host-FQDN und `execution_stopped: true` senden. Diese gesonderte Bestätigung beendet
den Lauf als `cancelled`; eine neue Installation benötigt anschließend eine
eigene Freigabe. Der Ablauf ist auch für `needs_review` nach Restore vorgesehen.
+44
View File
@@ -0,0 +1,44 @@
# Kompatibilität und Abnahme
Es gibt zum Auslieferungszeitpunkt **keinen auf Hardware oder in einer VM
freigegebenen Proxmox-ISO-Build**. Die Registrierung `test_status=passed` ist eine
vom Administrator dokumentierte Freigabe; sie ersetzt keinen tatsächlichen Test.
| Kombination | Status | Nachweis |
| --- | --- | --- |
| API/SQLite/Sicherheitslogik, Python 3.14 unter Windows | Automatisierte lokale Softwaretests | `python -m pytest` |
| Python 3.13 im bereitgestellten Dockerfile | Image gebaut; Tests, TLS-Start und Dienstneustart bestanden | Ergebnisse im [Testbericht](test-report.md) |
| Python 3.12+ | Deklarierte Python-Untergrenze; keine vollständige CI-Matrix | Auf gewählter Version testen |
| Proxmox VE ISO mit nativem Answer-Token und First-Boot-URL | Pro konkretem ISO-Build zu testen | Noch offen |
| UEFI, BIOS und Secure Boot | Je verwendetem Bootmodus zu testen | Noch offen |
| Netzunterbrechung/Neustart auf echtem Proxmox-Host | Runner-Softwaretests plus erforderlicher Integrationstest | Hardware-/VM-Test offen |
| 100 Hosts, zehn gleichzeitige Installationen | Planungsziel | Lasttest offen |
| RPO 24 Stunden / RTO zwei Stunden | Planungsziel | Betrieblicher Restore-Test offen |
Ein ISO ohne native Unterstützung für `--answer-auth-token` wird nicht freigegeben.
Die Assistant-Version allein belegt nicht die Funktionen der Komponenten in einer
älteren ISO. Alle Beispieldaten sind Platzhalter und stellen keine Freigabe dar.
## Freigabe eines Builds
Für jeden Build ein eigenes Prüfprotokoll außerhalb der Anwendung archivieren:
- Original-ISO und SHA-256, Assistant-Paketversion, Build-Befehl, Ergebnisdatei
und Prüfsumme; Tokens im Protokoll redigieren.
- Installer-Request als redigiertes JSON; UUID-, Seriennummer- und MAC-Zuordnung
sowie Ablehnung unbekannter und widersprüchlicher Identitäten.
- Antwortdatei ohne Geheimnisse, Prüfung mit dem zugehörigen Assistant, korrekte
Netzwerkwerte und Auswahl ausschließlich der vorgesehenen Testdatenträger.
- Bearer-Header, TLS-Verifikation, First-Boot-Download, Zeitpunkt des ersten
Starts und Vertrauen in die Server-CA.
- Modulabläufe `check`, `apply`, `verify`, Verlust einer Serverantwort,
Netzausfall, erneuter Start und kontrollierter Reboot. Unterbrochene nicht sichere
Änderungen müssen einen prüfbedürftigen Zustand ergeben.
- Abschluss nur nach erfolgreichen Pflichtprüfungen; danach keine erneute
Installationsantwort oder weitere Skriptausführung mit alten Berechtigungen.
- Hardware-/VM-Konfiguration und Bootverfahren. Secure Boot nur nennen,
wenn genau dieser Modus tatsächlich geprüft wurde.
Die Akzeptanzszenarien A01A16 des Feinkonzepts sind die vollständige fachliche
Abnahmeliste. Repositorytests decken die simulierbaren Server- und Runner-Teile ab,
nicht die Betriebssysteminstallation oder reale Ausfälle.
+241
View File
@@ -0,0 +1,241 @@
# Deployment mit einem Image aus der Container Registry
Proxmox AIS wird auf dem Zielserver aus einem bereits veröffentlichten
Containerimage gestartet. Der [GitHub-Actions-Workflow](github-actions.md) erstellt dieses Image; auf dem
Zielserver werden nur Docker Compose und die Betriebskonfiguration benötigt.
Diese Anleitung setzt einen erfolgreichen `container-publish`-Job und ein
weiterhin abrufbares Image voraus.
## Voraussetzungen und Dateien
Der Zielserver benötigt Linux auf amd64, Docker Engine, das Docker-Compose-Plugin
und einen HTTPS-Reverse-Proxy. Die Docker-Befehle unter einem Deploymentkonto
mit Docker-Zugriff ausführen. Dasselbe Konto für `docker login` und
`docker compose` verwenden, damit die Registry-Anmeldung verfügbar ist.
Die [Docker-Installationsanleitung](https://docs.docker.com/engine/install/debian/)
beschreibt beispielsweise die Installation auf Debian.
Python, Node.js, ein CI-Runner und Build-Werkzeuge gehören zur Build-Umgebung;
die Anwendung samt Laufzeit ist im veröffentlichten Image enthalten. Ein
Checkout des Anwendungsquellcodes ist auf dem Zielserver nicht erforderlich.
Ein beschreibbares Deploymentverzeichnis bereitstellen, beispielsweise
`/opt/proxmox-ais`, und diese beiden Dateien aus dem gewünschten Projektstand
herunterladen beziehungsweise dorthin kopieren:
| Datei aus dem Repository | Dateiname auf dem Zielserver |
| --- | --- |
| [compose.yaml](../compose.yaml) | `/opt/proxmox-ais/compose.yaml` |
| [config/deployment.env.example](../config/deployment.env.example) | `/opt/proxmox-ais/.env` |
Im weiteren Verlauf alle Compose-Befehle in diesem Verzeichnis ausführen:
```bash
cd /opt/proxmox-ais
chmod 600 .env
docker version
docker compose version
```
Die Compose-Datei verwendet das Image aus `PROVISIONER_IMAGE`. Ein fehlender
Wert führt bereits bei der Konfigurationsprüfung zu einem Fehler.
## Image und öffentliche Adresse festlegen
In GitHub unter **Actions** den erfolgreichen Workflow-Lauf mit
`container-publish` öffnen, unter **Artifacts** das Archiv `container-deploy`
herunterladen und entpacken. Es enthält `build.env` und `deploy.env`.
Die vollständige Zeile `PROVISIONER_IMAGE=…@sha256:…` aus `deploy.env` in die
`.env` auf dem Zielserver übernehmen. Der Digest legt genau das veröffentlichte
Image fest. Compose liest `deploy.env` nicht automatisch ein.
[GitHub-Artefakte herunterladen](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/download-workflow-artifacts).
Alternativ auf GitHub das zugehörige **Packages**-Paket öffnen und den vollständigen
Imagepfad mit einem tatsächlich vorhandenen Versions-Tag kopieren. Der aktuelle
Imagepfad der Beispieldatei sieht so aus:
```dotenv
PROVISIONER_IMAGE=ghcr.io/netivra/proxmox-ais:0.1.0
PUBLIC_URL=https://provision.example.net
MAINTENANCE=false
```
`PROVISIONER_IMAGE` und `PUBLIC_URL` für die Installation festlegen. Der
Imagepfad entspricht dem aktuellen Wert der Vorlage; die Verfügbarkeit des
Tags `0.1.0` wird damit nicht vorausgesetzt. Maßgeblich sind das
Pipeline-Artefakt oder die Registry-Anzeige deines Projekts.
Der Registry-Endpunkt ist `ghcr.io`. Für reproduzierbare Deployments den Digest
bevorzugen und die bisher verwendeten Digests für spätere Updates aufbewahren.
`PUBLIC_URL` ist die vollständige HTTPS-Basisadresse ohne zusätzlichen Pfad,
unter der Browser, Installer und installierte Hosts den Dienst erreichen.
DNS und Zertifikatskette müssen für diese Systeme gültig sein. Der
Reverse-Proxy auf dem Zielserver leitet diese Adresse an
`http://127.0.0.1:8080` weiter. Der Compose-Port ist nur auf Loopback gebunden.
Bei einem Proxy auf einem anderen Host oder in einem anderen Container dessen
Verbindung gezielt nach der [Betriebsanleitung](operations.md#tls-und-reverse-proxy)
einrichten; `127.0.0.1` bezeichnet dort jeweils das eigene System.
## An der Registry anmelden und Image laden
Öffentliche GHCR-Pakete lassen sich ohne Anmeldung herunterladen. Für ein
privates Paket einen GitHub Personal Access Token **(classic)** mit dem Scope
`read:packages` verwenden. Das zugehörige GitHub-Konto benötigt Lesezugriff auf
das Paket; bei Organisations-SSO den Token dafür autorisieren. Der Token wird
bei der Anmeldung als Passwort verdeckt eingegeben und gehört nicht in `.env`.
Das kurzlebige `GITHUB_TOKEN` des CI-Jobs ist kein Deployment-Zugang.
[GHCR-Authentifizierung](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic).
Nur für private Pakete anmelden und `GITHUB_USERNAME` durch den GitHub-Namen
des Token-Inhabers ersetzen:
```bash
docker login ghcr.io --username GITHUB_USERNAME
```
Konfiguration prüfen und das gewählte Image herunterladen:
```bash
docker compose config --quiet
docker compose pull provisioner
```
Erst nach erfolgreichem Pull fortfahren. `unauthorized` oder `denied` weist
auf Anmeldung beziehungsweise Zugriffsrechte hin; bei `manifest unknown` den
Imagepfad und den verfügbaren Tag/Digest prüfen. Bei internen Zertifikaten
den CA-Vertrauensanker im Docker-Dienst des Zielservers einrichten.
## Einmalige Initialisierung und Start
Nur bei der ersten Inbetriebnahme mit neuen Daten- und Schlüsselvolumes den
Administrator und den Master-Key anlegen:
```bash
docker compose run --rm --no-deps \
--volume proxmox-ais-keys:/run/secrets:rw provisioner init --username admin
```
Das Passwort wird zweimal interaktiv abgefragt. Es gibt kein Standardpasswort.
Das Schlüsselvolume ist für diesen einen Aufruf beschreibbar. Im Regelbetrieb
wird es schreibgeschützt eingebunden. Bei einer bestehenden Installation oder
einem Update diesen Initialisierungsbefehl nicht erneut ausführen.
Anschließend den Dienst starten:
```bash
docker compose up -d --no-build --wait
docker compose ps
docker compose logs --tail=100 provisioner
```
`--wait` wartet auf einen laufenden, gesunden Dienst gemäß dem Healthcheck im
Image. Der Proxyzugriff wird separat geprüft.
[Compose-Startoptionen](https://docs.docker.com/reference/cli/docker/compose/up/).
Mit `curl`, sofern auf dem Zielserver verfügbar, den lokalen Dienst und die
öffentliche HTTPS-Adresse prüfen; die Beispieladresse ersetzen:
```bash
curl --fail http://127.0.0.1:8080/health/ready
curl --fail https://provision.example.net/health/ready
```
Beide Aufrufe sollen `"status":"ready"` liefern. Danach die öffentliche
Adresse im Browser öffnen und mit dem angelegten Administrator anmelden.
## Daten und Schlüssel
Die Anwendung läuft im Container als UID/GID `10001:10001`. Die Compose-Datei
verwendet diese dauerhaften, von Docker verwalteten Volumes:
| Volume | Inhalt | Containerpfad |
| --- | --- | --- |
| `proxmox-ais-data` | SQLite-Datenbank und Artefakte | `/var/lib/proxmox-ais` |
| `proxmox-ais-keys` | Master-Key für verschlüsselte Geheimnisse | `/run/secrets` |
Für neue Volumes übernimmt Docker die vorbereiteten Verzeichnisse aus dem
Image; sie sind für den Dienstbenutzer angelegt. Beim Wiederverwenden oder
Wiederherstellen bestehender Volumes müssen Eigentümer und Rechte weiterhin
zu UID/GID 10001 passen. Lokale Bind-Mount-Verzeichnisse benötigen dieselben
passenden Rechte, falls die Compose-Datei später entsprechend angepasst wird.
[Docker-Volumes](https://docs.docker.com/engine/storage/volumes/).
Containerneustarts und Updates erhalten diese Volumes.
`docker compose down --volumes` würde sie löschen. Für mehrere getrennte Instanzen auf demselben
Host die Volume-Namen in Compose und beim Initialisierungsaufruf je Instanz
anpassen. Daten und Master-Key nach der
[Backup-Anleitung](operations.md#backup) getrennt sichern.
## Updates
Ein Wartungsfenster vorsehen. Vor dem ersten Neustart den Digest des
tatsächlich laufenden Images ermitteln, insbesondere bei Verwendung eines
beweglichen Tags:
```bash
AIS_RUNNING_IMAGE_ID=$(docker inspect --format '{{.Image}}' "$(docker compose ps -q provisioner)")
docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$AIS_RUNNING_IMAGE_ID"
```
Den zum Projekt passenden Registry-Digest zusammen mit dem Backup aufbewahren
und in `.env` als `PROVISIONER_IMAGE` eintragen. Damit verwendet auch der
Wartungsneustart genau die bisherige Version. Falls kein Registry-Digest
angezeigt wird, die bisherige Version vor dem Update eindeutig zuordnen.
Jetzt in `.env` `MAINTENANCE=true` setzen und die Einstellung mit der
bisherigen Version übernehmen:
```bash
docker compose up -d --no-build --wait
```
Der Wartungsmodus sperrt neue Installationsfreigaben. Bereits gestartete
Installer und Host-Runner werden dadurch nicht beendet. Ihre laufenden
Phasen vor dem eigentlichen Versionswechsel prüfen und einen geeigneten
Zeitpunkt für den Dienststopp abwarten.
Jetzt die Daten mit der bisher eingesetzten Anwendungsversion sichern und
den Master-Key separat verwahren. Die [Backup-Anleitung](operations.md#backup)
enthält dafür die Containerbefehle. Zusätzlich die aktuelle `.env`, den
bisherigen Image-Digest und die Proxykonfiguration sichern.
Danach die neue `PROVISIONER_IMAGE`-Zeile aus dem erfolgreichen
`container-publish`-Job in `.env` übernehmen. `PUBLIC_URL` und die
Volume-Namen beibehalten, `MAINTENANCE=true` gesetzt lassen. Folgender Ablauf
prüft und lädt das neue Image, bevor er den bisherigen Dienst stoppt:
```bash
(
set -eu
docker compose config --quiet
docker compose pull provisioner
docker compose stop provisioner
docker compose up -d --no-build --wait
docker compose ps
)
```
Bei einem fehlgeschlagenen Pull läuft der bisherige Dienst weiter. Nach dem
Update Readiness über HTTPS, Anmeldung und Inventar prüfen. Erst nach
erfolgreicher Prüfung in `.env` `MAINTENANCE=false` setzen und übernehmen:
```bash
docker compose up -d --no-build --wait
```
Der Initialisierungsbefehl gehört ausschließlich zur ersten Inbetriebnahme.
## Rückkehr zum vorherigen Stand
Falls der neue Dienst nicht gesund startet, zunächst
`docker compose logs --tail=100 provisioner` prüfen. Das vorherige Image darf
nur auf eine damit kompatible Datenbank zugreifen. Bei bestätigter
Kompatibilität die gesicherte `.env` beziehungsweise den bisherigen exakten
Image-Digest wiederherstellen und den Updateablauf erneut ausführen; den
Wartungsmodus erst nach erfolgreicher Prüfung beenden.
Hat die neue Version das Schema bereits verändert, den Dienst stoppen und
mit der zum Backup passenden Anwendungsversion nach der
[Restore-Anleitung](operations.md#restore) in ein neues leeres Datenvolume
wiederherstellen. Die vorhandenen Daten erhalten, bis die Wiederherstellung
geprüft ist. Der Restore setzt Maschinenberechtigungen zurück und erfordert
den dort beschriebenen Abgleich laufender Installationen.
+203
View File
@@ -0,0 +1,203 @@
# GitHub Actions und Container Registry
Der [Workflow](../.github/workflows/ci.yml) führt die Python-Tests aus, prüft die
JavaScript-Syntax und baut und testet das Anwendungsimage. Freigegebene Builds
werden in die GitHub Container Registry (GHCR) veröffentlicht. Der Workflow
endet beim Image-Push und aktualisiert keinen Zielserver. Die anschließenden
Betriebsschritte stehen in der [Deployment-Anleitung](deployment.md).
## Auslöser und Image-Tags
Der Workflow läuft bei Pushes auf alle Branches und Tags, bei Pull Requests
und beim manuellen Start mit `workflow_dispatch`.
| Ref und Ereignis | Container-Job | Veröffentlichung |
| --- | --- | --- |
| Pull Request, auch aus einem Fork | `container-verify` | Keine |
| Feature-Branch, ungeschützter Ref oder sonstiger Tag | `container-verify` | Keine |
| Geschützter Standardbranch bei Push oder manuellem Start | `container-publish` | `sha-<vollständiger Commit-SHA>` und `edge` |
| Geschützter Tag `vX.Y.Z` bei Push oder manuellem Start | `container-publish` | `sha-<vollständiger Commit-SHA>` und `X.Y.Z` |
Release-Tags müssen dem Muster `v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)`
vollständig entsprechen. Führende Nullen, Vorabversionen wie `v1.0.0-rc.1` und
Build-Zusätze veröffentlichen daher kein Image. Bei einem gültigen geschützten
Release-Tag muss dessen Version außerdem exakt `[project].version` in
[pyproject.toml](../pyproject.toml) entsprechen; eine Abweichung bricht den Lauf ab.
Das Image liegt für dieses Repository unter `ghcr.io/netivra/proxmox-ais`.
Der Workflow leitet den Pfad aus dem kleingeschriebenen GitHub-Repositorynamen
ab; bei einem Fork oder einer Umbenennung ändert sich deshalb der Imagepfad.
`edge` bezeichnet den zuletzt veröffentlichten Standardbranch-Build. Ein
erneuter Lauf eines älteren Commits kann `edge` zurücksetzen. SHA-Tags enthalten
alle 40 Zeichen des Commits; erneute Builds desselben Quellstands können durch
externe Build-Abhängigkeiten einen anderen Digest ergeben. Es gibt keinen
`latest`-Tag. Im Betrieb den Digest aus `deploy.env` verwenden und die
zugehörigen Registry-Versionen für Updates und Rollback aufbewahren.
## Jobs und Artefakte
`python-tests` und `javascript-check` prüfen den Quellstand. Das Skript
[ci/python-tests.sh](../ci/python-tests.sh) installiert `.[dev]` in einer
eigenen temporären virtuellen Umgebung, führt Pytest aus und entfernt die
Umgebung anschließend. Die JavaScript-Prüfung verwendet
`node --check provisioner/static/app.js`; sie ist kein Browsertest.
`container-policy` bestimmt anhand des GitHub-Ereignisses, des Ref-Schutzes
und des Branch- beziehungsweise Tag-Namens, welcher Container-Job laufen darf.
Nach erfolgreichen Python- und JavaScript-Prüfungen läuft genau einer der
Jobs `container-verify` und `container-publish`. Beide bauen über
[ci/container.sh](../ci/container.sh) ein `linux/amd64`-Image und prüfen es
mit dem [Container-Smoke-Test](../tests/container_smoke.py). Der Publish-Job
lädt genau das bereits getestete Image hoch, ohne einen zweiten Build.
Die Veröffentlichung wird im Skript erneut auf Ereignis und Ref geprüft;
dort erfolgt auch der Abgleich mit der Projektversion.
Publish-Jobs teilen eine Warteschlange mit `queue: max` und laufen einzeln.
Bis zu 100 Jobs können warten; ein laufender Publish-Job wird durch neue
Commits nicht abgebrochen. Die Reihenfolge richtet sich nach dem Eintritt in
die Warteschlange, nicht zwingend nach dem Alter der Commits. Bei Pull Requests
und ungeschützten Refs ersetzt ein neuer Lauf den vorherigen Lauf desselben Refs.
[GitHub-Parallelität und Warteschlangen](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency).
Der Smoke-Test prüft TLS, Anmeldung, CSRF, ausgelieferte Webdateien und
Datenerhalt über einen Dienstneustart. Er läuft als UID 10001 mit
schreibgeschütztem Dateisystem, entfernten Linux-Capabilities und temporären
Testdaten. Eine Proxmox-Installation ist kein Bestandteil dieses Tests.
Der Build verwendet `--provenance=false --sbom=false` und veröffentlicht
keine Provenance- oder SBOM-Attestierungen. Die OCI-Labels für Quellrepository,
Commit und Anwendungsversion bleiben erhalten.
| Artefakt in GitHub Actions | Inhalt | Aufbewahrung |
| --- | --- | --- |
| `python-test-results` | `reports/pytest.xml` als JUnit-Datei, auch nach fehlgeschlagenen Tests, sofern erzeugt | 7 Tage |
| `container-build` | `build.env` nach erfolgreichem Verify-Job | 7 Tage |
| `container-deploy` | `build.env` und `deploy.env` nach erfolgreichem Publish-Job | 30 Tage |
Die Aufbewahrung wird im Workflow mit `retention-days` gesetzt und ist durch
die übergeordneten GitHub-Einstellungen begrenzt. Die JUnit-Datei wird als
Download bereitgestellt; der Workflow installiert keinen zusätzlichen
Testberichtsdienst. Auf der Übersicht des jeweiligen Laufs unter **Artifacts**
das gewünschte Archiv herunterladen und entpacken.
[Workflow-Artefakte](https://docs.github.com/en/actions/tutorials/store-and-share-data),
[Artefakte herunterladen](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/download-workflow-artifacts).
`build.env` enthält den temporären lokalen Image-Tag, Commit und
Anwendungsversion. Dieser `ci-…`-Tag wird nicht veröffentlicht und beim
Jobende lokal entfernt. `deploy.env` enthält ausschließlich die
Deploymentreferenz `PROVISIONER_IMAGE=ghcr.io/netivra/proxmox-ais@sha256:…`.
Die Dateien enthalten keine Zugangsdaten. `deploy.env` wird auf dem Zielserver
nicht automatisch eingelesen; seine Image-Zeile in die dortige `.env` übernehmen.
## GitHub einrichten
Ein eigener CI-Runner ist nicht erforderlich. Alle Jobs verwenden
GitHub-gehostete Linux-amd64-Runner mit `ubuntu-24.04`. Der Workflow richtet
Python 3.13 über `actions/setup-python` und Node.js 24 über `actions/setup-node`
ein. Docker und Buildx werden vom Runner bereitgestellt und vor dem Build
geprüft. Bash, OpenSSL und der OpenSSH-Client werden dort ebenfalls verwendet.
Diese Build-Umgebung ist
unabhängig vom AIS-Host-Runner für die Postinstallation auf Proxmox-Hosts.
[GitHub-gehostete Runner](https://docs.github.com/en/actions/reference/runners/github-hosted-runners).
Im Repository unter **Settings → Actions → General** GitHub Actions aktivieren
und die im Workflow verwendeten Actions von `actions/*` zulassen. Diese sind
im Workflow auf vollständige Commit-SHAs festgelegt. Es werden
keine selbst angelegten Repository-Secrets oder CI-Variablen benötigt.
Der Workflow verwendet standardmäßig `contents: read`; nur
`container-publish` erhält zusätzlich `packages: write` und verwendet das
automatisch bereitgestellte `GITHUB_TOKEN` für die Anmeldung an `ghcr.io`.
[Actions-Einstellungen](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository).
GHCR-Pakete sind bei der ersten Veröffentlichung standardmäßig privat. Ein
bereits existierendes Paket muss mit diesem Repository verbunden sein und
dessen Actions Schreibzugriff haben. In den Paketeinstellungen unter
**Manage Actions access** gegebenenfalls `Netivra/Proxmox-AIS` mit Schreibrecht
hinzufügen. Dort auch die gewünschte Paketsichtbarkeit festlegen.
[GHCR und GITHUB_TOKEN](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-in-a-github-actions-workflow),
[Paketzugriff und Sichtbarkeit](https://docs.github.com/en/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility).
## Standardbranch und Release-Tags schützen
**Vor der ersten Veröffentlichung müssen aktive Schutzregeln für die
veröffentlichten Refs eingerichtet sein.** Der Workflow und das Skript
verlangen `github.ref_protected == true`. GitHub setzt dieses Merkmal, wenn
Branchschutz oder ein passendes Ruleset für den auslösenden Ref konfiguriert
ist. Ohne Schutz wird nur geprüft.
[GitHub-Ref-Kontext](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#github-context).
Unter den Repository-Einstellungen für Rulesets zwei Regeln anlegen und
jeweils **Enforcement status: Active** wählen:
| Ruleset | Ziel | Vorgesehene Regeln |
| --- | --- | --- |
| Standardbranch | **Include default branch**, etwa `main` | Pull Request und Review verlangen; Löschen und Force-Push verhindern; erfolgreiche Statuschecks verlangen |
| Releases | Tags mit dem Muster `v*` | Erstellung, Aktualisierung und Löschung beschränken; nur Release-Verantwortliche in die Bypass-Liste aufnehmen |
Für Pull Requests die Statuschecks `python-tests`, `javascript-check`,
`container-policy` und `container-verify` nach ihrem ersten Lauf als
verpflichtend auswählen. `container-publish` ist bei Pull Requests absichtlich
übersprungen und wird deshalb nicht als Pflichtprüfung eingerichtet.
Das Tag-Ruleset schützt alle `v*`-Tags; die engere Versionsprüfung übernimmt
zusätzlich das CI-Skript. Release-Verantwortliche setzen Tags auf einen
geprüften Commit mit der passenden Projektversion. Ein Tag auf einem Commit
des geschützten Standardbranches ist selbst noch kein geschützter Tag.
[Rulesets anlegen](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository),
[Verfügbare Schutzregeln](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets).
Die Regeln und Paketberechtigungen im GitHub-Projekt sind Teil der Einrichtung;
sie werden durch den Commit der Workflow-Datei nicht automatisch angelegt.
Welche Rulesets verfügbar sind, hängt von Repository-Sichtbarkeit und
GitHub-Tarif ab. Die CI-Abfragen ersetzen keine restriktiven Schreibrechte
auf Repository und Paket; Änderungen an Workflow und CI-Skripten im Review prüfen.
[Ruleset-Verfügbarkeit](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository).
## Erster Lauf und Release
Nach dem Push der Workflow-Datei unter **Actions** den Lauf öffnen.
Python- und JavaScript-Prüfung sowie der passende Container-Job müssen
erfolgreich sein. Für einen geschützten Standardbranch zusätzlich prüfen,
dass das GHCR-Paket `sha-<Commit>` und `edge` enthält und das Artefakt
`container-deploy` mit `deploy.env` bereitsteht. Ein lokaler Test ersetzt
diesen ersten GitHub-Lauf samt GHCR-Anmeldung und Push nicht.
Für ein Release zuerst `[project].version` auf die gewünschte Version setzen
und den geprüften Commit übernehmen. Anschließend durch ein für das
Release-Ruleset berechtigtes Konto den passenden Tag erstellen und pushen.
Im vorhandenen Checkout heißt der GitHub-Remote `GH-AIS`; in anderen Checkouts
den Namen mit `git remote -v` prüfen und im Befehl entsprechend ersetzen.
Für Projektversion `0.1.0`, sofern der Tag noch nicht existiert:
```bash
git tag -a v0.1.0 -m "Release 0.1.0"
git push GH-AIS v0.1.0
```
Der Release-Lauf muss zusätzlich den Image-Tag `0.1.0` erzeugen. Für einen
manuellen Branch-Lauf unter **Actions** den Workflow und **Run workflow**
wählen. Dafür muss die Workflow-Datei bereits auf dem Standardbranch liegen.
Ein manueller Lauf veröffentlicht nur, wenn dieselben Ref-Schutz- und
Versionsbedingungen erfüllt sind.
[Workflow manuell starten](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow).
Branch-Push und Pull Request können für denselben Quellstand getrennte Läufe
erzeugen. Pull Requests werden im Merge-Kontext geprüft; ihre Tests führen
keinen Registry-Push aus. Die Jobs laden keine produktiven Deployment-Zugänge.
## Fehlersuche und Deployment
Bei übersprungenem Publish-Job zuerst Ereignis, Standardbranch, Tagformat und
aktive Schutzregeln prüfen. Bei einer Versionsabweichung müssen Git-Tag und
`pyproject.toml` aufeinander abgestimmt werden. Bei `denied` oder `unauthorized`
im Publish-Job die Organisationsrichtlinien, `packages: write` und den
Actions-Zugriff auf das GHCR-Paket kontrollieren. Fehlt `container-deploy`,
die Logs des Build-, Smoke- und Push-Schritts prüfen; vor erfolgreicher
Veröffentlichung steht kein Deployment-Digest bereit.
Zum Deployment [compose.yaml](../compose.yaml) und
[config/deployment.env.example](../config/deployment.env.example) auf den
Zielserver kopieren und die Image-Zeile aus `deploy.env` verwenden.
Die [Deployment-Anleitung](deployment.md) beschreibt GHCR-Anmeldung,
Erstinitialisierung, HTTPS-Prüfung, Updates und Rollback. Für private Pakete
benötigt das Deploymentkonto einen Personal Access Token (classic) mit
`read:packages`; öffentliche Pakete lassen sich anonym laden.
+50
View File
@@ -0,0 +1,50 @@
# ISO auf einer Build-Maschine vorbereiten
Die ISO wird außerhalb des Webcontainers mit dem offiziellen
`proxmox-auto-install-assistant` erstellt. Der Container benötigt weder
privilegierte Rechte noch Zugriff auf einen Docker-Socket.
1. Original-ISO aus vertrauenswürdiger Proxmox-Quelle beziehen und ihre SHA-256
gegen den veröffentlichten Wert prüfen. Build und Assistant-Version festhalten.
2. In der Anwendung einen standortbezogenen Gruppentoken erzeugen. Den einmal
ausgegebenen vollständigen Wert `<gruppenname>:<secret>` sicher übernehmen.
3. Zertifikatsfingerprint des verwendeten TLS-Endpunkts über einen
vertrauenswürdigen Weg ermitteln. Bei einem Reverse-Proxy dessen Zertifikat verwenden.
4. Verfügbare Optionen prüfen und die ISO erstellen:
```bash
proxmox-auto-install-assistant prepare-iso --help
proxmox-auto-install-assistant prepare-iso SOURCE.iso \
--fetch-from http \
--url 'https://provision.example.net/installer/v1/answer' \
--cert-fingerprint '<SHA256-Zertifikatsfingerprint>' \
--answer-auth-token '<gruppenname>:<secret>'
```
URL-, Fingerprint- und Token-Optionen entsprechen der
[offiziellen Proxmox-Dokumentation](https://pdm.proxmox.com/docs/automated-installations.html#preparing-an-iso).
Die URL zeigt hier auf den eigenen Antwortdienst. Den Befehl mit echtem Token
nicht in öffentliche Logs oder gemeinsam genutzte Shell-History schreiben.
5. Ergebnis-ISO, Ausgangs-ISO, Assistant-Version, Build, Fingerprint und Gruppe
in der Medienverwaltung registrieren. Zunächst `draft` verwenden.
6. Den Build anhand der [Abnahmematrix](compatibility.md) im isolierten Labor
prüfen. Ein neuer Assistant aktualisiert die Komponenten einer alten ISO
nicht automatisch. Erst nach Prüfung `passed` setzen und Testnachweis referenzieren.
Der Installer sendet Hardwaremerkmale per POST und erhält bei eindeutiger,
freigegebener Zuordnung eine TOML-Antwort. Ein kurzes Wiederholungsfenster liefert
dieselbe Antwort für denselben Lauf. Nach dessen Ende oder Runner-Anmeldung
ist der Abruf gesperrt. Bei abgelehnter Zuordnung die Ursache korrigieren und
den Installationsversuch erneut starten.
Der dynamische First-Boot-Abschnitt verwendet `source = "from-url"` und
`ordering = "network-online"`. Der Installer lädt den Starthelfer, das installierte
System führt ihn beim ersten Boot aus. Diese Trennung beschreibt der
[offizielle First-Boot-Implementierungsentwurf](https://lists.proxmox.com/pipermail/pve-devel/2024-November/066649.html).
Die Gesamtfunktion muss mit der konkreten ISO praktisch geprüft werden.
Ein gemeinsamer ISO-Token ist vom Medium auslesbar, Hardwaremerkmale sind kopierbar.
Zugriff auf Medium und Provisionierungsnetz beschränken. Nach erfolgreicher
Installation die Bootreihenfolge auf das lokale System umstellen; eine bereits
ausgelieferte Antwort kann der Server nicht nachträglich ungültig machen.
+2502
View File
File diff suppressed because it is too large Load Diff
+216
View File
@@ -0,0 +1,216 @@
# Betrieb und Wiederherstellung
Für die Installation aus der GitHub Container Registry (GHCR) siehe
[Deployment-Anleitung](deployment.md). Alle `docker compose`-Befehle unten
werden im Deployment-Verzeichnis mit `compose.yaml` und `.env` ausgeführt.
`PROVISIONER_IMAGE` verweist auf das veröffentlichte Image, vorzugsweise per Digest.
Die CLI ist im Container enthalten; auf dem Zielserver ist keine eigene
Python-Installation erforderlich.
## Konfiguration
`proxmox-ais` liest die optionale TOML-Datei aus `APP_CONFIG`, anschließend
gleichnamige großgeschriebene Umgebungsvariablen. Relative Pfade beziehen sich
auf das Arbeitsverzeichnis.
Die mitgelieferte Compose-Datei übernimmt `PROVISIONER_IMAGE`, `PUBLIC_URL`
und `MAINTENANCE` aus `.env`. Weitere Anwendungseinstellungen bei Bedarf unter
`services.provisioner.environment` in `compose.yaml` ergänzen.
| Einstellung | Standard | Bedeutung |
| --- | --- | --- |
| `DATA_DIR` | `./data` | SQLite, Artefakte und Dienstsperre auf lokalem Speicher |
| `MASTER_KEY_FILE` | `./secrets/master.key` | Separat abgelegter Fernet-Schlüssel |
| `PUBLIC_URL` | `https://localhost:8080` | Extern erreichbare Basis-URL ohne Pfad |
| `SECURE_COOKIES` | `true` | Sessioncookies nur über HTTPS |
| `FOUR_EYES` | `true` | Autor darf eigene Versionen nicht veröffentlichen |
| `MAINTENANCE` | `false` | Neue Installationsfreigaben aussetzen |
| `ANSWER_WINDOW_SECONDS` | `300` | Wiederholungsfenster des Antwortabrufs |
| `ENROLLMENT_HOURS` | `4` | Maximale Registrierungsfrist |
| `LEASE_SECONDS` | `900` | Gültigkeit der Runner-Ausführungsberechtigung |
| `RUNNER_CA_FILE` | nicht gesetzt | Vertrauensanker für Runner bei interner CA |
`TESTING` und Bootstrap-Passwörter nicht im Regelbetrieb setzen. Benutzer werden
interaktiv mit `init` und danach über die Administrationsoberfläche angelegt.
Das Datenverzeichnis darf den Master-Key nicht enthalten. Unter Windows schützt
zusätzlich eine auf das Dienstkonto beschränkte NTFS-ACL das Schlüsselverzeichnis.
## TLS und Reverse-Proxy
Der Compose-Port ist nur an Loopback gebunden. Ein bestehender Proxy terminiert
TLS. Bei externem Proxy das private Container-/Verwaltungsnetz gezielt freigeben.
Der Dienst wertet Proxy-Header standardmäßig nicht aus; alle absoluten URLs
entstehen aus `PUBLIC_URL`. Bei einer nativen Installation kann der Dienst
TLS auch direkt terminieren:
```bash
proxmox-ais serve --host 0.0.0.0 --port 8080 \
--tls-cert /etc/proxmox-ais/fullchain.pem \
--tls-key /etc/proxmox-ais/server.key
```
Zertifikatskette und DNS müssen im Installer und auf installierten Hosts gültig
sein. Ein ISO-Fingerprint ersetzt nicht automatisch den CA-Vertrauensanker des
Runners. Bei interner CA `RUNNER_CA_FILE` konfigurieren und die gesamte Kette vom
Installer bis zum Runner prüfen. Keine TLS-Prüfung abschalten. Zertifikatsrotation
erfordert die Prüfung vorhandener Medien.
Capability-Tokens stehen zwangsläufig in Bootstrap-URLs. Der integrierte Server
schreibt daher keine Access-Logs. Im vorgeschalteten Proxy Bootstrap- und
Installer-Report-Pfade redigieren oder deren Access-Logging deaktivieren. Keine
Authorization-Header protokollieren. Skriptausgaben werden zusätzlich redigiert;
Skripte sollten Geheimnisse grundsätzlich nie ausgeben.
## Betrieb
Der Dienst verwendet genau einen Anwendungsprozess. Mehrere Uvicorn-Worker oder
gleichzeitige Dienste für dasselbe Datenverzeichnis verhindert die Dienstsperre.
SQLite benötigt lokalen Speicher mit zuverlässigen Dateisperren; kein NFS/SMB-Volume
einsetzen. Als Startwert sind 2 vCPU, 2 GB RAM und 20 GB Speicher ohne ISO-Archiv
vorgesehen; Kapazität muss unter realer Last geprüft werden.
`/health/live` prüft den Prozess, `/health/ready` Datenbank und Konfiguration.
Readiness, freien Speicher, Backupalter und ausbleibende Runner-Meldungen überwachen.
DHCP, DNS, NTP und Paketquellen sind externe Voraussetzungen. Der Server baut keine
eingehenden SSH-Verbindungen zu Hosts auf.
## Backup
Beim Registry-Deployment die Sicherung mit der aktuell eingesetzten Image-Version
erstellen, bevor `PROVISIONER_IMAGE` für ein Update geändert wird. Das
Hostverzeichnis `/srv/ais-backups` muss vorhanden und für UID/GID 10001
beschreibbar sein. Beispielsweise einmalig vom Administrator anlegen:
```bash
sudo install -d -o 10001 -g 10001 -m 0700 /srv/ais-backups
docker compose run --rm --no-deps --volume /srv/ais-backups:/backups \
provisioner backup /backups/2026-09-13
```
Für jede Sicherung einen neuen Zielnamen wählen. Bei einer nativen
Python-Installation lautet der entsprechende Befehl
`proxmox-ais backup /srv/ais-backups/2026-09-13`.
Das Ziel muss neu sein und außerhalb von `DATA_DIR` liegen. Die Anwendung kann
weiterlaufen: Die SQLite-Backup-API erzeugt einen konsistenten Snapshot, anschließend
werden unveränderliche Artefakte kopiert. Artefakte währenddessen nicht manuell
löschen. Die Sicherung enthält `database.sqlite3`, `artifacts/`, `settings.json`
und `manifest.json`. Der Master-Key wird ausgeschlossen; sein Fingerprint verhindert
die Wiederherstellung mit einem versehentlich falschen Schlüssel.
`settings.json` enthält die wirksamen Anwendungseinstellungen einschließlich
Standortvorgaben, Fristen und Freigaberegeln. Lokale Daten-/Schlüsselpfade und
Bootstrap-Zugangsdaten sind ausgeschlossen. Individuelle TOML-Datei,
Umgebungsvariablen, Proxykonfiguration, CA-/TLS-Dateien, ursprüngliche
ISOs und Prüfprotokolle separat sichern. Registrierte ISO-Metadaten liegen in der
Datenbank. Backup-Prüfsummen erkennen beschädigte Dateien; sie sind keine Signatur
gegen einen Angreifer mit Schreibzugriff. Backupverzeichnis deshalb schützen.
Den Master-Key aus `proxmox-ais-keys` über einen getrennten verschlüsselten
Sicherungsweg verwahren.
Eine lokale Kopie in ein bereits vorhandenes Zielverzeichnis ist möglich:
```bash
docker compose cp provisioner:/run/secrets/master.key /secure/offline/master.key
```
Die Schlüsselkopie ist genauso vertraulich wie alle gespeicherten Geheimnisse.
## Restore
1. Wartungsmodus aktivieren, neue Freigaben stoppen und Anwendungsdienst
herunterfahren. Die Host-Runner separat berücksichtigen: ein bereits
gestarteter Befehl kann noch laufen.
2. Bisheriges Datenverzeichnis erhalten. Ein neues leeres Datenverzeichnis
wählen und den zum Backup gehörenden Master-Key separat bereitstellen.
3. Die gleiche Anwendungsversion wie beim Backup verwenden. Beim
Registry-Deployment deren Digest in `.env` als `PROVISIONER_IMAGE` setzen
und herunterladen. Das Schlüsselvolume muss bereits den passenden
Master-Key als `master.key`, lesbar für UID/GID 10001, enthalten. In ein
neues, leeres Datenvolume wiederherstellen:
```bash
docker compose stop provisioner
docker compose pull provisioner
docker volume create proxmox-ais-data-restored
docker compose run --rm --no-deps \
--volume /srv/ais-backups:/backups:ro \
--volume proxmox-ais-data-restored:/var/lib/proxmox-ais \
provisioner restore /backups/2026-09-13
```
`proxmox-ais-data-restored` muss neu sein; existiert es schon, einen anderen
unbenutzten Namen wählen. Nach erfolgreichem Restore in `compose.yaml` unter
`volumes.ais-data.name` diesen neuen Namen eintragen. Das bisherige Datenvolume
bleibt erhalten. Bei einer nativen Python-Installation alternativ:
```bash
export DATA_DIR=/srv/proxmox-ais-restored
export MASTER_KEY_FILE=/secure/offline/master.key
proxmox-ais restore /srv/ais-backups/2026-09-13
```
4. Individuelle Konfiguration und Zertifikate wiederherstellen. Im Wartungsmodus
starten, anmelden und Inventar sowie aktuelle Hostzustände abgleichen.
Beim Registry-Deployment dafür `MAINTENANCE=true` in `.env` setzen und
`docker compose up -d --no-build --wait` ausführen.
5. Aktive Läufe stehen auf `needs_review`. Alte Sessions, Gerätezugänge,
Bootstrap-/Enrollment-/Report-Capabilities und Gruppentokens gelten nicht mehr;
offene Installationsfreigaben sind gesperrt. Gruppentokens und davon abhängige
Medien neu ausgeben. Zuvor ausgelieferte Installer nicht ungeprüft neu booten.
6. Installer und Runner auf jedem betroffenen Host lokal stoppen und den
tatsächlichen Zustand prüfen. Danach den verlassenen Lauf über
`POST /api/v1/runs/{id}/reconcile` abschließen: aktuelle `expected_version`,
aussagekräftige `reason`, `confirmation` mit dem Host-FQDN und
`execution_stopped: true` übermitteln. Die Aktion beendet den Lauf als
`cancelled`, sperrt seine Berechtigungen und schreibt einen Auditeintrag.
Erst danach ist eine neue ausdrückliche Installationsfreigabe möglich.
Der automatische Wiederanschluss alter Geräteidentitäten nach Restore
ist nicht Teil dieser Version.
Außerhalb eines Restore zuerst `/cancel` anfordern. Ein offline befindlicher
Runner kann seine bereits erhaltene Lease bis zum Ablauf behalten; eine
Abbruchanforderung zieht diese nicht zurück. `/reconcile` weist den Abgleich
deshalb zurück, solange eine zuvor ausgestellte Lease oder das
Antwortauslieferungsfenster noch gültig ist. Auch nach deren Ablauf ist die
ausdrückliche Bestätigung erforderlich, dass Installer und Runner lokal gestoppt
und kontrolliert wurden. Eine nicht mehr erreichbare Maschine ist kein Nachweis
dafür. Das Verfahren erteilt selbst keine Neuinstallationsfreigabe.
Restore überschreibt keine bestehende Datenbank. Die Dienstsperre verhindert
Restore während eines laufenden Dienstes im Zielverzeichnis. Ein fehlgeschlagener
Restore hinterlässt `RESTORE_FAILED`; der CLI-Start verweigert dann den Betrieb.
Nach Ursachenklärung erneut in ein neues leeres Ziel restaurieren.
Bereits heruntergeladene Antwortdateien kann der Server nicht zurückziehen.
Ebenso beendet eine Credential-Sperrung einen lokal laufenden Skriptprozess nicht
unmittelbar. Restore erfordert deshalb den physischen Zustandsabgleich der Hosts.
RPO 24 Stunden und RTO zwei Stunden sind Ziele aus dem Konzept. Erst wiederholte
Sicherung und zeitlich gemessene Wiederherstellung unter realen Bedingungen
weisen diese Ziele nach.
## Updates
Der vollständige Ablauf für Registry-Images steht unter
[Deployment aktualisieren](deployment.md#updates).
Vor dem ersten Neustart `PROVISIONER_IMAGE` auf den Digest des tatsächlich
laufenden Images festlegen, wie dort beschrieben. Anschließend
`MAINTENANCE=true` in `.env` setzen und die laufende Version mit
`docker compose up -d --no-build --wait` neu erstellen. Der Wartungsmodus
sperrt neue Freigaben; bereits gestartete Installations- und Modulabläufe werden
dadurch nicht beendet. Diese vor dem Update kontrolliert abschließen lassen.
Mit der bisherigen Version Daten und Schlüssel sichern und den alten
Image-Digest festhalten. Erst danach `PROVISIONER_IMAGE` ändern, das neue Image
mit `docker compose pull provisioner` laden, den Dienst mit
`docker compose stop provisioner` stoppen und mit
`docker compose up -d --no-build --wait` neu starten. Nach Prüfung von Anmeldung,
Inventar und Readiness `MAINTENANCE=false` setzen und erneut `up` ausführen.
Bei Updates wird `init` nicht wiederholt.
Die Datenbank
führt eine Migrationstabelle und `PRAGMA user_version`. Neuere unbekannte Schemas
werden abgewiesen. Downgrades erfolgen über die zur Sicherung passende Version
und den beschriebenen Restore; keine alte Anwendung auf eine bereits migrierte
Produktivdatenbank starten.
+29
View File
@@ -0,0 +1,29 @@
{
"name": "lab-system-single-disk",
"kind": "installation",
"target_builds": ["9.1-1"],
"reason": "Beispiel zur Anpassung im isolierten Labor",
"values": {
"root_secret_id": "REPLACE_WITH_SECRET_ID",
"global": {
"keyboard": "de",
"country": "de",
"timezone": "Europe/Berlin",
"mailto": "admin@example.net"
},
"network": {
"source": "from-answer",
"gateway": "192.0.2.1",
"dns": "192.0.2.53",
"filter": {"ID_NET_NAME_MAC": "enx020000000001"}
},
"disk_setup": {
"filesystem": "ext4",
"filter": {"ID_SERIAL_SHORT": "LAB_SYSTEM_DISK_001"},
"filter_match": "all",
"expected_count": 1,
"expected_serials": ["LAB_SYSTEM_DISK_001"],
"inventory_evidence": "REPLACE_WITH_VERIFIED_DISK_INVENTORY_REFERENCE"
}
}
}
+152
View File
@@ -0,0 +1,152 @@
# Softwaretestbericht
Stand: 13. September 2026. Die hier dokumentierten Softwaretests führen keine
Proxmox-Installation aus und verändern keine Hostdatenträger, Paketquellen oder
Produktivdienste. Die noch offenen praktischen Prüfungen stehen in der
[Kompatibilitätsmatrix](compatibility.md).
## Automatisierte Prüfungen
Die 49 Anwendungstests wurden unter Windows mit Python 3.14 ausgeführt:
47 Tests bestanden; zwei Linux-spezifische Prozesstests wurden dort übersprungen.
Die 68 zusätzlichen CI-Skripttests bestanden unter Windows mit Git Bash.
Insgesamt enthält die Testsuite jetzt 117 Testfälle; der aktuelle Gesamtlauf
unter Windows/Python 3.14.7 ergab 115 bestandene und zwei übersprungene Tests.
Unter Linux/Python 3.13.15 bestanden alle 117 Tests über das migrierte
CI-Einstiegsskript `ci/python-tests.sh`, einschließlich der OpenSSH-Prüfung.
| Testgruppe | Umfang | Geprüftes Verhalten |
| --- | --- | --- |
| `tests/test_acceptance.py` | 14 bestanden | Authentifizierung, CSRF, Rollen und Vieraugenregel; Ablehnung unbekannter, gesperrter und widersprüchlicher Hosts; Gruppenzuordnung und Freigabeablauf; zehn parallele Antwortabrufe mit genau einem Lauf; unveränderliche Konfiguration; Enrollment, Signaturen, Nonce-Replay, fremde Läufe, Ereignisreihenfolge und Pflichtverifikation; Artefaktmanipulation; Verschlüsselung; Backup/Restore und Schlüsselprüfung; Abgleich verlassener Läufe erst nach Lease-Ablauf und ausdrücklicher lokaler Bestätigung |
| `tests/test_protocol.py` | 8 bestanden | Tatsächlicher Runner gegen die HTTP-API über einen Testtransport mit gültigen Ed25519-Signaturen; Check-/Apply-Abläufe, verlorene Abschlussantwort, simulierter Neustart, fehlgeschlagene Verifikation und Operatorfortsetzung, wiederholte Abbruchbestätigung, optionale Fehler mit verpflichtender Abschlussprüfung sowie redigierte Logs |
| `tests/test_runner.py` | Windows: 25 bestanden, 2 übersprungen; Linux-CI: 27 bestanden | Dauerhafte Checkpoints, Wiederaufnahme nach Unterbrechung, Ablauf von Leases, Rebootbudget, Queue- und Logbegrenzung, Secrets-Redigierung, Hashprüfung, TLS-Zwang, eigenständiger Starthelfer, lokale Hardwaremerkmale, echte OpenSSL-Signaturen, SSH-Schlüsselprüfung, Repositoryprüfung, Bash-Syntaxprüfung sowie Prozess-Timeout und Ausgaberedigierung |
| `tests/test_ci.py` | 68 bestanden | Veröffentlichung nur von geschützten Standardbranch-/Versions-Refs und erlaubten GitHub-Ereignissen; Release-Versionsabgleich; GHCR-Pfade; kein Push nach Build-/Smoke-Fehlern; Digest-Artefakte; temporäre Docker-Zugangsdaten und Image-Tags; Isolation nach Run-ID, Wiederholung und Job-ID; eigene Python-Testumgebung mit Aufräumen bei Erfolg und Fehlern; fehlende Host-Werkzeuge verhindern übersprungene Pflichtprüfungen |
Die API- und Protokolltests verwenden eine temporäre SQLite-Datenbank sowie
temporäre Schlüssel. Die Protokolltests führen die Runner-Steuerung und
Signaturprüfung aus; die eigentlichen Modulphasen werden simuliert. Runner-Tests
prüfen Bash-Syntax und eingebetteten Python-Code ohne Provisionierungsänderungen.
Das ist kein Nachweis für funktionierende Betriebssysteminstallation oder
erfolgreiche Konfiguration echter Proxmox-Dienste.
Unter Windows benötigt die Veröffentlichung von Bash-Modulen Git Bash.
Der vorhandene WSL-Bash-Starter ohne Linux-Distribution genügt nicht. Die Anwendung
erkennt die native Git-Bash-Installation für die Syntaxprüfung.
Alle Tests erneut ausführen:
```powershell
.\.venv\Scripts\python.exe -m pytest
```
Beim Testlauf können Upstream-DeprecationWarnings von Starlette/httpx und AnyIO
erscheinen. Sie sind von fehlgeschlagenen Tests zu unterscheiden.
## Containerprüfung
`proxmox-ais-server:0.1.0` wurde erfolgreich mit Python 3.13 und Debian slim
gebaut. Das Dockerfile fixiert die geprüfte Basis über den Digest
`sha256:9d2e5553305c7c7b0097999bb17187c69b921ccd6bc9d40e4bb5ebe652c00285`.
Ein kurzlebiger Container wurde mit schreibgeschütztem Dateisystem, deaktiviertem
Netzwerk und entfernten Linux-Capabilities gestartet. Die Prüfung ergab:
| Merkmal | Beobachtung |
| --- | --- |
| Benutzer | UID 10001 |
| Bash | `/usr/bin/bash`, GNU Bash 5.2.37 |
| OpenSSL | `/usr/bin/openssl`, OpenSSL 3.5.7 |
| CLI-Einstiegspunkt | `proxmox-ais`, Unterbefehle `init`, `serve`, `backup`, `restore` |
Auch Wheel-Erstellung und Installation wurden erfolgreich geprüft. In einem
früheren Linux-Testlauf im Anwendungsimage bestanden 48 Tests; ein Test wurde übersprungen, weil
`ssh-keygen` im Anwendungsimage nicht installiert ist. Die SSH-Schlüsselprüfung
wurde unter Windows ausgeführt. Der Linux-Lauf umfasste die POSIX-spezifischen
Prüfungen zur Prozessbeendigung nach Timeout und zur Redigierung von Ausgaben.
Der Testcontainer lief ohne Rootrechte, mit schreibgeschütztem Dateisystem,
entfernten Capabilities und `no-new-privileges`.
`tests/container_smoke.py` prüfte zusätzlich den installierten Dienst als echten
Prozess im Container: Anmeldung über TLS mit eigener vertrauenswürdiger Test-CA,
Ablehnung eines nicht vertrauten Zertifikats, CSRF, ausgelieferte Webdateien sowie
Erhalt von Hosts und Sitzungen nach einem Dienstneustart. Der Prozess lief als
UID 10001 mit schreibgeschütztem Root-Dateisystem, ohne Capabilities und mit
`no-new-privileges`; sämtliche Testdaten lagen in einem temporären Verzeichnis.
Die folgende Abfrage verändert weder Anwendungsdaten noch Konfiguration. Reproduzierbarer
Befehl für Bash unter Linux oder Git Bash:
```bash
docker run --rm --read-only --network none --cap-drop ALL \
--entrypoint python proxmox-ais-server:0.1.0 \
-c 'import os, shutil, subprocess; print(os.getuid()); print(shutil.which("bash")); print(shutil.which("openssl")); subprocess.run(["bash", "--version"], check=True); subprocess.run(["openssl", "version"], check=True)'
```
TLS-Reverse-Proxy, reale Provisionierungsnetze, Medienboot, Initialisierung und
Wiederherstellung in der späteren Betriebsumgebung müssen zusätzlich geprüft werden.
Ein erfolgreicher Image-Build allein ist keine Betriebsfreigabe.
## GitHub Actions und GHCR
Der [Workflow](../.github/workflows/ci.yml) verwendet GitHub-gehostete
Ubuntu-24.04-Runner, Python 3.13 und Node.js 24. Python- und JavaScript-Prüfung
müssen erfolgreich sein, bevor das Containerimage gebaut und getestet wird.
Die [Einrichtungsanleitung](github-actions.md) beschreibt Schutzregeln,
Paketberechtigungen und die erste Veröffentlichung.
Bei der Migration wurden die folgenden Prüfungen lokal ausgeführt:
- Die vollständige Windows-Testsuite bestand mit 115 erfolgreichen Tests;
zwei Linux-Prozesstests wurden plattformbedingt übersprungen.
- Unter Linux/Python 3.13.15 führte `ci/python-tests.sh` alle 117 Tests
erfolgreich als UID 10001 aus. Das Skript installierte die Abhängigkeiten
in einer eigenen virtuellen Umgebung, schrieb den JUnit-Bericht und
entfernte die Umgebung anschließend.
- Die 68 CI-Skripttests prüfen mit Docker- und Python-Testdoubles
GitHub-Ereignisse, geschützte Refs, Versionsabgleich, GHCR-Pfade,
Publish-Reihenfolge, Digest-Artefakte und Fehlerpfade. Run-ID, Wiederholung
und Job-ID isolieren temporäre Ressourcen. Ein Build- oder Smoke-Fehler
verhindert Registry-Anmeldung und Push.
- Die im Workflow eingebettete Veröffentlichungsentscheidung wurde mit
17 Fällen ausgeführt: geschützte Branches und Release-Tags, manueller Start,
andere Standardbranchnamen, Pull Requests, ungeschützte Refs, ungültige Tags
und Shell-Sonderzeichen im Ref-Namen.
- `actionlint` 1.7.12 prüfte den Workflow mit genau einer ausgenommenen
Schema-Meldung: Die aktuelle Version kennt `concurrency.queue` noch nicht.
`queue: max` wurde gegen die offizielle
[GitHub-Syntax](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency)
geprüft; die fehlende Linter-Unterstützung ist in
[actionlint #657](https://github.com/rhysd/actionlint/issues/657) dokumentiert.
Weitere Linter-Befunde gab es nicht.
- `node --check provisioner/static/app.js` und `docker compose config --quiet`
mit gesetzten Beispielwerten für Image und öffentliche URL waren erfolgreich.
- `ci/container.sh verify` lief mit GitHub-Umgebungsvariablen gegen den lokalen
Linux-Docker-Dienst 29.7.2. amd64-Build und Container-Smoke-Test bestanden:
vertrauenswürdiges TLS, Ablehnung eines fremden Zertifikats, Anmeldung,
CSRF, Webdateien sowie Erhalt von Hosts und Sitzungen beim Dienstneustart.
Das Skript erzeugte `build.env` und entfernte den temporären Image-Tag.
Ein tatsächlicher GitHub-Actions-Lauf einschließlich GHCR-Anmeldung,
Registry-Push und Artefakt-Upload wurde bei dieser lokalen Prüfung nicht
ausgeführt. Die Push-Fehlerpfade und Digest-Auswertung sind durch die
CI-Skripttests abgedeckt; die Paketberechtigungen und aktiven Schutzregeln
müssen im GitHub-Repository eingerichtet und beim ersten Lauf bestätigt werden.
## Browserprüfung
In Chromium wurden Anmeldung, alle neun Verwaltungsansichten, Hostanlage und
Hostbearbeitung, Geheimnisreferenzen, Gruppentoken, ISO-Registrierung und Buildbefehl,
Modulvorlagen, Skript- und Profilveröffentlichung, Installationsvorschau,
Freigabe, Abbruch, Laufabgleich, Benutzeranlage und Leserrechte durchgespielt.
Die mobile Ansicht bei 390 Pixeln wurde ebenfalls geprüft. Es traten keine
JavaScript- oder CSP-Fehler auf. Die separate Testkonfiguration verwendete
temporäre Daten und deaktivierte die Vieraugenregel für den Formulartest mit
einem Administrator; die Vieraugenregel wurde in den API-Tests geprüft.
## API-Artefakt
[openapi.json](openapi.json) wird über `create_app(Settings(...)).openapi()` aus
dem aktuellen Anwendungscode erzeugt. Der Export verwendet ausschließlich
temporäre Testverzeichnisse, keine produktiven Benutzer oder Schlüssel. Er
enthält Schemas und Endpunktbeschreibungen, keine Datenbankinhalte, Sessions oder
Geheimniswerte. Bei Änderungen an den API-Modellen muss der Export erneuert werden.