121 lines
4.8 KiB
Plaintext
121 lines
4.8 KiB
Plaintext
|
|
#!/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
|