377 lines
11 KiB
Nix
377 lines
11 KiB
Nix
{ 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;
|
|
};
|
|
};
|
|
};
|
|
}
|