Files
archy/tests/regression/runtime-uplink-systemd.py
T

165 lines
9.1 KiB
Python

#!/usr/bin/python3
"""Real PID1/netns fixture, only inside the dedicated disposable container."""
import hashlib
import ast
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import time
def run(*argv, **kwargs):
return subprocess.run(argv, check=True, capture_output=True, text=True, timeout=20, **kwargs).stdout.strip()
assert os.environ.get('RUNTIME_UPLINK_DISPOSABLE') == '1'
assert Path('/run/systemd/container').exists()
assert run('hostname') == 'runtime-uplink-fixture'
SOURCE = Path('/opt/uplink')
helper = SOURCE / 'runtime_uplink.py'
spec = importlib.util.spec_from_file_location('uplink', helper)
uplink = importlib.util.module_from_spec(spec)
spec.loader.exec_module(uplink)
def net(*args):
return run('ip', 'netns', 'exec', 'archipelago-runtime', 'ip', *args)
def state(unit, property):
return run('systemctl', 'show', unit, '-p', property, '--value')
def until(test):
deadline = time.monotonic() + 15
while not test():
assert time.monotonic() < deadline, 'fixture deadline'
time.sleep(.1)
run('ip', 'netns', 'add', 'archipelago-runtime')
# A misleading wg interface with an address and carrier is NOT a default.
net('link', 'add', 'wg0', 'type', 'dummy')
net('addr', 'add', '10.44.0.1/16', 'dev', 'wg0')
net('link', 'set', 'wg0', 'up')
net('link', 'set', 'lo', 'up')
runtime = Path('/run/uplink-runtime')
runtime.mkdir()
sentinel = runtime / 'wallet-sentinel'
sentinel.write_bytes(b'disposable retained data: never reset')
baseline = hashlib.sha256(sentinel.read_bytes()).hexdigest()
ns = Path('/run/netns/archipelago-runtime')
ns_before = (ns.stat().st_dev, ns.stat().st_ino)
# Render the actual C01 bootstrap template, changing only paths and the Podman
# command to a sentinel. The barrier, ordering, timeout and restart behavior are
# the production directives. No Podman/app operation occurs in this fixture.
template = (SOURCE / 'archipelago-runtime-podman.service').read_text()
template = template.replace('/usr/libexec/runtime_uplink.py', str(helper)).replace('%t', str(runtime))
template = template.replace('ExecStart=/usr/bin/podman info --format={{.Store.GraphRoot}}',
'ExecStart=/usr/bin/touch /run/bootstrap-ran')
template = template.replace('[Service]', '[Service]\nNetworkNamespacePath=/run/netns/archipelago-runtime')
Path('/etc/systemd/system/uplink-bootstrap.service').write_text(template)
# Project the exact user-manager preflight directives into a PID1-owned fixture
# unit. This tests the gate before manager exec, not a full C01 migration.
manager = (SOURCE / '81-c01-runtime-access.conf.in').read_text()
directives = '\n'.join(line for line in manager.splitlines() if line.startswith(('ExecCondition=', 'NetworkNamespacePath=', 'TimeoutStartSec=')))
directives = directives.replace('/usr/libexec/runtime_uplink.py', str(helper)).replace('/run/user/1000', str(runtime))
Path('/etc/systemd/system/uplink-manager.service').write_text(
'[Service]\nType=oneshot\nRemainAfterExit=yes\nExecStart=/usr/bin/touch /run/manager-ran\n' + directives + '\n')
modules = next(ast.literal_eval(node.value) for node in ast.parse((SOURCE / 'provision-d09-lifecycle.py').read_text()).body
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id == 'MODULES' for t in node.targets))
assert 'runtime_uplink.py' in modules
assert '--c01' in (SOURCE / 'archipelago-runtime-worker.service').read_text()
assert 'ExecCondition=' in (SOURCE / '79-runtime-uplink.conf').read_text()
Path('/etc/systemd/system/uplink-stopped.service').write_text('[Service]\nExecStart=/usr/bin/touch /run/stopped-app-ran\n')
run('systemctl', 'daemon-reload')
run('systemctl', 'start', '--no-block', 'uplink-bootstrap.service')
run('systemctl', 'start', '--no-block', 'uplink-manager.service')
until(lambda: state('uplink-bootstrap.service', 'SubState') in ('condition', 'start-pre'))
time.sleep(1.5)
assert not Path('/run/bootstrap-ran').exists()
assert not Path('/run/manager-ran').exists()
# Actual kernel default, deliberately another nonstandard name. No WAN packets.
net('link', 'add', 'wg-real-default', 'type', 'dummy')
net('addr', 'add', '192.0.2.2/24', 'dev', 'wg-real-default')
net('link', 'set', 'wg-real-default', 'up')
net('route', 'add', 'default', 'via', '192.0.2.1', 'dev', 'wg-real-default')
until(lambda: state('uplink-bootstrap.service', 'ActiveState') == 'active')
assert Path('/run/bootstrap-ran').exists()
until(lambda: Path('/run/manager-ran').exists())
assert state('uplink-bootstrap.service', 'NRestarts') == '0'
# Exercise the actual unprivileged USER-unit namespace lookup permissions too.
run('ip', 'netns', 'exec', 'archipelago-runtime', 'runuser', '-u', 'nobody', '--',
'python3', '-I', '-S', str(helper), '--c01', '--runtime-dir', str(runtime), '--timeout=5')
# Retain a distinct real nsfs object with a misleading link but no default.
run('ip', 'netns', 'add', 'retained-rootless')
run('ip', '-n', 'retained-rootless', 'link', 'add', 'wg0', 'type', 'dummy')
run('ip', '-n', 'retained-rootless', 'addr', 'add', '10.44.0.1/16', 'dev', 'wg0')
run('ip', '-n', 'retained-rootless', 'link', 'set', 'wg0', 'up')
retained = runtime / 'containers/networks/rootless-netns/rootless-netns'
retained.parent.mkdir(parents=True)
retained.touch()
run('mount', '--bind', '/run/netns/retained-rootless', str(retained))
pin = os.open(retained, os.O_RDONLY)
retained_id = uplink.namespace_id(pin)
info = retained.parent / 'info.json'
info.write_text('{"IPAddresses":["192.0.2.2"]}')
before_info = info.read_bytes()
before_routes = run('ip', '-j', '-n', 'retained-rootless', 'route', 'show', 'table', 'all')
run('systemctl', 'stop', 'uplink-bootstrap.service')
Path('/run/bootstrap-ran').unlink()
run('systemctl', 'start', 'uplink-bootstrap.service')
assert state('uplink-bootstrap.service', 'ActiveState') == 'inactive'
assert 'retained rootless namespace requires owner reconciliation' in run('journalctl', '-u', 'uplink-bootstrap.service', '--no-pager')
assert not Path('/run/bootstrap-ran').exists()
assert uplink.namespace_id(pin) == retained_id == (retained.stat().st_dev, retained.stat().st_ino)
assert before_routes == run('ip', '-j', '-n', 'retained-rootless', 'route', 'show', 'table', 'all')
assert info.read_bytes() == before_info
assert hashlib.sha256(sentinel.read_bytes()).hexdigest() == baseline
assert (ns.stat().st_dev, ns.stat().st_ino) == ns_before
assert state('uplink-stopped.service', 'ActiveState') == 'inactive'
assert not Path('/run/stopped-app-ran').exists()
# Simulate the repaired network but stale Podman cache: still refuse, preserve.
info.write_text('{"IPAddresses":["10.44.0.1"]}')
before_info = info.read_bytes()
run('ip', '-n', 'retained-rootless', 'addr', 'add', '192.0.2.2/24', 'dev', 'wg0')
run('ip', '-n', 'retained-rootless', 'route', 'add', 'default', 'via', '192.0.2.1', 'dev', 'wg0')
run('systemctl', 'start', 'uplink-bootstrap.service')
assert state('uplink-bootstrap.service', 'ActiveState') == 'inactive'
assert 'address cache differs from uplink' in run('journalctl', '-u', 'uplink-bootstrap.service', '--no-pager')
assert info.read_bytes() == before_info and uplink.namespace_id(pin) == retained_id
# Only the fixture owner prepares consistent metadata; the helper never does.
info.write_text('{"IPAddresses":["192.0.2.2"]}')
run('systemctl', 'start', 'uplink-bootstrap.service')
assert state('uplink-bootstrap.service', 'ActiveState') == 'active'
assert uplink.namespace_id(pin) == retained_id
os.close(pin)
# Host namespace cannot impersonate C01 readiness even with an appointed path.
rejected = subprocess.run(['python3', '-I', '-S', str(helper), '--c01', '--runtime-dir', str(runtime), '--timeout=1'], capture_output=True, text=True)
assert rejected.returncode == 1 and 'outside the appointed' in rejected.stderr
# The actual legacy manager condition must skip an unavailable-uplink activation
# even with Restart=always. Outer fixture namespace deliberately has no default.
legacy = (SOURCE / '79-legacy-manager-uplink.conf').read_text()
legacy = legacy.replace('/opt/archipelago/scripts/runtime_uplink.py', str(helper))
legacy = legacy.replace('--runtime-dir=/run/user/1000', '--runtime-dir=/run/legacy-empty --timeout=1')
Path('/etc/systemd/system/uplink-legacy.service').write_text(
'[Service]\nExecStart=/usr/bin/touch /run/legacy-ran\nRestart=always\n' + legacy)
run('systemctl', 'daemon-reload')
run('systemctl', 'start', 'uplink-legacy.service')
time.sleep(2)
assert state('uplink-legacy.service', 'ActiveState') == 'inactive'
assert not Path('/run/legacy-ran').exists()
assert state('uplink-legacy.service', 'NRestarts') == '0'
print(json.dumps({'boot_order_no_default_blocked': True, 'real_default_releases_bootstrap': True,
'user_manager_preflight_ordered': True, 'provisioning_module_present': True,
'unprivileged_namespace_gate': True,
'misleading_interface_names_ignored': True, 'retained_broken_refused_preserved': True,
'healthy_retained_namespace_preserved': True, 'stale_cache_refused_preserved': True,
'stopped_app_preserved': True,
'host_cannot_satisfy_c01': True, 'legacy_deadline_no_restart_loop': True,
'daemon_restarts': 0}, sort_keys=True))