Add router-upgrade for nixpkgs upgrades on the router
router.update.channel names the nixpkgs branch to follow (unstable or a release like 26.05). router-upgrade check compares its newest tested revision with the running one; preview locks a copy of the configuration flake to it and lists the packages that change version and the size of the download without fetching anything; download fetches it; apply takes the lock and activates with commit-confirm, the lock going back on a rollback or a failed build. router-update keeps an upgraded nixpkgs pinned. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
+108
-3
@@ -26,6 +26,7 @@ let
|
||||
];
|
||||
};
|
||||
router = {
|
||||
update.channel = "26.05";
|
||||
wan.interface = "eth1";
|
||||
lans.main = {
|
||||
interface = "eth2";
|
||||
@@ -40,11 +41,16 @@ let
|
||||
}
|
||||
);
|
||||
|
||||
# Only so the test driver can talk to the installed system.
|
||||
# Only so the test driver can talk to the installed system; the channel is a
|
||||
# directory, and a newer nixpkgs a copy of the installed one (made below).
|
||||
local = pkgs.writeText "local.nix" ''
|
||||
{ modulesPath, ... }:
|
||||
{
|
||||
imports = [ (modulesPath + "/testing/test-instrumentation.nix") ];
|
||||
routerSettings.upgrade = {
|
||||
channelsUrl = "file:///tmp/channels";
|
||||
nixpkgsRef = "path:/var/lib/newer-nixpkgs?lastModified=${toString self.inputs.nixpkgs.lastModified}&rev=%rev%";
|
||||
};
|
||||
}
|
||||
'';
|
||||
|
||||
@@ -125,6 +131,8 @@ pkgs.testers.runNixOSTest {
|
||||
target.start()
|
||||
target.wait_for_unit("multi-user.target")
|
||||
|
||||
import json
|
||||
|
||||
def current():
|
||||
return target.succeed("readlink -f /run/current-system").strip()
|
||||
|
||||
@@ -250,8 +258,6 @@ pkgs.testers.runNixOSTest {
|
||||
with subtest("an assistant with a change token edits and checks the settings through MCP"):
|
||||
secret = curl(f"-d csrf={csrf('/tokens')} -d name=assistant -d scope=change {ui}/tokens | grep -o 'rtr_[A-Za-z0-9_-]*' | sed -n 1p").strip()
|
||||
assert secret.startswith("rtr_"), secret
|
||||
import json
|
||||
|
||||
def mcp(tool, args=None, token=secret):
|
||||
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": tool, "arguments": args or {}}})
|
||||
out = client.succeed(f"curl -sk -H 'Authorization: Bearer {token}' -d '{body}' {ui}/mcp")
|
||||
@@ -311,5 +317,104 @@ pkgs.testers.runNixOSTest {
|
||||
assert "/api/v1/settings/router/lans/{name}" in spec["paths"]
|
||||
# The log shows what was done, by whom.
|
||||
assert "update_settings" in curl(f"{ui}/tokens") and "assistant" in curl(f"{ui}/tokens")
|
||||
|
||||
# "A newer nixpkgs": the installed one, copied, and told it is the revision
|
||||
# the channel names (the running one). A different nixpkgs could not be
|
||||
# built here (even a changed version rebuilds the NixOS tools), so what is
|
||||
# tested is everything around the build.
|
||||
target.succeed("cp -r ${self.inputs.nixpkgs} /var/lib/newer-nixpkgs")
|
||||
running_version = target.succeed("cat /run/current-system/nixos-version")
|
||||
running_rev = "${self.inputs.nixpkgs.rev}"
|
||||
|
||||
def channel(rev):
|
||||
target.succeed(f"mkdir -p /tmp/channels/nixos-26.05 && printf %s {rev} > /tmp/channels/nixos-26.05/git-revision")
|
||||
|
||||
with subtest("router-upgrade check needs a channel and a reachable revision"):
|
||||
rc, out = target.execute("router-upgrade check 2>&1")
|
||||
assert rc != 0 and "cannot read" in out, out
|
||||
channel("not-a-revision")
|
||||
rc, out = target.execute("router-upgrade check 2>&1")
|
||||
assert rc != 0 and "unexpected answer" in out, out
|
||||
target.succeed("jq 'del(.router.update)' /var/lib/router/config/settings.json > /tmp/s.json && cp /var/lib/router/config/settings.json /tmp/settings.orig && cp /tmp/s.json /var/lib/router/config/settings.json")
|
||||
rc, out = target.execute("router-upgrade check 2>&1")
|
||||
assert rc != 0 and "no channel" in out, out
|
||||
target.succeed("cp /tmp/settings.orig /var/lib/router/config/settings.json")
|
||||
rc, out = target.execute("router-upgrade apply 2>&1")
|
||||
assert rc != 0 and "nothing is previewed" in out, out
|
||||
|
||||
with subtest("a newer revision is reported, the running one is up to date"):
|
||||
rev = "0123456789abcdef0123456789abcdef01234567"
|
||||
channel(rev)
|
||||
out = target.succeed("router-upgrade check 2>&1")
|
||||
assert "a newer revision is available" in out, out
|
||||
assert json.loads(target.succeed("cat /var/lib/router/upgrade/check.json"))["upToDate"] is False
|
||||
channel(running_rev)
|
||||
out = target.succeed("router-upgrade check 2>&1")
|
||||
assert "up to date" in out, out
|
||||
assert json.loads(target.succeed("cat /var/lib/router/upgrade/check.json"))["upToDate"] is True
|
||||
out = target.succeed("router-upgrade preview 2>&1")
|
||||
assert "up to date" in out, out
|
||||
target.succeed("test ! -e /var/lib/router/upgrade/preview.json")
|
||||
|
||||
with subtest("a preview lists the changes and the download without fetching or touching anything"):
|
||||
before = current()
|
||||
out = target.succeed("router-upgrade preview --force 2>&1")
|
||||
assert "no package changes" in out and "to fetch:" in out, out
|
||||
preview = json.loads(target.succeed("cat /var/lib/router/upgrade/preview.json"))
|
||||
assert preview["rev"] == running_rev and preview["version"] == running_version and preview["changes"] == [], preview
|
||||
assert preview["ref"].startswith("path:/var/lib/newer-nixpkgs?") and "rev=" + running_rev in preview["ref"] and "narHash=sha256-" in preview["ref"], preview
|
||||
assert preview["rebootNeeded"] is False and preview["builds"] == 0, preview
|
||||
target.succeed("test ! -e /var/lib/router/upgrade/result")
|
||||
assert current() == before
|
||||
target.succeed("test ! -e /var/lib/router/config/nixpkgs-pin.json")
|
||||
|
||||
with subtest("the package list names what changes in version, and only that"):
|
||||
target.succeed("printf '%s\\n' linux-6.12.5 openssl-3.4.1 openssl-3.4.1-bin zlib-1.3.1 frr-10.1 ripgrep-14.1 rg-1.0 glibc-2.40-66 glibc-2.40-66-getent > /tmp/old")
|
||||
target.succeed("printf '%s\\n' linux-6.12.8 openssl-3.4.2 zlib-1.3.1 zlib-1.3.2 frr-10.2 cmake-4.0 bash-5.3 glibc-2.40-66 foo-1.tar.gz > /tmp/new")
|
||||
target.succeed("sed -n '/^changes() {/,/^}/p' /run/current-system/sw/bin/router-upgrade > /tmp/changes.sh && test -s /tmp/changes.sh")
|
||||
out = target.succeed("bash -c 'source /tmp/changes.sh; changes /tmp/old /tmp/new' | jq -c 'map(.name + \":\" + .from + \">\" + .to)'")
|
||||
assert json.loads(out) == ["frr:10.1>10.2", "linux:6.12.5>6.12.8", "openssl:3.4.1>3.4.2"], out
|
||||
|
||||
with subtest("an upgrade that is not confirmed is rolled back, the lock and the pin included"):
|
||||
lock = target.succeed("cat /var/lib/router/config/flake.lock")
|
||||
# The packages are the same, so there is nothing to activate unless the
|
||||
# settings changed too (an upgrade applies what is saved, as `router-apply` does).
|
||||
target.succeed("grep -q 192.168.10.130 /var/lib/router/config/settings.json && sed -i 's/192.168.10.130/192.168.10.140/' /var/lib/router/config/settings.json")
|
||||
target.succeed("router-upgrade download >&2")
|
||||
target.succeed("test -e /var/lib/router/upgrade/result")
|
||||
target.succeed("router-upgrade apply 20s >&2")
|
||||
assert current() != before
|
||||
target.succeed("systemctl is-active router-rollback.timer")
|
||||
assert json.loads(target.succeed("cat /var/lib/router/config/nixpkgs-pin.json"))["rev"] == running_rev
|
||||
target.succeed("test ! -e /var/lib/router/upgrade/preview.json && test ! -e /var/lib/router/upgrade/result")
|
||||
target.wait_until_succeeds(f"test $(readlink -f /run/current-system) = {before}", timeout=180)
|
||||
target.succeed("test ! -e /var/lib/router/config/nixpkgs-pin.json && test ! -e /var/lib/router/config.pre-update")
|
||||
assert target.succeed("cat /var/lib/router/config/flake.lock") == lock
|
||||
|
||||
with subtest("a confirmed upgrade stays, pins nixpkgs and rebuilds to exactly itself"):
|
||||
target.succeed("router-upgrade preview --force >&2")
|
||||
target.succeed("router-upgrade apply 5min >&2")
|
||||
target.succeed("router-confirm >&2")
|
||||
target.fail("systemctl is-active router-rollback.timer")
|
||||
upgraded = current()
|
||||
assert upgraded != before
|
||||
assert target.succeed("readlink -f /nix/var/nix/profiles/system").strip() == upgraded
|
||||
assert json.loads(target.succeed("cat /var/lib/router/config/nixpkgs-pin.json"))["rev"] == running_rev
|
||||
assert "newer-nixpkgs" in target.succeed("cat /var/lib/router/config/flake.lock")
|
||||
target.succeed("test ! -e /var/lib/router/config.pre-update")
|
||||
assert "no changes" in target.succeed("router-apply 2>&1")
|
||||
|
||||
with subtest("an upgrade that fails to build puts the configuration back"):
|
||||
channel("fedcba9876543210fedcba9876543210fedcba98")
|
||||
target.succeed("router-upgrade preview >&2")
|
||||
pin = target.succeed("cat /var/lib/router/config/nixpkgs-pin.json")
|
||||
# An unknown option, after the preview: the build of the apply fails.
|
||||
target.succeed("cp /var/lib/router/config/settings.json /tmp/settings.good && jq '.router.nonsense = true' /tmp/settings.good > /var/lib/router/config/settings.json")
|
||||
rc, out = target.execute("router-upgrade apply 5min 2>&1")
|
||||
assert rc != 0 and "the upgrade failed" in out, out
|
||||
assert target.succeed("cat /var/lib/router/config/nixpkgs-pin.json") == pin
|
||||
target.succeed("test ! -e /var/lib/router/config.pre-update")
|
||||
target.succeed("cp /tmp/settings.good /var/lib/router/config/settings.json")
|
||||
assert current() == upgraded
|
||||
'';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user