This repository has no description
0

Configure Feed

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

superconfig / tools / pxe-serve / pxe-serve.sh
7.3 kB 200 lines
1#!/usr/bin/env bash 2set -euo pipefail 3 4PROG="pxe-serve" 5 6# Defaults 7HTTP_PORT="8079" 8IFACE="" 9TARGET="" 10NO_DHCP="" 11 12usage() { 13 cat >&2 <<EOF 14Usage: $PROG [OPTIONS] <flake>#<machine> [interface] 15 16Netboot a machine straight from its flake: builds the machine's netboot image 17variant, serves it, and points PXE clients at it. 18 19 $PROG .#atlas 20 21The machine needs a \`netboot\` entry in image.modules (see machines/atlas). 22Everything 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 32By default this acts as a proxy DHCP server: it answers only the PXE boot 33question and never hands out addresses, so it works on a network whose DHCP 34server belongs to somebody else. The client must be on the same L2 segment. 35 36Runs in the foreground. Press Ctrl+C to stop; the built payload stays in the 37nix store, the served directory is temporary. 38 39Options: 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 46EOF 47} 48 49while [[ $# -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 85done 86 87if [[ -z $TARGET ]]; then 88 echo "$PROG: need a machine, e.g. $PROG .#atlas" >&2 89 usage 90 exit 1 91fi 92 93# "<flake>#<machine>", or a bare machine name in the current flake. 94if [[ $TARGET == *#* ]]; then 95 FLAKE="${TARGET%%#*}" 96 MACHINE="${TARGET##*#}" 97else 98 FLAKE="." 99 MACHINE="$TARGET" 100fi 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 ".". 105case $FLAKE in 106 . | ./* | /* | ../*) FLAKE_REF=$(cd "$FLAKE" && pwd -P) ;; 107 *) FLAKE_REF="$FLAKE" ;; 108esac 109 110if [[ -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 116fi 117 118ADDR=$(ip -4 -brief addr show dev "$IFACE" 2>/dev/null | awk '{print $3}' | cut -d/ -f1 | head -n1) 119if [[ -z $ADDR ]]; then 120 echo "$PROG: $IFACE has no IPv4 address to serve from" >&2 121 exit 1 122fi 123 124CFG="${FLAKE}#nixosConfigurations.${MACHINE}.config.system.build.images.netboot.passthru.config" 125 126echo "$PROG: building netboot payload for ${MACHINE} (this pulls in the whole store, so it takes a while)" 127KERNEL=$(nix build --no-link --print-out-paths "${CFG}.system.build.kernel") 128KERNEL_FILE=$(nix eval --raw "${CFG}.system.boot.loader.kernelFile") 129INITRD=$(nix build --no-link --print-out-paths "${CFG}.system.build.netbootRamdisk") 130TOPLEVEL=$(nix eval --raw "${CFG}.system.build.toplevel") 131PARAMS=$(nix eval --raw --apply 'ps: builtins.concatStringsSep " " ps' "${CFG}.boot.kernelParams") 132# Served separately from the initrd: iPXE can only hold so much in 133# EfiBootServicesData, so stage 1 fetches this itself over HTTP. 134SQUASHFS=$(nix build --no-link --print-out-paths "${CFG}.system.build.squashfsStore") 135 136# iPXE gets the chain script baked in rather than asking DHCP for a filename. 137# In proxy-DHCP mode dnsmasq answers through pxe-service, so a --dhcp-boot 138# aimed at the second-stage (tag:ipxe) request never reaches iPXE and it dies 139# with "Nothing to boot: No such file or directory" (ipxe.org/2d03e13b) right 140# after loading. With the script embedded it only needs an address, which the 141# network's own DHCP server hands out. 142# 143# Written to a file instead of passed inline because the expression embeds a 144# URL and a flake path, and quoting that through the shell is a trap. 145IPXE_EXPR=$(mktemp -t "${PROG}-ipxe-XXXXXX.nix") 146cat >"$IPXE_EXPR" <<EOF 147let 148 flake = builtins.getFlake "${FLAKE_REF}"; 149in 150flake.nixosConfigurations."${MACHINE}".pkgs.ipxe.override { 151 embedScript = builtins.toFile "chain.ipxe" '' 152 #!ipxe 153 dhcp 154 chain http://${ADDR}:${HTTP_PORT}/netboot.ipxe 155 ''; 156} 157EOF 158IPXE=$(nix build --no-link --print-out-paths --impure --file "$IPXE_EXPR") 159rm -f "$IPXE_EXPR" 160 161# A stable directory rather than a mktemp one with a cleanup trap: the trap 162# was the reason Ctrl+C did nothing useful. A bash INT handler does not exit 163# after running, so it deleted the served directory out from under a still 164# running dnsmasq and then went back to waiting. Only symlinks live here, so 165# leaving them behind between runs costs nothing. 166# 167# Not under XDG_RUNTIME_DIR: dnsmasq drops privileges after startup, and 168# /run/user/$UID is mode 0700, so the unprivileged dnsmasq cannot traverse it 169# ("TFTP directory ... inaccessible: Permission denied", then it refuses to 170# start). Everything served is a symlink into the world-readable nix store, 171# so a world-traversable directory costs nothing. 172ROOT="/var/tmp/${PROG}/${MACHINE}" 173rm -rf "$ROOT" 174mkdir -p "$ROOT" 175chmod 755 "/var/tmp/${PROG}" "$ROOT" 176 177ln -s "${KERNEL}/${KERNEL_FILE}" "$ROOT/bzImage" 178ln -s "${INITRD}/initrd" "$ROOT/initrd" 179ln -s "${SQUASHFS}" "$ROOT/nix-store.squashfs" 180ln -s "${IPXE}/ipxe.efi" "$ROOT/ipxe.efi" 181ln -s "${IPXE}/undionly.kpxe" "$ROOT/undionly.kpxe" 182 183# store.url= is what stage 1 fetches the squashfs from; passing it on the 184# command line instead of baking it into the image keeps the image valid when 185# the serving address changes. 186cat >"$ROOT/netboot.ipxe" <<EOF 187#!ipxe 188kernel http://${ADDR}:${HTTP_PORT}/bzImage init=${TOPLEVEL}/init initrd=initrd ${PARAMS} store.url=http://${ADDR}:${HTTP_PORT}/nix-store.squashfs 189initrd http://${ADDR}:${HTTP_PORT}/initrd 190boot 191EOF 192 193echo "$PROG: serving ${MACHINE} on ${IFACE} (${ADDR}) over http://${ADDR}:${HTTP_PORT}" 194echo "$PROG: iPXE fetches $(stat -Lc %s "$ROOT/bzImage" | awk '{printf "%.0f MB", $1/1048576}') kernel + $(stat -Lc %s "$ROOT/initrd" | awk '{printf "%.0f MB", $1/1048576}') initrd; stage 1 then fetches $(stat -Lc %s "$ROOT/nix-store.squashfs" | awk '{printf "%.2f GB", $1/1073741824}') of store itself" 195 196# exec, so there is no wrapper process between the terminal and dnsmasq: Ctrl+C 197# reaches it directly and pxe-share's own trap does the privileged teardown 198# (firewall rules, darkhttpd). pxe-share re-execs itself through sudo; the nix 199# builds above deliberately ran as the calling user. 200exec pxe-share ${NO_DHCP:+--no-dhcp} --http --http-port "$HTTP_PORT" "$ROOT" "$IFACE"