Files
ProxMenux/oci/src/proxmenux_oci/gpu.py
T
MacRimiandClaude Opus 5 bcabcb618c feat(oci): run official container images as native LXC containers
Adds the OCI manager: an engine that turns a Docker Compose file into an
LXC definition, a catalog of 365 applications drawn from LinuxServer.io
and other container image sources, and a per-instance registry recording
what each container was built from. Reachable from the main menu.

Catalog text is translated like every other string in the project: the
taglines go through translate() and land in lang/*.json, so the entries
read in all eight languages instead of only English.

Translation cache builder:
- a failed translation leaves the key absent rather than writing English,
  which previously made the string count as translated forever
- a result identical to a 3+ word source is rejected, catching a provider
  that silently returns the text it was given
- strings that are nothing but glossary terms keep their source spelling
  instead of being discarded as failures
- no backoff between attempts when the provider is deterministic
- application names are protected so "HAOS One" survives translation
- argos joins the provider list, and the workflow reads the OCI sources

Audit & Report:
- findings that moved in the wrong direction between runs are reported
  alongside the ones that improved
- an accepted risk can carry a review date and is flagged when it falls due
- backup checks explain in plain language what they looked at and what to
  do next

Monitor:
- disks can be excluded from periodic reads, and an idle disk says so
  instead of showing a stale temperature
- per-disk identity survives a controller or enclosure change
- scheduled Borg backups resolve their SSH key from the repository entry
- PVE upgrades log the package list and the resulting dpkg changes

The web build no longer copies scripts/ into public/: the documentation
links to GitHub, so nothing read that folder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 18:24:59 +02:00

89 lines
4.8 KiB
Python

"""Image-specific GPU contracts, applied after catalog overlays on regeneration."""
from __future__ import annotations
from typing import Any
from .i18n import translate
LSIO_DEVICE_INIT = {"boinc", "emby", "jellyfin", "plex", "tvheadend"}
def apply_gpu_contract(template: dict[str, Any]) -> None:
profile = template.get("proxmox", {}).get("installer_profile", {})
if profile.get('selkies'):
apply_selkies_contract(template)
groups = [profile, *profile.get("hardware_acceleration", {}).get("profiles", [])]
requests = [item for group in groups for key in ("device_requests", "optional_devices")
for item in group.get(key, [])]
gpu = [item for item in requests if item.get("kind") == "nvidia-runtime"
or str(item.get("host_path_default", "")).startswith(("/dev/dri", "/dev/kfd"))]
if not gpu:
return
image = template.get("container_contract", {}).get("image", {}).get("reference", "")
repository = image.split("@", 1)[0].split(":", 1)[0]
for item in gpu:
if str(item.get("host_path_default", "")).startswith("/dev/dri/renderD"):
item["container_path_strategy"] = "same-as-host"
profile["gpu_validation"] = {
"device_inventory": "host-sysfs-and-stat",
"application_acceleration": "requires-workload-test",
"tone_mapping": "not-implied-by-device-access",
}
if repository in {"lscr.io/linuxserver/" + app for app in LSIO_DEVICE_INIT} or profile.get('selkies'):
profile["device_permissions"] = {
"strategy": "linuxserver-native-init",
"service_user": "abc",
"environment": "ATTACHED_DEVICES_PERMS",
"paths": "all-resolved-selected-character-devices",
}
elif repository == "jlesage/handbrake":
for item in gpu:
item["append_host_device_gid_to_environment"] = "SUP_GROUP_IDS"
def apply_selkies_contract(template):
profile = template['proxmox']['installer_profile']
if not template['container_contract']['image']['reference'].startswith('lscr.io/linuxserver/'):
raise ValueError(translate('The Selkies profile requires a verified LinuxServer image'))
environment = template['container_contract']['environment']
if not any(e['name'] == 'LC_ALL' for e in environment):
environment.append({'name': 'LC_ALL', 'example': '', 'required': False,
'sensitive': False, 'source': 'upstream-documentation',
'prompt': 'Language/locale (e.g. es_ES.UTF-8; translation of every application is not guaranteed)'})
mounts = profile.setdefault('tmpfs_mounts', [])
if not any(m['container_path'] == '/run/nginx' for m in mounts):
mounts.append({'id': 'nginx-runtime', 'container_path': '/run/nginx',
'default_size_mb': 1, 'minimum_size_mb': 1, 'prompt_size': False,
'mount_options': ['rw', 'nosuid', 'nodev', 'mode=0755']})
if profile.get('hardware_acceleration') or profile.get('device_requests'):
return
profile['optional_devices'] = [d for d in profile.get('optional_devices', [])
if not str(d.get('host_path_default', '')).startswith('/dev/dri')]
profile['hardware_acceleration'] = {
'prompt': 'Selkies desktop and streaming acceleration', 'default': 'none',
'profiles': [
{'id': 'none', 'label': 'No GPU (CPU)', 'device_requests': [],
'environment': [{'name': 'AUTO_GPU', 'value': 'false'}]},
{'id': 'vaapi', 'label': 'Intel/AMD (streaming rendering and encoding)',
'device_requests': [{'id': 'selkies-render', 'kind': 'character-device',
'path_prompt': 'Intel/AMD render node', 'host_path_default': '/dev/dri/renderD128',
'container_path_strategy': 'same-as-host', 'mode': '0660',
'deny_write': False, 'gid_strategy': 'host-device-gid',
'drm_vendor_ids': ['0x8086', '0x1002']}],
'environment': [{'name': 'PIXELFLUX_WAYLAND', 'value': 'true'},
{'name': 'AUTO_GPU', 'value': 'false'}],
'environment_from_devices': {'DRINODE': ['selkies-render'],
'DRI_NODE': ['selkies-render'],
'ATTACHED_DEVICES_PERMS': ['selkies-render']}}
]}
def apply_profile_image(template, hardware_profile):
"""Resolve an upstream image channel declared by the selected GPU profile."""
import copy
profiles = template.get('proxmox', {}).get('installer_profile', {}).get('hardware_acceleration', {}).get('profiles', [])
selected = next((profile for profile in profiles if profile['id'] == hardware_profile), {})
if selected.get('image'):
template['container_contract']['image'] = copy.deepcopy(selected['image'])