Encoding: Ansible-Projekt und Grundzustand für gn-encoding eingerichtet und verifiziert.

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 <vibe@mistral.ai>
This commit is contained in:
2026-10-19 14:42:14 +02:00
parent 92c846f527
commit ba7d829410
12 changed files with 556 additions and 24 deletions
+10
View File
@@ -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).
+46
View File
@@ -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.
+12
View File
@@ -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
@@ -0,0 +1,8 @@
---
all:
children:
llm_servers:
hosts:
ghostnet-ki:
ansible_host: 10.0.0.90
ansible_user: ansible-deploy
@@ -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
@@ -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.
@@ -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: ""
@@ -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