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

15 KiB
Raw Blame History

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 Monitoring: GNMON-001-PLAN_Architecture.md Samba/NFS: GNAI-017, GNAI-019 — 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

/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):

{
  "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).