# 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 n eth1 eth2 dhcp n gw 192.168.10.1 hunter2hunter2 hunter2hunter2 /dev/vda ''; # PPPoE on VLAN 1001 of the NIC that also serves the LAN, with IPv6. vlanAnswers = pkgs.writeText "vlan-answers" '' /dev/vda y eth1 1001 n eth1.1001 eth1 pppoe someone@isp pppoe-secret gw Home.test home.test 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\\nn\\neth1\\neth2\\ndhcp\\nn\\n\\n\\n\\nhunter2hunter2\\nhunter2hunter2\\n\\nno\\n' | router-setup") installer.fail("blkid /dev/vda1") with subtest("router-setup writes a PPPoE uplink on a VLAN, shared with the LAN, with IPv6"): installer.succeed("router-setup --config-only /tmp/vlan-config < ${vlanAnswers} >&2") s = installer.succeed("cat /tmp/vlan-config/config/settings.json") import json r = json.loads(s)["router"] assert r["wan"]["interface"] == "eth1", s assert r["wan"]["ipv4"]["pppoe"]["vlan"] == 1001, s assert r["wan"]["ipv4"]["pppoe"]["username"] == "someone@isp", s assert r["wan"]["ipv6"] == {"enable": True, "transport": "ppp"}, s assert r["lans"]["lan"]["interface"] == "eth1" and r["lans"]["lan"]["ipv6"] == {"pd": True}, s assert "vlan" not in r["lans"]["lan"], s assert r["dns"]["domain"] == "home.test", s assert "pppoe-secret" not in s installer.succeed("grep -qx pppoe-secret /tmp/vlan-config/pppoe-password") 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() ''; }