Files
infrastructure/llm-server/encoding/ansible/roles/video_encoding/tasks/main.yml
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

586 lines
20 KiB
YAML

---
- 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.primary_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: Remove the superseded local pipeline directories when empty
ansible.builtin.shell: |
set -euo pipefail
changed=0
for dir in {{ video_encoding_legacy_local_directories | map('quote') | join(' ') }}; do
if [ -d "$dir" ]; then
if [ -n "$(find "$dir" -mindepth 1 -print -quit)" ]; then
echo "Refusing to remove non-empty legacy directory: $dir"
exit 1
fi
rmdir "$dir"
changed=1
fi
done
[ "$changed" = 0 ] || echo CHANGED
args:
executable: /bin/bash
register: video_encoding_legacy_cleanup
changed_when: "'CHANGED' in video_encoding_legacy_cleanup.stdout"
- name: Install the NFS client
ansible.builtin.apt:
name: "{{ video_encoding_pipeline_nfs_client_package }}"
state: present
update_cache: true
cache_valid_time: 3600
install_recommends: false
- name: Probe whether the pipeline mount is already active
ansible.builtin.command:
argv:
- /usr/bin/findmnt
- --mountpoint
- "{{ video_encoding_pipeline_mount }}"
check_mode: false
changed_when: false
failed_when: false
register: video_encoding_pipeline_mountpoint_probe
- name: Create the pipeline mountpoint outside the service homes
ansible.builtin.file:
path: "{{ video_encoding_pipeline_mount }}"
state: directory
owner: root
group: root
mode: "0755"
when: video_encoding_pipeline_mountpoint_probe.rc != 0
- name: Manage the pipeline NFS mount unit
ansible.builtin.copy:
content: |
[Unit]
Description=Ghostnet encoding pipeline NFS export (GNENC-002)
Requires=network-online.target
After=network-online.target
[Mount]
What={{ video_encoding_nfs_server }}:{{ video_encoding_nfs_export }}
Where={{ video_encoding_pipeline_mount }}
Type=nfs4
Options={{ video_encoding_pipeline_mount_options }}
[Install]
WantedBy=multi-user.target
dest: "/etc/systemd/system/{{ video_encoding_pipeline_mount_unit }}"
owner: root
group: root
mode: "0644"
- name: Reload systemd for the pipeline mount unit
ansible.builtin.systemd_service:
daemon_reload: true
when: not ansible_check_mode
- name: Enable and mount the pipeline NFS export
ansible.builtin.systemd_service:
name: "{{ video_encoding_pipeline_mount_unit }}"
enabled: true
state: started
when: not ansible_check_mode
- name: Verify the pipeline mount is active
ansible.builtin.command:
argv:
- /usr/bin/systemctl
- is-active
- "{{ video_encoding_pipeline_mount_unit }}"
check_mode: false
changed_when: false
register: video_encoding_pipeline_mount_status
failed_when: video_encoding_pipeline_mount_status.stdout != "active"
when: not ansible_check_mode
- name: Stat the mounted export root
ansible.builtin.stat:
path: "{{ video_encoding_pipeline_mount }}"
register: video_encoding_export_root
when: not ansible_check_mode
- name: Verify the mounted export root ownership and mode
ansible.builtin.assert:
that:
- video_encoding_export_root.stat.isdir
- video_encoding_export_root.stat.uid == video_encoding_export_root_uid | int
- video_encoding_export_root.stat.gid == video_encoding_export_root_gid | int
- >-
'%04o' % (video_encoding_export_root.stat.mode | int(0, 8)) ==
video_encoding_export_root_mode
fail_msg: >-
The mounted export root must be {{ video_encoding_export_root_uid }}:{{ video_encoding_export_root_gid }}
{{ video_encoding_export_root_mode }} (PVE host side, GNAI-017 section 4.1);
found {{ video_encoding_export_root.stat.uid }}:{{ video_encoding_export_root.stat.gid }}
{{ video_encoding_export_root.stat.mode }}.
success_msg: The pipeline export root is mounted with the declared identity.
when: not ansible_check_mode
- name: Collect the HandBrake preset files from the profiles directory
ansible.builtin.set_fact:
video_encoding_profile_files: >-
{{ lookup('ansible.builtin.fileglob', role_path ~ '/../../../profiles/*.json', wantlist=True)
| map('basename') | map('regex_replace', '\.json$', '') | list }}
- name: Require the declared profiles to match the versioned preset files
ansible.builtin.assert:
that:
- video_encoding_profile_names | sort == video_encoding_profile_files | sort
fail_msg: >-
The declared video_encoding_profile_names do not match the preset files
under llm-server/encoding/profiles/ (found:
{{ video_encoding_profile_files | join(', ') }}).
success_msg: The declared profiles match the versioned preset files.
- name: Create the gn-download pipeline directories on the export
ansible.builtin.shell: |
set -euo pipefail
changed=0
while read -r mode path; do
[ -n "${path:-}" ] || continue
if [ ! -d "$path" ]; then
install -d -m "$mode" "$path"
changed=1
elif [ "$(stat -c '%a' "$path")" != "$mode" ]; then
chmod "$mode" "$path"
changed=1
fi
done <<'EOF'
{% for d in [
(video_encoding_pipeline_mount ~ '/input', '2777'),
(video_encoding_pipeline_mount ~ '/jd', '2770'),
(video_encoding_pipeline_mount ~ '/jd/dlc_done', '2770'),
(video_encoding_pipeline_mount ~ '/jd/failed', '2770'),
(video_encoding_pipeline_mount ~ '/queue', '2770') ] %}
{{ d[1] }} {{ d[0] }}
{% endfor %}
{% for p in video_encoding_profile_names %}
2777 {{ video_encoding_pipeline_mount }}/input/{{ p }}
2770 {{ video_encoding_pipeline_mount }}/queue/{{ p }}
{% endfor %}
EOF
[ "$changed" = 0 ] || echo CHANGED
args:
executable: /bin/bash
become: true
become_user: gn-download
environment:
HOME: /var/lib/ghostnet-download
register: video_encoding_download_layout
changed_when: "'CHANGED' in video_encoding_download_layout.stdout"
when: not ansible_check_mode
- name: Create the gn-encoding pipeline directories on the export
ansible.builtin.shell: |
set -euo pipefail
changed=0
while read -r mode path; do
[ -n "${path:-}" ] || continue
if [ ! -d "$path" ]; then
install -d -m "$mode" "$path"
changed=1
elif [ "$(stat -c '%a' "$path")" != "$mode" ]; then
chmod "$mode" "$path"
changed=1
fi
done <<'EOF'
{% for d in [
(video_encoding_pipeline_mount ~ '/work', '0750'),
(video_encoding_pipeline_mount ~ '/output', '2777'),
(video_encoding_pipeline_mount ~ '/output/_failed', '2777'),
(video_encoding_pipeline_mount ~ '/state', '2750'),
(video_encoding_pipeline_mount ~ '/state/jobs', '0750'),
(video_encoding_pipeline_mount ~ '/state/tmp', '0750') ] %}
{{ d[1] }} {{ d[0] }}
{% endfor %}
{% for p in video_encoding_profile_names %}
2777 {{ video_encoding_pipeline_mount }}/output/{{ p }}
{% endfor %}
EOF
[ "$changed" = 0 ] || echo CHANGED
args:
executable: /bin/bash
become: true
become_user: gn-encoding
environment:
HOME: "{{ video_encoding_account.home }}"
register: video_encoding_worker_layout
changed_when: "'CHANGED' in video_encoding_worker_layout.stdout"
when: not ansible_check_mode
- name: Verify the pipeline directory layout on the export
ansible.builtin.shell: |
set -uo pipefail
failed=0
while read -r uid gid mode path; do
[ -n "${path:-}" ] || continue
if [ ! -d "$path" ]; then
echo "MISSING $path"
failed=1
continue
fi
read -r auid agid amode <<<"$(stat -c '%u %g %a' "$path")"
if [ "$auid" != "$uid" ] || [ "$agid" != "$gid" ] || [ "$amode" != "$mode" ]; then
echo "WRONG $path (expected $uid:$gid $mode, found $auid:$agid $amode)"
failed=1
fi
done <<'EOF'
{% for d in [
(video_encoding_pipeline_mount ~ '/input', '983', '2777'),
(video_encoding_pipeline_mount ~ '/jd', '983', '2770'),
(video_encoding_pipeline_mount ~ '/jd/dlc_done', '983', '2770'),
(video_encoding_pipeline_mount ~ '/jd/failed', '983', '2770'),
(video_encoding_pipeline_mount ~ '/queue', '983', '2770'),
(video_encoding_pipeline_mount ~ '/work', '982', '0750'),
(video_encoding_pipeline_mount ~ '/output', '982', '2777'),
(video_encoding_pipeline_mount ~ '/output/_failed', '982', '2777'),
(video_encoding_pipeline_mount ~ '/state', '982', '2750'),
(video_encoding_pipeline_mount ~ '/state/jobs', '982', '0750'),
(video_encoding_pipeline_mount ~ '/state/tmp', '982', '0750') ] %}
{{ d[1] }} {{ d[2] }} {{ d[0] }}
{% endfor %}
{% for p in video_encoding_profile_names %}
983 970 2777 {{ video_encoding_pipeline_mount }}/input/{{ p }}
983 970 2770 {{ video_encoding_pipeline_mount }}/queue/{{ p }}
982 970 2777 {{ video_encoding_pipeline_mount }}/output/{{ p }}
{% endfor %}
EOF
if [ "$failed" = 1 ]; then
echo "Pipeline layout verification failed"
exit 1
fi
echo LAYOUT_OK
args:
executable: /bin/bash
become_user: gn-encoding
environment:
HOME: "{{ video_encoding_account.home }}"
changed_when: false
register: video_encoding_layout_verify
when: not ansible_check_mode
- 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
pipeline_mount: "{{ video_encoding_nfs_server }}:{{ video_encoding_nfs_export }} -> {{ video_encoding_pipeline_mount }}"
pipeline_profiles: "{{ video_encoding_profile_names | default([]) }}"
quadlet_directory: /etc/containers/systemd/users/982
podman_store: "{{ video_encoding_podman_info.store.graphRoot }}"
gpu_access: none (verified)
workload_defined: false