mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-06 12:52:05 +00:00
154 lines
5.6 KiB
Python
154 lines
5.6 KiB
Python
"""Opt-in fresh-QEMU lab: actual generated units, restricted SSH and native USB URBs."""
|
|
|
|
import contextlib, os, select, shutil, sys, termios, time, tty
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "scripts/devices"))
|
|
from check_usbip_kernel import m, run, put, gadget, find
|
|
|
|
|
|
def wait(test, seconds=30):
|
|
end = time.monotonic() + seconds
|
|
while time.monotonic() < end:
|
|
value = test()
|
|
if value:
|
|
return value
|
|
time.sleep(0.1)
|
|
raise AssertionError("timeout")
|
|
|
|
|
|
def transfer(g, serial):
|
|
client = wait(
|
|
lambda: next(
|
|
(
|
|
p
|
|
for p in Path("/sys/class/tty").glob("ttyACM*")
|
|
if "vhci_hcd" in str(p.resolve())
|
|
and any(
|
|
(q / "serial").exists()
|
|
and (q / "serial").read_text().strip() == serial
|
|
for q in p.resolve().parents
|
|
)
|
|
),
|
|
None,
|
|
)
|
|
)
|
|
with contextlib.ExitStack() as stack:
|
|
fds = [
|
|
os.open(path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
|
|
for path in (
|
|
"/dev/ttyGS" + (g / "functions/acm.usb0/port_num").read_text().strip(),
|
|
"/dev/" + client.name,
|
|
)
|
|
]
|
|
for fd in fds:
|
|
stack.callback(os.close, fd)
|
|
tty.setraw(fd)
|
|
termios.tcflush(fd, termios.TCIOFLUSH)
|
|
for src, dst in ((fds[0], fds[1]), (fds[1], fds[0])):
|
|
payload = b"actual-systemd-ssh-usbip-serial\n"
|
|
os.write(src, payload)
|
|
assert select.select([dst], [], [], 5)[0]
|
|
assert os.read(dst, len(payload)) == payload
|
|
|
|
|
|
def main():
|
|
assert sys.argv[1:] == [
|
|
"--disposable-vm"
|
|
], "Requires --disposable-vm on a fresh throwaway QEMU VM"
|
|
assert not Path("/etc/proxmenux/usbip").exists()
|
|
assert "QEMU" in Path("/sys/class/dmi/id/sys_vendor").read_text()
|
|
for name in ("usbip_manager.py", "usbip_protocol.py", "usbip_lifetime.py"):
|
|
target = Path("/usr/local/share/proxmenux/scripts/devices") / name
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy(m.SCRIPT.parent / name, target)
|
|
run("modprobe", "dummy_hcd", "num=3")
|
|
run("modprobe", "libcomposite")
|
|
run("modprobe", "usbip-host")
|
|
run("modprobe", "vhci-hcd")
|
|
udcs = sorted(p.name for p in Path("/sys/class/udc").iterdir())
|
|
assert len(udcs) == 3
|
|
assert not list(Path("/sys/kernel/config/usb_gadget").iterdir())
|
|
a = gadget("service-a", "service-a", udcs[0])
|
|
b = gadget("service-b", "service-b", udcs[1])
|
|
time.sleep(0.5)
|
|
key = Path("/root/service-usbip-key")
|
|
run("ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key))
|
|
m.authorize_key(str(key) + ".pub")
|
|
public = Path("/etc/ssh/ssh_host_ed25519_key.pub").read_text().split()
|
|
Path("/root/.ssh/known_hosts").write_text(
|
|
"127.0.0.1 " + public[0] + " " + public[1] + "\n"
|
|
)
|
|
imports = []
|
|
try:
|
|
m.update_server(True)
|
|
sa = m.add_export(wait(lambda: find("service-a"))["busid"])
|
|
sb = m.add_export(wait(lambda: find("service-b"))["busid"])
|
|
wait(lambda: len([e for e in m.catalog() if e["status"] == "exported"]) == 2)
|
|
remote = m.validate_remote("127.0.0.1", 22, str(key))
|
|
for selected, port in ((sa, 33240), (sb, 33241)):
|
|
imports.append(m.add_import(remote, selected["id"], port))
|
|
wait(lambda: len(m.imported_ports()) == 2)
|
|
transfer(a, "service-a")
|
|
transfer(b, "service-b")
|
|
print(
|
|
"PASS: actual generated server/client systemd units and restricted SSH carry bidirectional native serial on two USBs",
|
|
flush=True,
|
|
)
|
|
for cycle in range(3):
|
|
put(a / "UDC", "\n")
|
|
time.sleep(0.2)
|
|
transfer(b, "service-b")
|
|
put(a / "UDC", udcs[0])
|
|
wait(lambda: find("service-a"))
|
|
time.sleep(0.5)
|
|
# An old vhci port may still exist; wait for the new guest driver and verify data.
|
|
wait(
|
|
lambda: next(
|
|
(
|
|
e
|
|
for e in m.catalog()
|
|
if e["id"] == sa["id"] and e["status"] == "exported"
|
|
),
|
|
None,
|
|
)
|
|
)
|
|
time.sleep(6)
|
|
transfer(a, "service-a")
|
|
transfer(b, "service-b")
|
|
print(
|
|
"PASS: three physical reconnects automatically reattach through restricted SSH; second serial connection survives",
|
|
flush=True,
|
|
)
|
|
m.remove_export(sa["id"])
|
|
assert find("service-a")["driver"] == "usb"
|
|
transfer(b, "service-b")
|
|
print(
|
|
"PASS: stop-sharing waits for real release and returns local driver while independent client keeps transferring",
|
|
flush=True,
|
|
)
|
|
for item in imports:
|
|
m.remove_import(item["id"])
|
|
imports = []
|
|
m.update_server(False)
|
|
assert not m.read_json(m.state_path(), {})
|
|
assert find("service-b")["driver"] == "usb"
|
|
print(
|
|
"PASS: disable stops listeners, removes owned bindings, leaves both local USB drivers restored",
|
|
flush=True,
|
|
)
|
|
finally:
|
|
for item in imports:
|
|
with contextlib.suppress(Exception):
|
|
m.remove_import(item["id"])
|
|
with contextlib.suppress(Exception):
|
|
m.update_server(False)
|
|
print(
|
|
run("journalctl", "-u", m.SERVER_UNIT, "--no-pager", "-n", "20"), flush=True
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|