Allow previewing an older NixOS release with --downgrade
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -228,7 +228,7 @@ Upgrading nixpkgs (the kernel and every package) on the router, without a new re
|
||||
|
||||
- **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` (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`.
|
||||
- **`preview [--force] [--downgrade]`**: 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 (unstable calls itself 26.11, so following 26.05 from unstable is one) unless `--downgrade` (the page's "downgrade anyway") is given; it then warns that state written by the newer release may not work. Not exercised in a VM test: the test's nixpkgs copy has the same release. 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.
|
||||
|
||||
@@ -41,7 +41,7 @@ let
|
||||
];
|
||||
text = ''
|
||||
usage() {
|
||||
echo "usage: router-upgrade check | preview [--force] | download | apply [timeout] | --request" >&2
|
||||
echo "usage: router-upgrade check | preview [--force] [--downgrade] | download | apply [timeout] | --request" >&2
|
||||
echo " check the newest revision of router.update.channel, against the running one" >&2
|
||||
echo " preview the packages that change and the size of the download (nothing is fetched)" >&2
|
||||
echo " download fetch and build what was previewed, without activating it" >&2
|
||||
@@ -56,12 +56,13 @@ let
|
||||
[ -r "$req" ] || die "no request"
|
||||
action=$(jq -er .action "$req")
|
||||
force=$(jq -r 'if .force then "--force" else "" end' "$req")
|
||||
downgrade=$(jq -r 'if .downgrade then "--downgrade" else "" end' "$req")
|
||||
timeout=$(jq -r '.timeout // ""' "$req")
|
||||
rm -f "$req"
|
||||
# The web UI shows the last run (a finished unit forgets its journal).
|
||||
exec > >(tee ${dir}/last.log) 2>&1
|
||||
case $action in
|
||||
preview) set -- preview ''${force:+"$force"} ;;
|
||||
preview) set -- preview ''${force:+"$force"} ''${downgrade:+"$downgrade"} ;;
|
||||
apply) set -- apply ''${timeout:+"$timeout"} ;;
|
||||
check | download) set -- "$action" ;;
|
||||
*) die "unknown request: $action" ;;
|
||||
@@ -127,8 +128,14 @@ let
|
||||
}
|
||||
|
||||
preview() {
|
||||
local force=0
|
||||
[ "''${1:-}" != --force ] || force=1
|
||||
local force=0 downgrade=0 arg
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--force) force=1 ;;
|
||||
--downgrade) downgrade=1 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
check
|
||||
rm -rf ${dir}/preview.json ${dir}/flake ${dir}/result
|
||||
if [ "$same" = true ] && [ "$force" -eq 0 ]; then return 0; fi
|
||||
@@ -155,8 +162,11 @@ let
|
||||
# The release (26.05) of a version like 26.05.20260101.abcdef0 or 26.11pre-git.
|
||||
release() { sed -E 's/^([0-9]+\.[0-9]+).*/\1/' <<<"$1"; }
|
||||
newer=$(printf '%s\n%s\n' "$(release "$new_version")" "$(release "$running_version")" | sort -V | tail -1)
|
||||
[ "$newer" = "$(release "$new_version")" ] \
|
||||
|| die "$new_version is older than the running $running_version: NixOS cannot be downgraded"
|
||||
if [ "$newer" != "$(release "$new_version")" ]; then
|
||||
[ "$downgrade" -eq 1 ] \
|
||||
|| die "$new_version is older than the running $running_version: NixOS cannot be downgraded safely (use --downgrade to try anyway)"
|
||||
echo "router-upgrade: warning: $new_version is older than the running $running_version; state written by the newer release may not work with it"
|
||||
fi
|
||||
drv=$(nix eval --raw "$attr.build.toplevel.drvPath")
|
||||
|
||||
nix path-info -r /run/current-system | names > ${dir}/old.names
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
</label>
|
||||
<button class="secondary">Save</button>
|
||||
</form>
|
||||
<p class="hint">The channel only chooses what an upgrade offers; saving it changes nothing on the router. NixOS cannot go back to
|
||||
an older release.</p>
|
||||
<p class="hint">The channel only chooses what an upgrade offers; saving it changes nothing on the router. Going back to an older release
|
||||
(unstable to 26.05) is refused unless you tick "downgrade anyway" when previewing; state written by the newer
|
||||
release may not work with the older one.</p>
|
||||
</section>
|
||||
|
||||
{{if $d.Updating}}
|
||||
@@ -54,6 +55,7 @@
|
||||
<form method="post" action="/upgrade/preview" class="actions">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
{{if and $d.Check $d.Check.UpToDate}}<label><input type="checkbox" name="force"> look anyway</label>{{end}}
|
||||
<label><input type="checkbox" name="downgrade"> downgrade anyway</label>
|
||||
<button class="secondary"{{if or $d.Run.Running $d.Updating}} disabled{{end}}>Preview</button>
|
||||
</form>
|
||||
<p class="hint">Works it out from the new revision's description: no package is downloaded.</p>
|
||||
|
||||
@@ -121,8 +121,8 @@ type UpgradeRun = UpdateStatus
|
||||
func (s System) UpgradeStatus() UpgradeRun { return unitRun("router-upgrade.service") }
|
||||
|
||||
// requestUpgrade asks the router-upgrade unit for one step.
|
||||
func (s System) requestUpgrade(action string, force bool, timeout string) error {
|
||||
data, _ := json.Marshal(map[string]any{"action": action, "force": force, "timeout": timeout})
|
||||
func (s System) requestUpgrade(action string, force, downgrade bool, timeout string) error {
|
||||
data, _ := json.Marshal(map[string]any{"action": action, "force": force, "downgrade": downgrade, "timeout": timeout})
|
||||
if err := os.MkdirAll(s.upgradeDir(), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -254,7 +254,7 @@ func (a *app) upgradeStep(w http.ResponseWriter, r *http.Request, _ session) {
|
||||
a.upgradeBack(w, r, "Preview first: it says what would change.")
|
||||
return
|
||||
}
|
||||
if err := a.sys.requestUpgrade(action, r.PostFormValue("force") == "on", ""); err != nil {
|
||||
if err := a.sys.requestUpgrade(action, r.PostFormValue("force") == "on", r.PostFormValue("downgrade") == "on", ""); err != nil {
|
||||
a.upgradeBack(w, r, fmt.Sprintf("Could not start it: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -135,8 +135,8 @@ func TestUpgradePage(t *testing.T) {
|
||||
if r := request(); r["action"] != "check" || started != 1 {
|
||||
t.Fatalf("%v started %d", r, started)
|
||||
}
|
||||
post("/upgrade/preview", url.Values{"force": {"on"}})
|
||||
if r := request(); r["action"] != "preview" || r["force"] != true {
|
||||
post("/upgrade/preview", url.Values{"force": {"on"}, "downgrade": {"on"}})
|
||||
if r := request(); r["action"] != "preview" || r["force"] != true || r["downgrade"] != true {
|
||||
t.Fatalf("%v", r)
|
||||
}
|
||||
if resp := post("/upgrade/format-disk", url.Values{}); resp.StatusCode != http.StatusNotFound {
|
||||
|
||||
Reference in New Issue
Block a user