This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

tools.pxe-serve: init netboot a flake machine over PXE

+217
+20
tools/pxe-serve/flake-module.nix
··· 1 + { ... }: 2 + { 3 + perSystem = 4 + { pkgs, config, ... }: 5 + { 6 + packages.pxe-serve = 7 + (pkgs.writeShellApplication { 8 + name = "pxe-serve"; 9 + runtimeInputs = [ 10 + pkgs.nix 11 + pkgs.iproute2 12 + pkgs.gawk 13 + pkgs.coreutils 14 + config.packages.pxe-share 15 + ]; 16 + text = builtins.readFile ./pxe-serve.sh; 17 + }).overrideAttrs 18 + { passthru.usage = builtins.readFile ./usage.kdl; }; 19 + }; 20 + }
+187
tools/pxe-serve/pxe-serve.sh
··· 1 + #!/usr/bin/env bash 2 + set -euo pipefail 3 + 4 + PROG="pxe-serve" 5 + 6 + # Defaults 7 + HTTP_PORT="8079" 8 + IFACE="" 9 + TARGET="" 10 + NO_DHCP="" 11 + 12 + usage() { 13 + cat >&2 <<EOF 14 + Usage: $PROG [OPTIONS] <flake>#<machine> [interface] 15 + 16 + Netboot a machine straight from its flake: builds the machine's netboot image 17 + variant, serves it, and points PXE clients at it. 18 + 19 + $PROG .#atlas 20 + 21 + The machine needs a \`netboot\` entry in image.modules (see machines/atlas). 22 + Everything is derived, so nothing has to be kept in sync by hand: 23 + 24 + * kernel, initrd and kernel command line come from the machine's netboot 25 + variant (\`system.build.images.netboot\`) 26 + * the iPXE script is generated against the serving interface's own address, 27 + which is the bit that is wrong in any pre-baked image the moment you move 28 + to a different network 29 + * kernel and initrd go over HTTP, because an initrd carrying a whole nix 30 + store is far too big for TFTP 31 + 32 + By default this acts as a proxy DHCP server: it answers only the PXE boot 33 + question and never hands out addresses, so it works on a network whose DHCP 34 + server belongs to somebody else. The client must be on the same L2 segment. 35 + 36 + Runs in the foreground. Press Ctrl+C to stop; the built payload stays in the 37 + nix store, the served directory is temporary. 38 + 39 + Options: 40 + --no-dhcp Don't answer DHCP; only serve the files. Use when 41 + another DHCP server already points clients at us. 42 + --http-port <port> Port to serve kernel/initrd on (default: ${HTTP_PORT}) 43 + -i, --interface <iface> Interface to serve on 44 + (default: the one carrying the default route) 45 + -h, --help Show this help 46 + EOF 47 + } 48 + 49 + while [[ $# -gt 0 ]]; do 50 + case $1 in 51 + --no-dhcp) 52 + NO_DHCP=1 53 + shift 54 + ;; 55 + --http-port) 56 + HTTP_PORT="$2" 57 + shift 2 58 + ;; 59 + -i | --interface) 60 + IFACE="$2" 61 + shift 2 62 + ;; 63 + -h | --help) 64 + usage 65 + exit 0 66 + ;; 67 + -*) 68 + echo "$PROG: unknown option: $1" >&2 69 + usage 70 + exit 1 71 + ;; 72 + *) 73 + if [[ -z $TARGET ]]; then 74 + TARGET="$1" 75 + elif [[ -z $IFACE ]]; then 76 + IFACE="$1" 77 + else 78 + echo "$PROG: unexpected argument: $1" >&2 79 + usage 80 + exit 1 81 + fi 82 + shift 83 + ;; 84 + esac 85 + done 86 + 87 + if [[ -z $TARGET ]]; then 88 + echo "$PROG: need a machine, e.g. $PROG .#atlas" >&2 89 + usage 90 + exit 1 91 + fi 92 + 93 + # "<flake>#<machine>", or a bare machine name in the current flake. 94 + if [[ $TARGET == *#* ]]; then 95 + FLAKE="${TARGET%%#*}" 96 + MACHINE="${TARGET##*#}" 97 + else 98 + FLAKE="." 99 + MACHINE="$TARGET" 100 + fi 101 + [[ -n $FLAKE ]] || FLAKE="." 102 + 103 + # builtins.getFlake (used for the iPXE override below) needs an absolute path 104 + # or a URL, not a relative one like ".". 105 + case $FLAKE in 106 + . | ./* | /* | ../*) FLAKE_REF=$(cd "$FLAKE" && pwd -P) ;; 107 + *) FLAKE_REF="$FLAKE" ;; 108 + esac 109 + 110 + if [[ -z $IFACE ]]; then 111 + IFACE=$(ip -4 route show default | awk '{print $5; exit}') 112 + if [[ -z $IFACE ]]; then 113 + echo "$PROG: no default route to pick an interface from; pass --interface" >&2 114 + exit 1 115 + fi 116 + fi 117 + 118 + ADDR=$(ip -4 -brief addr show dev "$IFACE" 2>/dev/null | awk '{print $3}' | cut -d/ -f1 | head -n1) 119 + if [[ -z $ADDR ]]; then 120 + echo "$PROG: $IFACE has no IPv4 address to serve from" >&2 121 + exit 1 122 + fi 123 + 124 + CFG="${FLAKE}#nixosConfigurations.${MACHINE}.config.system.build.images.netboot.passthru.config" 125 + 126 + echo "$PROG: building netboot payload for ${MACHINE} (this pulls in the whole store, so it takes a while)" 127 + KERNEL=$(nix build --no-link --print-out-paths "${CFG}.system.build.kernel") 128 + KERNEL_FILE=$(nix eval --raw "${CFG}.system.boot.loader.kernelFile") 129 + INITRD=$(nix build --no-link --print-out-paths "${CFG}.system.build.netbootRamdisk") 130 + TOPLEVEL=$(nix eval --raw "${CFG}.system.build.toplevel") 131 + PARAMS=$(nix eval --raw --apply 'ps: builtins.concatStringsSep " " ps' "${CFG}.boot.kernelParams") 132 + 133 + # iPXE gets the chain script baked in rather than asking DHCP for a filename. 134 + # In proxy-DHCP mode dnsmasq answers through pxe-service, so a --dhcp-boot 135 + # aimed at the second-stage (tag:ipxe) request never reaches iPXE and it dies 136 + # with "Nothing to boot: No such file or directory" (ipxe.org/2d03e13b) right 137 + # after loading. With the script embedded it only needs an address, which the 138 + # network's own DHCP server hands out. 139 + # 140 + # Written to a file instead of passed inline because the expression embeds a 141 + # URL and a flake path, and quoting that through the shell is a trap. 142 + IPXE_EXPR=$(mktemp -t "${PROG}-ipxe-XXXXXX.nix") 143 + cat >"$IPXE_EXPR" <<EOF 144 + let 145 + flake = builtins.getFlake "${FLAKE_REF}"; 146 + in 147 + flake.nixosConfigurations."${MACHINE}".pkgs.ipxe.override { 148 + embedScript = builtins.toFile "chain.ipxe" '' 149 + #!ipxe 150 + dhcp 151 + chain http://${ADDR}:${HTTP_PORT}/netboot.ipxe 152 + ''; 153 + } 154 + EOF 155 + IPXE=$(nix build --no-link --print-out-paths --impure --file "$IPXE_EXPR") 156 + rm -f "$IPXE_EXPR" 157 + 158 + # A stable directory rather than a mktemp one with a cleanup trap: the trap 159 + # was the reason Ctrl+C did nothing useful. A bash INT handler does not exit 160 + # after running, so it deleted the served directory out from under a still 161 + # running dnsmasq and then went back to waiting. Only symlinks live here, so 162 + # leaving them behind between runs costs nothing. 163 + ROOT="${XDG_RUNTIME_DIR:-/tmp}/${PROG}/${MACHINE}" 164 + rm -rf "$ROOT" 165 + mkdir -p "$ROOT" 166 + chmod 755 "$ROOT" 167 + 168 + ln -s "${KERNEL}/${KERNEL_FILE}" "$ROOT/bzImage" 169 + ln -s "${INITRD}/initrd" "$ROOT/initrd" 170 + ln -s "${IPXE}/ipxe.efi" "$ROOT/ipxe.efi" 171 + ln -s "${IPXE}/undionly.kpxe" "$ROOT/undionly.kpxe" 172 + 173 + cat >"$ROOT/netboot.ipxe" <<EOF 174 + #!ipxe 175 + kernel http://${ADDR}:${HTTP_PORT}/bzImage init=${TOPLEVEL}/init initrd=initrd ${PARAMS} 176 + initrd http://${ADDR}:${HTTP_PORT}/initrd 177 + boot 178 + EOF 179 + 180 + echo "$PROG: serving ${MACHINE} on ${IFACE} (${ADDR}), kernel+initrd over http://${ADDR}:${HTTP_PORT}" 181 + echo "$PROG: initrd is $(stat -Lc %s "$ROOT/initrd" | awk '{printf "%.1f GB", $1/1073741824}') — expect that much to cross the wire on every boot" 182 + 183 + # exec, so there is no wrapper process between the terminal and dnsmasq: Ctrl+C 184 + # reaches it directly and pxe-share's own trap does the privileged teardown 185 + # (firewall rules, darkhttpd). pxe-share re-execs itself through sudo; the nix 186 + # builds above deliberately ran as the calling user. 187 + exec pxe-share ${NO_DHCP:+--no-dhcp} --http --http-port "$HTTP_PORT" "$ROOT" "$IFACE"
+10
tools/pxe-serve/usage.kdl
··· 1 + bin "pxe-serve" 2 + about "Netboot a machine from its flake: builds its netboot image variant and serves it over PXE" 3 + 4 + flag "--no-dhcp" help="Don't answer DHCP; only serve the files (another DHCP server points clients at us)" 5 + flag "--http-port <port>" help="Port to serve kernel/initrd on (default: 8079)" 6 + flag "-i --interface <iface>" help="Interface to serve on (default: the one carrying the default route)" 7 + flag "-h --help" help="Show help message" 8 + 9 + arg "<flake#machine>" help="Machine to netboot, e.g. .#atlas" 10 + arg "[interface]" help="Interface to serve on"