Reuse an existing install or backup in router-setup

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
This commit is contained in:
2026-10-29 13:34:21 +01:00
co-authored by Claude Sonnet 5.5
parent 8f65587f99
commit e684ddb06c
2 changed files with 314 additions and 85 deletions
+79 -1
View File
@@ -2,6 +2,12 @@
# 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;
@@ -35,6 +41,15 @@ let
/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";
@@ -48,6 +63,7 @@ pkgs.testers.runNixOSTest {
(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 = [
@@ -55,8 +71,15 @@ pkgs.testers.runNixOSTest {
2
];
virtualisation.fileSystems."/".autoFormat = true;
virtualisation.emptyDiskImages = [ 1024 ];
# 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 [ ];
};
@@ -118,5 +141,60 @@ pkgs.testers.runNixOSTest {
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()
'';
}