ch-runner: move more logic out of elb

This commit is contained in:
Else Someone 2026-02-19 19:24:44 +02:00
parent 0617d97ebf
commit 97f2ba4c66

View file

@ -272,92 +272,85 @@ in
# NOTE: Used to be an even uglier bash script, but, for now, execline makes for easier comparisons against spectrum # NOTE: Used to be an even uglier bash script, but, for now, execline makes for easier comparisons against spectrum
uvms.cloud-hypervisor.runner = uvms.cloud-hypervisor.runner =
let let
addProcess = getExe addProcess'; superviseVm = getExe superviseVm';
addProcess' = pkgs.writers.writePython3Bin "add-process" { } '' superviseVm' = pkgs.writers.writePython3Bin "supervise-vm" { } ''
import os import os
import select
import socket
import subprocess import subprocess
import sys
from argparse import ArgumentParser from argparse import ArgumentParser
from contextlib import contextmanager, ExitStack from contextlib import contextmanager, ExitStack
from threading import Thread, Semaphore
parser = ArgumentParser() parser = ArgumentParser("supervise-vm")
parser.add_argument("events_path") parser.add_argument("--vm")
parser.add_argument("--then", action="append") parser.add_argument("--prefix", default="$HOME/uvms/$VM")
parser.add_argument("--sock", default="$PREFIX/supervisor.sock")
parser.add_argument("--vm-config")
MSG_SIZE = 16 MSG_SIZE = 16
SHMEM = {} ELB_DIR = "${lib.getBin pkgs.execline}/bin" # noqa: E501
S6_DIR = "${lib.getBin pkgs.s6}/bin" # noqa: E501
CH_DIR = "${lib.getBin package}/bin" # noqa: E501
SOCKETBINDER_PATH = S6_DIR + "/s6-ipcserver-socketbinder" # noqa: E501
CH_PATH = CH_DIR + "/cloud-hypervisor"
CHR_PATH = CH_DIR + "/ch-remote"
TAPS_PATH = "${lib.getExe uvmsPkgs.taps}" # noqa: E501
PASSTHRU_PATH = ":".join([ELB_DIR, S6_DIR, CH_DIR])
PASSTHRU_ENV = {
**{
k: v
for k, v in os.environ.items()
if k.startswith("RUST")
or k.startswith("WAYLAND")
or k in [
"TAPS_SOCK",
]
},
"HOME": os.environ.get("HOME", os.getcwd()),
"PATH": PASSTHRU_PATH,
}
def send(sock, msg): def configure_execline(prefix, vm, check=True, **defaults):
assert len(msg) <= MSG_SIZE, len(msg) def execline(*args, check=check, **kwargs):
return sock.send(msg.ljust(MSG_SIZE)) return subprocess.run(
["execlineb", "-c", "\n".join(args)],
**defaults,
executable=ELB_DIR + "/execlineb",
env={
**PASSTHRU_ENV,
"PATH": PASSTHRU_PATH,
"PREFIX": prefix,
"VM": vm,
},
check=check,
cwd=prefix,
**kwargs)
return execline
def recv(sock): def preprocess_args(args_mut):
msg = sock.recv(MSG_SIZE) keys = [
# assert len(msg) <= MSG_SIZE, len(msg) k
assert len(msg) <= MSG_SIZE, len(msg) for k, v
return (msg.split() + [b""])[0] in args_mut._get_kwargs()
if isinstance(v, str)]
for k in keys:
def serve_impl(events_path, listener): v = getattr(args_mut, k)
SHMEM["server"] = True if "$HOME" in v:
setattr(
cons = [] args_mut,
state = "up" k,
while state == "up" or cons != []: v.replace("$HOME", PASSTHRU_ENV["HOME"]))
if state == "up": for k in keys:
rs, ws, es = select.select([listener, *cons], [], []) v = getattr(args_mut, k)
else: if "$VM" in v:
rs, ws, es = select.select(cons, cons, []) setattr(args_mut, k, v.replace("$VM", args.vm))
events = [] for k in keys:
for r in rs: v = getattr(args_mut, k)
if r is listener: if "$PREFIX" in v:
r, _ = r.accept() setattr(args_mut, k, v.replace("$PREFIX", args.prefix))
cons.append(r) return args_mut
else:
events.append(recv(r))
if any(e == b"killall" for e in events):
state = "down"
if state == "down":
for w in ws:
with s_lock:
send(w, b"die")
w.close()
cons.remove(w)
for w in es:
w.close()
cons.remove(w)
def serve(events_path):
base_dir = os.path.dirname(events_path)
if base_dir:
os.makedirs(base_dir, exist_ok=True)
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0)
listener.setblocking(False)
try:
listener.bind(events_path)
listener.listen()
return serve_impl(events_path, listener)
except OSError as e:
EADDRINUSE = 98
if e.errno != EADDRINUSE:
raise
finally:
listener.close()
os.remove(events_path)
def register(events_path):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0)
sock.connect(events_path)
return sock
@contextmanager @contextmanager
@ -368,110 +361,77 @@ in
f() f()
@contextmanager
def run_ch(vm_prefix):
args = [
SOCKETBINDER_PATH,
"-B",
vm_prefix + "/vmm.sock",
CH_PATH,
"--api-socket",
"fd=0",
]
p = subprocess.Popen(
args,
shell=False)
try:
p.wait(1.0)
needs_cleanup = False
except subprocess.TimeoutExpired:
needs_cleanup = True
if not os.path.exists(vm_prefix + "/vmm.sock"):
raise RuntimeError(f"{vm_prefix}/vmm.sock should exist by now")
if p.returncode is not None:
raise RuntimeError("CH exited early")
try:
yield p
finally:
try:
p.poll()
except: # noqa: E722
pass
if p.returncode is None:
p.terminate() # CH handles SIG{INT,TERM}?
p.wait()
unlink_paths = [
vm_prefix + "/vmm.sock",
vm_prefix + "/vmm.sock.lock",
vm_prefix + "/vsock.sock",
] if needs_cleanup else []
for p in unlink_paths:
if os.path.exists(p):
os.remove(p)
if __name__ == "__main__": if __name__ == "__main__":
args, args_next = parser.parse_known_args() args, args_next = parser.parse_known_args()
preprocess_args(args)
os.makedirs(args.prefix, exist_ok=True)
execline = configure_execline(
prefix=args.prefix,
vm=args.vm)
ch_remote = [
"ch-remote",
"--api-socket",
args.prefix + "/vmm.sock",
]
with ExitStack() as cleanup: with ExitStack() as cleanup:
if args_next: ch = cleanup.enter_context(run_ch(args.prefix))
p = subprocess.Popen( execline(*ch_remote, "create", args.vm_config)
args_next, execline(
shell=False) TAPS_PATH, "pass",
then_cmds = reversed(getattr(args, "then") or []) *ch_remote, "add-net",
if not args_next: "id=wan,fd=3,mac=00:00:00:00:00:01")
then_cmds = [] execline(*ch_remote, "boot")
try: execline(*ch_remote, "info")
p.wait(0.5) ch.wait()
then_cmds = []
except subprocess.TimeoutExpired:
pass
for f in then_cmds:
def run_f():
subprocess.run(f)
cleanup.enter_context(defer(run_f))
maybe_server = Thread(
target=serve,
args=(args.events_path,),
daemon=True)
maybe_server.start()
maybe_server.join(0.5)
assert (
("server" in SHMEM) == bool(maybe_server.is_alive)
), (SHMEM, maybe_server)
if args_next:
s = register(args.events_path)
s_lock = Semaphore()
if args_next:
def watch_p(p, s):
p.wait()
with s_lock:
try:
send(s, b"killall")
except BrokenPipeError:
pass
def watch_s(p, s):
while True:
if recv(s) == b"die":
p.terminate()
break
s_watcher = Thread(
target=watch_s,
args=(p, s),
daemon=True)
s_watcher.start()
watch_p(p, s)
s_watcher.join()
s.close()
if SHMEM.get("server", False):
maybe_server.join()
exit_code = 0
if args_next:
exit_code |= p.returncode
sys.exit(exit_code)
''; '';
ch = getExe package;
chr = getExe' package "ch-remote";
in in
writeElb "run-${hostName}" '' writeElb "run-${hostName}" ''
importas -i HOME HOME ${superviseVm} --vm-config=${chSettingsFile} --vm=${hostName}
importas -SsD "${chr} --api-socket=${vmmSock}" CHR
importas -SsD "${uvmPrefix}" PREFIX
define EVENTS ''${PREFIX}/events.sock
define -s ADD_PROC "${addProcess} ''${EVENTS}"
cd $PREFIX
background {
$ADD_PROC --then ${getExe (
writeElb "rm-vmmsock" ''
importas -i HOME HOME
rm -f ${vmmSock}
rm -f ${uvmPrefix}/vsock.sock
''
)} ${getExe (
writeElb "ch" ''
importas -Si 1
importas -Si 2
s6-ipcserver-socketbinder -B $1
exec -a "uuvm/''${2} cloud-hypervisor" ${ch} --api-socket fd=0
''
)} ${vmmSock} ${hostName}
}
foreground { sleep 0.1 }
ifelse -n { test -S ${vmmSock} } { echo "Apparently ${vmmSock} does not exist" }
foreground { echo "Loading the configuration" }
if { $CHR create ${chSettingsFile} }
foreground { echo "Adding TAP" }
if { ${lib.getExe uvmsPkgs.taps} pass $CHR add-net "id=wan,fd=3,mac=00:00:00:00:00:01" }
foreground { echo "Booting" }
if { $CHR boot }
if { $CHR info }
''; '';
} }
(lib.mkIf cfg.enable { (lib.mkIf cfg.enable {