Files
infrastructure/docs/platform/llm-server/encoding/GNENC-002-PLAN_Pipeline.md
T
spektr 69930f815a Encoding: Pipeline-Konten, NFS-Mount, Layout und Handover-Simulation umgesetzt und verifiziert.
- workload_accounts: gn-media (GID 970), gn-download (983), gn-dashboard (984);
  gn-media als primäre Gruppe der Pipeline-Konten — AUTH_SYS ueber NFSv4 wirkte
  serverseitig nicht mit Supplementary-Gruppen (Betriebsbefund 30.10.2026);
  Sudoers des Deploy-Kontos auf (ALL:ALL) verbreitert (become_user)
- video_encoding: NFS-Client, Mount-Unit und Pipeline-Layout auf dem Export
  (10.0.0.2:/media/datenhdd3/Encoding -> /var/lib/ghostnet-pipeline, ausserhalb
  der Homes); Layout-Anlage als Dienstkonten wegen root_squash; Verifikation
  der Verzeichnisrechte als gn-encoding (2777/2770/0750/2750, Gruppe 970)
- pve_media_cron: Legacy-Crontab (chown root:root + chmod -R 0777 auf allen
  Medienplatten, alle 10 min) entfernt — haette jedes echte Berechtigungsmodell
  plattgewalzt; Backup unter /var/backups/ghostnet/pve/; verify-production
  prueft das Fehlen ab
- verify-pipeline-handover.yml: Handover-Simulation über alle Identitaeten
  (CT-101-.dlc-Drop -> gn-download -> queue -> gn-encoding -> output/state ->
  gn-dashboard); einmal vollstaendig bestanden (30.10.2026)
- Doku: GNENC-000/001/002, GNAI-017 (6.10/6.11), GNMON-001 auf Stand gebracht

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-10-30 20:47:42 +01:00

278 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Encoding-Pipeline
**Dokumenten-ID:** GNENC-002-PLAN
**Titel:** Encoding-Pipeline: Komponenten, Konten, Verzeichnislayout und Datenfluss
**Stand:** 2026-10-30
**Status:** Finaler Plan nach Betreiberentscheidungen vom 19.10.2026; externe Abhängigkeiten umgesetzt (GNAI-019)
**Vorgänger:** [GNENC-001-PLAN_Architecture.md](GNENC-001-PLAN_Architecture.md)
**Monitoring:** [GNMON-001-PLAN_Architecture.md](../monitoring/GNMON-001-PLAN_Architecture.md)
**Samba/NFS:** [GNAI-017](../../proxmox-server/lxc/ct-101-samba/GNAI-017-PLAN_Samba-NFS-Export.md), [GNAI-019](../../proxmox-server/lxc/ct-101-samba/GNAI-019-DEC_Ansible-Migration.md) — SMB auf CT-101, Kernel-NFS auf dem PVE-Host
## 1. Architekturprinzip
Jede Komponente führt einen Schritt aus und lebt in einem Podman-Container
(rootless, eigener Store je Konto). Der Handover erfolgt ausschließlich über
das Filesystem auf dem NFS-Mount `/var/lib/ghostnet-encoding`: Jede Komponente
liest einen Input-Ordner, der der Output-Ordner des vorangegangenen Schritts
ist. In-Progress-Daten liegen in Work-Ordnern; Übergaben erfolgen per atomarem
`mv` (`rename` innerhalb desselben Mounts). Dadurch erkennt der nächste Schritt
niemals halbfertige Dateien.
## 2. Komponenten
| Komponente | Konto | Image | Aufgabe | Betrieb |
| --- | --- | --- | --- | --- |
| VPN-Sidecar | `gn-download` (983) | `qmcgaw/gluetun` (gepinnter Digest) | ProtonVPN-WireGuard-Tunnel mit Kill Switch; jDownloader2 teilt seinen Netzwerk-Namespace, gesamter Download-Verkehr läuft durch den Tunnel | dauerhaft |
| Download | `gn-download` (983) | `jlesage/jdownloader-2` (gepinnter Digest) | liest `.dlc` aus `input/<Profil>/`, lädt über den VPN-Tunnel herunter, entpackt nach `jd/`, Event-Scripter verschiebt den Release-Ordner atomar nach `queue/<Profil>/<Release>/` und räumt die `.dlc` auf | dauerhaft |
| Encoding-Worker | `gn-encoding` (982) | Worker-Image (Eigenbau, gepinnt) | scannt `queue/`, filtert, encodet alle gültigen Videodateien, validiert, räumt auf, schreibt Status, Housekeeping | dauerhaft; Fenster-Check 00:00–06:00 Europe/Berlin |
Dashboard und Metrics-Collector werden im Monitoring-Bereich betrieben
(GNMON-001); das Dashboard liest `state/` read-only.
## 3. Konten und Berechtigungen
| Konto | UID/GID | Sub-ID-Bereich | Home | Config | Gruppen |
| --- | --- | --- | --- | --- | --- |
| `gn-encoding` | 982 | 493216–558751 | `/var/lib/ghostnet-encoding` | `/etc/ghostnet-encoding` | `gn-media` |
| `gn-download` | 983 | 558752–624287 | `/var/lib/ghostnet-download` | `/etc/ghostnet-download` | `gn-media` |
| `gn-dashboard` | 984 | 624288–689823 | `/var/lib/ghostnet-dashboard` | `/etc/ghostnet-dashboard` | `gn-media` (nur lesend) |
Gruppe `gn-media`: gemeinsame Gruppe der drei Konten für die Übergabe-
Verzeichnisse und den lesenden Zugriff auf `state/`; GID 970. Alle Konten sind
gesperrt (Passwort `!`, Shell `/usr/sbin/nologin`, Linger aktiv).
**Anmerkung 30.10.2026 (primäre Gruppe):** Die drei Pipeline-Konten tragen
`gn-media` als **primäre** Gruppe (die eigenen GID-Gruppen 982/983/984 bleiben
für die Config-Verzeichnisse als Supplementary-Gruppe erhalten). Grund:
AUTH_SYS über NFSv4 wirkte serverseitig nicht mit Supplementary-Gruppen —
`mkdir` als 982/983 bekam EACCES, obwohl `gn-media` Gruppen-Schreibrechte
hatte; mit primärer Gruppe 970 war der Zugriff sofort erfolgreich
(Betriebsbefund 30.10.2026, Testreihe auf ghostnet-ki). Zudem gilt die
LXC-idmap-Diskrepanz: die guest-seitige Gruppe `gn-media` (GID 970) in CT-101
erscheint auf dem Host als GID 100970 — die SMB-Schreibpfade laufen über
World-Write (Mode 2777), nicht über Gruppen-Abgleich; siehe Abschnitt 4.
## 4. Verzeichnislayout
```text
/var/lib/ghostnet-pipeline/ (NFS-Mount: 10.0.0.2:/media/datenhdd3/Encoding, PVE-Host; außerhalb der Konto-Homes)
├── input/ gn-media 2777 SMB-Drop ([Encode-In])
│ └── <Profil>/ gn-media 2777 nur .dlc
├── jd/ gn-media 2770 JD2-Arbeitsverzeichnis (gn-download)
│ ├── <Release>/ Download + Entpacken
│ ├── dlc_done/ erfolgreich abgearbeitete .dlc
│ └── failed/ .dlc mit Download-Fehler
├── queue/<Profil>/<Release>/ gn-media 2770 Roh-Queue, atomar befüllt
├── work/<Release>/ gn-encoding 0750 Encode-Ziele in Arbeit
├── output/ gn-media 2777 Abholung ([Encode])
│ ├── <Profil>/<Release>/ gn-media 2777 Erfolge bis Abholung
│ └── _failed/<Release>/ gn-media 2777 Quarantäne
└── state/ gn-media 2750 Status (Dashboard liest)
├── jobs/<job_id>.json gn-encoding 0750 Status pro Videodatei
├── tmp/ gn-encoding 0750 atomare Schreib-Zwischenlage
└── system.json Collector 0644 Systemmetriken (Monitoring)
```
`state/` ist `gn-encoding:gn-media` mit Mode `2750`, damit `gn-dashboard` lesen
kann; `state/jobs/` und `state/tmp/` bleiben `0750` (nur Worker).
`state/system.json` wird vom Metrics-Collector des Monitoring-Bereichs mit
`0644` geschrieben.
Handover-Regeln:
- Übergabe immer per atomarem `mv` innerhalb des Mounts; Work-Dateien liegen
nie in Übergabe-Ordnern.
- `jd/`, `queue/` mit Mode `2770` (setgid); `state/` mit `2750` (setgid).
- `input/` (inkl. Profil-Ordner) und der gesamte `output/`-Baum (inkl.
Release- und `_failed/`-Verzeichnisse, vom Worker angelegt) mit Mode
`2777` (setgid) — Welt-Schreibrecht für den SMB-Schreibpfad aus CT-101
(Betreiberentscheidung 30.10.2026, siehe unten). Neue Dateien erben die
Gruppe `gn-media`.
- jDownloader2 läuft mit `UMASK=022`, damit der Worker die Dateien lesen kann.
- Der Worker löscht Quelldateien und geleerte Release-Ordner aus `queue/`;
Löschrechte folgen aus dem Schreibrecht der Gruppe am Verzeichnis (kein
Sticky-Bit).
- `work/` wird pro Lauf geleert; `state/tmp/` dient nur atomaren
Schreibzugriffen.
**Berechtigungsmodell (Betreiberentscheidung 30.10.2026):** Die SMB-Zugriffe
aus CT-101 laufen über guest-root bzw. beliebige Gast-Identitäten (Host-UIDs
100000+, da `admin users` Dateioperationen als root ausführt); die
Pipeline-Verzeichnisse gehören via NFS den realen UIDs 982/983 mit primärer
Gruppe 970. Da diese im LXC-idmap nicht abgebildet sind, tragen `input/` und
der `output/`-Baum Mode `2777` (World-Write); `jd/`, `queue/`, `work/` und
`state/` bleiben eng (2770/0750/2750). Der frühere Legacy-Cron auf dem
PVE-Host (`chown -R root:root` + `chmod -R 0777` auf allen Medienplatten alle
10 Minuten, installiert 11.10.2026) hätte jedes echte
Berechtigungsmodell plattgewalzt und wurde am 30.10.2026 entfernt (Rolle
`pve_media_cron`, Backup der Crontab unter `/var/backups/ghostnet/pve/`).
**Layout-Anlage:** Wegen `root_squash` auf dem Export kann Ansible als root
keine chowned-Verzeichnisse anlegen; die Struktur wird als `gn-encoding`
beziehungsweise `gn-download` angelegt (`become_user`; die Rolle
`video_encoding` erzeugt und verifiziert das Layout vollständig — Stand
30.10.2026 umgesetzt, Verifikation bestanden).
**Abholung:** Die fertigen Outputs werden über CT-101 per SMB abgeholt: Share
`[Encode]` (Pfad `/media/datenhdd3/Encoding/output`, RW für `ghost`, lesend für
`daidalaw`/`debugofen`). Die Entnahme per `mv` in die Bibliotheks-Shares
(Filme/Serien/Anime) räumt `output/` auf. Ein optionaler Clean-Job
(Worker-Housekeeping, Abschnitt 6) löscht Dateien aus `output/`, die älter als
30 Tage sind. Auf `ghostnet-ki` läuft kein smbd. Der `.dlc`-Drop erfolgt über
Share `[Encode-In]` (Pfad `/media/datenhdd3/Encoding/input`, RW für `ghost`)
— siehe GNAI-017.
## 5. Ablauf: Download (jDownloader2 über VPN)
1. `.dlc`-Datei in `input/<Profil>/` ablegen (Profil = Preset-Name) — per SMB
über die Share `Encode-In` oder direkt auf dem Mount.
2. jDownloader2 lädt über den VPN-Tunnel herunter und entpackt nach
`jd/<Release>/`.
3. Event-Scripter (versioniert in git, ohne Secrets, per Ansible in die
JD2-Config deployed):
- Paket fertig: atomares `mv jd/<Release>/ queue/<Profil>/<Release>/`;
`.dlc` nach `jd/dlc_done/`.
- Download-Fehler: `.dlc` nach `jd/failed/`.
4. Credentials liegen server-lokal in `/etc/ghostnet-download/jdownloader/`
(root-owned, Gruppe `gn-download`, Mode 0750, als `/config` gemountet). Im
Repository liegt nur ein Platzhalter/Beispiel; Ansible prüft Existenz, nie
Inhalt.
5. Die Web-UI des Images wird nicht extern freigegeben; Zugriff nur über
localhost/SSH-Tunnel.
**VPN-Tunnel (gluetun-Sidecar):**
- Image `qmcgaw/gluetun` (Digest gepinnt), Konto `gn-download`; JD2-Container
mit `Network=container:<vpn>` teilt den Netzwerk-Namespace — JD2 hat keine
eigene IP, der gesamte Verkehr verlässt den Host über ProtonVPN.
- ProtonVPN über WireGuard (`VPN_SERVICE_PROVIDER=protonvpn`); die Config
(Private Key, Server, Land) stammt aus dem ProtonVPN-Account und liegt
server-lokal in `/etc/ghostnet-download/vpn.env` (root-owned, `0640
root:gn-download`, per `EnvironmentFile=` geladen). Im Repository nur
`vpn.env.example`.
- Kill Switch aktiv: gluetuns Firewall blockiert jeglichen Nicht-VPN-Traffic,
sobald der Tunnel fällt.
- Quadlet: `NET_ADMIN` + `--device /dev/net/tun`. TUN in der rootless User-
Namespace wird bei der Umsetzung auf dem Host verifiziert; Fallback ist ein
host-seitiger WireGuard-Service mit Policy-Routing für den Download-Pfad.
- Hinweis: Hoster limitieren gelegentlich VPN-IPs; bei stockenden Downloads
Serverland in `vpn.env` wechseln. Die Management-VPN (CT-105) bleibt davon
unberührt.
## 6. Ablauf: Encoding-Worker
Pro Release (älteste zuerst, genau ein gleichzeitiger Encode), pro Videodatei
sequenziell:
1. **Scan & Filter:** `queue/<Profil>/<Release>/` durchsuchen. Video-Endungen
(case-insensitiv): `mkv, mp4, m4v, avi, mov, wmv, flv, webm, mpg, mpeg,
ts, m2ts, vob, 3gp, ogv`. Dateien < 100 MB und Nicht-Video-Dateien werden
ignoriert. Es gibt keine Hauptdatei; alle gültigen Videodateien werden
encodet.
2. **Status `queued` → `encoding`** in `state/jobs/<job_id>.json` (inkl.
Fortschritt aus dem HandBrakeCLI-Output).
3. **Encode:** `HandBrakeCLI --import-preset <Profil>.json -Z "<Profil>" -i
<quelle> -o work/<Release>/<stem>.mkv`. Presets liegen unter `profiles/`,
werden per Ansible auf den Host deployed und read-only gemountet.
4. **Validierung** (am Ziel in `work/`, vor der Übergabe); Status `verifying`:
- HandBrakeCLI-Exit 0
- Zieldatei nicht leer
- `ffprobe` findet mindestens einen Videostream
- `ffmpeg -v error -i <ziel> -f null -`: Exit 0 **und** leere stderr
(ffmpeg dekodiert tolerant; Fehlerzeilen auf stderr gelten als Defekt)
- ffprobe-Dauerabweichung Ziel vs. Quelle ≤ 1 s
5. **Erfolg:** atomares `mv` nach `output/<Profil>/<Release>/`, Quelldatei
löschen, Status `done`.
**Fehler:** Zieldatei löschen, Quelldatei nach `output/_failed/<Release>/`
verschieben, Status `failed` mit Fehlermeldung. Übrige Dateien des Releases
werden weiter verarbeitet.
6. Release-Ordner in `queue/` löschen, sobald leer.
**Zeitfenster:** Vor jedem neuen Release prüft der Worker, ob 00:00–06:00 Uhr
Europe/Berlin aktiv ist; außerhalb idlet er. Ein laufendes Release wird nicht
abgebrochen und darf über 06:00 hinaus laufen.
**Housekeeping (optional, standardmäßig aktiviert, deaktivierbar):** Im
Idle-Durchlauf löscht der Worker Dateien in `output/`, die älter als 30 Tage
sind (Clean-Job als Sicherheitsnetz; die Abholung per `mv` räumt regulär auf).
## 7. Statusmodell
Eine JSON-Datei pro Videodatei unter `state/jobs/<job_id>.json`, atomar
geschrieben (Schreibzugriff in `state/tmp/`, dann `mv`):
```json
{
"job_id": "<Release>__<Dateiname>",
"release": "<Release>",
"profile": "AV1 MKV 1080p30 RF25",
"source": "queue/<Profil>/<Release>/<Datei>",
"target": "output/<Profil>/<Release>/<Datei>.mkv",
"status": "queued | encoding | verifying | done | failed",
"progress_pct": 0.0,
"source_duration_s": 7231.4,
"target_duration_s": 7231.2,
"source_bytes": 0,
"target_bytes": 0,
"started_at": "…",
"finished_at": "…",
"error": null
}
```
Lebenszyklus: `queued` → `encoding` → `verifying` → `done` | `failed`.
Aufbewahrung: `done` nach 30 Tagen gelöscht; `failed` bis zur manuellen
Prüfung.
`state/system.json` (Systemmetriken) wird vom Metrics-Collector des
Monitoring-Bereichs bereitgestellt (GNMON-001).
## 8. Ressourcenlimits
Quadlet-/cgroup-Limits, in der User-Session durchgereicht:
| Ressource | Encoding-Worker | jDownloader2 | VPN-Sidecar |
| --- | --- | --- | --- |
| MemoryMax | 16G | 2G | 256M |
| CPUWeight | 100 | 50 | 50 |
| Nice | 10 | 10 | — |
| IOWeight | 50 | 20 | — |
Keine CPU-Obergrenze fürs Encoding. Feinjustierung nach einem repräsentativen
Lasttest unter Inferenzlast (GNENC-001 Abschnitt 4).
## 9. Monitoring (Auslagerung)
Dashboard und Metrics-Collector werden im Monitoring-Bereich geplant und
betrieben (GNMON-001, `llm-server/monitoring/`). Die Encoding-Pipeline stellt
`state/jobs/*.json` bereit; der Collector schreibt `state/system.json`; das
Dashboard mountet `state/` und `queue/` read-only und zeigt Ressourcen sowie
Encoding-Übersicht. Das Dashboard hat keinen GPU- und keinen Schreibzugriff.
## 10. Images
| Image | Basis | Inhalt | Pinning |
| --- | --- | --- | --- |
| Worker | `linuxserver/handbrake:1.11.2-ls48` @ `sha256:fda92bd1…48a3c7` | + python3, ffmpeg | Digest nach Bau in Ansible-Vars gepinnt; Build rootless als `gn-encoding` |
| jDownloader2 | `jlesage/jdownloader-2` | — | Digest gepinnt; Pull als `gn-download` |
| VPN-Sidecar | `qmcgaw/gluetun` | — | Digest gepinnt; Pull als `gn-download` |
Containerfiles unter `llm-server/encoding/images/`, Skripte unter
`llm-server/encoding/scripts/`, Presets unter `llm-server/encoding/profiles/`.
Skripte und Presets werden per Ansible auf den Host deployed und read-only in
die Container gemountet (Updates ohne Image-Rebuild, versioniert über git).
## 11. Offene Punkte
1. **Output-Pickup über CT-101 (SMB) einmal als geschlossener Ablauf
nachweisen:** Die Handover-Simulation (`verify-pipeline-handover.yml`)
lief am 30.10.2026 vollständig durch (`.dlc`-Drop aus CT-101 →
gn-download → queue → gn-encoding → output/state → gn-dashboard); das
Lesen/Löschen des Output-Testreleases über CT-101 wurde durch den
gleichzeitigen Server-Shutdown nicht mehr abgeschlossen und ist als
kurzer Nachtest zu wiederholen.
2. Feinjustierung der Ressourcenlimits nach Lasttest unter Inferenzlast
(GNENC-001 Abschnitt 4).
3. Rootless-TUN-Verifizierung für den VPN-Sidecar auf dem Host (Fallback:
host-seitiger WireGuard mit Policy-Routing).