sysmig: migration runner + initial 4 migrations (swap, daemon trims)
- up/down/status interface, history in /var/lib/sysmig/applied - 001 swap 2GB + swappiness=10, 002-004 trim unneeded daemons (~56MB) - google-osconfig-agent intentionally kept - all messages/comments in ASCII English (console font safety)
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
# sysmig
|
||||||
|
|
||||||
|
Lightweight tool that manages host state in DB-migration (Flyway) style.
|
||||||
|
Started for a 1GB VM (`ocrt-postgres`), but deployable to any machine
|
||||||
|
via cloud-init.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Migrations: `migrations/NNN-name.sh`, receiving `up`/`down` 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
3. Commit & push, then on the server: `git pull && sudo bash sysmig/sysmig up`
|
||||||
|
|
||||||
|
## cloud-init integration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
#cloud-config
|
||||||
|
runcmd:
|
||||||
|
- [ git, clone, <REPO_URL>, /opt/sysmig ]
|
||||||
|
- [ bash, /opt/sysmig/sysmig, up ]
|
||||||
|
```
|
||||||
|
|
||||||
|
For a private repo you need an auth strategy:
|
||||||
|
- **deploy key** (existing machines): register a read-only key on the repo
|
||||||
|
- **fleet rollout**: make the repo public, or embed a read-only token in
|
||||||
|
the https URL
|
||||||
|
|
||||||
|
## Current migrations
|
||||||
|
|
||||||
|
| # | name | effect |
|
||||||
|
|----|----------------------------|-----------------------------------------------|
|
||||||
|
| 001 | create-swap | 2GB swap + fstab + vm.swappiness=10 |
|
||||||
|
| 002 | disable-networkd-dispatcher| reclaim ~27MB RAM |
|
||||||
|
| 003 | purge-exim4 | reclaim ~21MB RAM (package removed) |
|
||||||
|
| 004 | purge-haveged | reclaim ~8MB RAM (package removed) |
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- `005-install-postgres` - install PostgreSQL + create cluster
|
||||||
|
- `010-tune-postgres` - 1GB tuning (shared_buffers=128MB, work_mem=4MB,
|
||||||
|
max_connections=30, ...)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 001-create-swap - Create 2GB swap + fstab entry + vm.swappiness=10
|
||||||
|
# up: enable swap (skipped if swap already active)
|
||||||
|
# down: disable swap and remove file/fstab/sysctl entries
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
up)
|
||||||
|
if swapon --show --noheadings | grep -q .; then
|
||||||
|
echo " swap already active - skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ ! -f /swapfile ]]; then
|
||||||
|
fallocate -l 2G /swapfile || {
|
||||||
|
dd if=/dev/zero of=/swapfile bs=1M count=2048 status=none
|
||||||
|
}
|
||||||
|
chmod 600 /swapfile
|
||||||
|
mkswap /swapfile >/dev/null
|
||||||
|
fi
|
||||||
|
swapon /swapfile
|
||||||
|
grep -q '^/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
||||||
|
if ! grep -rqs 'vm.swappiness' /etc/sysctl.d/ /etc/sysctl.conf; then
|
||||||
|
echo 'vm.swappiness=10' > /etc/sysctl.d/99-swap.conf
|
||||||
|
sysctl -p /etc/sysctl.d/99-swap.conf >/dev/null
|
||||||
|
fi
|
||||||
|
echo " swap 2GB enabled + fstab entry + vm.swappiness=10"
|
||||||
|
;;
|
||||||
|
down)
|
||||||
|
swapoff /swapfile
|
||||||
|
rm -f /swapfile
|
||||||
|
sed -i '\|^/swapfile|d' /etc/fstab
|
||||||
|
grep -q '^vm.swappiness=10$' /etc/sysctl.d/99-swap.conf 2>/dev/null \
|
||||||
|
&& rm -f /etc/sysctl.d/99-swap.conf || true
|
||||||
|
echo " swap removed"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 002-disable-networkd-dispatcher - Stop network event hook daemon (~27MB)
|
||||||
|
# Unnecessary on a simple server with static network config. Package kept.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
up) systemctl disable --now networkd-dispatcher.service
|
||||||
|
echo " networkd-dispatcher stopped and disabled" ;;
|
||||||
|
down) systemctl enable --now networkd-dispatcher.service
|
||||||
|
echo " networkd-dispatcher re-enabled" ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 003-purge-exim4 - Remove default local mail server (~21MB)
|
||||||
|
# Unnecessary if local mail (e.g. cron output) is not used.
|
||||||
|
# Down reinstalls with Debian defaults (local delivery only).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
up) systemctl disable --now exim4.service 2>/dev/null || true
|
||||||
|
apt-get purge -y exim4-daemon-light >/dev/null
|
||||||
|
apt-get autoremove --purge -y >/dev/null
|
||||||
|
echo " exim4 stopped + package purged + deps cleaned" ;;
|
||||||
|
down) apt-get update -qq
|
||||||
|
apt-get install -y exim4-daemon-light >/dev/null
|
||||||
|
systemctl enable --now exim4.service
|
||||||
|
echo " exim4 reinstalled + restarted (default config)" ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 004-purge-haveged - Remove entropy daemon (~8MB)
|
||||||
|
# Kernel 6.12 collects enough entropy on its own; haveged is redundant.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
up) systemctl disable --now haveged.service 2>/dev/null || true
|
||||||
|
apt-get purge -y haveged >/dev/null
|
||||||
|
apt-get autoremove --purge -y >/dev/null
|
||||||
|
echo " haveged stopped + package purged" ;;
|
||||||
|
down) apt-get update -qq
|
||||||
|
apt-get install -y haveged >/dev/null
|
||||||
|
systemctl enable --now haveged.service
|
||||||
|
echo " haveged reinstalled + restarted" ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# sysmig - Host state migration tool (DB-migration style)
|
||||||
|
#
|
||||||
|
# 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)
|
||||||
|
#
|
||||||
|
# - Migration files: ./migrations/NNN-*.sh (receive up|down as $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 ...)
|
||||||
|
# - Safe to re-run on every boot (cloud-init); idempotent
|
||||||
|
# ============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MIG_DIR="$(cd "$(dirname "$0")/migrations" && pwd)"
|
||||||
|
STATE_DIR=/var/lib/sysmig
|
||||||
|
STATE_FILE=$STATE_DIR/applied
|
||||||
|
|
||||||
|
say() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
|
||||||
|
note() { printf ' %s\n' "$*"; }
|
||||||
|
|
||||||
|
require_root() {
|
||||||
|
[[ $EUID -eq 0 ]] || { echo "root required: sudo bash $0 ..." >&2; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
is_applied() { grep -qx "$1" "$STATE_FILE" 2>/dev/null; }
|
||||||
|
|
||||||
|
cmd_up() {
|
||||||
|
require_root
|
||||||
|
mkdir -p "$STATE_DIR"; touch "$STATE_FILE"
|
||||||
|
local n=0 f name
|
||||||
|
for f in "$MIG_DIR"/*.sh; do
|
||||||
|
name=$(basename "$f")
|
||||||
|
is_applied "$name" && continue
|
||||||
|
say "UP $name"
|
||||||
|
if bash "$f" up; then
|
||||||
|
echo "$name" >> "$STATE_FILE"
|
||||||
|
n=$((n+1))
|
||||||
|
else
|
||||||
|
echo "FAILED - aborting (earlier migrations stay applied)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ $n -eq 0 ]]; then note "all migrations already applied"
|
||||||
|
else note "$n migration(s) applied, recorded in state file"; fi
|
||||||
|
echo; say "memory"; free -h
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_down_last() {
|
||||||
|
require_root
|
||||||
|
[[ -s "$STATE_FILE" ]] || { note "nothing to roll back"; return 0; }
|
||||||
|
local name; name=$(tail -n 1 "$STATE_FILE")
|
||||||
|
local f="$MIG_DIR/$name"
|
||||||
|
[[ -f "$f" ]] || { echo "migration file not found: $f" >&2; exit 1; }
|
||||||
|
say "DOWN $name"
|
||||||
|
bash "$f" down
|
||||||
|
sed -i '$d' "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
say "migration status (history: $STATE_FILE)"
|
||||||
|
local f name
|
||||||
|
for f in "$MIG_DIR"/*.sh; do
|
||||||
|
name=$(basename "$f")
|
||||||
|
if is_applied "$name"; then note "[applied] $name"
|
||||||
|
else note "[pending] $name"; fi
|
||||||
|
done
|
||||||
|
echo; say "memory / swap"
|
||||||
|
free -h
|
||||||
|
swapon --show 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() { sed -n '2,15p' "$0"; exit 1; }
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
up) cmd_up ;;
|
||||||
|
down) require_root
|
||||||
|
if [[ "${2:-}" == "all" ]]; then
|
||||||
|
while [[ -s "$STATE_FILE" ]]; do cmd_down_last; done
|
||||||
|
note "full rollback complete"
|
||||||
|
else
|
||||||
|
cmd_down_last
|
||||||
|
fi ;;
|
||||||
|
status) cmd_status ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user