# 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)