feat: Add support for ipxe boot ephemeral systems

This commit is contained in:
UGA Innovation Factory
2025-12-10 14:42:22 -05:00
committed by Hunter Halloran
parent 1412529b0f
commit 8a4e574b90
11 changed files with 307 additions and 6 deletions

View File

@@ -2,6 +2,7 @@
let
nixpkgs = inputs.nixpkgs;
lib = nixpkgs.lib;
pkgs = nixpkgs.legacyPackages.${system};
nixos-generators = inputs.nixos-generators;
mkInstaller = hostName:
@@ -36,16 +37,56 @@ let
inherit format;
};
mkNetboot = hostName:
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = { inherit inputs; };
modules = hosts.modules.${hostName} ++ [
"${nixpkgs}/nixos/modules/installer/netboot/netboot.nix"
{
disko.enableConfig = lib.mkForce false;
services.upower.enable = lib.mkForce false;
}
];
};
hostNames = builtins.attrNames hosts.nixosConfigurations;
installerPackages = lib.listToAttrs (lib.concatMap (name:
let cfg = hosts.nixosConfigurations.${name}; in
if lib.elem "iso" cfg.config.host.buildMethods then [{
if lib.elem "installer-iso" cfg.config.host.buildMethods then [{
name = "installer-iso-${name}";
value = (mkInstaller name).config.system.build.isoImage;
}] else []
) hostNames);
isoPackages = lib.listToAttrs (lib.concatMap (name:
let cfg = hosts.nixosConfigurations.${name}; in
if lib.elem "iso" cfg.config.host.buildMethods then [{
name = "iso-${name}";
value = mkGenerator name "iso";
}] else []
) hostNames);
ipxePackages = lib.listToAttrs (lib.concatMap (name:
let cfg = hosts.nixosConfigurations.${name}; in
if lib.elem "ipxe" cfg.config.host.buildMethods then [{
name = "ipxe-${name}";
value =
let
build = (mkNetboot name).config.system.build;
in
pkgs.symlinkJoin {
name = "netboot-artifacts-${name}";
paths = [
build.netbootRamdisk
build.kernel
build.netbootIpxeScript
];
};
}] else []
) hostNames);
lxcPackages = lib.listToAttrs (lib.concatMap (name:
let cfg = hosts.nixosConfigurations.${name}; in
if lib.elem "lxc" cfg.config.host.buildMethods then [{
@@ -70,4 +111,4 @@ let
}] else []
) hostNames);
in
installerPackages // lxcPackages // proxmoxPackages
installerPackages // isoPackages // ipxePackages // lxcPackages // proxmoxPackages

View File

@@ -25,8 +25,8 @@
};
buildMethods = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "iso" ];
description = "List of allowed build methods (iso, lxc, proxmox).";
default = [ "installer-iso" ];
description = "List of allowed build methods (installer-iso, iso, ipxe, lxc, proxmox).";
};
};
@@ -102,7 +102,7 @@
# Hide the OS choice for bootloaders.
# It's still possible to open the bootloader list by pressing any key
# It will just not appear on screen unless a key is pressed
loader.timeout = 0;
loader.timeout = lib.mkDefault 0;
};
# Set your time zone.

View File

@@ -30,6 +30,7 @@
host.filesystem.swapSize = lib.mkDefault "16G";
host.filesystem.device = lib.mkDefault "/dev/nvme0n1";
host.buildMethods = lib.mkDefault [ "installer-iso" ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;

View File

@@ -0,0 +1,54 @@
{ inputs, ... }:
[
(
{
config,
lib,
modulesPath,
...
}:
{
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
boot.initrd.availableKernelModules = [
"xhci_pci"
"nvme"
"usb_storage"
"sd_mod"
"sdhci_pci"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
boot.kernelParams = [
"quiet"
"splash"
"boot.shell_on_fail"
"udev.log_priority=3"
"rd.systemd.show_status=auto"
];
# Ephemeral setup: No swap, no disk
host.filesystem.swapSize = lib.mkForce "0G";
host.filesystem.device = lib.mkForce "/dev/null"; # Dummy device
host.buildMethods = lib.mkDefault [ "iso" "ipxe" ];
# Disable Disko config since we are running from RAM/ISO
disko.enableConfig = lib.mkForce false;
# Define a dummy root filesystem to satisfy assertions
fileSystems."/" = {
device = "none";
fsType = "tmpfs";
options = [ "defaults" "size=50%" "mode=755" ];
};
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
)
{
modules.sw.enable = true;
modules.sw.type = "stateless-kiosk";
}
]

View File

@@ -37,6 +37,7 @@
host.filesystem.device = lib.mkDefault "/dev/nvme0n1";
host.filesystem.swapSize = lib.mkDefault "34G";
host.buildMethods = lib.mkDefault [ "installer-iso" ];
# Suspend / logind behavior
services.upower.enable = lib.mkDefault true;

View File

@@ -45,6 +45,7 @@
host.filesystem.swapSize = lib.mkDefault "8G";
host.filesystem.device = lib.mkDefault "/dev/mmcblk0";
host.buildMethods = lib.mkDefault [ "installer-iso" ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;

View File

@@ -60,4 +60,7 @@
};
};
};
# Ephemeral Configuration (Live ISO / Netboot)
nix-ephemeral.count = 1;
}

View File

@@ -32,6 +32,7 @@ in
"desktop"
"tablet-kiosk"
"headless"
"stateless-kiosk"
];
default = "desktop";
description = "Type of system configuration: 'desktop' for normal OS, 'tablet-kiosk' for tablet/kiosk mode.";
@@ -91,7 +92,7 @@ in
];
}
# Import Desktop or Kiosk modules based on type
(mkIf (cfg.type == "desktop") (
(mkIf (cfg.type == "desktop") (
import ./desktop {
inherit
config
@@ -121,5 +122,15 @@ in
;
}
))
(mkIf (cfg.type == "stateless-kiosk") (
import ./stateless-kiosk {
inherit
config
lib
pkgs
inputs
;
}
))
]);
}

View File

@@ -0,0 +1,28 @@
{
config,
lib,
pkgs,
inputs,
...
}:
lib.mkMerge [
(import ./kiosk-browser.nix {
inherit
config
lib
pkgs
inputs
;
})
(import ./net.nix {
inherit
config
lib
pkgs
inputs
;
})
{
services.openssh.enable = false;
}
]

View File

@@ -0,0 +1,118 @@
{ config, lib, pkgs, ... }:
let
kioskPolicies = {
DisableAppUpdate = true;
DisableFirefoxStudies = true;
DisableTelemetry = true;
DisablePocket = true;
DisableSetDesktopBackground = true;
DisableFeedbackCommands = true;
DontCheckDefaultBrowser = true;
OverrideFirstRunPage = "";
OverridePostUpdatePage = "";
NoDefaultBookmarks = true;
DisableProfileImport = true;
Permissions = {
Camera = { Allow = ["homeassistant.lan"]; };
Microphone = { Allow = ["homeassistant.lan"]; };
Location = { Allow = ["homeassistant.lan"]; };
Notifications = { Allow = ["homeassistant.lan"]; };
Clipboard = { Allow = ["homeassistant.lan"]; };
Fullscreen = { Allow = ["homeassistant.lan"]; };
};
};
extraPrefs = pkgs.writeText "kiosk-prefs.js" ''
pref("browser.shell.checkDefaultBrowser", false);
pref("browser.startup.homepage_override.mstone", "ignore");
pref("startup.homepage_welcome_url", "");
pref("startup.homepage_welcome_url.additional", "");
pref("browser.sessionstore.resume_from_crash", false);
pref("browser.sessionstore.max_resumed_crashes", 0);
pref("network.captive-portal-service.enabled", false);
pref("network.connectivity-service.enabled", false);
pref("browser.messaging-system.whatsNewPanel.enabled", false);
pref("browser.aboutwelcome.enabled", false);
pref("privacy.popups.showBrowserMessage", false);
'';
firefoxWrapped = pkgs.wrapFirefox pkgs.firefox-unwrapped {
extraPolicies = kioskPolicies;
extraPrefsFiles = [ extraPrefs ];
};
firefoxKiosk = pkgs.writeShellScriptBin "firefoxkiosk" ''
#!/usr/bin/env bash
set -eu
BASE="http://homeassistant.lan:8123"
get_primary_mac() {
for dev in /sys/class/net/*; do
iface="$(basename "$dev")"
[ "$iface" = "lo" ] && continue
if [ -f "$dev/type" ] && [ "$(cat "$dev/type")" = "1" ]; then
cat "$dev/address"
return 0
fi
done
return 1
}
MAC="$(get_primary_mac 2>/dev/null || echo "")"
MAC="$(echo "$MAC" | tr '[:upper:]' '[:lower:]')"
case "$MAC" in
"00:e0:4c:46:0b:32") STATION="1" ;;
"00:e0:4c:46:07:26") STATION="2" ;;
"00:e0:4c:46:05:94") STATION="3" ;;
"00:e0:4c:46:07:11") STATION="4" ;;
"00:e0:4c:46:08:02") STATION="5" ;;
"00:e0:4c:46:08:5c") STATION="6" ;;
*) ;;
esac
DEFAULT_PATH="lovelace/0"
PATH_PART="$DEFAULT_PATH"
BROWSER_ID="" # browser_mod identifier
if [ -n "$STATION" ]; then
PATH_PART="assembly-line/$STATION"
BROWSER_ID="Station%20$STATION"
fi
URL="$BASE/$PATH_PART"
# Add BrowserID query param if we have one
if [ -n "$BROWSER_ID" ]; then
if [[ "$URL" == *"?"* ]]; then
URL="$URL&BrowserID=$BROWSER_ID"
else
URL="$URL?BrowserID=$BROWSER_ID"
fi
fi
sleep 2
exec ${firefoxWrapped}/bin/firefox --kiosk "$URL"
'';
in
{
environment.systemPackages = [ firefoxKiosk ];
services.xserver.enable = false;
services.seatd.enable = true;
services.cage = {
enable = true;
user = "engr-ugaif";
program = "${firefoxKiosk}/bin/firefoxkiosk";
};
systemd.services.cage = {
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
};
}

View File

@@ -0,0 +1,43 @@
{ config, lib, pkgs, inputs, ... }:
{
# Minimal container networking (systemd-networkd)
networking = {
useNetworkd = true;
networkmanager.enable = false;
dhcpcd.enable = false;
useDHCP = false;
useHostResolvConf = false;
};
systemd.network = {
enable = true;
wait-online.enable = true;
networks."10-wired" = {
matchConfig.Type = "ether";
networkConfig = {
LinkLocalAddressing = false;
DHCP = "no";
VLAN = [ "vlan5" ];
};
linkConfig.RequiredForOnline = "no";
};
netdevs."20-vlan5" = {
netdevConfig = {
Kind = "vlan";
Name = "vlan5";
};
vlanConfig.Id = 5;
};
networks."30-vlan5" = {
matchConfig.Name = "vlan5";
networkConfig = {
DHCP = "ipv4";
IPv6AcceptRA = true;
};
linkConfig.RequiredForOnline = "routable";
};
};
}