add per-migration status case (read-only drift check), integrate into sysmig status
status convention: print one line, exit 0 OK / 1 DRIFT / 2 unknown (e.g. needs root). sysmig status now shows [applied ✓/✗/?] per migration and exits 1 on drift. DB checks fall back to peer auth when non-root.
This commit is contained in:
@@ -10,17 +10,20 @@ via cloud-init.
|
|||||||
sudo bash /opt/sysmig/sysmig up # apply all pending migrations (idempotent)
|
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 # roll back the last migration
|
||||||
sudo bash /opt/sysmig/sysmig down all # roll back everything (reverse order)
|
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
|
- Applied history: `/var/lib/sysmig/applied` - applied entries never re-run
|
||||||
- On failure the run aborts at that step; earlier steps stay applied
|
- 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
|
## Adding a new migration
|
||||||
|
|
||||||
1. Create `migrations/NNN-name.sh` (number higher than existing; leave gaps)
|
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`
|
3. Commit & push, then on the server: `git pull && sudo bash sysmig/sysmig up`
|
||||||
|
|
||||||
## cloud-init integration
|
## cloud-init integration
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ case "${1:-}" in
|
|||||||
fi
|
fi
|
||||||
echo " swap 2GB enabled + fstab entry + vm.swappiness=10"
|
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)
|
down)
|
||||||
swapoff /swapfile
|
swapoff /swapfile
|
||||||
rm -f /swapfile
|
rm -f /swapfile
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ set -euo pipefail
|
|||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
up) systemctl disable --now networkd-dispatcher.service
|
up) systemctl disable --now networkd-dispatcher.service
|
||||||
echo " networkd-dispatcher stopped and disabled" ;;
|
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
|
down) systemctl enable --now networkd-dispatcher.service
|
||||||
echo " networkd-dispatcher re-enabled" ;;
|
echo " networkd-dispatcher re-enabled" ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ case "${1:-}" in
|
|||||||
apt-get purge -y exim4-daemon-light >/dev/null
|
apt-get purge -y exim4-daemon-light >/dev/null
|
||||||
apt-get autoremove --purge -y >/dev/null
|
apt-get autoremove --purge -y >/dev/null
|
||||||
echo " exim4 stopped + package purged + deps cleaned" ;;
|
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
|
down) apt-get update -qq
|
||||||
apt-get install -y exim4-daemon-light >/dev/null
|
apt-get install -y exim4-daemon-light >/dev/null
|
||||||
systemctl enable --now exim4.service
|
systemctl enable --now exim4.service
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ case "${1:-}" in
|
|||||||
apt-get purge -y haveged >/dev/null
|
apt-get purge -y haveged >/dev/null
|
||||||
apt-get autoremove --purge -y >/dev/null
|
apt-get autoremove --purge -y >/dev/null
|
||||||
echo " haveged stopped + package purged" ;;
|
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
|
down) apt-get update -qq
|
||||||
apt-get install -y haveged >/dev/null
|
apt-get install -y haveged >/dev/null
|
||||||
systemctl enable --now haveged.service
|
systemctl enable --now haveged.service
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ case "${1:-}" in
|
|||||||
mount "$MOUNTPOINT" # via fstab entry - also validates it
|
mount "$MOUNTPOINT" # via fstab entry - also validates it
|
||||||
echo " $dev mounted at $MOUNTPOINT (noatime, fstab entry added)"
|
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)
|
down)
|
||||||
# sysmig rolls back in reverse order, so 006 (postgres) is already down
|
# sysmig rolls back in reverse order, so 006 (postgres) is already down
|
||||||
if findmnt -rn "$MOUNTPOINT" >/dev/null; then umount "$MOUNTPOINT"; fi
|
if findmnt -rn "$MOUNTPOINT" >/dev/null; then umount "$MOUNTPOINT"; fi
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ EOF
|
|||||||
fi
|
fi
|
||||||
pg_lsclusters --no-header
|
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)
|
down)
|
||||||
{ pg_lsclusters --no-header 2>/dev/null || true; } | while read -r ver name _; do
|
{ pg_lsclusters --no-header 2>/dev/null || true; } | while read -r ver name _; do
|
||||||
pg_ctlcluster "$ver" "$name" stop 2>/dev/null || true
|
pg_ctlcluster "$ver" "$name" stop 2>/dev/null || true
|
||||||
|
|||||||
@@ -29,6 +29,20 @@ case "${1:-}" in
|
|||||||
"SELECT current_user || ' (superuser=' || rolsuper || ')' FROM pg_roles WHERE rolname = current_user")
|
"SELECT current_user || ' (superuser=' || rolsuper || ')' FROM pg_roles WHERE rolname = current_user")
|
||||||
echo " peer-auth check: $check"
|
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)
|
down)
|
||||||
if role_exists; then
|
if role_exists; then
|
||||||
if runuser -u postgres -- psql -v ON_ERROR_STOP=1 -qc "DROP ROLE $ROLE"; then
|
if runuser -u postgres -- psql -v ON_ERROR_STOP=1 -qc "DROP ROLE $ROLE"; then
|
||||||
|
|||||||
@@ -30,6 +30,20 @@ case "${1:-}" in
|
|||||||
echo " database $DB created, owner $ROLE"
|
echo " database $DB created, owner $ROLE"
|
||||||
fi
|
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)
|
down)
|
||||||
if ! db_exists; then
|
if ! db_exists; then
|
||||||
echo " database $DB does not exist - nothing to do"
|
echo " database $DB does not exist - nothing to do"
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
# sudo bash sysmig up Apply all pending migrations in order
|
# sudo bash sysmig up Apply all pending migrations in order
|
||||||
# sudo bash sysmig down Roll back the last applied migration
|
# sudo bash sysmig down Roll back the last applied migration
|
||||||
# sudo bash sysmig down all Roll back everything (reverse order)
|
# 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)
|
# - 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
|
# - status case: read-only check, one output line, exit 0 OK / 1 DRIFT / 2 ?
|
||||||
# (e.g. 005-install-postgres.sh, 010-tune-postgres.sh ...)
|
|
||||||
# - Safe to re-run on every boot (cloud-init); idempotent
|
# - Safe to re-run on every boot (cloud-init); idempotent
|
||||||
|
# - Adding work: drop NNN-*.sh into migrations/ -> applied on next up
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -62,15 +62,29 @@ cmd_down_last() {
|
|||||||
|
|
||||||
cmd_status() {
|
cmd_status() {
|
||||||
say "migration status (history: $STATE_FILE)"
|
say "migration status (history: $STATE_FILE)"
|
||||||
local f name
|
local f name line rc drift=0
|
||||||
for f in "$MIG_DIR"/*.sh; do
|
for f in "$MIG_DIR"/*.sh; do
|
||||||
name=$(basename "$f")
|
name=$(basename "$f")
|
||||||
if is_applied "$name"; then note "[applied] $name"
|
if ! is_applied "$name"; then note "[pending] $name"; continue; fi
|
||||||
else note "[pending] $name"; 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
|
done
|
||||||
|
if (( drift )); then
|
||||||
|
echo
|
||||||
|
say "DRIFT detected - inspect above; repair manually or roll back/forward (down/up)"
|
||||||
|
fi
|
||||||
echo; say "memory / swap"
|
echo; say "memory / swap"
|
||||||
free -h
|
free -h
|
||||||
swapon --show 2>/dev/null || true
|
swapon --show 2>/dev/null || true
|
||||||
|
exit $drift
|
||||||
}
|
}
|
||||||
|
|
||||||
usage() { sed -n '2,15p' "$0"; exit 1; }
|
usage() { sed -n '2,15p' "$0"; exit 1; }
|
||||||
|
|||||||
Reference in New Issue
Block a user