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