{ lib, config, pkgs, ... }: let cfg = config.services.every-channel.ipxe-qemu; scriptsRoot = ../..; firstDns = lib.head cfg.boot.userNet.dnsServers; qemuBin = "${cfg.package}/bin/qemu-system-x86_64"; qemuImgBin = "${cfg.package}/bin/qemu-img"; bootNetdevArg = lib.escapeShellArg "user,id=boot0,dns=${firstDns},hostname=${cfg.boot.userNet.hostname},tftp=${cfg.boot.userNet.tftpDir},bootfile=${cfg.boot.userNet.bootFilename}"; bootDeviceArg = lib.escapeShellArg "${cfg.boot.userNet.model},netdev=boot0"; lanNetdevArg = lib.escapeShellArg "tap,id=lan0,fd=3"; lanDeviceArg = lib.escapeShellArg "${cfg.lan.model},netdev=lan0"; driveArg = lib.escapeShellArg "if=virtio,format=qcow2,file=${cfg.disk.path}"; effectiveChainUrl = if cfg.boot.chainUrl != null then cfg.boot.chainUrl else "http://10.0.2.2:${toString cfg.boot.http.port}/netboot.ipxe"; bootScript = pkgs.writeText "every-channel-qemu.ipxe" '' #!ipxe dhcp set dns ${firstDns} chain ${effectiveChainUrl} || shell ''; bootAsset = pkgs.ipxe.override { embedScript = bootScript; additionalTargets = { "bin/undionly.kpxe" = null; }; firmwareBinary = "undionly.kpxe"; }; bootExtraArgs = lib.concatMapStringsSep "\n" (arg: " cmd+=(${lib.escapeShellArg arg})") cfg.extraArgs; runner = pkgs.writeShellApplication { name = "every-channel-ipxe-qemu"; runtimeInputs = [ pkgs.coreutils pkgs.iproute2 cfg.package ]; text = '' set -euo pipefail state_dir=${lib.escapeShellArg cfg.stateDir} tftp_dir=${lib.escapeShellArg cfg.boot.userNet.tftpDir} boot_file=${lib.escapeShellArg cfg.boot.userNet.bootFilename} disk_path=${lib.escapeShellArg cfg.disk.path} disk_size=${lib.escapeShellArg cfg.disk.size} lan_ifname=${lib.escapeShellArg cfg.lan.macvtap.name} enable_kvm=${lib.escapeShellArg (lib.boolToString cfg.enableKvm)} enable_lan=${lib.escapeShellArg (lib.boolToString cfg.lan.enable)} serve_http=${lib.escapeShellArg (lib.boolToString cfg.boot.http.enable)} http_bind_ip=${lib.escapeShellArg cfg.boot.http.bindIp} http_port=${toString cfg.boot.http.port} http_root=${lib.escapeShellArg cfg.boot.http.root} cleanup() { if [[ "$enable_lan" == true ]]; then ip link del "$lan_ifname" 2>/dev/null || true fi } terminate() { if [[ -n "''${http_pid:-}" ]]; then kill "$http_pid" 2>/dev/null || true wait "$http_pid" 2>/dev/null || true fi if [[ -n "''${qemu_pid:-}" ]]; then kill "$qemu_pid" 2>/dev/null || true wait "$qemu_pid" 2>/dev/null || true fi exit 0 } trap cleanup EXIT trap terminate TERM INT install -d -m 0755 "$state_dir" "$tftp_dir" "$(dirname "$disk_path")" install -m 0644 ${bootAsset}/undionly.kpxe "$tftp_dir/$boot_file" if [[ "$serve_http" == true ]]; then if [[ ! -d "$http_root" ]]; then echo "error: boot HTTP root not found: $http_root" >&2 exit 1 fi ${pkgs.python3}/bin/python3 ${scriptsRoot}/scripts/netboot-http-server.py \ --bind-ip "$http_bind_ip" \ --port "$http_port" \ --root "$http_root" & http_pid=$! fi if [[ ! -f "$disk_path" ]]; then ${qemuImgBin} create -f qcow2 "$disk_path" "$disk_size" >/dev/null fi cmd=( ${lib.escapeShellArg qemuBin} -name ${lib.escapeShellArg cfg.name} -machine ${lib.escapeShellArg cfg.machine} -cpu ${lib.escapeShellArg cfg.cpu} -smp ${toString cfg.vcpus} -m ${toString cfg.memoryMiB} -nographic -boot ${lib.escapeShellArg cfg.boot.order} -serial ${lib.escapeShellArg cfg.serial} -device virtio-rng-pci ) if [[ "$enable_kvm" == true ]]; then cmd+=(-enable-kvm) fi cmd+=( -netdev ${bootNetdevArg} -device ${bootDeviceArg} ) if [[ "$enable_lan" == true ]]; then ip link del "$lan_ifname" 2>/dev/null || true ip link add link ${lib.escapeShellArg cfg.lan.macvtap.interface} name "$lan_ifname" type macvtap mode ${lib.escapeShellArg cfg.lan.macvtap.mode} ip link set "$lan_ifname" up tap_index="$(< /sys/class/net/$lan_ifname/ifindex)" exec 3<>"/dev/tap''${tap_index}" cmd+=( -netdev ${lanNetdevArg} -device ${lanDeviceArg} ) fi cmd+=( -drive ${driveArg} ) ${bootExtraArgs} "''${cmd[@]}" & qemu_pid=$! wait "$qemu_pid" ''; }; in { options.services.every-channel.ipxe-qemu = { enable = lib.mkEnableOption "every.channel iPXE/QEMU boot VM"; package = lib.mkOption { type = lib.types.package; default = pkgs.qemu_kvm; description = "QEMU package providing qemu-system-x86_64 and qemu-img."; }; name = lib.mkOption { type = lib.types.str; default = "every-channel-ipxe"; description = "QEMU guest name."; }; machine = lib.mkOption { type = lib.types.str; default = "q35,accel=kvm"; description = "QEMU machine string."; }; cpu = lib.mkOption { type = lib.types.str; default = "host"; description = "QEMU CPU model."; }; enableKvm = lib.mkOption { type = lib.types.bool; default = true; description = "Use KVM acceleration when available."; }; vcpus = lib.mkOption { type = lib.types.ints.positive; default = 2; description = "Virtual CPU count."; }; memoryMiB = lib.mkOption { type = lib.types.ints.positive; default = 4096; description = "Guest memory in MiB."; }; serial = lib.mkOption { type = lib.types.str; default = "mon:stdio"; description = "Serial/monitor wiring passed to QEMU."; }; stateDir = lib.mkOption { type = lib.types.str; default = "/var/lib/every-channel/ipxe-qemu"; description = "Persistent state directory for qcow2 disk and boot assets."; }; disk = { path = lib.mkOption { type = lib.types.str; default = "${cfg.stateDir}/disk.qcow2"; description = "Persistent qcow2 disk path."; }; size = lib.mkOption { type = lib.types.str; default = "16G"; description = "Size used when initializing the qcow2 disk."; }; }; boot = { order = lib.mkOption { type = lib.types.str; default = "order=n"; description = "QEMU boot order, defaulting to network first."; }; chainUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Optional explicit iPXE chain target fetched by the guest; defaults to the local boot.http server."; }; http = { enable = lib.mkOption { type = lib.types.bool; default = true; description = "Serve a host-local HTTP root for the guest netboot flow."; }; bindIp = lib.mkOption { type = lib.types.str; default = "127.0.0.1"; description = "Bind address used for the host-local boot HTTP server."; }; port = lib.mkOption { type = lib.types.port; default = 8080; description = "Port used for the host-local boot HTTP server."; }; root = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Directory containing kernel/initrd/netboot.ipxe served to the guest."; }; }; userNet = { hostname = lib.mkOption { type = lib.types.str; default = "every-channel-ipxe"; description = "Hostname advertised on the user-mode boot network."; }; dnsServers = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "1.1.1.1" ]; description = "DNS servers used by the user-mode boot NIC; the first entry is applied."; }; tftpDir = lib.mkOption { type = lib.types.str; default = "${cfg.stateDir}/tftp"; description = "TFTP directory used by the user-mode boot NIC."; }; bootFilename = lib.mkOption { type = lib.types.str; default = "undionly.kpxe"; description = "Boot asset filename exposed on the user-mode boot NIC."; }; model = lib.mkOption { type = lib.types.str; default = "e1000"; description = "NIC model used for the boot network."; }; }; }; lan = { enable = lib.mkOption { type = lib.types.bool; default = false; description = "Attach a second NIC to a real LAN using macvtap."; }; model = lib.mkOption { type = lib.types.str; default = "virtio-net-pci"; description = "NIC model used for the LAN attachment."; }; macvtap = { interface = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Host interface used as the lower device for macvtap."; }; name = lib.mkOption { type = lib.types.str; default = "ecqemu0"; description = "macvtap link name created while the guest runs."; }; mode = lib.mkOption { type = lib.types.enum [ "private" "vepa" "bridge" "passthru" "source" ]; default = "bridge"; description = "macvtap mode used for the LAN attachment."; }; }; }; extraArgs = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; description = "Additional arguments appended to the QEMU command."; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.boot.userNet.dnsServers != [ ]; message = "services.every-channel.ipxe-qemu.boot.userNet.dnsServers must not be empty"; } { assertion = (!cfg.lan.enable) || (cfg.lan.macvtap.interface != null); message = "services.every-channel.ipxe-qemu.lan.macvtap.interface must be set when lan.enable is true"; } { assertion = (!cfg.boot.http.enable) || (cfg.boot.http.root != null); message = "services.every-channel.ipxe-qemu.boot.http.root must be set when boot.http.enable is true"; } ]; environment.systemPackages = [ runner ]; systemd.tmpfiles.rules = [ "d ${cfg.stateDir} 0755 root root -" "d ${cfg.boot.userNet.tftpDir} 0755 root root -" ]; systemd.services.every-channel-ipxe-qemu = { description = "every.channel iPXE QEMU VM"; after = [ "local-fs.target" "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; path = [ pkgs.coreutils pkgs.iproute2 cfg.package ]; serviceConfig = { Type = "simple"; ExecStart = "${runner}/bin/every-channel-ipxe-qemu"; Restart = "always"; RestartSec = 5; WorkingDirectory = cfg.stateDir; }; }; }; }