Repository navigation
vm: run VM replays side by side; keep installed boxes across replay-only changes #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+456
−29
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
c4c7c9d
vm: run VM replays side by side; keep installed boxes across replay-o…
claude 04522d2
vm: never stop the mirror shim under a running VM replay (shim-stop)
claude 39854c8
vm: base.qcow2 last in an install; shim-stop only a wend checkout's t…
claude 92d4c13
Merge main into #112 (archive source/R2/client, EPEL xz/zst primary, …
claude bd7e004
Merge main into #112 (#104 apt signatures, archive index signing, gat…
claude 3dcce73
vm: shim-stop signals the process it inspected, through a pidfd (review)
claude 8f95d64
vm: the shim lock follows the shim's port, not $WEND_WORK (review)
claude f90d0bd
vm: shim locks in a directory nobody else can write (review)
claude 4df4573
vm: head() keeps the checkout's config from running anything (review)
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| """VM replays side by side (tools/vm.py), by behaviour, the parts that don't need KVM: two guests started at once get | ||
| ssh forwards on different host ports and both run; a guest whose port another process takes first asks again for | ||
| another one; the mirror shim can't be stopped while a run uses it, whatever its WEND_WORK (shim-stop); a second run | ||
| that needs a box another run is installing waits for it; and the box cache key follows what the install runs, not the | ||
| whole file (build_source). QEMU is a stand-in that binds its hostfwd port as QEMU does and says what QEMU says when | ||
| it can't. It runs the PR's own vm.py, so the gate's schema job doesn't run it: the client job does. | ||
| python3 tools/test_vm_parallel.py""" | ||
| import os | ||
| import re | ||
| import shutil | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
| import threading | ||
| import time | ||
|
|
||
| sys.dont_write_bytecode = True | ||
| ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||
| sys.path.insert(0, os.path.join(ROOT, "tools")) | ||
| tmp = tempfile.mkdtemp(prefix="wend-test-vm-") | ||
| os.environ["WEND_WORK"] = os.path.join(tmp, "work") # before vm reads it | ||
| import vm # noqa: E402 | ||
|
|
||
| bad = 0 | ||
|
|
||
|
|
||
| def say(ok, what, detail=""): | ||
| global bad | ||
| print(("passed " if ok else "WRONG ") + what + ("" if ok or not detail else f"\n {detail}")) | ||
| bad += not ok | ||
|
|
||
|
|
||
| bin_dir = os.path.join(tmp, "bin") | ||
| os.makedirs(bin_dir) | ||
| open(os.path.join(bin_dir, "qemu-system-x86_64"), "w").write(f"""#!{sys.executable} | ||
| import re, socket, sys, time | ||
| net = sys.argv[sys.argv.index("-netdev") + 1] | ||
| port = int(re.search(r"hostfwd=tcp:127\\.0\\.0\\.1:(\\d+)-", net).group(1)) | ||
| s = socket.socket() | ||
| try: | ||
| s.bind(("127.0.0.1", port)) | ||
| s.listen() | ||
| except OSError: | ||
| sys.stderr.write(f"qemu-system-x86_64: -netdev {{net}}: Could not set up host forwarding rule 'tcp:127.0.0.1:{{port}}'\\n") | ||
| sys.exit(1) | ||
| time.sleep(60) | ||
| """) | ||
| os.chmod(os.path.join(bin_dir, "qemu-system-x86_64"), 0o755) | ||
| os.environ["PATH"] = bin_dir + os.pathsep + os.environ["PATH"] | ||
|
|
||
| try: | ||
| # two guests at once: each its own port, both up, and ssh asks for that port | ||
| g1, g2 = (vm.Guest(os.path.join(tmp, f"g{i}.qcow2"), "key", 22) for i in (1, 2)) | ||
| g1.start() | ||
| g2.start() | ||
| say(g1.running() and g2.running(), "two guests run side by side") | ||
| say(g1.fwd != g2.fwd, "each guest forwards ssh from a host port of its own", f"{g1.fwd} and {g2.fwd}") | ||
| argv = g1.ssh_argv("true") | ||
| say(argv[argv.index("-p") + 1] == str(g1.fwd), "ssh goes to the guest's own forwarded port", str(argv)) | ||
| say(not re.search(r"hostfwd=tcp:127\.0\.0\.1:10022", " ".join(g1.argv())), "no fixed port is left in qemu's line") | ||
|
|
||
| # a port taken between the pick and QEMU's bind: the guest asks again | ||
| real, picks = vm.free_port, [g1.fwd] | ||
| vm.free_port = lambda: picks.pop() if picks else real() | ||
| g3 = vm.Guest(os.path.join(tmp, "g3.qcow2"), "key", 22) | ||
| try: | ||
| g3.start() | ||
| say(g3.running() and g3.fwd not in (g1.fwd, g2.fwd), "a guest whose port was taken first starts on another one", | ||
| f"port {g3.fwd}") | ||
| except RuntimeError as e: | ||
| say(False, "a guest whose port was taken first starts on another one", str(e)) | ||
| finally: | ||
| vm.free_port = real | ||
|
|
||
| # anything else QEMU refuses is not retried | ||
| g4 = vm.Guest(os.path.join(tmp, "g4.qcow2"), "key", 22) | ||
| open(os.path.join(bin_dir, "qemu-system-x86_64"), "w").write( | ||
| f"#!{sys.executable}\nimport sys\nsys.stderr.write('could not open disk image\\n')\nsys.exit(1)\n") | ||
| t0 = time.time() | ||
| try: | ||
| g4.start() | ||
| say(False, "qemu failing for another reason fails at once, not five times") | ||
| except RuntimeError as e: | ||
| say("could not open disk image" in str(e) and time.time() - t0 < 6, | ||
| "qemu failing for another reason fails at once, not five times", f"{time.time() - t0:.0f}s: {e}") | ||
| for g in vm.GUESTS: | ||
| g.kill() | ||
| shutil.rmtree(os.path.dirname(g.monitor_path), ignore_errors=True) | ||
|
|
||
| # one run installs a box, a second waits for it, then finds it installed | ||
| lock, order = os.path.join(tmp, "work", "box.lock"), [] | ||
|
|
||
| def holder(ready): | ||
| with vm.Locked(lock, "installing the box"): | ||
| ready.set() | ||
| time.sleep(1) | ||
| order.append("first done") | ||
|
|
||
| ready = threading.Event() | ||
| t = threading.Thread(target=holder, args=(ready,)) | ||
| t.start() | ||
| ready.wait() | ||
| with vm.Locked(lock, "installing the box"): | ||
| order.append("second in") | ||
| t.join() | ||
| say(order == ["first done", "second in"], "a run waits while another installs the same box", str(order)) | ||
|
|
||
| # the shim: shim-stop refuses while a run holds it, stops it once none does, and stops nothing else | ||
| fake = os.path.join(tmp, "checkout", "tools", "mirror.py") # a checkout's shim: vm.py and sandbox.py beside it | ||
| os.makedirs(os.path.dirname(fake)) | ||
| open(fake, "w").write("import time\ntime.sleep(60)\n") | ||
| for f in ("vm.py", "sandbox.py"): | ||
| open(os.path.join(os.path.dirname(fake), f), "w").close() | ||
| port = str(vm.free_port()) # a port no real shim uses, so its lock is this test's alone | ||
| shim = subprocess.Popen([sys.executable, fake, port, os.path.join(tmp, "cache")]) | ||
| # the run holds the port's lock under another WEND_WORK than shim-stop's: the lock follows the port | ||
| run = subprocess.Popen([sys.executable, "-B", "-c", "import sys, time; sys.path.insert(0, sys.argv[1]); import vm; " | ||
| "vm.use_shim([int(sys.argv[2])]); print('holding', flush=True); time.sleep(60)", | ||
| os.path.join(ROOT, "tools"), port], stdout=subprocess.PIPE, text=True, | ||
| env={**os.environ, "WEND_WORK": os.path.join(tmp, "other-work")}) | ||
| run.stdout.readline() | ||
| stop = lambda pid: subprocess.run([sys.executable, "-B", os.path.join(ROOT, "tools", "vm.py"), "shim-stop", str(pid)], | ||
| capture_output=True, text=True) # noqa: E731 | ||
| p = stop(shim.pid) | ||
| say(p.returncode == 3 and shim.poll() is None, "shim-stop leaves the shim running while a VM run uses it", | ||
| f"rc {p.returncode}: {p.stdout}{p.stderr}") | ||
| run.kill() | ||
| run.wait() | ||
| p = stop(shim.pid) | ||
| time.sleep(0.5) | ||
| say(p.returncode == 0 and shim.poll() is not None, "shim-stop stops the shim once no run uses it", | ||
| f"rc {p.returncode}: {p.stdout}{p.stderr}") | ||
| elsewhere = os.path.join(tmp, "srv", "mirror.py") # a mirror.py that isn't a wend checkout's | ||
| os.makedirs(os.path.dirname(elsewhere)) | ||
| shutil.copyfile(fake, elsewhere) | ||
| for what, argv in [("a process that isn't the shim", ["-c", "import time; time.sleep(60)"]), | ||
| ("another program's mirror.py", [elsewhere, port])]: | ||
| other = subprocess.Popen([sys.executable, *argv]) | ||
| p = stop(other.pid) | ||
| say(p.returncode == 2 and other.poll() is None, f"shim-stop refuses {what}", | ||
| f"rc {p.returncode}: {p.stdout}{p.stderr}") | ||
| other.kill() | ||
| shim.kill() | ||
| os.remove(os.path.join(vm.SHIM_LOCKS, f"{port}.users")) | ||
|
|
||
| # a lock directory someone else could swap files in is refused, not used | ||
| real_locks = vm.SHIM_LOCKS | ||
| vm.SHIM_LOCKS = os.path.join(tmp, "open-locks") | ||
| os.mkdir(vm.SHIM_LOCKS) | ||
| os.chmod(vm.SHIM_LOCKS, 0o1777) | ||
| try: | ||
| vm.shim_lock(1) | ||
| say(False, "a world-writable lock directory is refused") | ||
| except SystemExit as e: | ||
| say("writable by nobody else" in str(e), "a world-writable lock directory is refused", str(e)) | ||
| vm.SHIM_LOCKS = real_locks | ||
|
|
||
| # the cache key: what the install runs, and nothing it doesn't | ||
| src = open(os.path.join(ROOT, "tools", "vm.py")).read() | ||
| key = vm.build_source() | ||
|
|
||
| def key_after(old, new): | ||
| assert src.count(old) == 1, old | ||
| p = os.path.join(tmp, "vm-changed.py") | ||
| open(p, "w").write(src.replace(old, new)) | ||
| return vm.build_source(p) | ||
|
|
||
| for what, old in [("the hop walk", "def walk_vm("), ("the shelter's confinement proof", "def shelter_confine("), | ||
| ("relocate", "def relocate("), ("prune", "def prune("), ("main", "def main(")]: | ||
| say(key_after(f"\n{old}", f"\nUNUSED_BY_BUILD = 1\n\n\n{old}") == key, | ||
| f"a change beside {what} keeps the installed boxes") | ||
| say(key_after(" hops, start_net, efi_start = [], None, None\n", | ||
| " hops, start_net, efi_start = [], None, None # changed\n") == key, | ||
| "a change inside the hop walk keeps the installed boxes") | ||
| for what, old, new in [("the kickstart", 'def kickstart(box_doc, adapter, key_pub):\n', | ||
| 'def kickstart(box_doc, adapter, key_pub):\n pass\n'), | ||
| ("the install", "def install(box_id, box_doc, d):\n", | ||
| "def install(box_id, box_doc, d):\n pass\n"), | ||
| ("Guest's reboot", " def reboot(self):\n", " def reboot(self):\n pass\n"), | ||
| ("a constant the install uses", 'MAC = "52:54:00:12:34:56"', 'MAC = "52:54:00:12:34:57"'), | ||
| ("the box's checks", "def checks(g, box_doc, adapter):\n", | ||
| "def checks(g, box_doc, adapter):\n pass\n")]: | ||
| say(key_after(old, new) != key, f"a change to {what} installs the boxes again") | ||
| finally: | ||
| for g in vm.GUESTS: | ||
| g.kill() | ||
| shutil.rmtree(tmp, ignore_errors=True) | ||
|
|
||
| print("ok" if not bad else f"{bad} WRONG") | ||
| sys.exit(1 if bad else 0) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.