mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-29 18:16:43 +00:00
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>
125 lines
4.8 KiB
Python
125 lines
4.8 KiB
Python
"""Preservation profiles for native DRM devices and NVIDIA Toolkit runtimes."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import oci_runtime_settings as runtime_settings
|
|
import oci_nvidia_dynamic as dynamic
|
|
import oci_gpu_devices as drm
|
|
import oci_nvidia_runtime as nvidia
|
|
from oci_ui import translate
|
|
|
|
|
|
def dynamic_mode(deployment):
|
|
return any(d.get('kind') == 'nvidia-runtime' and d.get('runtime_mode') == 'dynamic'
|
|
for d in deployment.get('devices', []))
|
|
|
|
|
|
def check_dynamic(config, value, deployment):
|
|
hooks = [line.split(': ', 1)[1] for line in config.decode().splitlines()
|
|
if line.startswith('lxc.hook.mount: ')]
|
|
if len(hooks) != 1:
|
|
raise ValueError(translate('The dynamic NVIDIA hook is missing or duplicated'))
|
|
match = re.fullmatch(r'/usr/local/lib/proxmenux/oci/nvidia-mount-([a-f0-9]{64})\.sh', hooks[0])
|
|
if not match:
|
|
raise ValueError(translate('The NVIDIA hook path does not belong to the installer'))
|
|
capabilities = next((e['value'] for e in reversed(deployment.get('environment', []))
|
|
if e['name'] == 'NVIDIA_DRIVER_CAPABILITIES'), 'compute,utility,video')
|
|
return dynamic.validate(config, value, value, hooks[0], match[1], capabilities)
|
|
|
|
|
|
def verify_baseline(expected, deployment):
|
|
if not dynamic_mode(deployment):
|
|
verify(expected)
|
|
return
|
|
drm.verify({p: v for p, v in expected.items() if p != nvidia.KEY})
|
|
if dynamic.gpu_identity(expected[nvidia.KEY]) != dynamic.gpu_identity(nvidia.snapshot()):
|
|
raise ValueError(translate('The selected GPU changed'))
|
|
|
|
|
|
def drm_plan(deployment):
|
|
return dict(deployment, devices=[d for d in deployment.get('devices', []) if d.get('kind') != 'nvidia-runtime'])
|
|
|
|
|
|
def planned(deployment):
|
|
result = drm.planned(drm_plan(deployment))
|
|
if nvidia.enabled(deployment):
|
|
value = nvidia.snapshot()
|
|
if set(result) & set(value['devices']):
|
|
raise ValueError(translate('Duplicated NVIDIA devices'))
|
|
result[nvidia.KEY] = value
|
|
return result
|
|
|
|
|
|
def verify(expected):
|
|
drm.verify({p: v for p, v in expected.items() if p != nvidia.KEY})
|
|
if nvidia.KEY in expected:
|
|
nvidia.verify(expected[nvidia.KEY])
|
|
|
|
|
|
def check(config, deployment):
|
|
config = runtime_settings.filter_config(config, deployment)
|
|
expected = planned(deployment)
|
|
value = expected.get(nvidia.KEY)
|
|
if value:
|
|
nvidia.check_devices(config, value)
|
|
if dynamic_mode(deployment):
|
|
check_dynamic(config, value, deployment)
|
|
else:
|
|
nvidia.check_mounts(config, value)
|
|
filtered = []
|
|
for line in config.splitlines(keepends=True):
|
|
if re.match(rb'dev[0-9]+: ', line):
|
|
fields = dict(part.split('=', 1) for part in line.decode().strip().split(': ', 1)[1].split(','))
|
|
if fields.get('path') in value['devices']:
|
|
continue
|
|
filtered.append(line)
|
|
filtered = b''.join(filtered)
|
|
drm.check(filtered, drm_plan(deployment))
|
|
else:
|
|
if nvidia.mount_lines(config):
|
|
raise ValueError(translate('LXC entries outside the selected acceleration profile'))
|
|
drm.check(config, deployment)
|
|
return expected
|
|
|
|
|
|
def capture(config):
|
|
result = drm.capture(config)
|
|
if any(isinstance(p, str) and p.startswith('/dev/nvidia') for p in drm.actual_devices(config)):
|
|
result[nvidia.KEY] = nvidia.snapshot()
|
|
return result
|
|
|
|
|
|
def verify_observation(expected, observed):
|
|
if observed.get('gpu_devices', {}) != expected:
|
|
raise ValueError(translate('The acceleration evidence differs from the verified inventory'))
|
|
verify(expected)
|
|
|
|
|
|
def validate_runtime(vmid, deployment):
|
|
if nvidia.enabled(deployment):
|
|
value = nvidia.snapshot()
|
|
if dynamic_mode(deployment):
|
|
rows = nvidia.command('pct', 'exec', str(vmid), '--', 'nvidia-smi', nvidia.QUERY, '--format=csv,noheader')
|
|
if sorted(line.strip() for line in rows.splitlines() if line.strip()) != value['gpus']:
|
|
raise ValueError(translate('NVML does not match the current host driver'))
|
|
else:
|
|
nvidia.validate_runtime(vmid, value)
|
|
|
|
|
|
def check_recovery_entries(config, state):
|
|
runtime_settings.check_recovery(config, state)
|
|
for key in ('record', 'candidate_contract'):
|
|
config = runtime_settings.filter_config(config, state.get(key, {}).get('deployment', {}))
|
|
entries = nvidia.mount_lines(config)
|
|
if not entries:
|
|
return
|
|
values = [state.get(key, {}).get(nvidia.KEY) for key in ('original_gpu_devices', 'desired_gpu_devices')]
|
|
for value in values:
|
|
if value:
|
|
try:
|
|
nvidia.check_mounts(config, value, complete=False)
|
|
return
|
|
except ValueError:
|
|
continue
|
|
raise ValueError(translate('LXC entries outside the NVIDIA inventory of the journal'))
|