Files
infrastructure/docs/platform/llm-server/encoding/GNENC-002-PLAN_Pipeline.md
T
spektr 88b063a234 Encoding/CT-101: PVE-Host-NFS konvergiert, Berechtigungsmodell entschieden, Verifikation dokumentiert.
- pve_nfs_export: NFSv4-only via /etc/nfs.conf ([nfsd] vers2/vers3=n; Debian-13-Unit
  startet nfsd ohne EnvironmentFile, RPCNFSDARGS wirkungslos), Restart-Handler,
  selbstheilende nfsd-Versionspruefung; verify-production um Export-Root- und
  Versions-Assertions erweitert
- Autorisierte Konvergenz auf dem PVE-Host: Export-Root 100000:970 2775,
  /proc/fs/nfsd/versions -3 +4 +4.1 +4.2, verify ok=17 changed=0, Idempotenz changed=0
- Berechtigungsmodell (Betreiberentscheidung 30.10.2026): Mode 2777 auf input/- und
  output/-Baum der Pipeline (LXC-idmap-Diskrepanz guest-root vs. UID 982/983:970)
- GNENC-000/001/002, GNAI-017/018/019 auf NFS-auf-PVE-Host-Stand gebracht;
  CT-101-Verifikation ok=47 changed=0 dokumentiert; AGENTS.md-Seccomp-Hinweis korrigiert

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

14 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 (LXC-idmap): Die guest-seitige Gruppe gn-media (GID 970) in CT-101 erscheint auf dem Host als GID 100970 und gleicht die reale Host-GID 970 der Pipeline nicht ab — die Gruppen-Alignment wirkt nur innerhalb von ghostnet-ki. Die SMB-Schreibpfade aus CT-101 laufen über guest-root (Host-UID 100000, da admin users Dateioperationen als root ausführt); siehe Abschnitt 11.

4. Verzeichnislayout

/var/lib/ghostnet-encoding/          (NFS-Mount: 10.0.0.2:/media/datenhdd3/Encoding, PVE-Host)
├── 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 (Host-UID 100000; admin users führt Dateioperationen als root aus). Da die Pipeline-Verzeichnisse via NFS den realen UIDs 982/983:970 gehören und diese im LXC-idmap nicht abgebildet sind (im Gast: nobody:nogroup), tragen input/ und der output/-Baum Mode 2777; jd/, queue/, work/ und state/ bleiben eng (2770/0750).

Layout-Anlage: Wegen root_squash auf dem Export kann Ansible als root keine chowned-Verzeichnisse anlegen; die Struktur wird als gn-encoding angelegt (runuser; Muster der Rolle video_encoding) oder vom Worker beim Start erzeugt und geprüft.

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. NFSv4.2-Mount-Abnahme von ghostnet-ki gegen 10.0.0.2 — inkl. Schreib-/Lesetest als UID 982/983 über den Mount und mv-Simulation als ghost über SMB; mit der Encoding-Implementierung.
  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).