Files
ProxMenux/AppImage/scripts/oci_operations.py
T
MacRimi 9b5cefb81a feat(oci): GPU selection per host and per image, one notification per update, and App tab for stack containers
- Immich asks what runs its video and its recognition in one menu, in both
  modes, and gives the GPU to the server and to Machine learning; AMD uses ROCm
- Frigate, Ollama, llama.cpp, Faster Whisper and Piper take the image built
  for the chosen GPU
- The acceleration menu offers only what the host can run
- An update or a recreation sends one notification with its result instead of
  the stop, backup and start of each container
- A private bridge with nothing connected is not reported as down
- Secondary containers of a stack appear in the App tab with their version and
  logo; Secure Gateway shows the same update state in both views
- A mistyped value in the wizard asks the same question again
2026-10-02 21:45:38 +02:00

63 lines
2.4 KiB
Python

"""Containers an OCI manager operation is working on.
An update or a recreation stops, backs up and starts its containers. The OCI
engine marks them while it works and reports the result itself, so their
stop, start and backup notices are part of the operation, not news.
"""
import json
import os
import re
import time
MARKERS = '/run/proxmenux/oci-operations'
# The last start of an operation is noticed a little after it returned.
GRACE_SECONDS = 180
# A mark left behind by an operation that died is not trusted for ever.
STALE_SECONDS = 6 * 3600
QUIET_EVENTS = frozenset({
'vm_start', 'vm_stop', 'vm_shutdown', 'vm_restart',
'ct_start', 'ct_stop', 'ct_shutdown', 'ct_restart',
'backup_start', 'backup_complete',
})
_OCI_BACKUP_PATH = '/proxmenux/oci/instances/'
def active(vmid, now=None, root=MARKERS) -> bool:
try:
with open(os.path.join(root, str(int(vmid))), encoding='utf-8') as handle:
mark = json.load(handle)
started = float(mark.get('started') or 0)
ended = mark.get('ended')
except (OSError, ValueError, TypeError):
return False
now = time.time() if now is None else now
if ended is None:
return now - started < STALE_SECONDS
return now - float(ended) < GRACE_SECONDS
def _vmids(event) -> set:
data = event.data or {}
found = set()
for value in (data.get('vmid'), getattr(event, 'entity_id', '')):
if str(value or '').isdigit():
found.add(int(value))
if event.event_type.startswith('backup_'):
text = ' '.join(str(data.get(key) or '') for key in ('reason', 'pve_message', 'vmname', 'guests'))
found.update(int(value) for value in re.findall(r'\((\d{3,})\)|vzdump-(?:lxc|qemu)-(\d+)-', text)
for value in value if value)
return found
def quiet(event, root=MARKERS) -> bool:
"""Whether the event is a step of a running OCI operation."""
if event.event_type not in QUIET_EVENTS or event.severity in ('CRITICAL', 'WARNING'):
return False
data = event.data or {}
if event.event_type.startswith('backup_') and any(
_OCI_BACKUP_PATH in str(data.get(key) or '') for key in ('reason', 'pve_message', 'filename')):
# The working copy of an update lives in the OCI registry of the host.
return True
vmids = _vmids(event)
return bool(vmids) and all(active(vmid, root=root) for vmid in vmids)