diff --git a/pkgs/clan-cli/clan_cli/vms/qemu.py b/pkgs/clan-cli/clan_cli/vms/qemu.py index 9f5969fff..f8c90f95d 100644 --- a/pkgs/clan-cli/clan_cli/vms/qemu.py +++ b/pkgs/clan-cli/clan_cli/vms/qemu.py @@ -94,6 +94,7 @@ def qemu_command( virtiofsd_socket: Path, qmp_socket_file: Path, qga_socket_file: Path, + portmap: list[tuple[int, int]] = [], ) -> QemuCommand: kernel_cmdline = [ (Path(nixos_config["toplevel"]) / "kernel-params").read_text(), @@ -103,6 +104,7 @@ def qemu_command( ] if not vm.waypipe: kernel_cmdline.append("console=tty0") + hostfwd = ",".join(f"hostfwd=tcp::{h}-:{g}" for h, g in portmap) # fmt: off command = [ "qemu-kvm", @@ -116,7 +118,7 @@ def qemu_command( # speed-up boot by not waiting for the boot menu "-boot", "menu=off,strict=on", "-device", "virtio-rng-pci", - "-netdev", "user,id=user.0", + "-netdev", f"user,id=user.0,{hostfwd}", "-device", "virtio-net-pci,netdev=user.0,romfile=", "-chardev", f"socket,id=char1,path={virtiofsd_socket}", "-device", "vhost-user-fs-pci,chardev=char1,tag=nix-store", diff --git a/pkgs/clan-cli/clan_cli/vms/run.py b/pkgs/clan-cli/clan_cli/vms/run.py index 6f2082d70..57cd3c618 100644 --- a/pkgs/clan-cli/clan_cli/vms/run.py +++ b/pkgs/clan-cli/clan_cli/vms/run.py @@ -108,6 +108,7 @@ def run_vm( cachedir: Path | None = None, socketdir: Path | None = None, nix_options: list[str] = [], + portmap: list[tuple[int, int]] = [], ) -> None: with ExitStack() as stack: machine = Machine(name=vm.machine_name, flake=vm.flake_url) @@ -168,6 +169,7 @@ def run_vm( virtiofsd_socket=virtiofsd_socket, qmp_socket_file=qmp_socket_file, qga_socket_file=qga_socket_file, + portmap=portmap, ) packages = ["nixpkgs#qemu"] @@ -199,7 +201,9 @@ def run_command( vm: VmConfig = inspect_vm(machine=machine_obj) - run_vm(vm, nix_options=args.option) + portmap = [(h, g) for h, g in (p.split(":") for p in args.publish)] + + run_vm(vm, nix_options=args.option, portmap=portmap) def register_run_parser(parser: argparse.ArgumentParser) -> None: @@ -207,4 +211,13 @@ def register_run_parser(parser: argparse.ArgumentParser) -> None: "machine", type=str, help="machine in the flake to run" ) add_dynamic_completer(machine_action, complete_machines) + # option: --publish 2222:22 + parser.add_argument( + "--publish", + "-p", + action="append", + type=str, + default=[], + help="Forward ports from host to guest", + ) parser.set_defaults(func=lambda args: run_command(args)) diff --git a/pkgs/clan-cli/tests/helpers/vms.py b/pkgs/clan-cli/tests/helpers/vms.py index 2e3a2ae47..0661708ce 100644 --- a/pkgs/clan-cli/tests/helpers/vms.py +++ b/pkgs/clan-cli/tests/helpers/vms.py @@ -1,4 +1,6 @@ +import contextlib import os +import socket import sys import threading import traceback @@ -12,11 +14,21 @@ from clan_cli.qemu.qmp import QEMUMonitorProtocol from . import cli -def run_vm_in_thread(machine_name: str) -> None: +def find_free_port() -> int: + """Find an unused localhost port from 1024-65535 and return it.""" + with contextlib.closing(socket.socket(type=socket.SOCK_STREAM)) as sock: + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +def run_vm_in_thread(machine_name: str, ssh_port: int | None = None) -> int: # runs machine and prints exceptions + if ssh_port is None: + ssh_port = find_free_port() + def run() -> None: try: - cli.run(["vms", "run", machine_name]) + cli.run(["vms", "run", machine_name, "--publish", f"{ssh_port}:22"]) except Exception: # print exception details print(traceback.format_exc(), file=sys.stderr) @@ -26,7 +38,7 @@ def run_vm_in_thread(machine_name: str) -> None: t = threading.Thread(target=run, name="run") t.daemon = True t.start() - return + return ssh_port # wait for qmp socket to exist