mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-05 04:57:18 +00:00
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>
104 lines
5.3 KiB
Python
104 lines
5.3 KiB
Python
"""Optional user mounts, separate from the image's required persistence."""
|
|
from pathlib import PurePosixPath
|
|
|
|
from .i18n import translate
|
|
from .ui import UserCancelled
|
|
|
|
|
|
def valid_path(value):
|
|
if (not value.startswith('/') or value == '/' or
|
|
any(c.isspace() or c in ',\x00' for c in value) or
|
|
any(part in ('.', '..') for part in value.split('/'))):
|
|
raise ValueError(translate('Invalid absolute path; avoid spaces, commas and relative segments'))
|
|
value = str(PurePosixPath(value))
|
|
if value.startswith('//'):
|
|
raise ValueError(translate('Invalid path'))
|
|
return value
|
|
|
|
|
|
def overlaps(a, b):
|
|
return a == b or a.startswith(b.rstrip('/') + '/') or b.startswith(a.rstrip('/') + '/')
|
|
|
|
|
|
def validate_mount(mount, existing):
|
|
target = valid_path(mount['container_path'])
|
|
protected = ('/bin', '/sbin', '/etc', '/usr', '/lib', '/lib64', '/proc', '/sys', '/dev', '/run')
|
|
if any(overlaps(target, p) for p in protected):
|
|
raise ValueError(translate('The custom path cannot hide system directories'))
|
|
if any(overlaps(target, valid_path(m['container_path'])) for m in existing):
|
|
raise ValueError(translate('The custom path overlaps another mount'))
|
|
if mount['type'] == 'managed-volume':
|
|
if int(mount['size_gb']) < 1 or not mount.get('backup'):
|
|
raise ValueError(translate('Invalid internal volume'))
|
|
elif mount['type'] == 'host-bind':
|
|
valid_path(mount['source'])
|
|
if mount.get('backup'):
|
|
raise ValueError(translate('Bind mounts are not included in vzdump'))
|
|
else:
|
|
raise ValueError(translate('Invalid mount type'))
|
|
return target
|
|
|
|
|
|
def ask_custom_mounts(ui, mounts, storage):
|
|
result = list(mounts)
|
|
while ui.confirm(translate('Add an extra custom path'), False):
|
|
target = ui.ask(translate('Path inside the container (e.g. /media-extra)'))
|
|
mode = ui.choose(translate('Data location'), [
|
|
('managed-volume', translate('Container volume (included in backups)')),
|
|
('host-bind', translate('Host directory (not included in Proxmox backups)')),
|
|
], 'managed-volume')
|
|
if mode is None:
|
|
raise UserCancelled(translate('Custom path cancelled'))
|
|
mount = {'type': mode, 'container_path': target, 'custom': True,
|
|
'source': storage, 'size_gb': None, 'backup': mode == 'managed-volume',
|
|
'read_only': ui.confirm(translate('Mount read-only'), False),
|
|
'create_if_missing': mode == 'host-bind'}
|
|
if mode == 'managed-volume':
|
|
mount['source'] = ui.ask(translate('Proxmox storage for the volume'), storage)
|
|
mount['size_gb'] = int(ui.ask(translate('Volume size in GB'), '8'))
|
|
else:
|
|
mount['source'] = ui.ask(translate('Host directory (created if it does not exist)'), '/mnt/oci-shared/custom')
|
|
mount['container_path'] = validate_mount(mount, result)
|
|
result.append(mount)
|
|
return result
|
|
|
|
|
|
def ask_stack_custom_mounts(ui, services, storage):
|
|
"""Collect extra paths once, then attach each path to selected stack members."""
|
|
if not ui.confirm(translate('Add extra paths to this stack'), False):
|
|
return
|
|
options = [(service['name'], service['name']) for service in services]
|
|
defaults = [service['name'] for service in services if service.get('main')]
|
|
while True:
|
|
selected = ui.checklist(translate('Containers that will receive this path'),
|
|
options, defaults or [options[0][0]])
|
|
if not selected or set(selected) - {name for name, _ in options}:
|
|
raise ValueError(translate('Select at least one stack container'))
|
|
mode = ui.choose(translate('Data location'), [
|
|
('managed-volume', translate('Container volume (included in backups)')),
|
|
('host-bind', translate('Host directory (not included in Proxmox backups)')),
|
|
], 'host-bind' if len(selected) > 1 else 'managed-volume')
|
|
if mode is None:
|
|
raise UserCancelled(translate('Custom path cancelled'))
|
|
source = (ui.ask(translate('Proxmox storage for the volume'), storage)
|
|
if mode == 'managed-volume' else
|
|
ui.ask(translate('Host directory (created if it does not exist)'), '/mnt/oci-shared/custom'))
|
|
size = int(ui.ask(translate('Volume size in GB'), '8')) if mode == 'managed-volume' else None
|
|
read_only = ui.confirm(translate('Mount read-only'), False)
|
|
default_target = '/media-extra'
|
|
additions = []
|
|
for service in services:
|
|
if service['name'] not in selected:
|
|
continue
|
|
target = ui.ask(f"{service['name']}: {translate('Path inside the container')}", default_target)
|
|
mount = {'type': mode, 'container_path': target, 'custom': True,
|
|
'source': source, 'size_gb': size, 'backup': mode == 'managed-volume',
|
|
'read_only': read_only, 'create_if_missing': mode == 'host-bind'}
|
|
mount['container_path'] = validate_mount(mount, service['deployment']['mounts'])
|
|
additions.append((service, mount))
|
|
default_target = target
|
|
for service, mount in additions:
|
|
service['deployment']['mounts'].append(mount)
|
|
if not ui.confirm(translate('Add another extra path to this stack'), False):
|
|
break
|