Follow the channel server's redirect and offer the stable releases
channels.nixos.org answers <branch>/git-revision with a redirect, which router-upgrade check did not follow: on a real router it saw an empty answer. The test channel is now an HTTP server that redirects the same way. The page labelled the running release "stable", but nixos-unstable already calls itself the next release (26.11 does not exist as a branch yet). It offers none, unstable and the stable releases listed in the code (26.05). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -227,14 +227,14 @@ verity. The image module and its test (`checks.image`) stay as they are for anyo
|
||||
Upgrading nixpkgs (the kernel and every package) on the router, without a new release of nixos-router. Step 1 (CLI and setting) and step 2 (web UI page) are done; the notice of a new NixOS release follows.
|
||||
|
||||
- **Setting** `router.update.channel`: `unstable` or a release like `26.05` (branch `nixos-26.05`); null: the packages that came with the installed release. Read from the saved `settings.json` when upgrading; applying settings does not change nixpkgs.
|
||||
- **`router-upgrade check`**: the branch's newest revision from `<channels.nixos.org>/<branch>/git-revision` (the last one Hydra's tests passed), against the running one (`nixos-version --revision`). Writes `/var/lib/router/upgrade/check.json`.
|
||||
- **`router-upgrade check`**: the branch's newest revision from `<channels.nixos.org>/<branch>/git-revision` (a redirect to releases.nixos.org, so curl follows it; the last revision Hydra's tests passed), against the running one (`nixos-version --revision`). Writes `/var/lib/router/upgrade/check.json`.
|
||||
- **`preview [--force]`**: locks a copy of the configuration flake to that revision (`nix flake lock --override-input nixos-router/nixpkgs`), evaluates it and lists the packages of the running system that have another version in the new system's derivations, how much `nix build --dry-run` would download, and how many derivations would be built here. Nothing is downloaded: evaluation fetches the nixpkgs source only. Approximate: a rebuild without a new version is not listed. Refuses a NixOS release older than the running one. A changed `linux` sets `rebootNeeded`. Result in `preview.json`.
|
||||
- **`download`**: fetches and builds it (needs 3 GiB free), so a flaky uplink does not matter at `apply`.
|
||||
- **`apply [timeout]`**: takes the previewed lock into the configuration directory and runs `router-apply` (commit-confirm). The directory as it was is kept in `config.pre-update` like for updates: a rollback or a failed build puts the lock back. Refused if the configuration flake changed since the preview.
|
||||
- **Pin**: `nixpkgs-pin.json` in the configuration directory (channel, revision, flake reference with `narHash`). `router-update` locks it again after it installs a new release of nixos-router, so a release does not undo an upgrade; the reference is found in the store by its hash, offline.
|
||||
- **Web UI page** `/upgrade` ("Packages", under System): the channel as a select (none, `unstable`, the running release; a configured one that is none of these stays listed; saved to `settings.json` and the confirmed settings, so it never shows as unapplied), buttons Check, Preview (with "look anyway" when up to date), Download now and Upgrade. The preview shows the version table, download size, local builds (a warning above 50) and a reboot note for a new kernel. The page asks for a step in `upgrade/request.json` and starts `router-upgrade.service`, which runs `router-upgrade --request` (not restarted or stopped by a switch, like `router-apply`); it shows `check.json`/`preview.json` (not those of another channel, nor a check from before the last upgrade) and `upgrade/last.log`, which the unit tees (a finished one-shot unit has no journal to find). Download and Upgrade need a preview and no change waiting for confirmation; a request without a `channel` field is refused rather than clearing it.
|
||||
- **Web UI page** `/upgrade` ("Packages", under System): the channel as a select (none, `unstable`, and the stable releases in `stableChannels` in `upgrade.go`, 26.05 for now, until the router can ask the channel server which exist: the running release says nothing, since nixos-unstable already calls itself the next one; a configured channel that is none of these stays listed; saved to `settings.json` and the confirmed settings, so it never shows as unapplied), buttons Check, Preview (with "look anyway" when up to date), Download now and Upgrade. The preview shows the version table, download size, local builds (a warning above 50) and a reboot note for a new kernel. The page asks for a step in `upgrade/request.json` and starts `router-upgrade.service`, which runs `router-upgrade --request` (not restarted or stopped by a switch, like `router-apply`); it shows `check.json`/`preview.json` (not those of another channel, nor a check from before the last upgrade) and `upgrade/last.log`, which the unit tees (a finished one-shot unit has no journal to find). Download and Upgrade need a preview and no change waiting for confirmation; a request without a `channel` field is refused rather than clearing it.
|
||||
- **Module options** (for tests): `routerSettings.upgrade.channelsUrl`, `routerSettings.upgrade.nixpkgsRef` (`%rev%`).
|
||||
- **Test** (`apply`): the channel is a `file://` directory and the "newer" nixpkgs is the installed one under a `path:` reference with the same `lastModified` and `rev`: a really different nixpkgs cannot be built offline (any version change rebuilds the NixOS tools). Covers check, preview (including the package-list rules), download, an unconfirmed upgrade rolled back with its lock and pin, a confirmed one, and a failing build.
|
||||
- **Test** (`apply`): the channel is a small HTTP server on the client that redirects like channels.nixos.org, and the "newer" nixpkgs is the installed one under a `path:` reference with the same `lastModified` and `rev`: a really different nixpkgs cannot be built offline (any version change rebuilds the NixOS tools). Covers check, preview (including the package-list rules), download, an unconfirmed upgrade rolled back with its lock and pin, a confirmed one, and a failing build.
|
||||
|
||||
## Not done yet (2026-11-01, after 0.12.0)
|
||||
|
||||
|
||||
@@ -84,7 +84,8 @@ let
|
||||
|
||||
newest_rev() {
|
||||
local rev
|
||||
rev=$(curl -fsS --max-time 30 "${up.channelsUrl}/$branch/git-revision") \
|
||||
# (channels.nixos.org answers with a redirect to releases.nixos.org.)
|
||||
rev=$(curl -fsSL --max-time 30 "${up.channelsUrl}/$branch/git-revision") \
|
||||
|| die "cannot read ${up.channelsUrl}/$branch/git-revision (no internet, or no channel of that name)"
|
||||
[[ $rev =~ ^[0-9a-f]{40}$ ]] || die "unexpected answer for $branch: ''${rev:0:60}"
|
||||
echo "$rev"
|
||||
|
||||
@@ -145,12 +145,18 @@ type upgradeData struct {
|
||||
Pending bool // a system waits for confirmation
|
||||
}
|
||||
|
||||
// stableChannels are the stable NixOS releases the page offers (the branch
|
||||
// nixos-26.05 and so on). They are listed here until the router can ask the
|
||||
// channel server which exist; the release a router runs says nothing about it:
|
||||
// nixos-unstable already calls itself the release that comes next.
|
||||
var stableChannels = []string{"26.05"}
|
||||
|
||||
// channelChoices are the branches worth offering: none (the packages that came
|
||||
// with the release), unstable and the running release; NixOS cannot go back
|
||||
// to an older one. A configured channel that is none of these stays listed.
|
||||
func channelChoices(version, current string) []channelChoice {
|
||||
// with the release), unstable and the stable releases. A configured channel
|
||||
// that is none of these stays listed.
|
||||
func channelChoices(current string) []channelChoice {
|
||||
choices := []channelChoice{{"", "None: the packages that came with this release of nixos-router"}, {"unstable", "unstable (newest, may break)"}}
|
||||
if r := releaseOf(version); r != "" {
|
||||
for _, r := range stableChannels {
|
||||
choices = append(choices, channelChoice{r, r + " (stable)"})
|
||||
}
|
||||
if current != "" && !slices.ContainsFunc(choices, func(c channelChoice) bool { return c.Value == current }) {
|
||||
@@ -166,7 +172,7 @@ func (a *app) upgradeData() upgradeData {
|
||||
if s, err := loadSettings(a.sys.settingsPath()); err == nil {
|
||||
d.Channel = s.channel()
|
||||
}
|
||||
d.Choices = channelChoices(d.Version, d.Channel)
|
||||
d.Choices = channelChoices(d.Channel)
|
||||
// What router-upgrade printed: a finished one-shot unit has no journal to find.
|
||||
if data, err := os.ReadFile(filepath.Join(a.sys.upgradeDir(), "last.log")); err == nil {
|
||||
d.Run.Log = tail(string(data), 64<<10)
|
||||
@@ -204,7 +210,7 @@ func (a *app) upgradeChannel(w http.ResponseWriter, r *http.Request, _ session)
|
||||
return
|
||||
}
|
||||
if v != "" && !channelRe.MatchString(v) {
|
||||
a.upgradeBack(w, r, "Not saved: the channel is unstable or a release like 26.11.")
|
||||
a.upgradeBack(w, r, "Not saved: the channel is unstable or a release like 26.05.")
|
||||
return
|
||||
}
|
||||
if err := a.sys.setChannel(v); err != nil {
|
||||
|
||||
@@ -24,14 +24,14 @@ func TestReleaseOfAndChannelChoices(t *testing.T) {
|
||||
}
|
||||
return strings.Join(v, ",")
|
||||
}
|
||||
if got := values(channelChoices("26.11.2026", "")); got != ",unstable,26.11" {
|
||||
if got := values(channelChoices("")); got != ",unstable,26.05" {
|
||||
t.Fatalf("choices: %q", got)
|
||||
}
|
||||
// A configured channel that is not offered (a release that is gone) is not lost.
|
||||
if got := values(channelChoices("26.11.2026", "27.05")); got != ",unstable,26.11,27.05" {
|
||||
if got := values(channelChoices("25.11")); got != ",unstable,26.05,25.11" {
|
||||
t.Fatalf("choices: %q", got)
|
||||
}
|
||||
if got := values(channelChoices("26.11.2026", "unstable")); got != ",unstable,26.11" {
|
||||
if got := values(channelChoices("unstable")); got != ",unstable,26.05" {
|
||||
t.Fatalf("choices: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -119,13 +119,13 @@ func TestUpgradePage(t *testing.T) {
|
||||
if resp := post("/upgrade/channel", url.Values{}); !strings.Contains(resp.Header.Get("Location"), "no+channel") {
|
||||
t.Fatalf("%s", resp.Header.Get("Location"))
|
||||
}
|
||||
post("/upgrade/channel", url.Values{"channel": {"26.11"}})
|
||||
post("/upgrade/channel", url.Values{"channel": {"26.05"}})
|
||||
post("/upgrade/channel", url.Values{})
|
||||
s, _ := loadSettings(sys.settingsPath())
|
||||
if s.channel() != "26.11" {
|
||||
if s.channel() != "26.05" {
|
||||
t.Fatalf("channel %q", s.channel())
|
||||
}
|
||||
if !strings.Contains(page(), `value="26.11" selected`) {
|
||||
if !strings.Contains(page(), `value="26.05" selected`) {
|
||||
t.Fatal("the saved channel is not selected")
|
||||
}
|
||||
})
|
||||
@@ -159,7 +159,7 @@ func TestUpgradePage(t *testing.T) {
|
||||
t.Fatal("the unit was started without a preview")
|
||||
}
|
||||
os.MkdirAll(a.sys.upgradeDir(), 0o700)
|
||||
os.WriteFile(filepath.Join(a.sys.upgradeDir(), "preview.json"), []byte(`{"channel":"26.11","rev":"0123456789abcdef0123456789abcdef01234567","version":"26.11.20261110.0123456","time":"2026-11-10T10:00:00Z","download":"(350.5 MiB download, 1.2 GiB unpacked)","builds":3,"rebootNeeded":true,"changes":[{"name":"frr","from":"10.1","to":"10.2"},{"name":"linux","from":"6.12.5","to":"6.12.8"}]}`), 0o600)
|
||||
os.WriteFile(filepath.Join(a.sys.upgradeDir(), "preview.json"), []byte(`{"channel":"26.05","rev":"0123456789abcdef0123456789abcdef01234567","version":"26.11.20261110.0123456","time":"2026-11-10T10:00:00Z","download":"(350.5 MiB download, 1.2 GiB unpacked)","builds":3,"rebootNeeded":true,"changes":[{"name":"frr","from":"10.1","to":"10.2"},{"name":"linux","from":"6.12.5","to":"6.12.8"}]}`), 0o600)
|
||||
body := page()
|
||||
for _, want := range []string{"frr", "10.1", "10.2", "linux", "350.5 MiB", "3 derivations", "after the next reboot", "Upgrade to 26.11.20261110.0123456"} {
|
||||
if !strings.Contains(body, want) {
|
||||
@@ -185,13 +185,13 @@ func TestUpgradePage(t *testing.T) {
|
||||
t.Run("a preview of another channel is not shown", func(t *testing.T) {
|
||||
post("/upgrade/channel", url.Values{"channel": {"unstable"}})
|
||||
if strings.Contains(page(), "linux") {
|
||||
t.Fatal("the preview of 26.11 is shown for unstable")
|
||||
t.Fatal("the preview of 26.05 is shown for unstable")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a check from before the last upgrade is not shown", func(t *testing.T) {
|
||||
post("/upgrade/channel", url.Values{"channel": {"26.11"}})
|
||||
check := `{"channel":"26.11","branch":"nixos-26.11","time":"2026-11-09T10:00:00Z","running":{"rev":"aaa","version":"%s"},"newest":{"rev":"0123456789abcdef"},"upToDate":false}`
|
||||
post("/upgrade/channel", url.Values{"channel": {"26.05"}})
|
||||
check := `{"channel":"26.05","branch":"nixos-26.05","time":"2026-11-09T10:00:00Z","running":{"rev":"aaa","version":"%s"},"newest":{"rev":"0123456789abcdef"},"upToDate":false}`
|
||||
os.WriteFile(filepath.Join(a.sys.upgradeDir(), "check.json"), []byte(strings.Replace(check, "%s", "26.11.20261001.old", 1)), 0o600)
|
||||
if strings.Contains(page(), "A newer revision is available") {
|
||||
t.Fatal("an outdated check is shown")
|
||||
|
||||
+29
-3
@@ -42,13 +42,13 @@ let
|
||||
);
|
||||
|
||||
# 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).
|
||||
# server on the client, 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";
|
||||
channelsUrl = "http://192.168.10.50:8080";
|
||||
nixpkgsRef = "path:/var/lib/newer-nixpkgs?lastModified=${toString self.inputs.nixpkgs.lastModified}&rev=%rev%";
|
||||
};
|
||||
}
|
||||
@@ -63,6 +63,26 @@ let
|
||||
modules = [ "${configDir}/local.nix" ];
|
||||
}).config.system.build.toplevel;
|
||||
|
||||
channelServer = pkgs.writeScript "channels.py" ''
|
||||
#!${pkgs.python3}/bin/python3
|
||||
import http.server
|
||||
|
||||
class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory="/srv/channels", **kwargs)
|
||||
|
||||
def do_GET(self):
|
||||
parts = self.path.strip("/").split("/")
|
||||
if len(parts) == 2 and parts[1] == "git-revision":
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/files/" + self.path.strip("/"))
|
||||
self.end_headers()
|
||||
else:
|
||||
super().do_GET()
|
||||
|
||||
http.server.ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
|
||||
'';
|
||||
|
||||
sharedDisk = {
|
||||
virtualisation.diskImage = "./target.qcow2";
|
||||
virtualisation.diskSize = 8192;
|
||||
@@ -99,6 +119,9 @@ pkgs.testers.runNixOSTest {
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
# channels.nixos.org: <branch>/git-revision is a redirect to the file.
|
||||
networking.firewall.allowedTCPPorts = [ 8080 ];
|
||||
environment.etc."channels.py".source = channelServer;
|
||||
};
|
||||
|
||||
target = {
|
||||
@@ -326,8 +349,11 @@ pkgs.testers.runNixOSTest {
|
||||
running_version = target.succeed("cat /run/current-system/nixos-version")
|
||||
running_rev = "${self.inputs.nixpkgs.rev}"
|
||||
|
||||
client.succeed("mkdir -p /srv/channels/files/nixos-26.05 && systemd-run --unit=channels /etc/channels.py")
|
||||
client.wait_for_open_port(8080)
|
||||
|
||||
def channel(rev):
|
||||
target.succeed(f"mkdir -p /tmp/channels/nixos-26.05 && printf %s {rev} > /tmp/channels/nixos-26.05/git-revision")
|
||||
client.succeed(f"printf %s {rev} > /srv/channels/files/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")
|
||||
|
||||
Reference in New Issue
Block a user