mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
142 lines
4.5 KiB
Python
142 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Clone a FUSE mount from an LXC namespace into the Proxmox host namespace."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ctypes
|
|
import json
|
|
import os
|
|
import platform
|
|
import sys
|
|
from pathlib import Path
|
|
from functools import lru_cache
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _messages() -> dict:
|
|
# This helper is also installed alone in libexec: importing oci_ui would
|
|
# break that existing caller. Read the same catalog using stdlib only.
|
|
base = Path(os.environ.get('PMX_BASE_DIR', '/usr/local/share/proxmenux'))
|
|
try:
|
|
language = json.loads((base / 'config.json').read_text(encoding='utf-8')).get('language', 'en')
|
|
if language == 'en' or not isinstance(language, str) or not language:
|
|
return {}
|
|
data = json.loads((base / 'lang' / (language + '.json')).read_text(encoding='utf-8'))
|
|
return data if isinstance(data, dict) else {}
|
|
except (OSError, ValueError, AttributeError):
|
|
return {}
|
|
|
|
|
|
def translate(text: str) -> str:
|
|
value = _messages().get(text)
|
|
return value if isinstance(value, str) and value else text
|
|
|
|
|
|
AT_FDCWD = -100
|
|
AT_EMPTY_PATH = 0x1000
|
|
AT_RECURSIVE = 0x8000
|
|
CLONE_NEWNS = 0x00020000
|
|
MOVE_MOUNT_F_EMPTY_PATH = 0x00000004
|
|
MOUNT_ATTR_RDONLY = 0x00000001
|
|
OPEN_TREE_CLONE = 1
|
|
|
|
SYSCALLS = {
|
|
"x86_64": (428, 429, 442),
|
|
"amd64": (428, 429, 442),
|
|
"aarch64": (428, 429, 442),
|
|
"arm64": (428, 429, 442),
|
|
}
|
|
|
|
|
|
class MountAttr(ctypes.Structure):
|
|
_fields_ = [
|
|
("attr_set", ctypes.c_uint64),
|
|
("attr_clr", ctypes.c_uint64),
|
|
("propagation", ctypes.c_uint64),
|
|
("userns_fd", ctypes.c_uint64),
|
|
]
|
|
|
|
|
|
def fail(step: str) -> None:
|
|
error = ctypes.get_errno()
|
|
raise OSError(error, f"{step}: {os.strerror(error)}")
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) != 5 or sys.argv[4] not in {"rw", "ro"}:
|
|
print(f"{translate('Usage:')} {sys.argv[0]} PID SOURCE TARGET rw|ro", file=sys.stderr)
|
|
return 2
|
|
machine = platform.machine().lower()
|
|
if machine not in SYSCALLS:
|
|
print(f"{translate('Unsupported host architecture:')} {machine}", file=sys.stderr)
|
|
return 2
|
|
open_tree_nr, move_mount_nr, mount_setattr_nr = SYSCALLS[machine]
|
|
pid, source, target, mode = sys.argv[1:]
|
|
# The host catalog is no longer reachable after chroot into the container.
|
|
_messages()
|
|
libc = ctypes.CDLL(None, use_errno=True)
|
|
libc.syscall.restype = ctypes.c_long
|
|
libc.setns.argtypes = (ctypes.c_int, ctypes.c_int)
|
|
libc.setns.restype = ctypes.c_int
|
|
|
|
host_ns = os.open("/proc/self/ns/mnt", os.O_RDONLY | os.O_CLOEXEC)
|
|
host_root = os.open("/", os.O_PATH | os.O_DIRECTORY | os.O_CLOEXEC)
|
|
ct_ns = os.open(f"/proc/{pid}/ns/mnt", os.O_RDONLY | os.O_CLOEXEC)
|
|
ct_root = os.open(f"/proc/{pid}/root", os.O_PATH | os.O_DIRECTORY | os.O_CLOEXEC)
|
|
try:
|
|
if libc.setns(ct_ns, CLONE_NEWNS) != 0:
|
|
fail(translate("enter container namespace"))
|
|
os.fchdir(ct_root)
|
|
os.chroot(".")
|
|
os.chdir("/")
|
|
tree = libc.syscall(
|
|
open_tree_nr,
|
|
AT_FDCWD,
|
|
os.fsencode(source),
|
|
OPEN_TREE_CLONE | os.O_CLOEXEC,
|
|
)
|
|
if tree < 0:
|
|
fail(translate("clone source mount tree"))
|
|
try:
|
|
if mode == "ro":
|
|
attributes = MountAttr(attr_set=MOUNT_ATTR_RDONLY)
|
|
result = libc.syscall(
|
|
mount_setattr_nr,
|
|
tree,
|
|
ctypes.c_char_p(b""),
|
|
AT_EMPTY_PATH | AT_RECURSIVE,
|
|
ctypes.byref(attributes),
|
|
ctypes.sizeof(attributes),
|
|
)
|
|
if result != 0:
|
|
fail(translate("make cloned mount tree read-only"))
|
|
if libc.setns(host_ns, CLONE_NEWNS) != 0:
|
|
fail(translate("return to host namespace"))
|
|
os.fchdir(host_root)
|
|
os.chroot(".")
|
|
os.chdir("/")
|
|
result = libc.syscall(
|
|
move_mount_nr,
|
|
tree,
|
|
ctypes.c_char_p(b""),
|
|
AT_FDCWD,
|
|
os.fsencode(target),
|
|
MOVE_MOUNT_F_EMPTY_PATH,
|
|
)
|
|
if result != 0:
|
|
fail(translate("publish mount tree"))
|
|
finally:
|
|
os.close(tree)
|
|
finally:
|
|
for descriptor in (ct_root, ct_ns, host_root, host_ns):
|
|
os.close(descriptor)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main())
|
|
except OSError as exc:
|
|
print(exc, file=sys.stderr)
|
|
raise SystemExit(1)
|