Add a generic installer that asks for the basics and builds offline
The installer ISO no longer needs a prebuilt system: `router-setup` asks for the disk, the WAN and LAN interfaces, DHCP or PPPoE (optionally on a VLAN), host name, LAN address and a password, writes settings.json and the secrets, builds that router offline (the ISO carries a reference router system whose closure has everything the build needs) and installs it with `router-install --system`. The password is used for the web UI and, hashed, as root's console password (routerSettings sets root's hashedPasswordFile; it applies when the account is created at installation). checks.setup answers router-setup on the console and checks the installed box: its settings, the web UI and console login with that password, and a rebuild that reproduces the installed system. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -21,6 +21,8 @@
|
|||||||
# The installer pieces; `lib.mkInstaller` wires them up.
|
# The installer pieces; `lib.mkInstaller` wires them up.
|
||||||
installer = ./modules/installer/install-script.nix;
|
installer = ./modules/installer/install-script.nix;
|
||||||
installer-iso = ./modules/installer/iso.nix;
|
installer-iso = ./modules/installer/iso.nix;
|
||||||
|
# The generic installer's `router-setup` (see lib.setupModule).
|
||||||
|
setup = ./modules/installer/setup.nix;
|
||||||
# Appliance disk image: dm-verity store, volatile root, state on /var.
|
# Appliance disk image: dm-verity store, volatile root, state on /var.
|
||||||
image = ./modules/image;
|
image = ./modules/image;
|
||||||
# Configuration from a settings file (JSON), rebuilt on the device.
|
# Configuration from a settings file (JSON), rebuilt on the device.
|
||||||
@@ -64,7 +66,16 @@
|
|||||||
}:
|
}:
|
||||||
pkgs.runCommand "router-config" { } ''
|
pkgs.runCommand "router-config" { } ''
|
||||||
mkdir -p $out
|
mkdir -p $out
|
||||||
cat > $out/flake.nix <<'EOF'
|
cp ${self.lib.configFlake pkgs} $out/flake.nix
|
||||||
|
cp ${settings} $out/settings.json
|
||||||
|
${pkgs.lib.optionalString (local != null) "cp ${local} $out/local.nix"}
|
||||||
|
'';
|
||||||
|
|
||||||
|
# The configuration directory's flake.nix (shared by mkConfigDir and
|
||||||
|
# router-setup, so both build exactly the same system).
|
||||||
|
lib.configFlake =
|
||||||
|
pkgs:
|
||||||
|
pkgs.writeText "flake.nix" ''
|
||||||
{
|
{
|
||||||
inputs.nixos-router.url = "path:${self.outPath}";
|
inputs.nixos-router.url = "path:${self.outPath}";
|
||||||
outputs =
|
outputs =
|
||||||
@@ -76,11 +87,58 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
EOF
|
|
||||||
cp ${settings} $out/settings.json
|
|
||||||
${pkgs.lib.optionalString (local != null) "cp ${local} $out/local.nix"}
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# The generic installer: `router-setup` asks for the basics, builds
|
||||||
|
# the router offline and installs it; the rest is done in the web UI.
|
||||||
|
# nix build .#installer
|
||||||
|
# router-setup's configuration: the flake template and the reference
|
||||||
|
# system whose closure makes the offline build possible. `local` is an
|
||||||
|
# extra module that the installed systems will import as well.
|
||||||
|
lib.setupModule =
|
||||||
|
{
|
||||||
|
local ? null,
|
||||||
|
}:
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [ self.nixosModules.setup ];
|
||||||
|
router.setup = {
|
||||||
|
flake = self.lib.configFlake pkgs;
|
||||||
|
reference =
|
||||||
|
(self.lib.mkRouterSystem {
|
||||||
|
settings = builtins.toFile "settings.json" (
|
||||||
|
builtins.toJSON {
|
||||||
|
router = {
|
||||||
|
wan.interface = "eth0";
|
||||||
|
lans.lan = {
|
||||||
|
interface = "eth1";
|
||||||
|
internet = true;
|
||||||
|
ipv4.address = "192.168.1.1";
|
||||||
|
dhcp.pool = {
|
||||||
|
start = "192.168.1.100";
|
||||||
|
end = "192.168.1.200";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
# A path, not a derivation: the module system would read its attributes.
|
||||||
|
modules = pkgs.lib.optional (local != null) "${local}";
|
||||||
|
}).config.system.build.toplevel;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# The generic installer ISO:
|
||||||
|
# nix build .#installer
|
||||||
|
nixosConfigurations.installer = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
self.nixosModules.installer-iso
|
||||||
|
(self.lib.setupModule { })
|
||||||
|
];
|
||||||
|
};
|
||||||
|
packages.x86_64-linux.installer = self.nixosConfigurations.installer.config.system.build.isoImage;
|
||||||
|
|
||||||
# Installer ISO for a given nixosSystem:
|
# Installer ISO for a given nixosSystem:
|
||||||
# nix build .#installer-example (see packages)
|
# nix build .#installer-example (see packages)
|
||||||
lib.mkInstaller =
|
lib.mkInstaller =
|
||||||
|
|||||||
@@ -21,25 +21,30 @@ let
|
|||||||
];
|
];
|
||||||
text = ''
|
text = ''
|
||||||
usage() {
|
usage() {
|
||||||
echo "usage: router-install [--yes] [--secrets DIR] <disk>" >&2
|
echo "usage: router-install [--yes] [--secrets DIR] [--system PATH] <disk>" >&2
|
||||||
echo " Wipes <disk> (e.g. /dev/nvme0n1) and installs ${cfg.system}." >&2
|
echo " Wipes <disk> (e.g. /dev/nvme0n1) and installs a router system." >&2
|
||||||
echo " --secrets DIR copied to /var/lib/router (PPPoE password, WireGuard keys, ...)" >&2
|
echo " --secrets DIR copied to /var/lib/router (PPPoE password, WireGuard keys, ...)" >&2
|
||||||
|
echo " --system PATH the system to install${lib.optionalString (cfg.system != null) " (default: ${cfg.system})"}" >&2
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
|
|
||||||
yes=0
|
yes=0
|
||||||
secrets=
|
secrets=
|
||||||
|
system=${if cfg.system != null then toString cfg.system else ""}
|
||||||
disk=
|
disk=
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
--yes) yes=1 ;;
|
--yes) yes=1 ;;
|
||||||
--secrets) shift; [ $# -gt 0 ] || usage; secrets=$1 ;;
|
--secrets) shift; [ $# -gt 0 ] || usage; secrets=$1 ;;
|
||||||
|
--system) shift; [ $# -gt 0 ] || usage; system=$1 ;;
|
||||||
-*) usage ;;
|
-*) usage ;;
|
||||||
*) [ -z "$disk" ] || usage; disk=$1 ;;
|
*) [ -z "$disk" ] || usage; disk=$1 ;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
[ -n "$disk" ] || usage
|
[ -n "$disk" ] || usage
|
||||||
|
[ -n "$system" ] || usage
|
||||||
|
[ -x "$system/bin/switch-to-configuration" ] || { echo "router-install: $system is not a NixOS system" >&2; exit 1; }
|
||||||
|
|
||||||
[ "$(id -u)" -eq 0 ] || { echo "router-install: must run as root" >&2; exit 1; }
|
[ "$(id -u)" -eq 0 ] || { echo "router-install: must run as root" >&2; exit 1; }
|
||||||
[ -b "$disk" ] || { echo "router-install: $disk is not a block device" >&2; exit 1; }
|
[ -b "$disk" ] || { echo "router-install: $disk is not a block device" >&2; exit 1; }
|
||||||
@@ -94,8 +99,8 @@ let
|
|||||||
echo "note: no --secrets given; put the PPPoE password etc. in /var/lib/router later" >&2
|
echo "note: no --secrets given; put the PPPoE password etc. in /var/lib/router later" >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ">> installing ${cfg.system}"
|
echo ">> installing $system"
|
||||||
nixos-install --system ${cfg.system} --root /mnt --no-root-passwd --no-channel-copy
|
nixos-install --system "$system" --root /mnt --no-root-passwd --no-channel-copy
|
||||||
|
|
||||||
echo ">> unmounting"
|
echo ">> unmounting"
|
||||||
umount -R /mnt
|
umount -R /mnt
|
||||||
@@ -106,13 +111,17 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.router.installer.system = lib.mkOption {
|
options.router.installer.system = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.nullOr lib.types.package;
|
||||||
description = "The system (a `system.build.toplevel`) `router-install` writes to disk.";
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The system (a `system.build.toplevel`) `router-install` writes to disk;
|
||||||
|
null if it is always given with `--system` (as `router-setup` does).
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
environment.systemPackages = [ installScript ];
|
environment.systemPackages = [ installScript ];
|
||||||
# The closure must be on the installer itself.
|
# The closure must be on the installer itself.
|
||||||
system.extraDependencies = [ cfg.system ];
|
system.extraDependencies = lib.optional (cfg.system != null) cfg.system;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,20 @@
|
|||||||
settings.PermitRootLogin = "prohibit-password";
|
settings.PermitRootLogin = "prohibit-password";
|
||||||
};
|
};
|
||||||
|
|
||||||
users.motd = ''
|
users.motd =
|
||||||
|
if config.router.installer.system != null then
|
||||||
|
''
|
||||||
|
|
||||||
nixos-router installer
|
nixos-router installer
|
||||||
1. (optional) scp -r secrets root@<this-host>:secrets
|
1. (optional) scp -r secrets root@<this-host>:secrets
|
||||||
2. router-install --secrets /root/secrets /dev/<disk>
|
2. router-install --secrets /root/secrets /dev/<disk>
|
||||||
The disk is erased after confirmation.
|
The disk is erased after confirmation.
|
||||||
|
''
|
||||||
|
else
|
||||||
|
''
|
||||||
|
|
||||||
|
nixos-router installer
|
||||||
|
Run `sudo router-setup`: it asks a few questions, erases the chosen disk
|
||||||
|
and installs the router. Everything else is set up in the web UI.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
# `router-setup`: the generic installer. Asks for the basics on the console
|
||||||
|
# (disk, uplink and LAN interfaces, PPPoE credentials, LAN address, web UI
|
||||||
|
# password), writes them as settings.json, builds that router on the spot
|
||||||
|
# (offline: the closure of `reference` brings the sources and everything the
|
||||||
|
# build needs) and installs it with `router-install`. Everything else is done
|
||||||
|
# in the web UI afterwards.
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.router.setup;
|
||||||
|
webui = pkgs.callPackage ../../pkgs/router-webui/package.nix { };
|
||||||
|
|
||||||
|
setup = pkgs.writeShellApplication {
|
||||||
|
name = "router-setup";
|
||||||
|
runtimeInputs = [
|
||||||
|
config.nix.package
|
||||||
|
webui
|
||||||
|
pkgs.coreutils
|
||||||
|
pkgs.gawk
|
||||||
|
pkgs.gnugrep
|
||||||
|
pkgs.jq
|
||||||
|
pkgs.mkpasswd
|
||||||
|
pkgs.util-linux
|
||||||
|
"/run/current-system/sw"
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
usage() {
|
||||||
|
echo "usage: router-setup [--local FILE]" >&2
|
||||||
|
echo " Asks a few questions, then ERASES the chosen disk and installs the router." >&2
|
||||||
|
echo " --local FILE extra NixOS module for this box (installed as config/local.nix)" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
local_nix=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case $1 in
|
||||||
|
--local) shift; [ $# -gt 0 ] || usage; local_nix=$1 ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
[ "$(id -u)" -eq 0 ] || { echo "router-setup: must run as root" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ask VAR PROMPT [DEFAULT]
|
||||||
|
# (Their locals must not share a name with the caller's variable.)
|
||||||
|
ask() {
|
||||||
|
local reply
|
||||||
|
read -r -p "$2''${3:+ [$3]}: " reply || { echo; echo "aborted" >&2; exit 1; }
|
||||||
|
printf -v "$1" '%s' "''${reply:-''${3:-}}"
|
||||||
|
}
|
||||||
|
ask_secret() {
|
||||||
|
local reply
|
||||||
|
read -r -s -p "$2: " reply || { echo; echo "aborted" >&2; exit 1; }
|
||||||
|
echo >&2
|
||||||
|
printf -v "$1" '%s' "$reply"
|
||||||
|
}
|
||||||
|
# choose VAR PROMPT ITEM...: by number or by name
|
||||||
|
choose() {
|
||||||
|
local var=$1 prompt=$2 pick="" i
|
||||||
|
shift 2
|
||||||
|
while :; do
|
||||||
|
i=1
|
||||||
|
for item in "$@"; do echo " $i) $item" >&2; i=$((i + 1)); done
|
||||||
|
ask pick "$prompt"
|
||||||
|
if [[ $pick =~ ^[0-9]+$ ]] && [ "$pick" -ge 1 ] && [ "$pick" -le $# ]; then
|
||||||
|
pick=''${!pick}
|
||||||
|
pick=''${pick%% *}
|
||||||
|
fi
|
||||||
|
for item in "$@"; do
|
||||||
|
if [ "''${item%% *}" = "$pick" ]; then
|
||||||
|
printf -v "$var" '%s' "$pick"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo " please pick one of the above" >&2
|
||||||
|
done
|
||||||
|
}
|
||||||
|
ipv4_ok() {
|
||||||
|
[[ $1 =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]] || return 1
|
||||||
|
for o in "''${BASH_REMATCH[@]:1}"; do [ "$o" -le 255 ] || return 1; done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set by ask/choose (printf -v).
|
||||||
|
disk="" wan="" lan="" wan_type="" host="" lan_address="" ssh_key="" answer=""
|
||||||
|
webui_password="" webui_password2=""
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "nixos-router setup"
|
||||||
|
echo "=================="
|
||||||
|
echo "A few questions; everything else can be changed later in the web UI."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Disks, without the medium this installer runs from.
|
||||||
|
boot_disk=""
|
||||||
|
if src=$(findmnt -no SOURCE /iso 2>/dev/null) && [ -n "$src" ]; then
|
||||||
|
boot_disk=/dev/$(lsblk -no PKNAME "$src" | head -n1)
|
||||||
|
fi
|
||||||
|
mapfile -t disks < <(lsblk -dnpo NAME,SIZE,MODEL,TYPE | awk '$NF == "disk" { $NF = ""; print }' \
|
||||||
|
| grep -v "^$boot_disk " || true)
|
||||||
|
[ ''${#disks[@]} -gt 0 ] || { echo "router-setup: no disk to install to" >&2; exit 1; }
|
||||||
|
echo "Disk to install to (it will be ERASED):"
|
||||||
|
choose disk "Disk" "''${disks[@]}"
|
||||||
|
|
||||||
|
nics=()
|
||||||
|
for dev in /sys/class/net/*; do
|
||||||
|
n=$(basename "$dev")
|
||||||
|
[ -e "$dev/device" ] || continue
|
||||||
|
state=$(cat "$dev/operstate" 2>/dev/null || echo "?")
|
||||||
|
nics+=("$n $(cat "$dev/address") link $state")
|
||||||
|
done
|
||||||
|
[ ''${#nics[@]} -gt 0 ] || { echo "router-setup: no network interfaces found" >&2; exit 1; }
|
||||||
|
echo
|
||||||
|
echo "Interface towards the internet (WAN):"
|
||||||
|
choose wan "WAN interface" "''${nics[@]}"
|
||||||
|
echo
|
||||||
|
echo "Interface for the local network (LAN):"
|
||||||
|
choose lan "LAN interface" "''${nics[@]}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Internet connection:"
|
||||||
|
choose wan_type "Connection" \
|
||||||
|
"dhcp (behind a cable modem, an ONT or another router)" \
|
||||||
|
"pppoe (DSL or fibre with a username and password)"
|
||||||
|
pppoe_user="" pppoe_password="" pppoe_vlan=""
|
||||||
|
if [ "$wan_type" = pppoe ]; then
|
||||||
|
while [ -z "$pppoe_user" ]; do ask pppoe_user "PPPoE username"; done
|
||||||
|
while [ -z "$pppoe_password" ]; do ask_secret pppoe_password "PPPoE password"; done
|
||||||
|
while :; do
|
||||||
|
ask pppoe_vlan "VLAN ID the provider requires (empty for none)" ""
|
||||||
|
[ -z "$pppoe_vlan" ] && break
|
||||||
|
[[ $pppoe_vlan =~ ^[0-9]+$ ]] && [ "$pppoe_vlan" -ge 1 ] && [ "$pppoe_vlan" -le 4094 ] && break
|
||||||
|
echo " a number from 1 to 4094" >&2
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [ "$wan" = "$lan" ] && [ -z "$pppoe_vlan" ]; then
|
||||||
|
echo "router-setup: WAN and LAN can only share an interface when PPPoE runs on a VLAN" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
ask host "Host name" "router"
|
||||||
|
while :; do
|
||||||
|
ask lan_address "Router address on the LAN (a /24 network)" "192.168.1.1"
|
||||||
|
ipv4_ok "$lan_address" && break
|
||||||
|
echo " an IPv4 address like 192.168.1.1" >&2
|
||||||
|
done
|
||||||
|
net=''${lan_address%.*}
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Password for the web UI and for root on the console (at least 8 characters):"
|
||||||
|
while :; do
|
||||||
|
ask_secret webui_password "Password"
|
||||||
|
ask_secret webui_password2 "Again"
|
||||||
|
[ "$webui_password" = "$webui_password2" ] && [ ''${#webui_password} -ge 8 ] && break
|
||||||
|
echo " the passwords differ or are too short" >&2
|
||||||
|
done
|
||||||
|
ask ssh_key "SSH public key for root (optional, one line)" ""
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Summary"
|
||||||
|
echo " disk: $disk (will be erased)"
|
||||||
|
echo " WAN: $wan, $wan_type''${pppoe_user:+ as $pppoe_user}''${pppoe_vlan:+ on VLAN $pppoe_vlan}"
|
||||||
|
echo " LAN: $lan, $lan_address/24, DHCP $net.100 - $net.200"
|
||||||
|
echo " host name: $host"
|
||||||
|
echo " web UI: https://$lan_address"
|
||||||
|
ask answer "Type the disk name ($disk) to install"
|
||||||
|
[ "$answer" = "$disk" ] || { echo "aborted" >&2; exit 1; }
|
||||||
|
|
||||||
|
work=$(mktemp -d /run/router-setup.XXXXXX)
|
||||||
|
chmod 0700 "$work"
|
||||||
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
mkdir -p "$work/secrets/config"
|
||||||
|
config=$work/secrets/config
|
||||||
|
cp ${cfg.flake} "$config/flake.nix"
|
||||||
|
chmod 0644 "$config/flake.nix"
|
||||||
|
if [ -n "$local_nix" ]; then cp "$local_nix" "$config/local.nix"; fi
|
||||||
|
|
||||||
|
keys=$( [ -n "$ssh_key" ] && jq -n --arg k "$ssh_key" '[$k]' || echo '[]')
|
||||||
|
jq -n \
|
||||||
|
--arg host "$host" --arg state "${cfg.stateVersion}" --argjson keys "$keys" \
|
||||||
|
--arg wan "$wan" --arg type "$wan_type" --arg user "$pppoe_user" --arg vlan "$pppoe_vlan" \
|
||||||
|
--arg lan "$lan" --arg addr "$lan_address" --arg net "$net" '
|
||||||
|
{
|
||||||
|
system: { hostName: $host, stateVersion: $state, sshKeys: $keys },
|
||||||
|
router: {
|
||||||
|
wan: ({ interface: $wan, ipv4: { type: $type } }
|
||||||
|
| if $type == "pppoe" then .ipv4.pppoe = ({ username: $user, passwordFile: "/var/lib/router/pppoe-password" }
|
||||||
|
+ (if $vlan == "" then {} else { vlan: ($vlan | tonumber) } end)) else . end),
|
||||||
|
lans: {
|
||||||
|
lan: {
|
||||||
|
interface: $lan, internet: true,
|
||||||
|
ipv4: { address: $addr },
|
||||||
|
dhcp: { pool: { start: ($net + ".100"), end: ($net + ".200") } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' > "$config/settings.json"
|
||||||
|
|
||||||
|
if [ "$wan_type" = pppoe ]; then
|
||||||
|
printf '%s' "$pppoe_password" > "$work/secrets/pppoe-password"
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$webui_password" | router-webui passwd "$work/secrets/webui-password"
|
||||||
|
(umask 077; printf '%s\n' "$webui_password" | mkpasswd -s > "$work/secrets/root-password")
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo ">> building the router system (no internet needed; takes a few minutes)"
|
||||||
|
system=$(nix build --no-link --print-out-paths --out-link "$work/system" \
|
||||||
|
"path:$config#nixosConfigurations.router.config.system.build.toplevel")
|
||||||
|
router-install --yes --secrets "$work/secrets" --system "$system" "$disk"
|
||||||
|
echo
|
||||||
|
echo "After rebooting, open https://$lan_address from the LAN."
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [ ./install-script.nix ];
|
||||||
|
|
||||||
|
options.router.setup = {
|
||||||
|
flake = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "The configuration flake (flake.nix) installed systems are built from.";
|
||||||
|
};
|
||||||
|
reference = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
description = ''
|
||||||
|
A router system built the same way (`lib.mkRouterSystem`). Its closure
|
||||||
|
is what lets the installer build the chosen configuration offline.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
stateVersion = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = config.system.nixos.release;
|
||||||
|
description = "system.stateVersion of installed routers.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
environment.systemPackages = [ setup ];
|
||||||
|
system.extraDependencies = [ cfg.reference ];
|
||||||
|
nix.settings.experimental-features = [
|
||||||
|
"nix-command"
|
||||||
|
"flakes"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -109,6 +109,10 @@ in
|
|||||||
networking.hostName = system.hostName or "router";
|
networking.hostName = system.hostName or "router";
|
||||||
time.timeZone = system.timeZone or "UTC";
|
time.timeZone = system.timeZone or "UTC";
|
||||||
users.users.root.openssh.authorizedKeys.keys = system.sshKeys or [ ];
|
users.users.root.openssh.authorizedKeys.keys = system.sshKeys or [ ];
|
||||||
|
# Written by router-setup (the web UI password). Users are mutable, so it
|
||||||
|
# only sets the password when the account is created, at installation;
|
||||||
|
# `passwd` changes it later.
|
||||||
|
users.users.root.hashedPasswordFile = lib.mkDefault "${cfg.stateDirectory}/root-password";
|
||||||
system.stateVersion = system.stateVersion or config.system.nixos.release;
|
system.stateVersion = system.stateVersion or config.system.nixos.release;
|
||||||
|
|
||||||
nix.settings.experimental-features = [
|
nix.settings.experimental-features = [
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
dns = import ./dns.nix { inherit self pkgs; };
|
dns = import ./dns.nix { inherit self pkgs; };
|
||||||
installer = import ./installer.nix { inherit self pkgs; };
|
installer = import ./installer.nix { inherit self pkgs; };
|
||||||
apply = import ./apply.nix { inherit self pkgs; };
|
apply = import ./apply.nix { inherit self pkgs; };
|
||||||
|
setup = import ./setup.nix { inherit self pkgs; };
|
||||||
image = import ./image.nix { inherit self pkgs; };
|
image = import ./image.nix { inherit self pkgs; };
|
||||||
wireguard = import ./wireguard.nix { inherit self pkgs; };
|
wireguard = import ./wireguard.nix { inherit self pkgs; };
|
||||||
lib = import ./lib.nix { inherit pkgs; };
|
lib = import ./lib.nix { inherit pkgs; };
|
||||||
|
|||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
# The generic installer: router-setup is answered on its console (DHCP uplink,
|
||||||
|
# LAN address, web UI password), builds the router offline and installs it.
|
||||||
|
# The installed box must come up with those settings, serve the web UI with
|
||||||
|
# that password, and rebuild to exactly itself.
|
||||||
|
{ self, pkgs }:
|
||||||
|
let
|
||||||
|
common = import ./common.nix;
|
||||||
|
|
||||||
|
# Only so the test driver can talk to the installed system.
|
||||||
|
local = pkgs.writeText "local.nix" ''
|
||||||
|
{ lib, modulesPath, ... }:
|
||||||
|
{
|
||||||
|
imports = [ (modulesPath + "/testing/test-instrumentation.nix") ];
|
||||||
|
# The instrumentation empties root's password; keep router-setup's.
|
||||||
|
users.users.root.hashedPasswordFile = lib.mkForce "/var/lib/router/root-password";
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
|
||||||
|
sharedDisk = {
|
||||||
|
virtualisation.diskImage = "./target.qcow2";
|
||||||
|
virtualisation.diskSize = 8192;
|
||||||
|
# Evaluating and building a NixOS system takes memory.
|
||||||
|
virtualisation.memorySize = 3072;
|
||||||
|
};
|
||||||
|
|
||||||
|
answers = pkgs.writeText "answers" ''
|
||||||
|
/dev/vda
|
||||||
|
eth1
|
||||||
|
eth2
|
||||||
|
dhcp
|
||||||
|
gw
|
||||||
|
192.168.10.1
|
||||||
|
hunter2hunter2
|
||||||
|
hunter2hunter2
|
||||||
|
|
||||||
|
/dev/vda
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
pkgs.testers.runNixOSTest {
|
||||||
|
name = "router-setup";
|
||||||
|
|
||||||
|
nodes = {
|
||||||
|
installer =
|
||||||
|
{ lib, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
sharedDisk
|
||||||
|
(self.lib.setupModule { inherit local; })
|
||||||
|
common.tools
|
||||||
|
];
|
||||||
|
environment.etc."router-test-local.nix".source = local;
|
||||||
|
# The interfaces router-setup offers.
|
||||||
|
virtualisation.vlans = [
|
||||||
|
1
|
||||||
|
2
|
||||||
|
];
|
||||||
|
virtualisation.fileSystems."/".autoFormat = true;
|
||||||
|
virtualisation.emptyDiskImages = [ 1024 ];
|
||||||
|
virtualisation.rootDevice = "/dev/vdb";
|
||||||
|
nix.settings.substituters = lib.mkForce [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
target = {
|
||||||
|
imports = [ sharedDisk ];
|
||||||
|
virtualisation.vlans = [
|
||||||
|
1
|
||||||
|
2
|
||||||
|
];
|
||||||
|
virtualisation.useBootLoader = true;
|
||||||
|
virtualisation.useEFIBoot = true;
|
||||||
|
virtualisation.useDefaultFilesystems = false;
|
||||||
|
virtualisation.efi.keepVariables = false;
|
||||||
|
virtualisation.fileSystems."/" = {
|
||||||
|
device = "/dev/disk/by-label/this-is-not-real-and-will-never-be-used";
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
installer.start()
|
||||||
|
installer.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
|
with subtest("router-setup refuses to install without the disk name"):
|
||||||
|
installer.fail("printf '/dev/vda\\neth1\\neth2\\ndhcp\\n\\n\\nhunter2hunter2\\nhunter2hunter2\\n\\nno\\n' | router-setup")
|
||||||
|
installer.fail("blkid /dev/vda1")
|
||||||
|
|
||||||
|
with subtest("router-setup builds the answered configuration offline and installs it"):
|
||||||
|
installer.succeed("router-setup --local /etc/router-test-local.nix < ${answers} >&2")
|
||||||
|
installer.shutdown()
|
||||||
|
|
||||||
|
target.state_dir = installer.state_dir
|
||||||
|
target.start()
|
||||||
|
target.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
|
with subtest("the installed router has the answered settings"):
|
||||||
|
assert target.succeed("hostname").strip() == "gw"
|
||||||
|
target.wait_until_succeeds("ip -4 addr show dev eth2 | grep -q 192.168.10.1/24")
|
||||||
|
target.succeed("grep -q '192.168.10.100 - 192.168.10.200' /etc/kea/dhcp4-server.conf")
|
||||||
|
settings = target.succeed("cat /var/lib/router/config/settings.json")
|
||||||
|
assert '"eth1"' in settings and '"dhcp"' in settings, settings
|
||||||
|
assert target.succeed("stat -c %a /var/lib/router/webui-password").strip() == "600"
|
||||||
|
|
||||||
|
with subtest("the web UI accepts the password given to router-setup"):
|
||||||
|
target.wait_for_unit("router-webui.service")
|
||||||
|
target.wait_until_succeeds("curl -sk -o /dev/null https://127.0.0.1/login")
|
||||||
|
code = target.succeed("curl -sk -o /dev/null -w '%{http_code}' -d password=hunter2hunter2 https://127.0.0.1/login")
|
||||||
|
assert code == "303", code
|
||||||
|
|
||||||
|
with subtest("root logs in on the console with the same password"):
|
||||||
|
target.wait_until_tty_matches("1", "login: ")
|
||||||
|
target.send_chars("root\n")
|
||||||
|
target.wait_until_tty_matches("1", "Password: ")
|
||||||
|
target.send_chars("hunter2hunter2\n")
|
||||||
|
target.wait_until_succeeds("pgrep -u root -t tty1 bash")
|
||||||
|
assert target.succeed("stat -c %a /var/lib/router/root-password").strip() == "600"
|
||||||
|
|
||||||
|
with subtest("the box rebuilds to exactly the installed system"):
|
||||||
|
out = target.succeed("router-apply 2>&1")
|
||||||
|
assert "no changes" in out, out
|
||||||
|
'';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user