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
6.6 kB 193 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 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. 142IPXE_EXPR=$(mktemp -t "${PROG}-ipxe-XXXXXX.nix") 143cat >"$IPXE_EXPR" <<EOF 144let 145 flake = builtins.getFlake "${FLAKE_REF}"; 146in 147flake.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} 154EOF 155IPXE=$(nix build --no-link --print-out-paths --impure --file "$IPXE_EXPR") 156rm -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# 164# Not under XDG_RUNTIME_DIR: dnsmasq drops privileges after startup, and 165# /run/user/$UID is mode 0700, so the unprivileged dnsmasq cannot traverse it 166# ("TFTP directory ... inaccessible: Permission denied", then it refuses to 167# start). Everything served is a symlink into the world-readable nix store, 168# so a world-traversable directory costs nothing. 169ROOT="/var/tmp/${PROG}/${MACHINE}" 170rm -rf "$ROOT" 171mkdir -p "$ROOT" 172chmod 755 "/var/tmp/${PROG}" "$ROOT" 173 174ln -s "${KERNEL}/${KERNEL_FILE}" "$ROOT/bzImage" 175ln -s "${INITRD}/initrd" "$ROOT/initrd" 176ln -s "${IPXE}/ipxe.efi" "$ROOT/ipxe.efi" 177ln -s "${IPXE}/undionly.kpxe" "$ROOT/undionly.kpxe" 178 179cat >"$ROOT/netboot.ipxe" <<EOF 180#!ipxe 181kernel http://${ADDR}:${HTTP_PORT}/bzImage init=${TOPLEVEL}/init initrd=initrd ${PARAMS} 182initrd http://${ADDR}:${HTTP_PORT}/initrd 183boot 184EOF 185 186echo "$PROG: serving ${MACHINE} on ${IFACE} (${ADDR}), kernel+initrd over http://${ADDR}:${HTTP_PORT}" 187echo "$PROG: initrd is $(stat -Lc %s "$ROOT/initrd" | awk '{printf "%.1f GB", $1/1073741824}') — expect that much to cross the wire on every boot" 188 189# exec, so there is no wrapper process between the terminal and dnsmasq: Ctrl+C 190# reaches it directly and pxe-share's own trap does the privileged teardown 191# (firewall rules, darkhttpd). pxe-share re-execs itself through sudo; the nix 192# builds above deliberately ran as the calling user. 193exec pxe-share ${NO_DHCP:+--no-dhcp} --http --http-port "$HTTP_PORT" "$ROOT" "$IFACE"