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.
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
@@ -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=<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)
|
||||
Reference in New Issue
Block a user