#!/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