Files
ProxMenux/oci/src/proxmenux_oci/custom_mounts.py
T

104 lines
5.3 KiB
Python
Raw Normal View History

"""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