From a187703a3a5a719fb3cb8464bf30f98f981e0d15 Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 21 Aug 2026 01:46:43 +0100 Subject: [PATCH] feat(vyos): pin a known-good config and restore it with one command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VyOS already has rollback, but `rollback 1` returns you to the *previous* revision, which may itself be broken — you can end up walking backwards through several bad commits hunting for the one that worked, at exactly the moment you have no network to look things up with. This pins a state a human has actually used and found working, so recovery is one step and needs no memory of how many changes ago things were fine. /config/vyos-known-good save pin the running config /config/vyos-known-good status when it was taken, how running differs /config/vyos-known-good diff what a restore would change /config/vyos-known-good restore go back to it Deliberately not automatic. A config is only known-good once someone has used the network; a snapshot taken after every commit would faithfully preserve the broken one. The restore is itself commit-confirmed, so even the recovery path is protected: if the snapshot is somehow wrong, or access is still broken and nothing can be confirmed, the router undoes the restore rather than leaving you worse off. Silence reverts. `save` refuses when there are uncommitted changes — a snapshot that did not match what is actually running would look like a safety net without being one. Two things found while building it, both of which made the script silently useless rather than fail loudly: - Sourcing `script-template` **resets the positional parameters**, so `$1` was empty by the time the case statement ran and every invocation fell through to the usage message. Arguments are captured before the source. - `0600` made the snapshot unreadable to the `vyos` user, so `status` and `diff` — the two commands you run while deciding whether to restore — showed nothing. Now 0660 root:vyattacfg, matching /config/config.boot. Installed on both routers with the current, verified-working config pinned (vyos001 1029 lines, vyos002 1019). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DMVzWZgiKW2wquf5z8S1yH --- migration/vyos-known-good | 120 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100755 migration/vyos-known-good diff --git a/migration/vyos-known-good b/migration/vyos-known-good new file mode 100755 index 0000000..7cf07fa --- /dev/null +++ b/migration/vyos-known-good @@ -0,0 +1,120 @@ +#!/bin/vbash +# Pin a config you have SEEN working, and get back to it with one command. +# +# Why this exists when VyOS already has rollback: `rollback 1` returns you to the +# previous revision, which may itself be broken -- you can walk backwards through +# several bad commits looking for the one that worked. This pins a state you +# explicitly confirmed was good, so recovery is one step and does not require +# remembering how many changes ago things last worked. +# +# It is deliberately NOT automatic. A config is only "known good" once a human +# has used the network and found it working; a script cannot judge that, and a +# snapshot taken automatically after every commit would faithfully preserve the +# broken one. +# +# vyos-known-good save mark the running config as known-good +# vyos-known-good status when it was taken, and how it differs from running +# vyos-known-good restore go back to it (commit-confirmed, so even this is safe) +# vyos-known-good diff what would change if you restored +# +# Lives in /config so it survives image upgrades, like vyos-unifi-switch. + +# Capture the arguments BEFORE sourcing script-template: sourcing it resets the +# positional parameters, so $1 is empty by the time the case statement runs and +# every invocation silently falls through to the usage message. +ACTION="${1:-status}" + +source /opt/vyatta/etc/functions/script-template + +GOOD="/config/known-good.boot" +META="/config/known-good.meta" +RUNNING="/config/config.boot" +CONFIRM_MINUTES="${CONFIRM_MINUTES:-5}" + +say() { printf '\033[0;36m[known-good]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[known-good]\033[0m %s\n' "$*" >&2; } +die() { printf '\033[0;31m[known-good]\033[0m %s\n' "$*" >&2; exit 1; } + +# The running config on disk is only current if nothing is uncommitted-and-unsaved. +# Saving a snapshot that does not match what is actually running would be worse +# than having no snapshot at all -- it would look like a safety net and not be one. +require_saved() { + if ! cli-shell-api sessionChanged >/dev/null 2>&1; then + return 0 + fi + die "there are uncommitted changes; commit and save first, or this snapshot would not match reality" +} + +cmd_save() { + require_saved + [ -r "$RUNNING" ] || die "cannot read $RUNNING" + sudo cp "$RUNNING" "$GOOD" + # 0660 root:vyattacfg, matching /config/config.boot. 0600 would make the + # snapshot unreadable to the vyos user, so `status` and `diff` -- the two you + # run while deciding whether to restore -- would silently show nothing. + sudo chmod 0660 "$GOOD"; sudo chgrp vyattacfg "$GOOD" + sudo chmod 0660 "$META" 2>/dev/null; sudo chgrp vyattacfg "$META" 2>/dev/null + { + echo "saved_at=$(date -Is)" + echo "saved_by=${SUDO_USER:-$USER}" + echo "hostname=$(hostname)" + echo "lines=$(wc -l < "$RUNNING")" + } | sudo tee "$META" >/dev/null + say "pinned $(wc -l < "$GOOD") lines as known-good on $(hostname)" + say "restore with: /config/vyos-known-good restore" +} + +cmd_status() { + [ -r "$GOOD" ] || { warn "no known-good snapshot on $(hostname) -- run 'save' while things work"; return 1; } + say "known-good on $(hostname):" + sed 's/^/ /' "$META" 2>/dev/null + local n + n="$(diff <(grep -vE '^\s*$' "$GOOD") <(grep -vE '^\s*$' "$RUNNING") 2>/dev/null | grep -c '^[<>]')" + if [ "${n:-0}" -eq 0 ]; then + say "running config MATCHES known-good" + else + warn "running config differs from known-good by $n line(s) -- 'diff' to see them" + fi +} + +cmd_diff() { + [ -r "$GOOD" ] || die "no known-good snapshot" + diff -u "$GOOD" "$RUNNING" | sed -E "s/(password|key|secret)[[:space:]]+\S+/\1 /I" || true +} + +cmd_restore() { + [ -r "$GOOD" ] || die "no known-good snapshot to restore" + say "restoring known-good on $(hostname) (taken $(grep -m1 saved_at "$META" 2>/dev/null | cut -d= -f2-))" + + # Commit-confirmed even here. If the known-good snapshot is itself somehow + # wrong, or the restore cannot be confirmed because access is still broken, + # the router undoes it rather than leaving you worse off. Silence reverts. + local script; script="$(mktemp)" + { + echo 'source /opt/vyatta/etc/functions/script-template' + echo 'configure' + echo "load $GOOD" + printf 'sudo sg vyattacfg "/usr/bin/config-mgmt commit_confirm -y -t=%s"\n' "$CONFIRM_MINUTES" + echo 'export IN_COMMIT_CONFIRM=t' + echo 'commit' + echo 'unset IN_COMMIT_CONFIRM' + echo 'exit' + } > "$script" + vbash "$script"; local rc=$? + rm -f "$script" + + [ $rc -eq 0 ] || die "restore failed (rc=$rc) -- nothing was committed" + say "" + say "RESTORED under a ${CONFIRM_MINUTES} minute timer." + say "Check the network NOW. If it works, confirm it:" + say " sudo sg vyattacfg '/usr/bin/config-mgmt confirm'" + say "If you do nothing, the router reverts on its own." +} + +case "$ACTION" in + save) cmd_save ;; + status) cmd_status ;; + diff) cmd_diff ;; + restore) cmd_restore ;; + *) die "usage: vyos-known-good {save|status|diff|restore}" ;; +esac