feat(vyos): pin a known-good config and restore it with one command
Some checks failed
CI/CD / lint (push) Failing after 9s
CI/CD / test (push) Failing after 10s
CI/CD / typecheck (push) Failing after 24s
CI/CD / build (push) Has been skipped
CI/CD / publish-rpm (push) Has been skipped
CI/CD / publish-deb (push) Has been skipped

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMVzWZgiKW2wquf5z8S1yH
This commit is contained in:
Michal
2026-08-21 01:46:43 +01:00
parent 86c2a36f00
commit a187703a3a

120
migration/vyos-known-good Executable file
View File

@@ -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 <REDACTED>/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