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:
wan
2026-08-31 15:49:01 +09:00
commit f69cb99b3f
6 changed files with 224 additions and 0 deletions
+53
View File
@@ -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, ...)
+36
View File
@@ -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
+18
View File
@@ -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
+17
View File
@@ -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
+89
View File
@@ -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