mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
Merge pull request #528 from jcastro/feature/oci-input-review
Support owner-aware OCI tmpfs mounts and enable Input
This commit is contained in:
@@ -2005,17 +2005,6 @@ for MOUNT_WARNING in ${MOUNT_WARNINGS[@]+"${MOUNT_WARNINGS[@]}"}; do
|
||||
msg_warn "$MOUNT_WARNING"
|
||||
done
|
||||
|
||||
while IFS=$'\t' read -r TARGET SIZE_MB OPTIONS; do
|
||||
[[ -n $TARGET ]] || continue
|
||||
[[ $TARGET == /* && $TARGET != *","* && $TARGET != *[[:space:]]* ]] \
|
||||
|| die "$(translate "Invalid tmpfs path:") $TARGET"
|
||||
[[ $SIZE_MB =~ ^[0-9]+$ && $SIZE_MB -gt 0 ]] || die "$(translate "Invalid tmpfs size:") $TARGET"
|
||||
[[ $OPTIONS =~ ^(rw|ro|nosuid|nodev|noexec|mode=0[0-7]{3})(,(rw|ro|nosuid|nodev|noexec|mode=0[0-7]{3}))*$ ]] || die "$(translate "Invalid tmpfs options:") $TARGET"
|
||||
TARGET_RELATIVE=${TARGET#/}
|
||||
printf 'lxc.mount.entry: tmpfs %s tmpfs %s,size=%sM,create=dir 0 0\n' \
|
||||
"$TARGET_RELATIVE" "$OPTIONS" "$SIZE_MB" >>"$CONF"
|
||||
done < <(jq -r '.tmpfs_mounts[]? | [.container_path,.size_mb,(.mount_options | join(","))] | @tsv' "$DEPLOYMENT_FILE")
|
||||
|
||||
DEVICE_INDEX=0
|
||||
RESOLVED_CHARACTER_DEVICES=()
|
||||
NVIDIA_RUNTIME_CONFIGURED=0
|
||||
@@ -2101,6 +2090,13 @@ prepare_managed_format_directories() {
|
||||
prepare_managed_format_directories "/var/lib/lxc/${VMID}/rootfs"
|
||||
apply_extra_hosts
|
||||
apply_installer_profile
|
||||
# Render tmpfs entries after the image and installer profile have set the
|
||||
# effective LXC init uid/gid. The same renderer validates and records them.
|
||||
TMPFS_CONFIG=$(mktemp)
|
||||
python3 "${SCRIPT_DIR}/oci_runtime_settings.py" --render-tmpfs "$DEPLOYMENT_FILE" "$CONF" \
|
||||
>"$TMPFS_CONFIG" || { rm -f "$TMPFS_CONFIG"; die "$(translate "Could not configure the tmpfs mounts")"; }
|
||||
cat "$TMPFS_CONFIG" >>"$CONF"
|
||||
rm -f "$TMPFS_CONFIG"
|
||||
ensure_runtime_home
|
||||
apply_rlimits
|
||||
apply_host_monitor
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Validate declared volatile mounts and native network sysctl includes."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
@@ -25,15 +27,63 @@ def sysctl_content(deployment):
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def tmpfs_lines(deployment):
|
||||
def _config_ids(config):
|
||||
"""Read the effective OCI init identity written by Proxmox."""
|
||||
if config is None:
|
||||
return 0, 0
|
||||
text = config.decode() if isinstance(config, bytes) else str(config)
|
||||
values = {}
|
||||
for line in text.splitlines():
|
||||
if ': ' not in line:
|
||||
continue
|
||||
key, value = line.split(': ', 1)
|
||||
if key in ('lxc.init.uid', 'lxc.init.gid'):
|
||||
if key in values:
|
||||
raise ValueError(translate('The OCI image user or group is ambiguous'))
|
||||
if not re.fullmatch(r'[0-9]{1,10}', value):
|
||||
raise ValueError(translate('The OCI image user or group is not numeric'))
|
||||
number = int(value)
|
||||
if number > 4294967294:
|
||||
raise ValueError(translate('The OCI image user or group is outside the supported range'))
|
||||
values[key] = number
|
||||
return values.get('lxc.init.uid', 0), values.get('lxc.init.gid', 0)
|
||||
|
||||
|
||||
def _application_ids(deployment, item):
|
||||
"""Resolve Compose-style application IDs inside the LXC user namespace."""
|
||||
environment = {}
|
||||
for variable in deployment.get('environment', []):
|
||||
environment[variable.get('name')] = variable.get('value')
|
||||
uid = item.get('uid')
|
||||
gid = item.get('gid')
|
||||
if uid is None:
|
||||
uid = next((environment[name] for name in ('PUID', 'USER_ID', 'UID')
|
||||
if environment.get(name) not in (None, '')), 0)
|
||||
if gid is None:
|
||||
gid = next((environment[name] for name in ('PGID', 'GROUP_ID', 'GID')
|
||||
if environment.get(name) not in (None, '')), 0)
|
||||
result = []
|
||||
for value in (uid, gid):
|
||||
if isinstance(value, bool) or not re.fullmatch(r'[0-9]{1,10}', str(value)):
|
||||
raise ValueError(translate('The application user or group is not numeric'))
|
||||
number = int(value)
|
||||
if number > 4294967294:
|
||||
raise ValueError(translate('The application user or group is outside the supported range'))
|
||||
result.append(number)
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def tmpfs_lines(deployment, config=None):
|
||||
result = []
|
||||
targets = set()
|
||||
persistent = [m['container_path'].rstrip('/') for m in deployment.get('mounts', [])]
|
||||
for item in deployment.get('tmpfs_mounts', []):
|
||||
target, size = item['container_path'], item['size_mb']
|
||||
owner_strategy = item.get('owner_strategy')
|
||||
if (not re.fullmatch(r'/[A-Za-z0-9_./-]+', target) or '..' in target.split('/')
|
||||
or '//' in target or target.endswith('/') or target in ('/etc','/usr','/bin','/lib','/lib64','/sbin','/proc','/sys','/dev','/run')
|
||||
or not (target.startswith(('/run/', '/tmp/', '/var/cache/')) or target == '/dev/shm')
|
||||
or '//' in target or target.endswith('/') or target in ('/etc','/usr','/bin','/lib','/lib64','/sbin','/proc','/sys','/dev')
|
||||
or target == '/run' and owner_strategy not in ('mapped-root', 'mapped-application-user', 'image-user')
|
||||
or not (target == '/run' or target.startswith(('/run/', '/tmp/', '/var/cache/')) or target == '/dev/shm')
|
||||
or isinstance(size, bool) or not isinstance(size, int) or size < 1):
|
||||
raise ValueError(translate('The tmpfs path or size is outside the supported profile'))
|
||||
if any(target == p or target.startswith(p+'/') or p.startswith(target+'/') for p in [*persistent,*targets]):
|
||||
@@ -43,11 +93,35 @@ def tmpfs_lines(deployment):
|
||||
raise ValueError(translate('Unsupported tmpfs options'))
|
||||
if len(options) != len(set(options)) or ('rw' in options and 'ro' in options):
|
||||
raise ValueError(translate('Contradictory tmpfs options'))
|
||||
mode = item.get('mode')
|
||||
if mode is not None:
|
||||
if not isinstance(mode, str) or not re.fullmatch(r'0[0-7]{3}', mode):
|
||||
raise ValueError(translate('Unsupported tmpfs mode'))
|
||||
if any(option.startswith('mode=') for option in options):
|
||||
raise ValueError(translate('The tmpfs mode is specified more than once'))
|
||||
if owner_strategy is not None:
|
||||
if owner_strategy == 'mapped-root':
|
||||
uid, gid = 0, 0
|
||||
elif owner_strategy == 'mapped-application-user':
|
||||
uid, gid = _application_ids(deployment, item)
|
||||
elif owner_strategy == 'image-user':
|
||||
uid, gid = _config_ids(config)
|
||||
else:
|
||||
raise ValueError(translate('Unsupported tmpfs owner strategy'))
|
||||
options = [*options, f'uid={uid}', f'gid={gid}']
|
||||
if mode is not None:
|
||||
options.append(f'mode={mode}')
|
||||
targets.add(target)
|
||||
result.append(f'tmpfs {target.lstrip("/")} tmpfs {",".join(options)},size={size}M,create=dir 0 0')
|
||||
return result
|
||||
|
||||
|
||||
def render_tmpfs(deployment_path, config_path):
|
||||
deployment = json.loads(Path(deployment_path).read_text())
|
||||
config = Path(config_path).read_bytes()
|
||||
return ''.join(f'lxc.mount.entry: {line}\n' for line in tmpfs_lines(deployment, config))
|
||||
|
||||
|
||||
# /etc/pve/lxc is the folder of the local node only; this one is the same on
|
||||
# every node of a cluster, so a migrated container still finds its include.
|
||||
CLUSTER_DIR = Path('/etc/pve/proxmenux')
|
||||
@@ -85,7 +159,7 @@ def seccomp_path(vmid):
|
||||
|
||||
|
||||
def check(config, deployment, vmid):
|
||||
expected = tmpfs_lines(deployment)
|
||||
expected = tmpfs_lines(deployment, config)
|
||||
lines = config.decode().splitlines()
|
||||
actual = [line.split(': ',1)[1] for line in lines if line.startswith('lxc.mount.entry: tmpfs ')]
|
||||
if sorted(actual) != sorted(expected):
|
||||
@@ -123,11 +197,11 @@ def file_binds(config):
|
||||
return found
|
||||
|
||||
|
||||
def filter_config(config, deployment):
|
||||
def filter_config(config, deployment, runtime_config=None):
|
||||
"""The configuration without the entries of the mounts the deployment
|
||||
declares itself: what is left belongs to the acceleration profile."""
|
||||
config = nested_mounts.without(config)
|
||||
declared = set(tmpfs_lines(deployment))
|
||||
declared = set(tmpfs_lines(deployment, runtime_config if runtime_config is not None else config))
|
||||
files = {m['source'] for m in deployment.get('mounts', []) if m.get('type') == 'host-bind'}
|
||||
|
||||
def own(line):
|
||||
@@ -141,7 +215,11 @@ def filter_config(config, deployment):
|
||||
|
||||
def check_recovery(config, state):
|
||||
plans = [state.get(key, {}).get('deployment', {}) for key in ('record', 'candidate_contract')]
|
||||
permitted = {line for plan in plans for line in tmpfs_lines(plan)}
|
||||
configs = [config]
|
||||
before_config = state.get('before_config')
|
||||
if isinstance(before_config, str):
|
||||
configs.append(before_config.encode())
|
||||
permitted = {line for plan in plans for source in configs for line in tmpfs_lines(plan, source)}
|
||||
for line in config.decode().splitlines():
|
||||
if line.startswith('lxc.mount.entry: tmpfs ') and line.split(': ', 1)[1] not in permitted:
|
||||
raise ValueError(translate('A tmpfs mount is not part of the journal; recovery blocked'))
|
||||
@@ -180,3 +258,11 @@ def restore(deployment, vmid):
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--render-tmpfs', nargs=2, metavar=('DEPLOYMENT', 'CONFIG'))
|
||||
args = parser.parse_args()
|
||||
if args.render_tmpfs:
|
||||
print(render_tmpfs(*args.render_tmpfs), end='')
|
||||
|
||||
Reference in New Issue
Block a user