fix(oci): start OCI containers on any node of a cluster

This commit is contained in:
MacRimi
2026-09-28 19:06:07 +02:00
parent 297c026c95
commit 79547dec35
11 changed files with 159 additions and 49 deletions
+7 -3
View File
@@ -286,8 +286,11 @@ apply_host_monitor() {
[[ -n ${HOST_MONITOR:-} ]] || return 0
# PVE permits lxc.include but not namespace keys directly in the CT config.
# This static, cluster-persistent companion must accompany cross-host restores.
local include=/etc/pve/lxc/proxmenux-host-monitor native
# /etc/pve/proxmenux is the same on every node of a cluster; /etc/pve/lxc
# is the folder of this node only.
local include=/etc/pve/proxmenux/host-monitor native
native=$'lxc.namespace.share.pid = 1\nlxc.namespace.share.net = 1'
mkdir -p /etc/pve/proxmenux || die "$(translate "Could not create the ProxMenux folder in /etc/pve")"
if [[ -e $include ]]; then
[[ $(cat "$include") == "$native" ]] || die "$(translate "A different host monitor include already exists; it is not overwritten:") $include"
else
@@ -300,7 +303,7 @@ apply_host_monitor() {
# Do not remove the Proxmox pre-start, autodev or post-stop hooks.
set_lxc_directive lxc.hook.mount ""
msg_ok "$(translate "Host monitor configured: shared PID and network namespaces, LXCFS disabled in this container")"
msg_info2 "$(translate "If you restore this container on another Proxmox host, copy this file to the same path first, because the container backup does not include it:") $include"
msg_info2 "$(translate "Every node of this cluster already has this file. If you restore this container on any other Proxmox host, copy it to the same path first, because the container backup does not include it:") $include"
}
verify_host_monitor() {
@@ -1661,7 +1664,8 @@ fi
SYSCTL_COUNT=$(jq '.security.sysctls? // [] | length' "$DEPLOYMENT_FILE")
if (( SYSCTL_COUNT > 0 )); then
SYSCTL_INCLUDE="/etc/pve/lxc/${VMID}.proxmenux-sysctls"
SYSCTL_INCLUDE="/etc/pve/proxmenux/${VMID}.sysctls"
mkdir -p /etc/pve/proxmenux || die "$(translate "Could not create the ProxMenux folder in /etc/pve")"
SYSCTL_TEMP=$(mktemp)
while IFS=$'\t' read -r SYSCTL_NAME SYSCTL_VALUE; do
[[ -n $SYSCTL_NAME ]] || continue
+8 -2
View File
@@ -28,14 +28,20 @@ LOG_DIR = Path('/var/log/proxmenux/oci')
# Each start is marked in the console log by a pre-start hook. The hook runs
# the script only when it exists and always succeeds: a hook that fails would
# stop the container from starting.
START_MARK_SCRIPT = Path(__file__).resolve().with_name('oci_console_mark.sh')
# The installed engine, not this file: the installer runs a copy of the
# engine from a temporary directory that is gone once it finishes.
START_MARK_SCRIPT = Path('/usr/local/share/proxmenux/oci/engine/remote/oci_console_mark.sh')
START_MARK = '=== ProxMenux: container started '
def start_mark_hook(vmid: int) -> str:
script = START_MARK_SCRIPT
# The hook runs before liblxc opens the console log, and liblxc refuses to
# start a container whose log directory is missing: on another cluster
# node, after a migration, it may never have been created.
# `test`, not `[`: a bracket in the configuration reads as a snapshot section.
return f"lxc.hook.pre-start: /bin/sh -c 'test -x {script} && {script} {int(vmid)}; exit 0'"
return (f"lxc.hook.pre-start: /bin/sh -c 'mkdir -p {LOG_DIR}; "
f"test -x {script} && {script} {int(vmid)}; exit 0'")
LOGROTATE = Path('/etc/logrotate.d/proxmenux-oci')
# copytruncate, because liblxc keeps the file open for as long as the
# container runs; moving it away would leave the application writing into the
+17 -5
View File
@@ -44,7 +44,17 @@ def tmpfs_lines(deployment):
return result
# /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')
def include_path(vmid):
return CLUSTER_DIR / f'{int(vmid)}.sysctls'
def legacy_include_path(vmid):
"""Where installations made before the cluster folder keep the include."""
return Path(f'/etc/pve/lxc/{int(vmid)}.proxmenux-sysctls')
@@ -56,10 +66,11 @@ def check(config, deployment, vmid):
raise ValueError(translate('The tmpfs mounts of the container differ from the saved record'))
includes = [line.split(': ',1)[1] for line in lines if line.startswith('lxc.include: ')]
content = sysctl_content(deployment)
if includes != ([str(include_path(vmid))] if content else []):
allowed = [[str(include_path(vmid))], [str(legacy_include_path(vmid))]] if content else [[]]
if includes not in allowed:
raise ValueError(translate('The sysctl include is unknown or differs from the saved record'))
if content:
path = include_path(vmid)
path = Path(includes[0])
info = path.lstat()
if not stat.S_ISREG(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o022:
raise ValueError(translate('The sysctl include is not a safe host file'))
@@ -82,8 +93,8 @@ def check_recovery(config, state):
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'))
if line.startswith('lxc.include: '):
path = include_path(state['vmid'])
if line.split(': ', 1)[1] != str(path):
path = Path(line.split(': ', 1)[1])
if path not in (include_path(state['vmid']), legacy_include_path(state['vmid'])):
raise ValueError(translate('An include is not part of the journal; recovery blocked'))
contents = {sysctl_content(plan) for plan in plans} - {''}
info = path.lstat()
@@ -99,6 +110,7 @@ def restore(deployment, vmid):
path = include_path(vmid)
if path.is_symlink():
raise ValueError(translate('The sysctl include is not restored over a symbolic link'))
path.parent.mkdir(parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(dir=path.parent, prefix='.oci-sysctl-')
try:
with os.fdopen(fd, 'w') as output:
@@ -107,7 +119,7 @@ def restore(deployment, vmid):
os.fsync(output.fileno())
# pmxcfs uses fixed permissions; ordinary filesystem fixtures still
# receive an explicit restrictive mode.
if path.parent != Path('/etc/pve/lxc'):
if Path('/etc/pve') not in path.parents:
os.chmod(temporary, 0o640)
os.replace(temporary, path)
finally: