From ab808088b8bc71e342bd40a22aa70c066f55f095 Mon Sep 17 00:00:00 2001 From: windyboy Date: Wed, 26 Aug 2026 11:26:53 +0800 Subject: [PATCH] Add production Makefile for routine VPS ops Wrap validate-repo.sh and routine Ansible playbooks with safe-by-default targets: read-only health/audit flows, CONFIRM=1 gates for mutating work, and LIMIT/TARGETS guards. Document entry point in AGENTS.md. --- AGENTS.md | 2 + Makefile | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 Makefile diff --git a/AGENTS.md b/AGENTS.md index f62a01c..676c7ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -100,6 +100,7 @@ not maintain a second copy of the machine table here. | UniFi local-service proxy bypass | [docs/unifi-openclash-localhost.md](docs/unifi-openclash-localhost.md) | | UniFi SSO login setting (Ansible) | `cd ansible && ansible-playbook playbooks/unifi-sso.yml --limit unifi` | | Routine Ansible operations | [runbooks/ansible-operations.md](runbooks/ansible-operations.md) | +| Routine make commands | `make help` (wraps `ansible-operations.md` read-only + gated flows) | | Issue → mergeable change | [runbooks/issue-to-merge.md](runbooks/issue-to-merge.md) | | Fix failing health/playbook run | [runbooks/fix-ci.md](runbooks/fix-ci.md) | | Release a reviewed change | [runbooks/release.md](runbooks/release.md) | @@ -166,4 +167,5 @@ hosts/ # per-host facts runbooks/ # step-by-step ops (README.md = index, _template.md = template) docs/ # upstream refs / design notes / research records (active + archive/) scripts/validate-repo.sh # repo-wide validation (run before merging) +Makefile # routine validate / health / gated ansible wrappers ``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..06c5526 --- /dev/null +++ b/Makefile @@ -0,0 +1,183 @@ +# VPS ops hub — routine validate / health / gated Ansible wrappers. +# See runbooks/ansible-operations.md for playbook semantics. + +SHELL := /usr/bin/env bash +.SHELLFLAGS := -eu -o pipefail -c + +.DEFAULT_GOAL := help + +REPO_ROOT := $(CURDIR) +ANSIBLE_DIR := $(REPO_ROOT)/ansible +export ANSIBLE_LOCAL_TEMP := $(REPO_ROOT)/.ansible/tmp +export ANSIBLE_HOME := $(REPO_ROOT)/.ansible + +LIMIT ?= +EXTRA ?= +VERBOSE ?= 0 +CONFIRM ?= 0 +TARGETS ?= +TRAEFIK ?= 0 + +LIMIT_FLAG := $(if $(LIMIT),--limit $(LIMIT),) +VERBOSE_FLAG := $(if $(filter 1,$(VERBOSE)),-v,$(if $(filter 2,$(VERBOSE)),-vvv,)) + +.PHONY: help validate check deps galaxy syntax ansible-prep \ + ping inventory audit health health-mailcow health-matrix \ + maint-preview baseline compose-check \ + install-healthchecks install-matrix-healthchecks compose-deploy reconcile + +help: + @printf '%s\n' \ + 'VPS ops hub — make targets (run from repo root)' \ + '' \ + 'Variables: LIMIT= CONFIRM=1 TARGETS= TRAEFIK=1 VERBOSE=0|1|2 EXTRA=...' \ + '' \ + 'Local / repo:' \ + ' validate, check scripts/validate-repo.sh (pre-merge gate)' \ + ' deps, galaxy ansible-galaxy collection install' \ + ' syntax ansible-playbook --syntax-check all playbooks' \ + '' \ + 'Read-only remote (ansible):' \ + ' ping ansible managed -m ping' \ + ' inventory ansible-inventory --graph' \ + ' audit playbooks/audit.yml' \ + ' health [LIMIT=…] playbooks/health-report.yml' \ + ' health-mailcow health --limit mailcow' \ + ' health-matrix health --limit matrix' \ + ' maint-preview playbooks/maintenance-preview.yml' \ + ' baseline playbooks/baseline.yml' \ + ' compose-check compose-deploy --check --diff (requires LIMIT=)' \ + '' \ + 'Mutating (require CONFIRM=1; host-scoped targets require LIMIT=):' \ + ' install-healthchecks playbooks/healthchecks.yml' \ + ' install-matrix-healthchecks playbooks/matrix-healthchecks.yml' \ + ' compose-deploy playbooks/compose-deploy.yml' \ + ' reconcile playbooks/compose-reconcile.yml (requires TARGETS=)' \ + '' \ + 'Examples:' \ + ' make validate' \ + ' make health LIMIT=mailcow' \ + ' make compose-check LIMIT=vaultwarden' \ + ' make compose-deploy LIMIT=vaultwarden CONFIRM=1' \ + ' make reconcile LIMIT=powerdns TARGETS=auth CONFIRM=1' \ + ' make reconcile LIMIT=vaultwarden TARGETS=vaultwarden TRAEFIK=1 CONFIRM=1' \ + '' \ + 'Advanced (not wrapped — use ansible-playbook directly):' \ + ' us4-firewalld, unifi-sso, k3s-server, matrix-stack, wireguard-harden,' \ + ' restic, rustdesk, email-alerts, mailcow update runbook' + +validate check: + @bash "$(REPO_ROOT)/scripts/validate-repo.sh" + +deps galaxy: ansible-prep + @command -v ansible-galaxy >/dev/null 2>&1 || { echo "ansible-galaxy not found; install Ansible first." >&2; exit 1; } + @cd "$(ANSIBLE_DIR)" && ansible-galaxy collection install -r requirements.yml + +syntax: ansible-prep + @if ! command -v ansible-playbook >/dev/null 2>&1; then \ + echo "ansible-playbook not found; syntax check skipped." >&2; \ + exit 0; \ + fi + @fail=0; \ + for p in "$(ANSIBLE_DIR)"/playbooks/*.yml; do \ + if ! (cd "$(ANSIBLE_DIR)" && ansible-playbook --syntax-check "playbooks/$$(basename "$$p")" >/dev/null 2>&1); then \ + echo "syntax-check failed: $$p" >&2; \ + fail=1; \ + fi; \ + done; \ + exit $$fail + +ansible-prep: + @mkdir -p "$(ANSIBLE_HOME)/tmp" "$(ANSIBLE_HOME)/ssh-control" + +define require_ansible + @command -v ansible-playbook >/dev/null 2>&1 || { echo "ansible-playbook not found; install Ansible first." >&2; exit 1; } +endef + +define require_limit + @if [ -z "$(LIMIT)" ]; then \ + echo "LIMIT is required (e.g. LIMIT=mailcow, LIMIT=vaultwarden, LIMIT=powerdns)." >&2; \ + exit 1; \ + fi +endef + +define require_confirm + @if [ "$(CONFIRM)" != "1" ]; then \ + echo "Mutating operation blocked. Re-run with CONFIRM=1" >&2; \ + exit 1; \ + fi +endef + +define require_targets + @if [ -z "$(TARGETS)" ]; then \ + echo "TARGETS is required (comma-separated service names, e.g. TARGETS=auth or TARGETS=vaultwarden)." >&2; \ + exit 1; \ + fi +endef + +ping: ansible-prep + $(require_ansible) + @cd "$(ANSIBLE_DIR)" && ansible managed -m ping $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA) + +inventory: ansible-prep + $(require_ansible) + @cd "$(ANSIBLE_DIR)" && ansible-inventory --graph $(EXTRA) + +audit: ansible-prep + $(require_ansible) + @cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/audit.yml $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA) + +health: ansible-prep + $(require_ansible) + @cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/health-report.yml $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA) + +health-mailcow: ansible-prep + $(require_ansible) + @cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/health-report.yml --limit mailcow $(VERBOSE_FLAG) $(EXTRA) + +health-matrix: ansible-prep + $(require_ansible) + @cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/health-report.yml --limit matrix $(VERBOSE_FLAG) $(EXTRA) + +maint-preview: ansible-prep + $(require_ansible) + @cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/maintenance-preview.yml $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA) + +baseline: ansible-prep + $(require_ansible) + @cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/baseline.yml $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA) + +compose-check: ansible-prep + $(require_ansible) + $(require_limit) + @cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/compose-deploy.yml --check --diff --limit $(LIMIT) $(VERBOSE_FLAG) $(EXTRA) + +install-healthchecks: ansible-prep + $(require_ansible) + $(require_confirm) + @cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/healthchecks.yml $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA) + +install-matrix-healthchecks: ansible-prep + $(require_ansible) + $(require_confirm) + @cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/matrix-healthchecks.yml $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA) + +compose-deploy: ansible-prep + $(require_ansible) + $(require_limit) + $(require_confirm) + @cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/compose-deploy.yml --limit $(LIMIT) \ + -e '{"compose_deploy_confirm": true}' $(VERBOSE_FLAG) $(EXTRA) + +reconcile: ansible-prep + $(require_ansible) + $(require_limit) + $(require_targets) + $(require_confirm) + @json=$$(python3 -c 'import json,sys; t=[x.strip() for x in sys.argv[1].split(",") if x.strip()]; \ + (not t) and sys.exit("TARGETS must contain at least one non-empty service name"); \ + d={"service_reconcile_confirm": True, "service_reconcile_targets": t}; \ + (sys.argv[2]=="1") and d.update({"service_reconcile_restart_traefik": True}); \ + print(json.dumps(d))' "$(TARGETS)" "$(TRAEFIK)"); \ + cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/compose-reconcile.yml --limit $(LIMIT) \ + -e "$$json" $(VERBOSE_FLAG) $(EXTRA)