- 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>
278 lines
15 KiB
Markdown
278 lines
15 KiB
Markdown
# 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).
|