router-setup finds earlier installs and router-backup-*.tar.gz files and reinstalls them with everything in /var/lib/router carried over. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
201 lines
8.2 KiB
Nix
201 lines
8.2 KiB
Nix
# 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.
|
|
#
|
|
# Then the same box is reinstalled twice, everything carrying over (settings
|
|
# with an unapplied change, passwords, the web UI's certificate): once from
|
|
# the installed disk itself, once from a web UI backup on a USB stick after
|
|
# the disk was wiped. The settings name an interface this machine lacks, so
|
|
# setup asks which one takes its place.
|
|
{ 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
|
|
'';
|
|
|
|
# Reinstall the configuration found first, on /dev/vda; eth1 replaces the
|
|
# missing enp9s0.
|
|
reuseAnswers = pkgs.writeText "reuse-answers" ''
|
|
1
|
|
/dev/vda
|
|
eth1
|
|
/dev/vda
|
|
'';
|
|
in
|
|
pkgs.testers.runNixOSTest {
|
|
name = "router-setup";
|
|
|
|
nodes = {
|
|
installer =
|
|
{ lib, ... }:
|
|
{
|
|
imports = [
|
|
sharedDisk
|
|
(self.lib.setupModule { inherit local; })
|
|
common.tools
|
|
];
|
|
environment.systemPackages = [ pkgs.dosfstools ];
|
|
environment.etc."router-test-local.nix".source = local;
|
|
# The interfaces router-setup offers.
|
|
virtualisation.vlans = [
|
|
1
|
|
2
|
|
];
|
|
virtualisation.fileSystems."/".autoFormat = true;
|
|
# vdb: the installer's root; vdc: a USB stick for a backup.
|
|
virtualisation.emptyDiskImages = [
|
|
1024
|
|
64
|
|
];
|
|
virtualisation.rootDevice = "/dev/vdb";
|
|
# The installer boots several times on the same root disk, whose Nix
|
|
# database would list store paths left behind in a tmpfs overlay.
|
|
virtualisation.writableStoreUseTmpfs = false;
|
|
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
|
|
|
|
def install_again(prepare=None):
|
|
installer.start()
|
|
installer.wait_for_unit("multi-user.target")
|
|
if prepare:
|
|
prepare()
|
|
out = installer.succeed("router-setup --local /etc/router-test-local.nix < ${reuseAnswers} 2>&1")
|
|
installer.shutdown()
|
|
target.start()
|
|
target.wait_for_unit("multi-user.target")
|
|
return out
|
|
|
|
def check_carried_over():
|
|
assert target.succeed("hostname").strip() == "gw2"
|
|
settings = target.succeed("cat /var/lib/router/config/settings.json")
|
|
assert '"enp9s0"' not in settings and '"eth1"' in settings, settings
|
|
assert target.succeed("sha256sum /var/lib/router/webui/cert.pem").split()[0] == cert
|
|
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
|
|
assert "no changes" in target.succeed("router-apply 2>&1")
|
|
|
|
with subtest("reinstalling from the installed disk carries everything over"):
|
|
# Saved but not applied; and an interface this machine does not have.
|
|
target.succeed("sed -i 's/\"hostName\": \"gw\"/\"hostName\": \"gw2\"/; s/\"interface\": \"eth1\"/\"interface\": \"enp9s0\"/'"
|
|
" /var/lib/router/config/settings.json")
|
|
cert = target.succeed("sha256sum /var/lib/router/webui/cert.pem").split()[0]
|
|
# A backup from the web UI, for the next subtest.
|
|
target.succeed("curl -sk -c /tmp/jar -o /dev/null -d password=hunter2hunter2 https://127.0.0.1/login")
|
|
token = target.succeed("curl -sk -b /tmp/jar https://127.0.0.1/advanced | grep -o 'name=\"csrf\" value=\"[^\"]*' | head -1 | cut -d'\"' -f4").strip()
|
|
target.succeed(f"curl -sfk -b /tmp/jar -d csrf={token} -o /tmp/router-backup-gw2.tar.gz https://127.0.0.1/backup")
|
|
assert "router/config/settings.json" in target.succeed("tar -tzf /tmp/router-backup-gw2.tar.gz").split()
|
|
# The installed system has no shared folder with the host: pass it as text.
|
|
backup = target.succeed("base64 -w0 /tmp/router-backup-gw2.tar.gz").strip()
|
|
target.shutdown()
|
|
|
|
out = install_again()
|
|
assert "installed on /dev/vda2: gw2" in out, out
|
|
assert "no interface enp9s0 (WAN)" in out, out
|
|
check_carried_over()
|
|
|
|
with subtest("a backup on a USB stick is found after the disk was wiped"):
|
|
target.shutdown()
|
|
|
|
def wipe_and_plug_in_stick():
|
|
installer.succeed("wipefs -a /dev/vda2 /dev/vda1 /dev/vda && blockdev --rereadpt /dev/vda && udevadm settle")
|
|
installer.succeed("mkfs.vfat /dev/vdc && udevadm settle && mount /dev/vdc /mnt")
|
|
installer.succeed(f"echo {backup} | base64 -d > /mnt/router-backup-gw2.tar.gz")
|
|
installer.succeed("umount /mnt")
|
|
|
|
out = install_again(wipe_and_plug_in_stick)
|
|
assert "backup router-backup-gw2.tar.gz on /dev/vdc: gw2" in out, out
|
|
assert "installed on" not in out, out
|
|
check_carried_over()
|
|
'';
|
|
}
|