From 8a4e574b9069ad182905d2bb9c96a26a7d2dd6df Mon Sep 17 00:00:00 2001 From: UGA Innovation Factory Date: Wed, 10 Dec 2025 14:42:22 -0500 Subject: [PATCH] feat: Add support for ipxe boot ephemeral systems --- artifacts.nix | 45 +++++++++- hosts/boot.nix | 6 +- hosts/types/nix-desktop.nix | 1 + hosts/types/nix-ephemeral.nix | 54 ++++++++++++ hosts/types/nix-laptop.nix | 1 + hosts/types/nix-surface.nix | 1 + inventory.nix | 3 + sw/default.nix | 13 ++- sw/stateless-kiosk/default.nix | 28 +++++++ sw/stateless-kiosk/kiosk-browser.nix | 118 +++++++++++++++++++++++++++ sw/stateless-kiosk/net.nix | 43 ++++++++++ 11 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 hosts/types/nix-ephemeral.nix create mode 100644 sw/stateless-kiosk/default.nix create mode 100644 sw/stateless-kiosk/kiosk-browser.nix create mode 100644 sw/stateless-kiosk/net.nix diff --git a/artifacts.nix b/artifacts.nix index cf1bcea..9b4e0cf 100644 --- a/artifacts.nix +++ b/artifacts.nix @@ -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 diff --git a/hosts/boot.nix b/hosts/boot.nix index 8001fc8..86fa595 100644 --- a/hosts/boot.nix +++ b/hosts/boot.nix @@ -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. diff --git a/hosts/types/nix-desktop.nix b/hosts/types/nix-desktop.nix index 36c9c16..e930c9d 100644 --- a/hosts/types/nix-desktop.nix +++ b/hosts/types/nix-desktop.nix @@ -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; diff --git a/hosts/types/nix-ephemeral.nix b/hosts/types/nix-ephemeral.nix new file mode 100644 index 0000000..c8043c7 --- /dev/null +++ b/hosts/types/nix-ephemeral.nix @@ -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"; + } +] diff --git a/hosts/types/nix-laptop.nix b/hosts/types/nix-laptop.nix index 2478a27..f4621e0 100644 --- a/hosts/types/nix-laptop.nix +++ b/hosts/types/nix-laptop.nix @@ -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; diff --git a/hosts/types/nix-surface.nix b/hosts/types/nix-surface.nix index b9fce24..8f40d97 100644 --- a/hosts/types/nix-surface.nix +++ b/hosts/types/nix-surface.nix @@ -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; diff --git a/inventory.nix b/inventory.nix index 02c9e08..e1f09f4 100644 --- a/inventory.nix +++ b/inventory.nix @@ -60,4 +60,7 @@ }; }; }; + + # Ephemeral Configuration (Live ISO / Netboot) + nix-ephemeral.count = 1; } diff --git a/sw/default.nix b/sw/default.nix index 44a96c5..43288ae 100644 --- a/sw/default.nix +++ b/sw/default.nix @@ -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 + ; + } + )) ]); } diff --git a/sw/stateless-kiosk/default.nix b/sw/stateless-kiosk/default.nix new file mode 100644 index 0000000..7bc0f16 --- /dev/null +++ b/sw/stateless-kiosk/default.nix @@ -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; + } +] diff --git a/sw/stateless-kiosk/kiosk-browser.nix b/sw/stateless-kiosk/kiosk-browser.nix new file mode 100644 index 0000000..89bc8c2 --- /dev/null +++ b/sw/stateless-kiosk/kiosk-browser.nix @@ -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" ]; + }; +} diff --git a/sw/stateless-kiosk/net.nix b/sw/stateless-kiosk/net.nix new file mode 100644 index 0000000..e0387f1 --- /dev/null +++ b/sw/stateless-kiosk/net.nix @@ -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"; + }; + }; +}