338 lines
12 KiB
Nix
338 lines
12 KiB
Nix
{ lib, config, pkgs, ... }:
|
|
|
|
let
|
|
cfg = config.services.every-channel.netboot;
|
|
scriptsRoot = ../..;
|
|
|
|
boolString = v: if v then "true" else "false";
|
|
|
|
mkExport = name: value: "export ${name}=${lib.escapeShellArg value}";
|
|
|
|
optionalExport = name: value:
|
|
if value == null then "" else mkExport name value;
|
|
|
|
optionalFileExport = name: path:
|
|
if path == null then ""
|
|
else ''
|
|
if [[ ! -r ${lib.escapeShellArg path} ]]; then
|
|
echo "error: required file is not readable: ${path}" >&2
|
|
exit 2
|
|
fi
|
|
export ${name}="$(tr -d '\\r\\n' < ${lib.escapeShellArg path})"
|
|
'';
|
|
|
|
stageToolchain = with pkgs; [
|
|
bash
|
|
coreutils
|
|
curl
|
|
gawk
|
|
gnugrep
|
|
gnused
|
|
gnutar
|
|
gzip
|
|
python3
|
|
];
|
|
|
|
ipxeToolchain = with pkgs; [
|
|
git
|
|
gnumake
|
|
gcc
|
|
binutils
|
|
perl
|
|
mtools
|
|
docker-client
|
|
];
|
|
in
|
|
{
|
|
options.services.every-channel.netboot = {
|
|
enable = lib.mkEnableOption "every.channel persistent netboot stage/serve services";
|
|
|
|
listenIP = lib.mkOption {
|
|
type = lib.types.str;
|
|
example = "10.20.30.2";
|
|
description = "IP address bound for TFTP/HTTP on the provisioning VLAN.";
|
|
};
|
|
|
|
interface = lib.mkOption {
|
|
type = lib.types.str;
|
|
example = "enp3s0";
|
|
description = "Network interface name on the provisioning VLAN.";
|
|
};
|
|
|
|
hostname = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "boot.every.channel";
|
|
description = "Boot server hostname advertised to DHCP/iPXE clients.";
|
|
};
|
|
|
|
rootDir = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/var/lib/every-channel-netboot";
|
|
description = "Persistent root directory containing staged HTTP and TFTP artifacts.";
|
|
};
|
|
|
|
httpPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 8080;
|
|
description = "HTTP port used for kernel/initrd/netboot script serving.";
|
|
};
|
|
|
|
tftpBootFilename = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "ec-ipxe.efi";
|
|
description = "Filename served via TFTP (DHCP option 67 in UniFi-only mode).";
|
|
};
|
|
|
|
chainTokenFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "/run/agenix/every-channel-netboot-chain-token";
|
|
description = "Optional file containing netboot chain token passed to stage/serve scripts.";
|
|
};
|
|
|
|
httpAllowedCIDRs = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
example = [ "10.20.30.0/24" ];
|
|
description = "Optional CIDR allowlist for HTTP artifact serving.";
|
|
};
|
|
|
|
openFirewall = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Open firewall ports for TFTP/HTTP (and ProxyDHCP ports when enabled).";
|
|
};
|
|
|
|
stageOnBoot = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Start the stage oneshot service during boot before serving.";
|
|
};
|
|
|
|
release = {
|
|
host = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "https://git.every.channel";
|
|
description = "Forge host used to fetch netboot release assets.";
|
|
};
|
|
|
|
repo = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "every-channel/every.channel";
|
|
description = "Forge repository containing netboot release assets.";
|
|
};
|
|
|
|
tag = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Optional release tag to stage; defaults to latest release.";
|
|
};
|
|
|
|
localTarball = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Optional local netboot tarball path; bypasses release API download when set.";
|
|
};
|
|
|
|
tokenFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Optional file containing Forge API token for private release access.";
|
|
};
|
|
|
|
verifyChecksums = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Verify staged release tarball using SHA256SUMS.txt when available.";
|
|
};
|
|
};
|
|
|
|
ipxe = {
|
|
buildEmbedded = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Build embedded iPXE (ec-ipxe.efi) before staging artifacts.";
|
|
};
|
|
|
|
path = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Optional prebuilt iPXE EFI binary path; used when buildEmbedded is false.";
|
|
};
|
|
|
|
allowRemoteDownload = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Allow remote iPXE URL download during staging. Disabled by default.";
|
|
};
|
|
|
|
sha256 = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Optional SHA256 digest expected for selected iPXE EFI binary.";
|
|
};
|
|
|
|
repo = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "https://github.com/ipxe/ipxe.git";
|
|
description = "iPXE source repository used for embedded EFI builds.";
|
|
};
|
|
|
|
ref = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Optional git ref/commit for iPXE build reproducibility.";
|
|
};
|
|
|
|
useDocker = lib.mkOption {
|
|
type = lib.types.enum [ "auto" "true" "false" ];
|
|
default = "auto";
|
|
description = "Container fallback mode for iPXE builds (auto/true/false).";
|
|
};
|
|
|
|
dockerImage = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "ubuntu:24.04";
|
|
description = "Docker image used when iPXE build runs in container mode.";
|
|
};
|
|
|
|
dockerPlatform = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "linux/amd64";
|
|
description = "Docker platform used for containerized iPXE build.";
|
|
};
|
|
};
|
|
|
|
proxyDhcp = {
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Enable dnsmasq ProxyDHCP chainloading mode.";
|
|
};
|
|
|
|
subnet = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "10.20.30.0/24";
|
|
description = "ProxyDHCP subnet (required when proxyDhcp.enable is true).";
|
|
};
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = cfg.proxyDhcp.enable -> cfg.proxyDhcp.subnet != null;
|
|
message = "services.every-channel.netboot.proxyDhcp.subnet must be set when proxyDhcp.enable is true";
|
|
}
|
|
{
|
|
assertion = cfg.ipxe.buildEmbedded || cfg.ipxe.path != null || cfg.ipxe.allowRemoteDownload;
|
|
message = "Set ipxe.buildEmbedded=true, or provide ipxe.path, or allow ipxe.allowRemoteDownload=true";
|
|
}
|
|
];
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d ${cfg.rootDir} 0750 root root -"
|
|
"d ${cfg.rootDir}/http 0750 root root -"
|
|
"d ${cfg.rootDir}/tftp 0750 root root -"
|
|
];
|
|
|
|
systemd.services.every-channel-netboot-ipxe = lib.mkIf cfg.ipxe.buildEmbedded {
|
|
description = "every.channel netboot embedded iPXE build";
|
|
after = [ "network-online.target" ];
|
|
wants = [ "network-online.target" ];
|
|
wantedBy = lib.mkIf cfg.stageOnBoot [ "multi-user.target" ];
|
|
path = stageToolchain ++ ipxeToolchain;
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_ROOT" cfg.rootDir}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_HOSTNAME" cfg.hostname}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_HTTP_PORT" (toString cfg.httpPort)}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_IPXE_FILENAME" cfg.tftpBootFilename}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_IPXE_REPO" cfg.ipxe.repo}
|
|
${optionalExport "EVERY_CHANNEL_NETBOOT_IPXE_REF" cfg.ipxe.ref}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_IPXE_USE_DOCKER" cfg.ipxe.useDocker}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_IPXE_DOCKER_IMAGE" cfg.ipxe.dockerImage}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_IPXE_DOCKER_PLATFORM" cfg.ipxe.dockerPlatform}
|
|
${optionalFileExport "EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN" cfg.chainTokenFile}
|
|
|
|
${scriptsRoot}/scripts/netboot-build-ipxe.sh
|
|
'';
|
|
};
|
|
|
|
systemd.services.every-channel-netboot-stage = {
|
|
description = "every.channel netboot artifact stage";
|
|
requires = lib.optionals cfg.ipxe.buildEmbedded [ "every-channel-netboot-ipxe.service" ];
|
|
after = [ "network-online.target" ] ++ lib.optionals cfg.ipxe.buildEmbedded [ "every-channel-netboot-ipxe.service" ];
|
|
wants = [ "network-online.target" ] ++ lib.optionals cfg.ipxe.buildEmbedded [ "every-channel-netboot-ipxe.service" ];
|
|
wantedBy = lib.mkIf cfg.stageOnBoot [ "multi-user.target" ];
|
|
path = stageToolchain;
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_ROOT" cfg.rootDir}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_HOSTNAME" cfg.hostname}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_HTTP_PORT" (toString cfg.httpPort)}
|
|
${mkExport "EVERY_CHANNEL_FORGE_HOST" cfg.release.host}
|
|
${mkExport "EVERY_CHANNEL_FORGE_REPO" cfg.release.repo}
|
|
${optionalExport "EVERY_CHANNEL_NETBOOT_RELEASE_TAG" cfg.release.tag}
|
|
${optionalExport "EVERY_CHANNEL_NETBOOT_TARBALL" cfg.release.localTarball}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_VERIFY_RELEASE_CHECKSUMS" (boolString cfg.release.verifyChecksums)}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_ALLOW_REMOTE_IPXE" (boolString cfg.ipxe.allowRemoteDownload)}
|
|
${mkExport "EVERY_CHANNEL_IPXE_EFI_FILENAME" cfg.tftpBootFilename}
|
|
${optionalExport "EVERY_CHANNEL_IPXE_EFI_SHA256" cfg.ipxe.sha256}
|
|
${optionalFileExport "EVERY_CHANNEL_FORGE_TOKEN" cfg.release.tokenFile}
|
|
${optionalFileExport "EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN" cfg.chainTokenFile}
|
|
|
|
${lib.optionalString cfg.ipxe.buildEmbedded (mkExport "EVERY_CHANNEL_IPXE_EFI_PATH" "${cfg.rootDir}/tftp/${cfg.tftpBootFilename}")}
|
|
${lib.optionalString (!cfg.ipxe.buildEmbedded && cfg.ipxe.path != null) (mkExport "EVERY_CHANNEL_IPXE_EFI_PATH" cfg.ipxe.path)}
|
|
|
|
${scriptsRoot}/scripts/netboot-stage.sh
|
|
'';
|
|
};
|
|
|
|
systemd.services.every-channel-netboot = {
|
|
description = "every.channel netboot HTTP + TFTP service";
|
|
requires = [ "every-channel-netboot-stage.service" ];
|
|
after = [ "network-online.target" "every-channel-netboot-stage.service" ];
|
|
wants = [ "network-online.target" "every-channel-netboot-stage.service" ];
|
|
wantedBy = lib.mkIf cfg.stageOnBoot [ "multi-user.target" ];
|
|
path = stageToolchain ++ [ pkgs.dnsmasq ];
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
Restart = "always";
|
|
RestartSec = "5s";
|
|
};
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_ROOT" cfg.rootDir}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_LISTEN_IP" cfg.listenIP}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_INTERFACE" cfg.interface}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_HOSTNAME" cfg.hostname}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_HTTP_PORT" (toString cfg.httpPort)}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_PROXY_DHCP" (boolString cfg.proxyDhcp.enable)}
|
|
${optionalExport "EVERY_CHANNEL_NETBOOT_PROXY_SUBNET" cfg.proxyDhcp.subnet}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_TFTP_BOOT_FILENAME" cfg.tftpBootFilename}
|
|
${mkExport "EVERY_CHANNEL_NETBOOT_HTTP_ALLOWED_CIDRS" (lib.concatStringsSep "," cfg.httpAllowedCIDRs)}
|
|
${optionalFileExport "EVERY_CHANNEL_NETBOOT_CHAIN_TOKEN" cfg.chainTokenFile}
|
|
|
|
${scriptsRoot}/scripts/netboot-serve.sh
|
|
'';
|
|
};
|
|
|
|
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.httpPort ];
|
|
networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall (
|
|
[ 69 ] ++ lib.optionals cfg.proxyDhcp.enable [ 67 4011 ]
|
|
);
|
|
};
|
|
}
|