mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
fix(oci): a shared host directory shows what the host has mounted inside it
This commit is contained in:
@@ -25,6 +25,7 @@ import xml.etree.ElementTree as ET
|
||||
|
||||
from allocate_private_network import allocate_network, existing_bridges, existing_networks
|
||||
import oci_instances
|
||||
import oci_nested_mounts
|
||||
from oci_ui import translate, msg_info, msg_ok, msg_warn, msg_error, msg_info2, stop_spinner, log
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
@@ -446,6 +447,7 @@ def attach_mounts(service, temporary):
|
||||
current = next(line.split(': ', 1)[1] for line in config.splitlines()
|
||||
if line.startswith(f'mp{index}: '))
|
||||
run('pct', 'set', vmid, f'--mp{index}', current + ',ro=1')
|
||||
oci_nested_mounts.follow(vmid)
|
||||
|
||||
|
||||
def wait_web(primary, url):
|
||||
|
||||
@@ -2024,6 +2024,12 @@ CREDENTIALS=$(jq -c --argjson environment "$DEPLOYMENT_ENVIRONMENT" '
|
||||
' "$TEMPLATE_FILE")
|
||||
RUNTIME_CREDENTIALS=$(jq '[.[] | select(.retrieval.method? == "container-console-pattern")] | length' <<<"$CREDENTIALS")
|
||||
|
||||
# A host directory is mounted with what the host has mounted inside it. Written
|
||||
# once every mount point is set and after the host monitor profile, which
|
||||
# clears the mount hooks.
|
||||
python3 "${SCRIPT_DIR}/oci_nested_mounts.py" follow "$VMID" \
|
||||
|| die "$(translate "Could not add the mount point:") CT $VMID"
|
||||
|
||||
# The console of the container is kept as its log, the way `docker logs` keeps
|
||||
# it, and the Proxmox console opens a shell when the image has one. Both are
|
||||
# set before the configuration is recorded, so the record carries them and an
|
||||
|
||||
@@ -30,6 +30,7 @@ import oci_instances as instances
|
||||
from oci_installation_state import image_from_archive, parse_config, private_directory, sha
|
||||
from verify_oci_archive import verify_archive
|
||||
import oci_host_mounts as host_mounts
|
||||
import oci_nested_mounts as nested_mounts
|
||||
import oci_accelerators as gpu_devices
|
||||
import oci_runtime_settings as runtime_settings
|
||||
import oci_image_cache as image_cache
|
||||
@@ -473,7 +474,10 @@ def preflight(record, candidate, config, coordinated=None):
|
||||
raise ValueError(translate('The imported OCI user or group is not numeric'))
|
||||
if 'lxc.init.groups' in cfg and not re.fullmatch(r'(?:[0-9]+(?:[ ,][0-9]+)*)?', cfg['lxc.init.groups']):
|
||||
raise ValueError(translate('The imported OCI groups are not numeric'))
|
||||
keys = {line.split(': ', 1)[0] for line in config.decode().splitlines() if ': ' in line}
|
||||
# What shows the content mounted inside a host directory follows the mount
|
||||
# points: it is checked against them and is not a setting of its own.
|
||||
nested_mounts.check(config)
|
||||
keys = {line.split(': ', 1)[0] for line in nested_mounts.without(config).decode().splitlines() if ': ' in line}
|
||||
runtime_keys = {'lxc.mount.entry'} if gpu_devices.nvidia.enabled(deployment) else set()
|
||||
if gpu_devices.dynamic_mode(deployment):
|
||||
runtime_keys = {'lxc.hook.mount', 'lxc.environment'}
|
||||
@@ -597,6 +601,7 @@ def freeze_host_sources(record, candidate, acknowledge_external_data, files=()):
|
||||
|
||||
def check_runtime_mounts(config, deployment):
|
||||
runtime_settings.check(config, deployment, deployment['vmid'])
|
||||
nested_mounts.check(config)
|
||||
actual = mounts(config)
|
||||
files = runtime_settings.file_binds(config)
|
||||
if files != {m['source']: bool(m.get('read_only')) for m in deployment.get('mounts', []) if is_file_bind(m, files)}:
|
||||
|
||||
@@ -13,6 +13,7 @@ import time
|
||||
|
||||
import oci_console
|
||||
import oci_instances as instances
|
||||
import oci_nested_mounts
|
||||
from oci_installation_state import command, image_from_archive, sha
|
||||
import oci_stack_replay
|
||||
from oci_ui import translate
|
||||
@@ -84,6 +85,7 @@ def create(root, args):
|
||||
# console as a shell, set before the stack records its configuration
|
||||
# so an update rebuilds them the same way.
|
||||
oci_console.configure(vmid)
|
||||
oci_nested_mounts.follow(vmid)
|
||||
return code
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Show in a container what the host has mounted inside a shared directory.
|
||||
|
||||
Proxmox mounts the directory of a mount point alone: a filesystem mounted
|
||||
inside it, as a network share under /mnt/pve, is left out and the container
|
||||
finds an empty folder in its place. Next to each of those mount points goes a
|
||||
raw LXC entry that mounts the same directory with everything inside it, and
|
||||
keeps following what the host mounts and unmounts while the container runs.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from oci_installation_state import parse_config
|
||||
from oci_ui import translate
|
||||
|
||||
ENTRY = re.compile(r'lxc\.mount\.entry: (/\S*) (\S+) none rbind,rslave(,ro)?,create=dir 0 0')
|
||||
# Read only reaches the directory alone; this takes it to what is inside.
|
||||
HOOK = re.compile(r"""lxc\.hook\.mount: /bin/sh -c 'mount -o remount,bind,ro=recursive "\$LXC_ROOTFS_MOUNT/(\S+)"'""")
|
||||
READ_ONLY = "lxc.hook.mount: /bin/sh -c 'mount -o remount,bind,ro=recursive \"$LXC_ROOTFS_MOUNT/{}\"'"
|
||||
# Trees of the kernel, mounted as the recipe of a host monitor lays them out.
|
||||
SYSTEM_TREES = ('/proc', '/sys', '/dev', '/run')
|
||||
# Characters with a meaning of their own in an LXC entry or in the hook.
|
||||
UNSAFE = re.compile(r'''[\s\\'"$`]''')
|
||||
|
||||
|
||||
def own(line):
|
||||
"""Whether a configuration line is one of those written here."""
|
||||
return bool(ENTRY.fullmatch(line) or HOOK.fullmatch(line))
|
||||
|
||||
|
||||
def groups(config):
|
||||
"""The lines of each host directory among the mount points, a directory
|
||||
before the ones mounted inside it."""
|
||||
found = {}
|
||||
for key, value in parse_config(config).items():
|
||||
if not re.fullmatch(r'mp[0-9]+', key) or not value.startswith('/'):
|
||||
continue
|
||||
source, *rest = value.split(',')
|
||||
options = dict(item.split('=', 1) for item in rest if '=' in item)
|
||||
target = options.get('mp', '').strip('/')
|
||||
if (not target or UNSAFE.search(source) or UNSAFE.search(target) or source == '/'
|
||||
or any(source == tree or source.startswith(tree + '/') for tree in SYSTEM_TREES)):
|
||||
continue
|
||||
if options.get('ro') == '1':
|
||||
found[target] = [f'lxc.mount.entry: {source} {target} none rbind,rslave,ro,create=dir 0 0',
|
||||
READ_ONLY.format(target)]
|
||||
else:
|
||||
found[target] = [f'lxc.mount.entry: {source} {target} none rbind,rslave,create=dir 0 0']
|
||||
return [found[target] for target in sorted(found)]
|
||||
|
||||
|
||||
def without(config):
|
||||
"""The configuration without the lines written here."""
|
||||
return b''.join(line for line in config.splitlines(keepends=True) if not own(line.decode().strip()))
|
||||
|
||||
|
||||
def check(config):
|
||||
"""Every line of this kind belongs to a mount point of the container, and
|
||||
none of a mount point is missing while another of it is there."""
|
||||
present = [line for line in config.decode().splitlines() if own(line)]
|
||||
expected = [line for group in groups(config) if any(line in present for line in group) for line in group]
|
||||
if sorted(present) != sorted(expected):
|
||||
raise ValueError(translate('A container mount has a source, backup or permission different from the saved record'))
|
||||
|
||||
|
||||
def follow(vmid):
|
||||
"""Write the lines of the mount points the container has now, and no
|
||||
others. Any snapshot section is left as it is."""
|
||||
conf = Path(f'/etc/pve/lxc/{int(vmid)}.conf')
|
||||
text = conf.read_text()
|
||||
current, _, snapshots = text.partition('\n[')
|
||||
kept = [line for line in current.splitlines() if not own(line)]
|
||||
wanted = [line for group in groups(current.encode()) for line in group]
|
||||
rebuilt = '\n'.join(kept + wanted) + '\n'
|
||||
if snapshots:
|
||||
rebuilt += '\n[' + snapshots
|
||||
if rebuilt != text:
|
||||
conf.write_text(rebuilt)
|
||||
return wanted
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('action', choices=('follow',))
|
||||
parser.add_argument('vmid', type=int)
|
||||
args = parser.parse_args(argv)
|
||||
if os.geteuid() != 0:
|
||||
parser.error(translate('Root privileges are required'))
|
||||
follow(args.vmid)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -33,6 +33,7 @@ import oci_carried_record as carried
|
||||
import oci_console
|
||||
import oci_instances as instances
|
||||
import oci_instance_transaction as transaction
|
||||
import oci_nested_mounts
|
||||
import oci_nvidia_refresh
|
||||
import oci_nvidia_runtime as nvidia
|
||||
import oci_runtime_settings as runtime_settings
|
||||
@@ -460,7 +461,7 @@ def check_host_resources(plan):
|
||||
f"{label}: {translate('it uses an NVIDIA GPU and this host has no working NVIDIA driver and Container Toolkit. Install them and run the recovery again.')}")
|
||||
for line in lines:
|
||||
key, _, value = line.partition(': ')
|
||||
if key == 'lxc.hook.mount' and value:
|
||||
if key == 'lxc.hook.mount' and value and not oci_nested_mounts.own(line):
|
||||
hook = carried.NVIDIA_HOOK.fullmatch(value)
|
||||
if not hook:
|
||||
plan['blockers'].append(f"{label}: {translate('unknown mount hook:')} {value}")
|
||||
@@ -737,7 +738,8 @@ def restore_host_files(entry):
|
||||
runtime_settings.restore(deployment, vmid)
|
||||
else:
|
||||
path.write_text(runtime_settings.sysctl_content(deployment))
|
||||
elif key == 'lxc.hook.mount' and value and not Path(value).is_file():
|
||||
elif (key == 'lxc.hook.mount' and value and not oci_nested_mounts.own(line)
|
||||
and not Path(value).is_file()):
|
||||
content = nvidia_hook_source(entry, carried.NVIDIA_HOOK.fullmatch(value)[1])
|
||||
Path(value).parent.mkdir(parents=True, exist_ok=True, mode=0o755)
|
||||
Path(value).write_text(content)
|
||||
|
||||
@@ -5,6 +5,7 @@ import re
|
||||
import stat
|
||||
import tempfile
|
||||
|
||||
import oci_nested_mounts as nested_mounts
|
||||
from oci_ui import translate
|
||||
|
||||
|
||||
@@ -125,6 +126,7 @@ def file_binds(config):
|
||||
def filter_config(config, deployment):
|
||||
"""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))
|
||||
files = {m['source'] for m in deployment.get('mounts', []) if m.get('type') == 'host-bind'}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import oci_gpu_devices as gpu_devices
|
||||
import oci_host_mounts as host_mounts
|
||||
import oci_instance_reconcile as reconcile
|
||||
import oci_instances as instances
|
||||
import oci_nested_mounts
|
||||
import oci_stack_replay as replay
|
||||
from oci_ui import msg_error, msg_info, msg_ok, translate
|
||||
|
||||
@@ -129,6 +130,8 @@ def apply(vmid, changes):
|
||||
if unused:
|
||||
run('pct', 'set', str(vmid), '--delete', unused)
|
||||
msg_ok(f"{translate('Path removed:')} {path}")
|
||||
if changes['add_mounts'] or changes['remove_mounts']:
|
||||
oci_nested_mounts.follow(vmid)
|
||||
|
||||
|
||||
def recorded_mounts(vmid):
|
||||
|
||||
@@ -9,6 +9,7 @@ import os
|
||||
|
||||
import oci_console
|
||||
import oci_gpu_devices
|
||||
import oci_nested_mounts
|
||||
from oci_ui import translate
|
||||
|
||||
|
||||
@@ -617,12 +618,13 @@ def normalize(record):
|
||||
preserved = {key: single(key) for key in ('arch', 'ostype', 'cmode', 'console', 'tty', 'cpuunits',
|
||||
'net0', 'net1', 'startup', 'hookscript', 'features', 'tags') if key in values}
|
||||
devices = [{'key': key, 'value': single(key)} for key in values if re.fullmatch(r'dev[0-9]+', key)]
|
||||
# The console log line and its start hook are not replayed: the installer
|
||||
# that rebuilds the member sets them itself, and replaying them too would
|
||||
# leave two of each.
|
||||
# The console log line, its start hook and what shows the content mounted
|
||||
# inside a host directory are not replayed: the installer that rebuilds the
|
||||
# member sets them itself, and replaying them too would leave two of each.
|
||||
raw_runtime = [line for line in config.splitlines() if line.startswith('lxc.')
|
||||
and line.partition(': ')[0] not in ('lxc.environment.runtime', 'lxc.init.cwd',
|
||||
'lxc.signal.halt', 'lxc.console.logfile')
|
||||
and not oci_nested_mounts.own(line)
|
||||
and not oci_console.is_start_mark_hook(line)]
|
||||
return {'schema_version': 1, 'deployment': plan, 'runtime': runtime,
|
||||
'preserved_native': preserved, 'generated_files': copy.deepcopy(files),
|
||||
|
||||
@@ -190,6 +190,8 @@ oci_apply_extra_mounts() {
|
||||
count=$((count + 1))
|
||||
done < <(jq -r '.extra_mounts[]? | [.type, .container_path, .source, (.size_gb // "-"), (.read_only // false)] | @tsv' "$DEPLOYMENT_FILE")
|
||||
if (( count > 0 )); then
|
||||
python3 "${SCRIPT_DIR}/oci_nested_mounts.py" follow "$vmid" \
|
||||
|| die "$(translate "Could not add the mount point:") CT $vmid"
|
||||
msg_ok "$(translate "Mount points added:") $count"
|
||||
fi
|
||||
return 0
|
||||
|
||||
@@ -1534,7 +1534,7 @@ def _run_remote_install(
|
||||
shutil.copy2(project_root / 'remote' / 'haos_healthcheck.py', temporary / 'haos_healthcheck.py')
|
||||
shutil.copy2(project_root / 'remote' / 'oci_installation_state.py', temporary / 'oci_installation_state.py')
|
||||
shutil.copy2(project_root / 'remote' / 'oci_instances.py', temporary / 'oci_instances.py')
|
||||
for helper in ('oci_ui.sh', 'oci_ui.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_host_mounts.py', 'oci_runtime_settings.py', 'oci_gpu_devices.py', 'oci_accelerators.py', 'oci_nvidia_runtime.py', 'oci_nvidia_refresh.py', 'oci_nvidia_dynamic.py', 'oci_update_current.py', 'oci_stack_replay.py', 'oci_stack_plan.py', 'oci_stack_transaction.py', 'oci_stack_native.py', 'oci_image_cache.py', 'nvidia_lxc_mount_lab.sh', 'oci_nvidia_setup.sh', 'oci_immich_ml.sh', 'oci_rocm_check.py'):
|
||||
for helper in ('oci_ui.sh', 'oci_ui.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_host_mounts.py', 'oci_nested_mounts.py', 'oci_runtime_settings.py', 'oci_gpu_devices.py', 'oci_accelerators.py', 'oci_nvidia_runtime.py', 'oci_nvidia_refresh.py', 'oci_nvidia_dynamic.py', 'oci_update_current.py', 'oci_stack_replay.py', 'oci_stack_plan.py', 'oci_stack_transaction.py', 'oci_stack_native.py', 'oci_image_cache.py', 'nvidia_lxc_mount_lab.sh', 'oci_nvidia_setup.sh', 'oci_immich_ml.sh', 'oci_rocm_check.py'):
|
||||
shutil.copy2(project_root / 'remote' / helper, temporary / helper)
|
||||
if deployment_kind == 'generic-multi-lxc-stack':
|
||||
shutil.copy2(project_root / 'remote' / 'install_generic_stack.py', temporary / 'install_generic_stack.py')
|
||||
@@ -1571,7 +1571,7 @@ def _run_remote_install(
|
||||
archive.add(project_root / 'remote' / 'haos_healthcheck.py', arcname='haos_healthcheck.py')
|
||||
archive.add(project_root / 'remote' / 'oci_installation_state.py', arcname='oci_installation_state.py')
|
||||
archive.add(project_root / 'remote' / 'oci_instances.py', arcname='oci_instances.py')
|
||||
for helper in ('oci_ui.sh', 'oci_ui.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_host_mounts.py', 'oci_runtime_settings.py', 'oci_gpu_devices.py', 'oci_accelerators.py', 'oci_nvidia_runtime.py', 'oci_nvidia_refresh.py', 'oci_nvidia_dynamic.py', 'oci_update_current.py', 'oci_stack_replay.py', 'oci_stack_plan.py', 'oci_stack_transaction.py', 'oci_stack_native.py', 'oci_image_cache.py', 'nvidia_lxc_mount_lab.sh', 'oci_nvidia_setup.sh', 'oci_immich_ml.sh', 'oci_rocm_check.py'):
|
||||
for helper in ('oci_ui.sh', 'oci_ui.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_host_mounts.py', 'oci_nested_mounts.py', 'oci_runtime_settings.py', 'oci_gpu_devices.py', 'oci_accelerators.py', 'oci_nvidia_runtime.py', 'oci_nvidia_refresh.py', 'oci_nvidia_dynamic.py', 'oci_update_current.py', 'oci_stack_replay.py', 'oci_stack_plan.py', 'oci_stack_transaction.py', 'oci_stack_native.py', 'oci_image_cache.py', 'nvidia_lxc_mount_lab.sh', 'oci_nvidia_setup.sh', 'oci_immich_ml.sh', 'oci_rocm_check.py'):
|
||||
archive.add(project_root / 'remote' / helper, arcname=helper)
|
||||
if deployment_kind == 'generic-multi-lxc-stack':
|
||||
archive.add(project_root / 'remote' / 'install_generic_stack.py', arcname='install_generic_stack.py')
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""A host directory is mounted with what the host has mounted inside it, as
|
||||
the network shares under /mnt/pve: Proxmox mounts the directory alone, and the
|
||||
container would find an empty folder in the place of each share."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "remote"))
|
||||
|
||||
import oci_nested_mounts as nested
|
||||
import oci_runtime_settings as runtime_settings
|
||||
import oci_stack_replay
|
||||
|
||||
CONFIG = """arch: amd64
|
||||
hostname: duplicati
|
||||
mp0: local-lvm:vm-113-disk-1,mp=/config,backup=1,size=16G
|
||||
mp1: /mnt/pve,mp=/source,backup=0,ro=1
|
||||
mp2: /mnt/backups,mp=/backups,backup=0
|
||||
unprivileged: 1
|
||||
"""
|
||||
READ_WRITE = "lxc.mount.entry: /mnt/backups backups none rbind,rslave,create=dir 0 0"
|
||||
READ_ONLY = "lxc.mount.entry: /mnt/pve source none rbind,rslave,ro,create=dir 0 0"
|
||||
HOOK = ("lxc.hook.mount: /bin/sh -c 'mount -o remount,bind,ro=recursive "
|
||||
"\"$LXC_ROOTFS_MOUNT/source\"'")
|
||||
|
||||
|
||||
def lines(config):
|
||||
return [line for group in nested.groups(config.encode()) for line in group]
|
||||
|
||||
|
||||
class LinesTests(unittest.TestCase):
|
||||
def test_every_host_directory_gets_an_entry_that_follows_the_host(self):
|
||||
self.assertEqual(lines(CONFIG), [READ_WRITE, READ_ONLY, HOOK])
|
||||
|
||||
def test_a_volume_gets_none(self):
|
||||
self.assertEqual(lines("mp0: local-lvm:vm-113-disk-1,mp=/config,backup=1,size=16G\n"), [])
|
||||
|
||||
def test_read_only_reaches_what_is_mounted_inside(self):
|
||||
self.assertIn("ro=recursive", HOOK)
|
||||
self.assertEqual(lines("mp0: /mnt/pve,mp=/source,backup=0,ro=1\n"), [READ_ONLY, HOOK])
|
||||
|
||||
def test_the_kernel_trees_of_a_host_monitor_stay_as_the_recipe_lays_them_out(self):
|
||||
config = ("mp0: /proc,mp=/host/proc,backup=0,ro=1\nmp1: /sys,mp=/host/sys,backup=0,ro=1\n"
|
||||
"mp2: /sys/fs/cgroup,mp=/host/sys/fs/cgroup,backup=0,ro=1\n")
|
||||
self.assertEqual(lines(config), [])
|
||||
|
||||
def test_a_path_that_would_break_the_entry_keeps_the_plain_mount_point(self):
|
||||
for source in ("/mnt/it's", "/mnt/a$b", "/mnt/a`b", '/mnt/a"b', "/mnt/a\\040b"):
|
||||
self.assertEqual(lines(f"mp0: {source},mp=/data,backup=0\n"), [], source)
|
||||
|
||||
def test_a_directory_goes_before_the_ones_mounted_inside_it(self):
|
||||
config = "mp0: /mnt/pve/media,mp=/data/media,backup=0\nmp1: /mnt/pve,mp=/data,backup=0\n"
|
||||
self.assertEqual([line.split()[2] for line in lines(config)], ["data", "data/media"])
|
||||
|
||||
|
||||
class CheckTests(unittest.TestCase):
|
||||
def config(self, *extra):
|
||||
return (CONFIG + "".join(line + "\n" for line in extra)).encode()
|
||||
|
||||
def test_the_lines_of_the_mount_points_are_accepted(self):
|
||||
nested.check(self.config(READ_WRITE, READ_ONLY, HOOK))
|
||||
|
||||
def test_a_container_installed_before_has_none_and_is_accepted(self):
|
||||
nested.check(self.config())
|
||||
|
||||
def test_an_entry_without_its_mount_point_is_refused(self):
|
||||
with self.assertRaises(ValueError):
|
||||
nested.check(self.config("lxc.mount.entry: /etc secrets none rbind,rslave,create=dir 0 0"))
|
||||
|
||||
def test_an_entry_that_is_not_read_only_as_its_mount_point_is_refused(self):
|
||||
with self.assertRaises(ValueError):
|
||||
nested.check(self.config("lxc.mount.entry: /mnt/pve source none rbind,rslave,create=dir 0 0"))
|
||||
|
||||
def test_a_read_only_entry_without_its_hook_is_refused(self):
|
||||
with self.assertRaises(ValueError):
|
||||
nested.check(self.config(READ_ONLY))
|
||||
|
||||
def test_a_repeated_entry_is_refused(self):
|
||||
with self.assertRaises(ValueError):
|
||||
nested.check(self.config(READ_WRITE, READ_WRITE))
|
||||
|
||||
|
||||
class OtherChecksTests(unittest.TestCase):
|
||||
def test_they_are_not_seen_as_part_of_an_acceleration_profile(self):
|
||||
config = (CONFIG + READ_WRITE + "\n" + READ_ONLY + "\n" + HOOK + "\n").encode()
|
||||
left = runtime_settings.filter_config(config, {"mounts": []}).decode()
|
||||
self.assertNotIn("lxc.mount.entry", left)
|
||||
self.assertNotIn("lxc.hook.mount", left)
|
||||
self.assertIn("mp1: /mnt/pve", left)
|
||||
|
||||
def test_another_hook_is_not_taken_for_one_of_these(self):
|
||||
other = "lxc.hook.mount: /usr/local/lib/proxmenux/oci/nvidia-mount-" + "a" * 64 + ".sh"
|
||||
self.assertFalse(nested.own(other))
|
||||
self.assertIn(other.encode(), nested.without((CONFIG + other + "\n" + HOOK + "\n").encode()))
|
||||
|
||||
def test_a_rebuilt_stack_member_does_not_replay_them(self):
|
||||
source = Path(oci_stack_replay.__file__).read_text()
|
||||
self.assertIn("not oci_nested_mounts.own(line)", source)
|
||||
|
||||
|
||||
class FollowTests(unittest.TestCase):
|
||||
def follow(self, text):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
conf = Path(directory) / "113.conf"
|
||||
conf.write_text(text)
|
||||
with patch.object(nested, "Path", lambda value: conf):
|
||||
nested.follow(113)
|
||||
return conf.read_text()
|
||||
|
||||
def test_the_lines_are_written_after_the_rest_of_the_configuration(self):
|
||||
self.assertEqual(self.follow(CONFIG), CONFIG + READ_WRITE + "\n" + READ_ONLY + "\n" + HOOK + "\n")
|
||||
|
||||
def test_writing_them_again_changes_nothing(self):
|
||||
once = self.follow(CONFIG)
|
||||
self.assertEqual(self.follow(once), once)
|
||||
|
||||
def test_the_lines_of_a_removed_mount_point_go_with_it(self):
|
||||
once = self.follow(CONFIG)
|
||||
removed = once.replace("mp1: /mnt/pve,mp=/source,backup=0,ro=1\n", "")
|
||||
self.assertEqual(self.follow(removed), removed.replace(READ_ONLY + "\n" + HOOK + "\n", ""))
|
||||
|
||||
def test_they_come_after_the_line_that_clears_the_hooks_of_a_host_monitor(self):
|
||||
text = self.follow(CONFIG + HOOK + "\nlxc.hook.mount: \n")
|
||||
self.assertLess(text.index("lxc.hook.mount: \n"), text.index(HOOK))
|
||||
self.assertEqual(text.count(HOOK), 1)
|
||||
|
||||
def test_a_snapshot_section_is_left_as_it_is(self):
|
||||
snapshot = "\n[before]\narch: amd64\nmp1: /mnt/old,mp=/old,backup=0\n"
|
||||
text = self.follow(CONFIG + snapshot)
|
||||
self.assertTrue(text.endswith(snapshot))
|
||||
self.assertNotIn("/mnt/old old", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user