Files
vps/Makefile
T
windyboy ab808088b8 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.
2026-08-26 11:27:02 +08:00

184 lines
6.6 KiB
Makefile

# 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=<group|host> CONFIRM=1 TARGETS=<svc[,svc]> 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)