diff --git a/README.md b/README.md index 5790193..7c810fd 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,20 @@ via cloud-init. sudo bash /opt/sysmig/sysmig up # apply all pending migrations (idempotent) sudo bash /opt/sysmig/sysmig down # roll back the last migration sudo bash /opt/sysmig/sysmig down all # roll back everything (reverse order) -bash /opt/sysmig/sysmig status # show status (no root required) +bash /opt/sysmig/sysmig status # history + drift check (exit 1 on drift) ``` -- Migrations: `migrations/NNN-name.sh`, receiving `up`/`down` as `$1` +- Migrations: `migrations/NNN-name.sh`, receiving `up`/`down`/`status` as `$1` - Applied history: `/var/lib/sysmig/applied` - applied entries never re-run - On failure the run aborts at that step; earlier steps stay applied +- `status` runs each applied migration's read-only drift check (exit 1 on + drift); DB checks need root, otherwise shown as `?` ## Adding a new migration 1. Create `migrations/NNN-name.sh` (number higher than existing; leave gaps) -2. Implement both `up` and `down` cases - both must actually work +2. Implement `up`, `down` and `status` cases - up/down must actually work; + `status` is a read-only check (exit 0 OK / 1 DRIFT / 2 unknown) 3. Commit & push, then on the server: `git pull && sudo bash sysmig/sysmig up` ## cloud-init integration diff --git a/migrations/001-create-swap.sh b/migrations/001-create-swap.sh index f6dde3f..a11a095 100644 --- a/migrations/001-create-swap.sh +++ b/migrations/001-create-swap.sh @@ -25,6 +25,14 @@ case "${1:-}" in fi echo " swap 2GB enabled + fstab entry + vm.swappiness=10" ;; + status) + swapon --show --noheadings 2>/dev/null | grep -q '^/swapfile ' \ + || { echo "DRIFT /swapfile not active"; exit 1; } + grep -q '^/swapfile ' /etc/fstab || { echo "DRIFT fstab entry missing"; exit 1; } + swp=$(cat /proc/sys/vm/swappiness 2>/dev/null || echo '?') + [[ $swp == 10 ]] || { echo "DRIFT vm.swappiness=$swp (want 10)"; exit 1; } + echo "OK swap active + fstab entry, swappiness=10" + ;; down) swapoff /swapfile rm -f /swapfile diff --git a/migrations/002-disable-networkd-dispatcher.sh b/migrations/002-disable-networkd-dispatcher.sh index 6aa0bdf..ea1c687 100644 --- a/migrations/002-disable-networkd-dispatcher.sh +++ b/migrations/002-disable-networkd-dispatcher.sh @@ -6,6 +6,15 @@ set -euo pipefail case "${1:-}" in up) systemctl disable --now networkd-dispatcher.service echo " networkd-dispatcher stopped and disabled" ;; + status) + en=$(systemctl is-enabled networkd-dispatcher.service 2>/dev/null || true) + ac=$(systemctl is-active networkd-dispatcher.service 2>/dev/null || true) + if [[ $en == disabled && $ac != active ]]; then + echo "OK networkd-dispatcher disabled, $ac" + else + echo "DRIFT networkd-dispatcher enabled=$en active=$ac (want disabled/inactive)" + exit 1 + fi ;; down) systemctl enable --now networkd-dispatcher.service echo " networkd-dispatcher re-enabled" ;; esac diff --git a/migrations/003-purge-exim4.sh b/migrations/003-purge-exim4.sh index 6adb4f3..381f9dc 100644 --- a/migrations/003-purge-exim4.sh +++ b/migrations/003-purge-exim4.sh @@ -11,6 +11,12 @@ case "${1:-}" in apt-get purge -y exim4-daemon-light >/dev/null apt-get autoremove --purge -y >/dev/null echo " exim4 stopped + package purged + deps cleaned" ;; + status) + if dpkg-query -W -f='${db:Status-Abbrev}' exim4-daemon-light 2>/dev/null | grep -q '^ii'; then + echo "DRIFT exim4-daemon-light is installed" + exit 1 + fi + echo "OK exim4-daemon-light not installed" ;; down) apt-get update -qq apt-get install -y exim4-daemon-light >/dev/null systemctl enable --now exim4.service diff --git a/migrations/004-purge-haveged.sh b/migrations/004-purge-haveged.sh index 4117710..21368a4 100644 --- a/migrations/004-purge-haveged.sh +++ b/migrations/004-purge-haveged.sh @@ -10,6 +10,12 @@ case "${1:-}" in apt-get purge -y haveged >/dev/null apt-get autoremove --purge -y >/dev/null echo " haveged stopped + package purged" ;; + status) + if dpkg-query -W -f='${db:Status-Abbrev}' haveged 2>/dev/null | grep -q '^ii'; then + echo "DRIFT haveged is installed" + exit 1 + fi + echo "OK haveged not installed" ;; down) apt-get update -qq apt-get install -y haveged >/dev/null systemctl enable --now haveged.service diff --git a/migrations/005-mount-pgdata.sh b/migrations/005-mount-pgdata.sh index 4f0004e..0ceb68b 100644 --- a/migrations/005-mount-pgdata.sh +++ b/migrations/005-mount-pgdata.sh @@ -30,6 +30,15 @@ case "${1:-}" in mount "$MOUNTPOINT" # via fstab entry - also validates it echo " $dev mounted at $MOUNTPOINT (noatime, fstab entry added)" ;; + status) + src=$(findmnt -rn -o SOURCE "$MOUNTPOINT" 2>/dev/null || true) + want=$(findfs UUID="$DISK_UUID" 2>/dev/null || true) + [[ -n $src ]] || { echo "DRIFT $MOUNTPOINT not mounted"; exit 1; } + [[ $src == "$want" ]] || { echo "DRIFT $MOUNTPOINT mounted from $src (want $want)"; exit 1; } + grep -q "^UUID=$DISK_UUID $MOUNTPOINT " /etc/fstab \ + || { echo "DRIFT fstab entry missing"; exit 1; } + echo "OK $src at $MOUNTPOINT + fstab entry" + ;; down) # sysmig rolls back in reverse order, so 006 (postgres) is already down if findmnt -rn "$MOUNTPOINT" >/dev/null; then umount "$MOUNTPOINT"; fi diff --git a/migrations/006-install-postgres.sh b/migrations/006-install-postgres.sh index d67791d..434de66 100644 --- a/migrations/006-install-postgres.sh +++ b/migrations/006-install-postgres.sh @@ -50,6 +50,18 @@ EOF fi pg_lsclusters --no-header ;; + status) + command -v pg_lsclusters >/dev/null 2>&1 \ + || { echo "DRIFT postgresql-common not installed"; exit 1; } + line=$(pg_lsclusters --no-header | awk -v v="$PGVER" '$1==v' | head -1) + [[ -n $line ]] || { echo "DRIFT no PG $PGVER cluster"; exit 1; } + read -r _ name _ st _ datadir _ <<<"$line" + [[ $st == online ]] || { echo "DRIFT cluster $PGVER/$name is '$st'"; exit 1; } + [[ $(findmnt -rn -o TARGET -T "$datadir") == "$PGROOT" ]] \ + || { echo "DRIFT $datadir not on $PGROOT"; exit 1; } + [[ -f $SRC ]] || { echo "DRIFT PGDG repo file $SRC missing"; exit 1; } + echo "OK PG $PGVER/$name online, data on $PGROOT, PGDG repo present" + ;; down) { pg_lsclusters --no-header 2>/dev/null || true; } | while read -r ver name _; do pg_ctlcluster "$ver" "$name" stop 2>/dev/null || true diff --git a/migrations/007-postgres-admin-role.sh b/migrations/007-postgres-admin-role.sh index 34a0e75..61bbf0c 100644 --- a/migrations/007-postgres-admin-role.sh +++ b/migrations/007-postgres-admin-role.sh @@ -29,6 +29,20 @@ case "${1:-}" in "SELECT current_user || ' (superuser=' || rolsuper || ')' FROM pg_roles WHERE rolname = current_user") echo " peer-auth check: $check" ;; + status) + if [[ $EUID -eq 0 ]]; then + q() { runuser -u postgres -- psql -d postgres -v ON_ERROR_STOP=1 -tAc "$1"; } + elif psql -d postgres -tAc 'SELECT 1' >/dev/null 2>&1; then + q() { psql -d postgres -v ON_ERROR_STOP=1 -tAc "$1"; } + else + echo "? needs root or peer DB access to verify" + exit 2 + fi + row=$(q "SELECT rolsuper::text || '/' || rolcanlogin FROM pg_roles WHERE rolname='$ROLE'") + [[ -n $row ]] || { echo "DRIFT role $ROLE does not exist"; exit 1; } + [[ $row == true/true ]] || { echo "DRIFT role $ROLE super/login=$row (want true/true)"; exit 1; } + echo "OK role $ROLE exists: superuser + login" + ;; down) if role_exists; then if runuser -u postgres -- psql -v ON_ERROR_STOP=1 -qc "DROP ROLE $ROLE"; then diff --git a/migrations/008-create-db-iwanhae.sh b/migrations/008-create-db-iwanhae.sh index 85c6c8e..89cd7ab 100644 --- a/migrations/008-create-db-iwanhae.sh +++ b/migrations/008-create-db-iwanhae.sh @@ -30,6 +30,20 @@ case "${1:-}" in echo " database $DB created, owner $ROLE" fi ;; + status) + if [[ $EUID -eq 0 ]]; then + q() { runuser -u postgres -- psql -d postgres -v ON_ERROR_STOP=1 -tAc "$1"; } + elif psql -d postgres -tAc 'SELECT 1' >/dev/null 2>&1; then + q() { psql -d postgres -v ON_ERROR_STOP=1 -tAc "$1"; } + else + echo "? needs root or peer DB access to verify" + exit 2 + fi + row=$(q "SELECT pg_get_userbyid(datdba) FROM pg_database WHERE datname='$DB'") + [[ -n $row ]] || { echo "DRIFT database $DB does not exist"; exit 1; } + [[ $row == "$ROLE" ]] || { echo "DRIFT database $DB owner=$row (want $ROLE)"; exit 1; } + echo "OK database $DB exists, owner $ROLE" + ;; down) if ! db_exists; then echo " database $DB does not exist - nothing to do" diff --git a/sysmig b/sysmig index 1771496..0af7796 100755 --- a/sysmig +++ b/sysmig @@ -5,13 +5,13 @@ # sudo bash sysmig up Apply all pending migrations in order # sudo bash sysmig down Roll back the last applied migration # sudo bash sysmig down all Roll back everything (reverse order) -# bash sysmig status Show status (no root required) +# bash sysmig status History + drift check (root: full detail) # -# - Migration files: ./migrations/NNN-*.sh (receive up|down as $1) +# - Migration files: ./migrations/NNN-*.sh (up|down|status via $1) # - Applied history: /var/lib/sysmig/applied (one filename per line, in order) -# - Adding work: drop a numbered file into migrations/ -> picked up on next up -# (e.g. 005-install-postgres.sh, 010-tune-postgres.sh ...) +# - status case: read-only check, one output line, exit 0 OK / 1 DRIFT / 2 ? # - Safe to re-run on every boot (cloud-init); idempotent +# - Adding work: drop NNN-*.sh into migrations/ -> applied on next up # ============================================================================ set -euo pipefail @@ -62,15 +62,29 @@ cmd_down_last() { cmd_status() { say "migration status (history: $STATE_FILE)" - local f name + local f name line rc drift=0 for f in "$MIG_DIR"/*.sh; do name=$(basename "$f") - if is_applied "$name"; then note "[applied] $name" - else note "[pending] $name"; fi + if ! is_applied "$name"; then note "[pending] $name"; continue; fi + rc=0; line=$(bash "$f" status 2>/dev/null) || rc=$? + if [[ $rc -eq 0 ]]; then + if [[ -n $line ]]; then note "[applied ✓] $name: $line" + else note "[applied ?] $name: (no status impl)"; fi + elif [[ $rc -eq 2 ]]; then + note "[applied ?] $name: $line" + else + drift=1 + note "[applied ✗ DRIFT] $name: $line" + fi done + if (( drift )); then + echo + say "DRIFT detected - inspect above; repair manually or roll back/forward (down/up)" + fi echo; say "memory / swap" free -h swapon --show 2>/dev/null || true + exit $drift } usage() { sed -n '2,15p' "$0"; exit 1; }