- 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>
14 KiB
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
mvinnerhalb des Mounts; Work-Dateien liegen nie in Übergabe-Ordnern. jd/,queue/mit Mode2770(setgid);state/mit2750(setgid).input/(inkl. Profil-Ordner) und der gesamteoutput/-Baum (inkl. Release- und_failed/-Verzeichnisse, vom Worker angelegt) mit Mode2777(setgid) — Welt-Schreibrecht für den SMB-Schreibpfad aus CT-101 (Betreiberentscheidung 30.10.2026, siehe unten). Neue Dateien erben die Gruppegn-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)
.dlc-Datei ininput/<Profil>/ablegen (Profil = Preset-Name) — per SMB über die ShareEncode-Inoder direkt auf dem Mount.- jDownloader2 lädt über den VPN-Tunnel herunter und entpackt nach
jd/<Release>/. - Event-Scripter (versioniert in git, ohne Secrets, per Ansible in die
JD2-Config deployed):
- Paket fertig: atomares
mv jd/<Release>/ queue/<Profil>/<Release>/;.dlcnachjd/dlc_done/. - Download-Fehler:
.dlcnachjd/failed/.
- Paket fertig: atomares
- Credentials liegen server-lokal in
/etc/ghostnet-download/jdownloader/(root-owned, Gruppegn-download, Mode 0750, als/configgemountet). Im Repository liegt nur ein Platzhalter/Beispiel; Ansible prüft Existenz, nie Inhalt. - Die Web-UI des Images wird nicht extern freigegeben; Zugriff nur über localhost/SSH-Tunnel.
VPN-Tunnel (gluetun-Sidecar):
- Image
qmcgaw/gluetun(Digest gepinnt), Kontogn-download; JD2-Container mitNetwork=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, perEnvironmentFile=geladen). Im Repository nurvpn.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.envwechseln. Die Management-VPN (CT-105) bleibt davon unberührt.
6. Ablauf: Encoding-Worker
Pro Release (älteste zuerst, genau ein gleichzeitiger Encode), pro Videodatei sequenziell:
- 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. - Status
queued→encodinginstate/jobs/<job_id>.json(inkl. Fortschritt aus dem HandBrakeCLI-Output). - Encode:
HandBrakeCLI --import-preset <Profil>.json -Z "<Profil>" -i <quelle> -o work/<Release>/<stem>.mkv. Presets liegen unterprofiles/, werden per Ansible auf den Host deployed und read-only gemountet. - Validierung (am Ziel in
work/, vor der Übergabe); Statusverifying:- HandBrakeCLI-Exit 0
- Zieldatei nicht leer
ffprobefindet mindestens einen Videostreamffmpeg -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
- Erfolg: atomares
mvnachoutput/<Profil>/<Release>/, Quelldatei löschen, Statusdone. Fehler: Zieldatei löschen, Quelldatei nachoutput/_failed/<Release>/verschieben, Statusfailedmit Fehlermeldung. Übrige Dateien des Releases werden weiter verarbeitet. - 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
- NFSv4.2-Mount-Abnahme von
ghostnet-kigegen10.0.0.2— inkl. Schreib-/Lesetest als UID 982/983 über den Mount und mv-Simulation alsghostüber SMB; mit der Encoding-Implementierung. - Feinjustierung der Ressourcenlimits nach Lasttest unter Inferenzlast (GNENC-001 Abschnitt 4).
- Rootless-TUN-Verifizierung für den VPN-Sidecar auf dem Host (Fallback: host-seitiger WireGuard mit Policy-Routing).