Files
ProxMenux/oci/src/proxmenux_oci/gpu.py
T
MacRimiandClaude Opus 5.5 4437a671d2 ProxMenux 1.2.6.2-beta: OCI containers in the Monitor, docs and fixes
OCI manager Apps
- App tab: containers installed from an OCI image are identified from their
  installation record; the application and image versions are shown and an
  update is detected by image digest; repository link; Refresh data.
- Updates tab for OCI containers: Update and Recreate run the same flow as the
  OCI menu in the Monitor terminal; the pre-update backup can be kept in a
  backup storage; scheduled image updates with an optional minimum age.
- Logs tab: console output of the application, kept on the host
  (lxc.console.logfile + logrotate) and followed live.
- The Proxmox console opens a shell (cmode: shell) when the image has one.
- A damaged image download is fetched again before failing.
- Multi-container applications open at their LAN address; volume mount
  points on block storage report their usage.

Monitor
- Proxmox notifications are delivered to a loopback-only HTTP listener when
  HTTPS is enabled, so they no longer fail certificate verification.
- Log persistence counts recurring patterns only; an ended burst is not
  reported as persistent and its warning clears on its own (#386).
- Proxmox notification config backups are deduplicated and capped at three.
- The update icon on the Apps page opens the container on its Updates tab.
- Version 1.2.6.2-beta and its release notes in every Monitor language.

Docs
- OCI manager Apps and Audit & Report rebuilt as per-page message files,
  with a new page for OCI containers in the Monitor.
- Seven pages fixed where rich-text tags were missing from t.rich.

Translations
- Spanish fixes across the OCI engine, the Monitor and the TUI menus.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 21:51:12 +02:00

116 lines
6.2 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 not profile.get('hardware_acceleration'):
# Fold optional Compose DRI devices into the Selkies GPU choice rather
# than asking twice or bypassing the acceleration menu.
removable = lambda device: (str(device.get('host_path_default', '')).startswith('/dev/dri')
and not device.get('required_by_compose'))
profile['device_requests'] = [d for d in profile.get('device_requests', []) if not removable(d)]
profile['optional_devices'] = [d for d in profile.get('optional_devices', [])
if not removable(d)]
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']}}
],
}
# NVIDIA needs the host driver/Toolkit and is unsupported by Alpine Selkies images.
hardware = profile.get('hardware_acceleration')
if not hardware or 'alpine' in profile['selkies'].get('base', '').lower():
return
profile['selkies']['nvidia'] = 'available-in-advanced-mode-with-compatible-host-driver-and-toolkit'
profiles = hardware.setdefault('profiles', [])
if any(item.get('id') == 'nvidia' for item in profiles):
return
profiles.append({
'id': 'nvidia',
'label': 'NVIDIA (Selkies rendering and NVENC)',
'device_requests': [{
'id': 'selkies-nvidia', 'kind': 'nvidia-runtime',
'device_selection': 'all-requested-by-compose',
}],
'environment': [
{'name': 'AUTO_GPU', 'value': 'true'},
{'name': 'NVIDIA_VISIBLE_DEVICES', 'value': 'all'},
{'name': 'NVIDIA_DRIVER_CAPABILITIES', 'value': 'compute,video,graphics,utility,display'},
],
})
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'])