From ba7d82941078dd87562e74f898cdbaffc1ac3e51 Mon Sep 17 00:00:00 2001 From: spektr Date: Mon, 19 Oct 2026 14:42:14 +0200 Subject: [PATCH] =?UTF-8?q?Encoding:=20Ansible-Projekt=20und=20Grundzustan?= =?UTF-8?q?d=20f=C3=BCr=20gn-encoding=20eingerichtet=20und=20verifiziert.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit llm-server/encoding/ansible/ verwaltet mit video-encoding.yml und der Rolle video_encoding den CPU-Only-Grundzustand des gesperrten Kontos gn-encoding (UID/GID 982): Konto-Identität und Passwortsperre, subordinate ID-Bereiche, Verzeichnisstruktur, Quadlet-Verzeichnis, Linger, getrennter rootless Podman-Store sowie verwehrter GPU-Zugriff (keine render/video-Gruppe, kein Lesezugriff auf /dev/kfd oder DRM-Render-Nodes). Ohne beschlossene Encoder-Entscheidungen stellt die Rolle sicher, dass kein Workload definiert ist. Check-Mode und zwei Läufe auf ghostnet-ki: jeweils ok=30 changed=0 failed=0. Preflight, host.yml (2x ok=55 changed=0) und ROCm-Probe (ok=33 changed=0) nach der Projekt-Aufteilung erneut bestanden. GNENC-000/001, GNAI-000 und GNAI-013 aktualisiert. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- docs/GNAI-000-DOC_Index.md | 9 +- ...rity-Closure-and-Repository-Restructure.md | 29 +- .../encoding/GNENC-000-DOC_Index.md | 3 + .../encoding/GNENC-001-PLAN_Architecture.md | 61 ++-- llm-server/encoding/README.md | 10 + llm-server/encoding/ansible/README.md | 46 +++ llm-server/encoding/ansible/ansible.cfg | 12 + .../ansible/inventories/host/hosts.yml | 8 + .../ansible/playbooks/video-encoding.yml | 11 + .../ansible/roles/video_encoding/README.md | 22 ++ .../roles/video_encoding/defaults/main.yml | 32 ++ .../roles/video_encoding/tasks/main.yml | 337 ++++++++++++++++++ 12 files changed, 556 insertions(+), 24 deletions(-) create mode 100644 llm-server/encoding/README.md create mode 100644 llm-server/encoding/ansible/README.md create mode 100644 llm-server/encoding/ansible/ansible.cfg create mode 100644 llm-server/encoding/ansible/inventories/host/hosts.yml create mode 100644 llm-server/encoding/ansible/playbooks/video-encoding.yml create mode 100644 llm-server/encoding/ansible/roles/video_encoding/README.md create mode 100644 llm-server/encoding/ansible/roles/video_encoding/defaults/main.yml create mode 100644 llm-server/encoding/ansible/roles/video_encoding/tasks/main.yml diff --git a/docs/GNAI-000-DOC_Index.md b/docs/GNAI-000-DOC_Index.md index ba3f059..ad08066 100644 --- a/docs/GNAI-000-DOC_Index.md +++ b/docs/GNAI-000-DOC_Index.md @@ -48,7 +48,8 @@ jeweiligen Fachdokumenten. Die priorisierte weitere Umsetzung beschreibt der | [GNAI-008](platform/llm-server/ai/GNAI-008-PLAN_Ansible-Deployment.md) | detaillierter Ansible-/Gitea-Planungsstand und Implementierungshistorie | | [GNAI-009](platform/llm-server/ai/GNAI-009-PLAN_Llama-Model-Evaluation.md) | reproduzierbarer Modellvergleich | | [GNAI-010](platform/llm-server/ai/GNAI-010-TECH_Llama-Serving.md) | technische Sollkonfiguration des Testing-Servings | -| [LLM-Server-Ansible](../llm-server/ansible/README.md) | ausführbare Host-/ROCm-Playbooks und Rollen für `10.0.0.90` | +| [LLM-Server-Host-Ansible](../llm-server/host/ansible/README.md) | ausführbare Host-/ROCm-Playbooks und Rollen für `10.0.0.90` | +| [Encoding-Ansible](../llm-server/encoding/ansible/README.md) | ausführbares Ansible-Projekt für den CPU-Encoding-Workload auf `10.0.0.90` | | [Keycloak-Ansible](../proxmox/lxc/ct-102-keycloak/ansible/README.md) | ausführbare Provisionierungs-, Restore- und Prüfautomation für CT 102 | ### Video-Encoding @@ -66,6 +67,10 @@ Am 22.09.2026 wurden KI-Host und Proxmox-LXCs in getrennte Unterverzeichnisse Am 05.10.2026 wurde die Dokumentation geräteorientiert neu geordnet: `platform/proxmox-server/` bildet den Proxmox-Kontext und seine LXCs ab, `platform/llm-server/` den physischen KI-Server mit AI und Encoding. +Am 19.10.2026 wurde das ausführbare LLM-Server-Projekt in die fachlichen +Hauptverzeichnisse `llm-server/host/` (Grundkonfiguration des Servers), +`llm-server/llm/` (LLM-Stack) und `llm-server/encoding/` (Encoding) aufgeteilt; +die Dokumentation folgt derselben Dreiteilung. ## 2. Belegter Stand @@ -79,7 +84,7 @@ Am 05.10.2026 wurde die Dokumentation geräteorientiert neu geordnet: | DNS / Pi-hole | CT 100 unter `10.0.0.14` mit digest-gepinntem Pi-hole 2026.09.0 als Podman-Quadlet; SSH/nftables, UDP/TCP-DNS, Split-DNS, Blocking/Allowlist, FRITZ!Box-Cutover, LXC-Reboot und Stop-Mode-Backup am 01.10.2026 abgenommen | vollständige App-/OIDC-Regression, praktischer FRITZ!Box-Rollback, mobile DuckDuckGo-/IPv6-Strategie, WireGuard-Profile und hostunabhängiger Restore-Test | | Reverse Proxy | CT-103-Zugänge, SSH-/Firewall-Baseline, Reboot und lokales Backup abgenommen; NPM 2.15.1 routet öffentlichen OIDC- und internen Admin-vHost getrennt; öffentliche Keycloak-Administration liefert 403 | Fail2Ban-Jails/Bans und Wiederanlauf; hostunabhängige Backupkopie/Restore-Test | | Keycloak | CT 102 unter `10.0.0.4` reproduzierbar per Ansible aufgebaut; gepinntes Keycloak 26.2.4 und PostgreSQL 16.15, Restore von CT 104, Container-/DB-Härtung, NPM-Cutover, interner Adminzugang, öffentliche Admin-Sperre, realer Vaultwarden-OIDC-Login und LXC-Neustart abgenommen | 14-tägige Stabilisierungsphase, praktischer NPM-Rückfall auf CT 104, Betriebs-/Backup-/Restore-SOP und anschließende Rückfallbereinigung | -| Encoding | Konto, Pfade und eigener rootless Store | Workload, Zeitsteuerung, Ressourcenlimits und Lasttest | +| Encoding | Konto, Pfade, eigener rootless Store und CPU-Isolation per Ansible verwaltet und verifiziert | Workload, Zeitsteuerung, Ressourcenlimits und Lasttest | Ein Zielbild, eine Betreiberangabe oder ein erfolgreicher Teiltest gilt nicht als vollständige Abnahme. Datierte Details stehen im zuständigen Fachdokument. diff --git a/docs/GNAI-013-PLAN_Security-Closure-and-Repository-Restructure.md b/docs/GNAI-013-PLAN_Security-Closure-and-Repository-Restructure.md index b958fe5..b2ddc2f 100644 --- a/docs/GNAI-013-PLAN_Security-Closure-and-Repository-Restructure.md +++ b/docs/GNAI-013-PLAN_Security-Closure-and-Repository-Restructure.md @@ -39,7 +39,10 @@ Die ausführbare Infrastruktur ist seit 14.10.2026 ebenfalls hostbezogen: ```text ghostnet.ai-infrastructure/ -├── llm-server/ansible/ # physischer KI-Server +├── llm-server/ +│ ├── host/ansible/ # Grundkonfiguration des physischen KI-Servers +│ ├── llm/ # LLM-Stack (entsteht mit dem AI-Vertical-Slice) +│ └── encoding/ansible/ # CPU-Encoding-Workload ├── proxmox/ │ ├── shared/scripts/ # nur LXC-übergreifende PVE-Helfer │ └── lxc/ @@ -130,12 +133,34 @@ hostbezogenen Playbooks oder Inventare. Pi-hole-Pfad abstimmen. - [x] Lokale Ansible-`.venv` wegen des veralteten Repository-Pfads neu erstellen und alle Syntaxprüfungen regulär ausführen. -- [ ] Lesenden Preflight sowie Host-/ROCm-Verifikation wiederholen. +- [x] Lesenden Preflight sowie Host-/ROCm-Verifikation wiederholen. - [ ] Internes, nicht öffentliches `llama.cpp`-Testing-Vertical-Slice nach GNAI-010 implementieren und bei 64k abnehmen. ## 4. Arbeitsnachweise +### 19.10.2026 – LLM-Server erreichbar, Projekt fachlich aufgeteilt, Encoding-Grundzustand + +- Das Netzwerkkabel des KI-Servers war versehentlich gezogen; nach dem + Wiederanschluss ist `10.0.0.90` aus dem LAN erreichbar. Lesender Preflight + aus dem neuen Pfad `llm-server/host/ansible/`: + `ok=5 changed=0 unreachable=0 failed=0` (ghostnet-ki, Debian 13.6, 14 vCPUs, + 64 GiB RAM, zwei Navi-21-PCI-Geräte `1002:73bf`). +- Das LLM-Server-Projekt wurde in die fachlichen Hauptverzeichnisse `host/`, + `llm/` und `encoding/` aufgeteilt (analog zu `docs/platform/llm-server/`). + `host.yml` lief zweimal aus `llm-server/host/ansible/` mit jeweils + `ok=55 changed=0 unreachable=0 failed=0`; der gepinnte ROCm-Container-Probe + (`verify-rocm-container.yml`) meldete `ok=33 changed=0 failed=0`. +- Encoding: `video-encoding.yml` aus `llm-server/encoding/ansible/` verifiziert + den `gn-encoding`-Grundzustand (gesperrtes Konto UID/GID 982, subordinate + ID-Bereiche, Verzeichnisse, Linger, rootless Podman-Store, keine GPU-Gruppen + und kein Zugriff auf `/dev/kfd` oder DRM-Render-Nodes). Check-Mode und zwei + aufeinanderfolgende Läufe meldeten jeweils `ok=30 changed=0 failed=0`. Es + existiert weiterhin kein Encoding-Image, -Container, -Quadlet und -Timer. +- Offen bleibt in Abschnitt E: die Resolver-Abstimmung des KI-Servers auf den + Pi-hole-Pfad. Ein kontrollierter Kaltstart des Servers wurde nicht + durchgeführt; der Server lief durch, nur das Netzwerkkabel war gezogen. + ### 05.10.2026 – Struktur, Geheimnisse und lokaler VPN-Client - Dokumente geräteorientiert verschoben; lokaler Linkvalidator meldet diff --git a/docs/platform/llm-server/encoding/GNENC-000-DOC_Index.md b/docs/platform/llm-server/encoding/GNENC-000-DOC_Index.md index 04e8377..226a484 100644 --- a/docs/platform/llm-server/encoding/GNENC-000-DOC_Index.md +++ b/docs/platform/llm-server/encoding/GNENC-000-DOC_Index.md @@ -22,6 +22,9 @@ automatisch ausgeführt werden. - Das gesperrte Servicekonto `gn-encoding` mit UID/GID 982 und eigenem rootless Podman-Store ist eingerichtet und verifiziert. +- Das Encoding-Ansible-Projekt unter `llm-server/encoding/ansible/` verwaltet + und verifiziert den Encoding-Grundzustand (Konto, Pfade, CPU-Isolation, + rootless Store); Check-Mode und Idempotenzlauf meldeten jeweils `changed=0`. - Es sind noch kein Encoding-Image, kein Container, keine Quadlet-Unit und kein Timer eingerichtet. diff --git a/docs/platform/llm-server/encoding/GNENC-001-PLAN_Architecture.md b/docs/platform/llm-server/encoding/GNENC-001-PLAN_Architecture.md index 376c87b..2fbf8e8 100644 --- a/docs/platform/llm-server/encoding/GNENC-001-PLAN_Architecture.md +++ b/docs/platform/llm-server/encoding/GNENC-001-PLAN_Architecture.md @@ -24,7 +24,7 @@ unabhängig konkurrierende Sollzustände auf denselben Host anwenden. | Repository | Host- und Deployment-Ansible bleibt im bestehenden Infrastructure-Repository | angenommen | | Dokumentation | Encoding liegt unter `docs/platform/llm-server/encoding/` und verwendet die Kennung `GNENC` | umgesetzt | | Ausführungsidentität | eigenes gesperrtes Konto `gn-encoding`, UID/GID 982 | eingerichtet und mit rootless Podman geprüft | -| Containertrennung | eigener rootless Podman-Store und eigener rootverwalteter Quadlet-Pfad | eingerichtet; noch keine Workload definiert | +| Containertrennung | eigener rootless Podman-Store und eigener rootverwalteter Quadlet-Pfad | eingerichtet; Grundzustand per Ansible verwaltet und verifiziert; noch keine Workload definiert | | Rechenressource | Encoding ist nach Betreiberfestlegung rein CPU-basiert und erhält keinen GPU-Zugriff | Planungsentscheidung; reale Last noch nicht gemessen | | Parallelität | darf parallel zu AI Production oder AI Testing laufen | Planungsentscheidung | | Zeitsteuerung | automatische Ausführung ausschließlich außerhalb der Stoßzeiten, zunächst nachts | Richtung festgelegt; konkrete Uhrzeiten fehlen | @@ -77,29 +77,39 @@ in GPU-Gruppen und keine `/dev/kfd`- oder `/dev/dri`-Weitergabe. ## 5. Repository-Grenzen -Vorgesehene Ansible-Struktur: +Am 19.10.2026 wurde das LLM-Server-Projekt fachlich in ein Hauptverzeichnis pro +Aufgabe aufgeteilt; die Struktur folgt den Bereichen unter +`docs/platform/llm-server/`: ```text -llm-server/ansible/ -├── playbooks/ -│ ├── host.yml -│ ├── ai.yml -│ ├── video-encoding.yml -│ └── site.yml -└── roles/ - ├── host_baseline/ - ├── podman_runtime/ - ├── workload_accounts/ - ├── ai_stack/ - ├── ai_environment_switch/ - └── video_encoding/ +llm-server/ +├── host/ansible/ # gemeinsame Plattform des physischen Servers +│ ├── playbooks/ +│ │ ├── host.yml # host_baseline, podman_runtime (shared), workload_accounts, amd_gpu_access +│ │ ├── preflight.yml +│ │ ├── bootstrap-deploy-account.yml +│ │ ├── verify-deploy-access.yml +│ │ └── verify-rocm-container.yml +│ └── roles/ +│ ├── host_baseline/ +│ ├── workload_accounts/ +│ ├── amd_gpu_access/ +│ └── rocm_container_probe/ +├── llm/ # LLM-Stack; entsteht mit dem AI-Vertical-Slice (GNAI-001 Meilenstein 2) +└── encoding/ansible/ # CPU-Encoding-Workload + ├── playbooks/ + │ └── video-encoding.yml # video_encoding + └── roles/ + └── video_encoding/ ``` -`host.yml` verwaltet ausschließlich gemeinsame Plattformzustände. -`ai.yml` darf nur KI-Zustände verändern. `video-encoding.yml` verwaltet nur das -Konto `gn-encoding`, seine Pfade, Images, Quadlets, Timer und Health Checks. Ein -späteres `site.yml` kann die unterstützte Gesamtkonfiguration in definierter -Reihenfolge anwenden. +`host.yml` verwaltet ausschließlich gemeinsame Plattformzustände; die + gemeinsame Rolle `workload_accounts` legt dabei auch das Konto `gn-encoding` +an. `video-encoding.yml` verwaltet den Encoding-spezifischen Zustand des Kontos +`gn-encoding` — seine Pfade, Images, Quadlets, Timer und Health Checks — und +setzt die Host-Baseline voraus. Ein künftiges `ai.yml` im `llm/`-Projekt darf nur +KI-Zustände verändern. Ein späteres `site.yml` kann die unterstützte +Gesamtkonfiguration in definierter Reihenfolge anwenden. Ein separates Quellcode-Repository wird erst benötigt, wenn eigene Encoding-Software, eine Oberfläche oder ein Queue-Dienst entwickelt wird. Ein @@ -126,6 +136,17 @@ dank Linger unabhängig von einer Anmeldung. Der Podman-Store wurde als rootless und getrennt von beiden KI-Stores geprüft. Es wurden noch keine Images oder Container angelegt. +Am 19.10.2026 wurde das Encoding-Ansible-Projekt unter +`llm-server/encoding/ansible/` eingerichtet. `video-encoding.yml` verwaltet und +verifiziert seitdem den Encoding-Grundzustand: Konto-Identität und Passwort- +sperre, subordinate ID-Bereiche, Verzeichnisstruktur, Quadlet-Verzeichnis, +Linger und aktiver User-Manager, getrennter rootless Podman-Store sowie die +CPU-Isolation (keine GPU-Gruppenmitgliedschaft, kein Lesezugriff auf `/dev/kfd` +oder DRM-Render-Nodes). Solange die offenen Entscheidungen in Abschnitt 7 nicht +getroffen sind, stellt die Rolle zusätzlich sicher, dass weder Quadlet-Dateien +noch Container für `gn-encoding` existieren. Check-Mode und zwei +aufeinanderfolgende Läufe meldeten jeweils `ok=30 changed=0 failed=0`. + ## 7. Offene Entscheidungen 1. Welcher Encoder und welches fest versionierte Container-Image werden genutzt? diff --git a/llm-server/encoding/README.md b/llm-server/encoding/README.md new file mode 100644 index 0000000..b271e73 --- /dev/null +++ b/llm-server/encoding/README.md @@ -0,0 +1,10 @@ +# Video encoding workload + +CPU-based video encoding on `ghostnet-ki`, separated from the LLM stack. The +workload runs rootless under the locked `gn-encoding` account, has no GPU +access and is scheduled outside peak hours. + +The executable Ansible control project lives under +[`ansible/`](ansible/README.md). Architecture, confirmed decisions and open +questions are documented under +[`../../docs/platform/llm-server/encoding/`](../../docs/platform/llm-server/encoding/GNENC-000-DOC_Index.md). diff --git a/llm-server/encoding/ansible/README.md b/llm-server/encoding/ansible/README.md new file mode 100644 index 0000000..3cd304d --- /dev/null +++ b/llm-server/encoding/ansible/README.md @@ -0,0 +1,46 @@ +# Video encoding Ansible control project + +This directory contains the reproducible control project for the CPU-based +video encoding workload on `ghostnet-ki`. It belongs to the `encoding/` task +area of the LLM-Server host root; the shared host baseline lives in +[`../../host/ansible/`](../../host/ansible/README.md) and must be applied first. + +`playbooks/video-encoding.yml` applies the `video_encoding` role: it verifies +the locked `gn-encoding` account, enforces CPU-only device isolation, manages +the encoding directories and proves the separate rootless Podman store. No +encoder image, Quadlet, timer or schedule is defined yet; those are open +decisions in +[`../../../docs/platform/llm-server/encoding/`](../../../docs/platform/llm-server/encoding/GNENC-000-DOC_Index.md). + +## Control-node setup + +Use the shared virtual environment at the repository root (see +[`../../host/ansible/README.md`](../../host/ansible/README.md)). The +`ansible-deploy` private key stays outside this repository: +`~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy`. + +## Apply and verify + +Run the commands from this directory: + +```bash +cd llm-server/encoding/ansible +../../../.venv/bin/ansible-playbook \ + --inventory inventories/host/hosts.yml \ + playbooks/video-encoding.yml \ + --syntax-check + +../../../.venv/bin/ansible-playbook \ + --inventory inventories/host/hosts.yml \ + playbooks/video-encoding.yml \ + --check --diff \ + --private-key ~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy + +../../../.venv/bin/ansible-playbook \ + --inventory inventories/host/hosts.yml \ + playbooks/video-encoding.yml \ + --private-key ~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy +``` + +Run the final command a second time. The second recap must report `changed=0` +and `failed=0`; otherwise the role has not demonstrated convergence. diff --git a/llm-server/encoding/ansible/ansible.cfg b/llm-server/encoding/ansible/ansible.cfg new file mode 100644 index 0000000..b13fcf0 --- /dev/null +++ b/llm-server/encoding/ansible/ansible.cfg @@ -0,0 +1,12 @@ +[defaults] +home = .ansible +host_key_checking = True +interpreter_python = auto_silent +local_tmp = .ansible/tmp +retry_files_enabled = False +roles_path = roles:../../../shared/ansible/roles +timeout = 15 + +[ssh_connection] +pipelining = True +ssh_args = -o ControlMaster=auto -o ControlPersist=60s diff --git a/llm-server/encoding/ansible/inventories/host/hosts.yml b/llm-server/encoding/ansible/inventories/host/hosts.yml new file mode 100644 index 0000000..cd0993d --- /dev/null +++ b/llm-server/encoding/ansible/inventories/host/hosts.yml @@ -0,0 +1,8 @@ +--- +all: + children: + llm_servers: + hosts: + ghostnet-ki: + ansible_host: 10.0.0.90 + ansible_user: ansible-deploy diff --git a/llm-server/encoding/ansible/playbooks/video-encoding.yml b/llm-server/encoding/ansible/playbooks/video-encoding.yml new file mode 100644 index 0000000..51cd429 --- /dev/null +++ b/llm-server/encoding/ansible/playbooks/video-encoding.yml @@ -0,0 +1,11 @@ +--- +- name: Apply the Ghostnet video encoding workload baseline + hosts: llm_servers + become: true + gather_facts: true + gather_subset: + - min + - network + + roles: + - role: video_encoding diff --git a/llm-server/encoding/ansible/roles/video_encoding/README.md b/llm-server/encoding/ansible/roles/video_encoding/README.md new file mode 100644 index 0000000..3a2acb9 --- /dev/null +++ b/llm-server/encoding/ansible/roles/video_encoding/README.md @@ -0,0 +1,22 @@ +# Video encoding baseline + +The `video_encoding` role manages the CPU-only video encoding workload +baseline for the locked `gn-encoding` service account (UID/GID 982) on +`ghostnet-ki`. + +It verifies the account identity, the locked password and the deterministic +subordinate ID ranges, enforces the absence of GPU device groups, manages the +root-owned configuration directory, the input/output/work data directories and +the administrator-managed rootless Quadlet directory, and proves that the +account cannot read `/dev/kfd` or any DRM render node and uses its own rootless +Podman store. + +While no encoder image is selected (`video_encoding_image` empty), the role +additionally guards that no Quadlet file and no container exists for the +account. Encoder image, schedule, resource limits, queue handling and health +checks are open decisions in +`docs/platform/llm-server/encoding/GNENC-001` and are not assumed here. + +The account itself is created by the shared `workload_accounts` role in the +host project (`llm-server/host/ansible`); this role requires the host baseline +to be applied first. diff --git a/llm-server/encoding/ansible/roles/video_encoding/defaults/main.yml b/llm-server/encoding/ansible/roles/video_encoding/defaults/main.yml new file mode 100644 index 0000000..c80d940 --- /dev/null +++ b/llm-server/encoding/ansible/roles/video_encoding/defaults/main.yml @@ -0,0 +1,32 @@ +--- +video_encoding_expected_hostname: ghostnet-ki +video_encoding_expected_distribution: Debian +video_encoding_expected_distribution_major_version: "13" +video_encoding_expected_ipv4_address: 10.0.0.90 + +video_encoding_account: + name: gn-encoding + uid: 982 + gid: 982 + home: /var/lib/ghostnet-encoding + config_directory: /etc/ghostnet-encoding + shell: /usr/sbin/nologin + subid_start: 493216 + +video_encoding_subid_count: 65536 + +video_encoding_directories: + - path: /var/lib/ghostnet-encoding/input + mode: "0750" + - path: /var/lib/ghostnet-encoding/output + mode: "0750" + - path: /var/lib/ghostnet-encoding/work + mode: "0750" + +video_encoding_quadlet_user_directory: /etc/containers/systemd/users + +# Open decisions from docs/platform/llm-server/encoding/GNENC-001 section 7 +# stay unset on purpose. Until an encoder image, a schedule and resource limits +# are decided, the role only manages the shared encoding baseline and guards +# that no workload is defined. +video_encoding_image: "" diff --git a/llm-server/encoding/ansible/roles/video_encoding/tasks/main.yml b/llm-server/encoding/ansible/roles/video_encoding/tasks/main.yml new file mode 100644 index 0000000..31335f8 --- /dev/null +++ b/llm-server/encoding/ansible/roles/video_encoding/tasks/main.yml @@ -0,0 +1,337 @@ +--- +- name: Verify the target host before changing encoding state + ansible.builtin.assert: + that: + - ansible_facts["hostname"] == video_encoding_expected_hostname + - ansible_facts["distribution"] == video_encoding_expected_distribution + - >- + ansible_facts["distribution_major_version"] == + video_encoding_expected_distribution_major_version + - >- + ansible_facts["default_ipv4"]["address"] == + video_encoding_expected_ipv4_address + fail_msg: >- + The connected host does not match the declared Ghostnet AI server. No + encoding changes may be applied. + success_msg: The expected Debian host ghostnet-ki is connected. + +- name: Verify encoding account declarations + ansible.builtin.assert: + that: + - video_encoding_account.name == "gn-encoding" + - video_encoding_account.uid | int == 982 + - video_encoding_account.gid | int == 982 + - video_encoding_account.home is match("^/") + - video_encoding_account.config_directory is match("^/") + - video_encoding_subid_count | int > 0 + fail_msg: The encoding account declaration is invalid. + +- name: Read the encoding service account entry + ansible.builtin.command: + argv: + - /usr/bin/getent + - passwd + - gn-encoding + check_mode: false + changed_when: false + register: video_encoding_account_entry + failed_when: false + +- name: Verify the encoding service account identity + ansible.builtin.assert: + that: + - video_encoding_account_entry.rc == 0 + - >- + video_encoding_account_entry.stdout.split(":")[2] | int == + video_encoding_account.uid | int + - >- + video_encoding_account_entry.stdout.split(":")[3] | int == + video_encoding_account.gid | int + - >- + video_encoding_account_entry.stdout.split(":")[5] == + video_encoding_account.home + - >- + video_encoding_account_entry.stdout.split(":")[6] == + video_encoding_account.shell + fail_msg: >- + The gn-encoding account is missing or does not match the declared + identity. Apply the shared host baseline + (llm-server/host/ansible playbooks/host.yml) first. + success_msg: The gn-encoding service account matches the declared identity. + +- name: Read the shadow entry of the encoding account + ansible.builtin.command: + argv: + - /usr/bin/getent + - shadow + - gn-encoding + check_mode: false + changed_when: false + register: video_encoding_shadow_entry + failed_when: false + +- name: Verify the encoding account password is locked + ansible.builtin.assert: + that: + - video_encoding_shadow_entry.rc == 0 + - video_encoding_shadow_entry.stdout.split(":")[1] == "!" + fail_msg: The gn-encoding account password is not locked. + +- name: Read the subordinate UID range of the encoding account + ansible.builtin.command: + argv: + - /usr/bin/grep + - -E + - "^gn-encoding:" + - /etc/subuid + check_mode: false + changed_when: false + register: video_encoding_subuid_entry + failed_when: false + +- name: Read the subordinate GID range of the encoding account + ansible.builtin.command: + argv: + - /usr/bin/grep + - -E + - "^gn-encoding:" + - /etc/subgid + check_mode: false + changed_when: false + register: video_encoding_subgid_entry + failed_when: false + +- name: Verify the subordinate ID ranges of the encoding account + ansible.builtin.assert: + that: + - video_encoding_subuid_entry.rc == 0 + - video_encoding_subgid_entry.rc == 0 + - >- + video_encoding_subuid_entry.stdout == + "gn-encoding:" ~ video_encoding_account.subid_start ~ ":" + ~ video_encoding_subid_count + - >- + video_encoding_subgid_entry.stdout == + "gn-encoding:" ~ video_encoding_account.subid_start ~ ":" + ~ video_encoding_subid_count + fail_msg: >- + The subordinate ID ranges of gn-encoding are missing or do not match the + declared range. Apply the shared host baseline first. + +- name: Read the supplementary groups of the encoding account + ansible.builtin.command: + argv: + - /usr/bin/id + - -nG + - gn-encoding + check_mode: false + changed_when: false + register: video_encoding_groups + +- name: Enforce CPU-only device isolation for the encoding account + ansible.builtin.assert: + that: + - "'render' not in video_encoding_groups.stdout.split()" + - "'video' not in video_encoding_groups.stdout.split()" + fail_msg: >- + gn-encoding must not be a member of a GPU device group. Encoding is + CPU-only; remove the supplementary group before applying encoding state. + success_msg: gn-encoding has no GPU device group membership. + +- name: Manage the root-owned encoding configuration directory + ansible.builtin.file: + path: "{{ video_encoding_account.config_directory }}" + state: directory + owner: root + group: "{{ video_encoding_account.gid }}" + mode: "0750" + +- name: Manage the encoding data directories + ansible.builtin.file: + path: "{{ video_encoding_directory.path }}" + state: directory + owner: "{{ video_encoding_account.uid }}" + group: "{{ video_encoding_account.gid }}" + mode: "{{ video_encoding_directory.mode }}" + loop: "{{ video_encoding_directories }}" + loop_control: + loop_var: video_encoding_directory + label: "{{ video_encoding_directory.path }}" + +- name: Manage the administrator-managed rootless Quadlet directory + ansible.builtin.file: + path: "{{ video_encoding_quadlet_user_directory }}/{{ video_encoding_account.uid }}" + state: directory + owner: root + group: root + mode: "0755" + +- name: Verify the persistent user manager is enabled + ansible.builtin.stat: + path: "/var/lib/systemd/linger/{{ video_encoding_account.name }}" + register: video_encoding_linger + +- name: Assert the persistent user manager is enabled + ansible.builtin.assert: + that: + - video_encoding_linger.stat.exists + fail_msg: >- + Linger is not enabled for gn-encoding. Enable it through the shared host + baseline so the rootless user manager survives logout. + +- name: Wait for the running user manager + ansible.builtin.command: + argv: + - /usr/bin/systemctl + - is-active + - "user@{{ video_encoding_account.uid }}.service" + check_mode: false + changed_when: false + register: video_encoding_user_manager + retries: 5 + delay: 1 + until: video_encoding_user_manager.stdout == "active" + +- name: Find DRM render nodes in sysfs + ansible.builtin.find: + paths: /sys/class/drm + patterns: "renderD*" + file_type: link + recurse: false + register: video_encoding_render_nodes + +- name: Build the GPU device node list + ansible.builtin.set_fact: + video_encoding_gpu_device_nodes: >- + {{ ['/dev/kfd'] + + (video_encoding_render_nodes.files + | map(attribute="path") + | map("basename") + | map("regex_replace", "^", "/dev/dri/") + | list) }} + +- name: Verify the host exposes the expected GPU device nodes + ansible.builtin.assert: + that: + - video_encoding_gpu_device_nodes | length == 3 + fail_msg: >- + Expected /dev/kfd and two DRM render nodes on ghostnet-ki, found + {{ video_encoding_gpu_device_nodes }}. + +- name: Probe GPU device node read access as the encoding account + ansible.builtin.command: + argv: + - /usr/sbin/runuser + - --user + - gn-encoding + - -- + - /usr/bin/test + - -r + - "{{ video_encoding_device_node }}" + check_mode: false + changed_when: false + failed_when: false + register: video_encoding_device_access + loop: "{{ video_encoding_gpu_device_nodes }}" + loop_control: + loop_var: video_encoding_device_node + label: "{{ video_encoding_device_node }}" + +- name: Enforce that the encoding account cannot read GPU device nodes + ansible.builtin.assert: + that: + - >- + video_encoding_device_access.results + | map(attribute="rc") + | select("equalto", 0) + | list | length == 0 + fail_msg: >- + gn-encoding can read a GPU device node. Encoding is CPU-only; remove the + device access before applying encoding state. + success_msg: gn-encoding cannot access /dev/kfd or any DRM render node. + +- name: Inspect the encoding rootless Podman store + ansible.builtin.command: + argv: + - /usr/sbin/runuser + - --user + - gn-encoding + - -- + - /usr/bin/env + - "HOME={{ video_encoding_account.home }}" + - "XDG_RUNTIME_DIR=/run/user/{{ video_encoding_account.uid }}" + - /usr/bin/podman + - info + - --format=json + chdir: "{{ video_encoding_account.home }}" + check_mode: false + changed_when: false + register: video_encoding_podman_info_command + +- name: Parse the encoding rootless Podman information + ansible.builtin.set_fact: + video_encoding_podman_info: >- + {{ video_encoding_podman_info_command.stdout | from_json }} + +- name: Verify the isolated rootless Podman store + ansible.builtin.assert: + that: + - video_encoding_podman_info.host.security.rootless + - >- + video_encoding_podman_info.store.graphRoot == + video_encoding_account.home + "/.local/share/containers/storage" + fail_msg: The gn-encoding Podman store is not rootless or not isolated. + success_msg: gn-encoding uses its own rootless Podman store. + +- name: List Quadlet files for the encoding account + ansible.builtin.find: + paths: "{{ video_encoding_quadlet_user_directory }}/{{ video_encoding_account.uid }}" + file_type: file + register: video_encoding_quadlet_files + when: video_encoding_image | length == 0 + +- name: List containers in the encoding rootless store + ansible.builtin.command: + argv: + - /usr/sbin/runuser + - --user + - gn-encoding + - -- + - /usr/bin/env + - "HOME={{ video_encoding_account.home }}" + - "XDG_RUNTIME_DIR=/run/user/{{ video_encoding_account.uid }}" + - /usr/bin/podman + - ps + - -a + - --format + - json + chdir: "{{ video_encoding_account.home }}" + check_mode: false + changed_when: false + register: video_encoding_podman_ps_command + when: video_encoding_image | length == 0 + +- name: Verify that no encoding workload is defined before the open decisions are made + ansible.builtin.assert: + that: + - video_encoding_quadlet_files.files | length == 0 + - (video_encoding_podman_ps_command.stdout | from_json) | length == 0 + fail_msg: >- + A Quadlet file or container exists for gn-encoding although no encoder + image has been selected. Remove it or complete the open decisions in + docs/platform/llm-server/encoding/GNENC-001 section 7 first. + success_msg: >- + No encoding workload is defined; the open decisions in GNENC-001 remain + the gate for images, Quadlets and timers. + when: video_encoding_image | length == 0 + +- name: Report the verified encoding baseline + ansible.builtin.debug: + msg: + account: "gn-encoding (UID/GID 982)" + config_directory: /etc/ghostnet-encoding + data_directories: "{{ video_encoding_directories | map(attribute='path') | list }}" + quadlet_directory: /etc/containers/systemd/users/982 + podman_store: "{{ video_encoding_podman_info.store.graphRoot }}" + gpu_access: none (verified) + workload_defined: false