diff --git a/scripts/integration-vm.py b/scripts/integration-vm.py index 2979676..0cd3620 100644 --- a/scripts/integration-vm.py +++ b/scripts/integration-vm.py @@ -144,6 +144,87 @@ def ensure_image(): return img +def net_ensure(): + r = sh(["virsh", "net-info", NET_NAME], check=False, capture=True) + if r.returncode != 0: + xml = RUN_DIR / "net.xml" + RUN_DIR.mkdir(parents=True, exist_ok=True) + xml.write_text(NET_XML) + sh(["virsh", "net-define", str(xml)]) + sh(["virsh", "net-autostart", NET_NAME]) + active = sh(["virsh", "net-info", NET_NAME], capture=True).stdout + if "Active: yes" not in active: + sh(["virsh", "net-start", NET_NAME]) + + +def _ssh_pubkey(): + for cand in ("id_ed25519.pub", "id_rsa.pub"): + p = pathlib.Path.home() / ".ssh" / cand + if p.exists(): + return p.read_text().strip() + raise SystemExit("no SSH public key found in ~/.ssh") + + +def up(host, name=None, mem_mib=DEFAULT_MEM_MIB, vcpus=DEFAULT_VCPUS): + free = free_mib(pathlib.Path("/proc/meminfo").read_text()) + if free < MIN_FREE_MIB: + raise SystemExit(f"refusing to start: only {free} MiB free (< {MIN_FREE_MIB})") + running = sh(["virsh", "list", "--name"], capture=True).stdout.split() + if any(n.startswith(NAME_PREFIX) for n in running): + raise SystemExit("an integration VM is already running (one at a time); " + "run `integration-vm prune` first") + name = name or vm_name(host) + img = ensure_image() + net_ensure() + RUN_DIR.mkdir(parents=True, exist_ok=True) + overlay = RUN_DIR / f"{name}.qcow2" + sh(["qemu-img", "create", "-f", "qcow2", "-F", "qcow2", "-b", str(img), str(overlay)]) + (RUN_DIR / "user-data").write_text(render_user_data(_ssh_pubkey(), "ansible")) + (RUN_DIR / "meta-data").write_text(render_meta_data(f"iid-{name}", name)) + seed = RUN_DIR / f"{name}-seed.img" + sh(["cloud-localds", str(seed), str(RUN_DIR / "user-data"), str(RUN_DIR / "meta-data")]) + DIAG_ROOT.mkdir(parents=True, exist_ok=True) + console = DIAG_ROOT / f"{name}-console.log" + sh(["virt-install", "--name", name, "--memory", str(mem_mib), "--vcpus", str(vcpus), + "--import", + "--disk", f"path={overlay},format=qcow2", + "--disk", f"path={seed},device=cdrom", + "--network", f"network={NET_NAME}", + "--osinfo", "debian13", + "--graphics", "none", + "--serial", f"file,path={console}", + "--noautoconsole"]) + ip = wait_for_ip(name) + wait_for_ssh(ip, "ansible") + (RUN_DIR / "current").write_text(f"{name}\n{ip}\n{host}\n") + print(f"VM {name} up at {ip}") + return name, ip + + +def wait_for_ip(name, timeout=120): + end = time.time() + timeout + while time.time() < end: + out = sh(["virsh", "domifaddr", name, "--source", "lease"], + check=False, capture=True).stdout + ip = parse_lease_ip(out) + if ip: + return ip + time.sleep(4) + raise SystemExit(f"timed out waiting for {name} to get a DHCP lease") + + +def wait_for_ssh(ip, user, timeout=180): + end = time.time() + timeout + while time.time() < end: + r = sh(["ssh", "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", "-o", "ConnectTimeout=5", + f"{user}@{ip}", "true"], check=False, capture=True) + if r.returncode == 0: + return + time.sleep(5) + raise SystemExit(f"timed out waiting for SSH to {ip}") + + def main(argv=None): p = argparse.ArgumentParser(prog="integration-vm", description=__doc__) sub = p.add_subparsers(dest="cmd", required=True)