From f69cb99b3f8d8235f132d1afd41a08d0a24d9143 Mon Sep 17 00:00:00 2001 From: iwanhae Date: Mon, 31 Aug 2026 15:46:47 +0900 Subject: [PATCH] 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) --- README.md | 53 +++++++++++ migrations/001-create-swap.sh | 36 ++++++++ migrations/002-disable-networkd-dispatcher.sh | 11 +++ migrations/003-purge-exim4.sh | 18 ++++ migrations/004-purge-haveged.sh | 17 ++++ sysmig | 89 +++++++++++++++++++ 6 files changed, 224 insertions(+) create mode 100644 README.md create mode 100644 migrations/001-create-swap.sh create mode 100644 migrations/002-disable-networkd-dispatcher.sh create mode 100644 migrations/003-purge-exim4.sh create mode 100644 migrations/004-purge-haveged.sh create mode 100644 sysmig diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d2b47f --- /dev/null +++ b/README.md @@ -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, , /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, ...) diff --git a/migrations/001-create-swap.sh b/migrations/001-create-swap.sh new file mode 100644 index 0000000..f6dde3f --- /dev/null +++ b/migrations/001-create-swap.sh @@ -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 diff --git a/migrations/002-disable-networkd-dispatcher.sh b/migrations/002-disable-networkd-dispatcher.sh new file mode 100644 index 0000000..6aa0bdf --- /dev/null +++ b/migrations/002-disable-networkd-dispatcher.sh @@ -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 diff --git a/migrations/003-purge-exim4.sh b/migrations/003-purge-exim4.sh new file mode 100644 index 0000000..6adb4f3 --- /dev/null +++ b/migrations/003-purge-exim4.sh @@ -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 diff --git a/migrations/004-purge-haveged.sh b/migrations/004-purge-haveged.sh new file mode 100644 index 0000000..4117710 --- /dev/null +++ b/migrations/004-purge-haveged.sh @@ -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 diff --git a/sysmig b/sysmig new file mode 100644 index 0000000..1771496 --- /dev/null +++ b/sysmig @@ -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