Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02994-7685-7940-bf34-3555b8256018
385 lines
21 KiB
Makefile
385 lines
21 KiB
Makefile
# -------- RailianceHosts Make Utilities --------
|
||
SHELL := /usr/bin/env bash
|
||
.DEFAULT_GOAL := help
|
||
|
||
# Set this to your Gitea host if you want 'remote-set' helper
|
||
GITEA ?= gitea.example.com
|
||
OWNER ?= coulomb
|
||
REPO ?= railiance-infra
|
||
SOURCE_REVISION ?= $(shell git rev-parse HEAD 2>/dev/null)
|
||
|
||
# New-host defaults (can be overridden: make new-host NAME=... TYPE=...)
|
||
TYPE ?= cpx11
|
||
REGION ?= nbg1
|
||
ROLE ?= core
|
||
IMG ?= ubuntu-24.04
|
||
USER ?= admin
|
||
|
||
# Decrypt Hetzner token at runtime (requires SOPS_AGE_KEY or keys.txt locally)
|
||
HCLOUD_TOKEN := $(shell sops -d --extract '["hetzner"]["token"]' secrets/hetzner-token.yaml 2>/dev/null)
|
||
|
||
# ---- Help ----
|
||
help: ## Show this help
|
||
@echo "RailianceHosts Commands"; \
|
||
grep -E '^[a-zA-Z0-9_-]+:.*?## ' $(MAKEFILE_LIST) | sort | sed 's/:.*##/: /'
|
||
|
||
# ---- Git hooks ----
|
||
hooks: ## Configure git to use repo-local hooks (.githooks) and ensure executables
|
||
@mkdir -p .githooks
|
||
git config core.hooksPath .githooks
|
||
@test -f .githooks/pre-commit || (echo "❌ Missing .githooks/pre-commit"; exit 1)
|
||
chmod +x .githooks/pre-commit
|
||
@echo "✔ hooks enabled and pre-commit is executable"
|
||
|
||
hooks-test: ## Test secrets hook blocks plaintext in secrets/
|
||
@mkdir -p secrets && echo 'PLAINTEXT_TEST=true' > secrets/_hook_test.yaml
|
||
@git add secrets/_hook_test.yaml || true
|
||
@if git commit -m "TEST: should be blocked" 2>/dev/null; then \
|
||
echo "❌ Hook did NOT block plaintext (check .githooks/pre-commit)"; \
|
||
git reset --soft HEAD~1; \
|
||
else \
|
||
echo "✔ Hook blocked plaintext as expected"; \
|
||
fi
|
||
@git restore --staged secrets/_hook_test.yaml || true
|
||
@rm -f secrets/_hook_test.yaml
|
||
|
||
# ---- SOPS / Age helpers ----
|
||
sops-setup: ## Copy age key to SOPS default path (~/.config/sops/age/keys.txt)
|
||
mkdir -p ~/.config/sops/age
|
||
cp -n ~/.config/age/key.txt ~/.config/sops/age/keys.txt || true
|
||
chmod 600 ~/.config/sops/age/keys.txt
|
||
@echo "✔ SOPS key path set (~/.config/sops/age/keys.txt). Alternatively export SOPS_AGE_KEY."
|
||
|
||
sops-edit: ## Edit the global secrets with SOPS
|
||
sops secrets/hetzner-token.yaml
|
||
|
||
sops-encrypt: ## Encrypt a file in place: make sops-encrypt FILE=secrets/foo.yaml
|
||
@[ -n "$(FILE)" ] || (echo "Usage: make sops-encrypt FILE=secrets/xxx.yaml" && exit 1)
|
||
sops --encrypt --in-place $(FILE)
|
||
@echo "✔ Encrypted $(FILE)"
|
||
|
||
sops-decrypt: ## Print decrypted file to stdout (for inspection) FILE=secrets/foo.sops.yaml
|
||
@[ -n "$(FILE)" ] || (echo "Usage: make sops-decrypt FILE=secrets/xxx.sops.yaml" && exit 1)
|
||
sops -d $(FILE)
|
||
|
||
sops-rotate: ## Check SOPS recipient drift; use the bounded tool for approved changes
|
||
python3 scripts/sops_rotation.py --check
|
||
|
||
check-secrets: ## Fail if any declared secret-bearing path is not encrypted
|
||
python3 scripts/check_secret_paths.py --tracked
|
||
|
||
# ---- Terraform (Hetzner) ----
|
||
validate-inventory: ## Validate adopted/provider-managed host declarations without provider access
|
||
python3 scripts/inventory_contract.py inventory/servers.yaml
|
||
|
||
validate-baseline: ## Validate the executable baseline and its Ansible/Goss consumers
|
||
python3 scripts/baseline_contract.py --check-repo
|
||
|
||
validate-handoff-readonly: ## Prove the live S1 handoff playbook has no remote mutation surface
|
||
python3 scripts/handoff_contract.py
|
||
|
||
validate-receipts: ## Validate committed metadata-only S1 receipt examples
|
||
python3 scripts/s1_receipt.py docs/evidence/s1-receipts/*.json
|
||
|
||
s1-handoff: ## Run the read-only live S1 verification gate and emit a receipt
|
||
python3 scripts/s1_handoff.py
|
||
|
||
s1-handoff-dry-run: ## Validate handoff inputs without host access; receipt is not-run
|
||
python3 scripts/s1_handoff.py --dry-run
|
||
|
||
tf-fmt: ## Terraform fmt
|
||
terraform -chdir=terraform/hetzner fmt -recursive
|
||
|
||
tf-init: ## Terraform init
|
||
terraform -chdir=terraform/hetzner init
|
||
|
||
tf-plan: tf-init ## Terraform plan (requires decrypted HCLOUD_TOKEN)
|
||
@echo "🔍 Running terraform plan..."
|
||
@[ -n "$(HCLOUD_TOKEN)" ] || (echo "HCLOUD_TOKEN empty; unlock secrets/hetzner-token.yaml with SOPS" && exit 1)
|
||
@export HCLOUD_TOKEN=$(HCLOUD_TOKEN); terraform -chdir=terraform/hetzner plan -var="hcloud_token=$(HCLOUD_TOKEN)"
|
||
|
||
tf-apply: ## Terraform apply (provision; exact approval required before init)
|
||
@test "$(APPROVE_TF_APPLY)" = "YES" || (echo "Refusing apply: review make tf-plan, then set APPROVE_TF_APPLY=YES" && exit 1)
|
||
@[ -n "$(HCLOUD_TOKEN)" ] || (echo "HCLOUD_TOKEN empty; unlock secrets/hetzner-token.yaml with SOPS" && exit 1)
|
||
@$(MAKE) tf-init
|
||
@export HCLOUD_TOKEN=$(HCLOUD_TOKEN); terraform -chdir=terraform/hetzner apply -auto-approve -var="hcloud_token=$(HCLOUD_TOKEN)"
|
||
|
||
tf-destroy: ## Terraform destroy (exact approval required before init)
|
||
@test "$(APPROVE_TF_DESTROY)" = "DESTROY-MANAGED-HETZNER" || (echo "Refusing destroy: set APPROVE_TF_DESTROY=DESTROY-MANAGED-HETZNER after exact plan review" && exit 1)
|
||
@[ -n "$(HCLOUD_TOKEN)" ] || (echo "HCLOUD_TOKEN empty; unlock secrets/hetzner-token.yaml with SOPS" && exit 1)
|
||
@$(MAKE) tf-init
|
||
@export HCLOUD_TOKEN=$(HCLOUD_TOKEN); terraform -chdir=terraform/hetzner destroy -auto-approve -var="hcloud_token=$(HCLOUD_TOKEN)"
|
||
|
||
# --- Terraform provider/lockfile helpers ---
|
||
TF_DIR := terraform/hetzner
|
||
TF_TOKEN := $(HCLOUD_TOKEN)
|
||
LOCKFILE := $(TF_DIR)/.terraform.lock.hcl
|
||
|
||
tf-lock-commit: ## Commit the current provider lockfile
|
||
@test -f $(LOCKFILE) || (echo "❌ $(LOCKFILE) not found. Run 'make tf-init' first."; exit 1)
|
||
@git add $(LOCKFILE)
|
||
@git commit -m "chore(terraform): lock providers" || echo "ℹ No lockfile changes to commit."
|
||
|
||
tf-providers-check: ## Check if newer provider versions are available (non-destructive)
|
||
@echo "🔎 Checking for provider upgrades (lockfile readonly)…"
|
||
@if terraform -chdir=$(TF_DIR) init -upgrade -lockfile=readonly >/dev/null 2>&1; then \
|
||
echo "✔ Providers up to date (no upgrades available)."; \
|
||
else \
|
||
echo "↗ Provider upgrades likely available (readonly lockfile blocked changes)."; \
|
||
echo " Run: make tf-providers-upgrade"; \
|
||
fi
|
||
|
||
tf-providers-upgrade: ## Upgrade providers (updates .terraform.lock.hcl)
|
||
@echo "⬆️ Upgrading providers…"
|
||
@terraform -chdir=$(TF_DIR) init -upgrade
|
||
@echo "— Diff for $(LOCKFILE):"
|
||
@git --no-pager diff -- $(LOCKFILE) || true
|
||
@echo "💡 If changes look good: make tf-lock-commit"
|
||
|
||
tf-providers-upgrade-commit: tf-providers-upgrade tf-lock-commit ## Upgrade providers and commit the lockfile
|
||
|
||
tf-providers-plan: ## Plan after an upgrade (uses HCLOUD_TOKEN if set)
|
||
@echo "🧪 Planning with upgraded providers…"
|
||
@terraform -chdir=$(TF_DIR) plan $(if $(TF_TOKEN),-var="hcloud_token=$(TF_TOKEN)")
|
||
|
||
|
||
# ---- Backup (Q3 Operability & Resilience — D4) ----
|
||
backup: ## Backup S1 OS config to /opt/backup/railiance/infra/ (age-encrypted, root required)
|
||
sudo tools/cmd/railiance-backup-s1
|
||
|
||
validate-s1-backup: ## Validate S1 backup declaration and recovery implementation without host changes
|
||
python3 scripts/s1_backup.py check
|
||
python3 -m unittest tests.test_s1_backup_recovery -v
|
||
|
||
s1-backup-status: ## Check newest local S1 backup integrity, freshness, count, and disk budget
|
||
python3 scripts/s1_backup.py status
|
||
|
||
s1-backup-prune-plan: ## Print retained bundles and their exact one-use deletion approval
|
||
python3 scripts/s1_backup.py prune-plan
|
||
|
||
s1-backup-prune: ## Apply reviewed prune plan: APPROVE_S1_BACKUP_PRUNE=PRUNE-S1-BACKUPS-...
|
||
@test -n "$(APPROVE_S1_BACKUP_PRUNE)" || (echo "Run make s1-backup-prune-plan, then pass its exact approval"; exit 1)
|
||
python3 scripts/s1_backup.py prune --approval "$(APPROVE_S1_BACKUP_PRUNE)"
|
||
|
||
s1-restore-inspect: ## Verify encrypted bundle metadata without a private key: BUNDLE=/absolute/path
|
||
@test -n "$(BUNDLE)" || (echo "Usage: make s1-restore-inspect BUNDLE=/absolute/path/to/s1-backup-*"; exit 1)
|
||
python3 scripts/s1_restore.py "$(BUNDLE)"
|
||
|
||
s1-restore-isolated: ## Decrypt only into explicit empty staging: BUNDLE=... DEST=/tmp/... IDENTITY=...
|
||
@test -n "$(BUNDLE)" && test -n "$(DEST)" && test -n "$(IDENTITY)" || (echo "Usage: make s1-restore-isolated BUNDLE=... DEST=/tmp/... IDENTITY=/path/to/age-identity"; exit 1)
|
||
python3 scripts/s1_restore.py "$(BUNDLE)" --extract-to "$(DEST)" --identity "$(IDENTITY)"
|
||
|
||
s1-offsite-review: ## Render the exact railiance-platform upload-contract approval
|
||
python3 scripts/s1_offsite.py review
|
||
|
||
s1-offsite-plan: ## Build a credential-free exact upload plan: BUNDLE=/absolute/path
|
||
@test -n "$(BUNDLE)" || (echo "Usage: make s1-offsite-plan BUNDLE=/absolute/path/to/s1-backup-*"; exit 1)
|
||
python3 scripts/s1_offsite.py plan "$(BUNDLE)"
|
||
|
||
s1-offsite-upload: ## Contained owner-routed upload: BUNDLE=... APPROVE_S1_OFFSITE_UPLOAD=UPLOAD-S1-OFFSITE-...
|
||
@test -n "$(BUNDLE)" && test -n "$(APPROVE_S1_OFFSITE_UPLOAD)" || (echo "Run make s1-offsite-plan, then pass BUNDLE and its exact approval"; exit 1)
|
||
warden access railiance-backup-offsite-lane --field RAILIANCE_BACKUP_NC_TOKEN --exec -- \
|
||
python3 scripts/s1_offsite.py upload "$(BUNDLE)" --approval "$(APPROVE_S1_OFFSITE_UPLOAD)"
|
||
|
||
s1-backup-deploy: ## Deploy and enable timer: HOST=... APPROVE_S1_BACKUP_DEPLOY=DEPLOY-RAIL-HO-WP-0012-S1-BACKUP-TIMER
|
||
@test -n "$(HOST)" || (echo "Usage: make s1-backup-deploy HOST=Railiance01 APPROVE_S1_BACKUP_DEPLOY=DEPLOY-RAIL-HO-WP-0012-S1-BACKUP-TIMER"; exit 1)
|
||
@test "$(APPROVE_S1_BACKUP_DEPLOY)" = "DEPLOY-RAIL-HO-WP-0012-S1-BACKUP-TIMER" || (echo "Refusing deployment: exact approval is absent"; exit 1)
|
||
@git diff --quiet && git diff --cached --quiet || (echo "Refusing deployment: commit the exact source first"; exit 1)
|
||
cd ansible && ansible-playbook playbooks/s1-backup.yaml --limit "$(HOST)" \
|
||
-e railiance_backup_deploy_approval="$(APPROVE_S1_BACKUP_DEPLOY)" \
|
||
-e railiance_backup_source_revision="$(SOURCE_REVISION)"
|
||
|
||
# ---- Ansible ----
|
||
ansible-bootstrap: ## Run base bootstrap play (users, ssh, ufw, sops-agent, custodian-agent)
|
||
cd ansible && ansible-playbook playbooks/bootstrap.yaml $(ANSIBLE_USER_FLAG)
|
||
|
||
provision-custodian-agent: ## Deploy custodian agent SSH key to all managed hosts
|
||
@python3 -c "import yaml; d=yaml.safe_load(open('ansible/inventory/group_vars/all.yaml')); k=d.get('custodian_agent_pubkey',''); exit(0 if k else 1)" \
|
||
|| (echo "ERROR: custodian_agent_pubkey is empty. Run: cd ~/the-custodian && make custodian-keygen"; exit 1)
|
||
cd ansible && ansible-playbook playbooks/custodian-agent.yaml $(ANSIBLE_USER_FLAG)
|
||
|
||
provision-custodian-agent-host: ## Deploy custodian agent key to one host: make provision-custodian-agent-host HOST=Railiance01
|
||
@test -n "$(HOST)" || (echo "Usage: make provision-custodian-agent-host HOST=Railiance01"; exit 1)
|
||
cd ansible && ansible-playbook playbooks/custodian-agent.yaml $(ANSIBLE_USER_FLAG) \
|
||
--limit "$(HOST)"
|
||
|
||
bootstrap-ssh-ca: ## Deploy OpenBao SSH CA trust + auth_principals: make bootstrap-ssh-ca SSH_CA_PUBKEY=/path/to/ca_user.pub
|
||
@test -n "$(SSH_CA_PUBKEY)" || (echo "Usage: make bootstrap-ssh-ca SSH_CA_PUBKEY=/path/to/ca_user.pub [HOST=Railiance01]"; exit 1)
|
||
cd ansible && ansible-playbook playbooks/bootstrap-ssh-ca.yaml $(ANSIBLE_USER_FLAG) \
|
||
-e ssh_ca_pubkey_path="$(SSH_CA_PUBKEY)" \
|
||
$(if $(HOST),--limit "$(HOST)",)
|
||
|
||
bootstrap-ssh-ca-host: ## Deploy SSH CA trust to one host: make bootstrap-ssh-ca-host HOST=Railiance01 SSH_CA_PUBKEY=...
|
||
@test -n "$(HOST)" && test -n "$(SSH_CA_PUBKEY)" || \
|
||
(echo "Usage: make bootstrap-ssh-ca-host HOST=Railiance01 SSH_CA_PUBKEY=/path/to/ca_user.pub"; exit 1)
|
||
$(MAKE) bootstrap-ssh-ca SSH_CA_PUBKEY="$(SSH_CA_PUBKEY)" HOST="$(HOST)"
|
||
|
||
# ---- Orchestration ----
|
||
apply: tf-fmt tf-apply ansible-bootstrap ## Provision via Terraform then converge via Ansible
|
||
|
||
deploy-stack: ## Print the full S1→S5 ordered deploy sequence (operator follows each step)
|
||
@echo ""
|
||
@echo "╔══════════════════════════════════════════════════════════════╗"
|
||
@echo "║ Railiance Stack — Full Deploy Sequence ║"
|
||
@echo "║ See docs/deploy-stack.md for full runbook ║"
|
||
@echo "╚══════════════════════════════════════════════════════════════╝"
|
||
@echo ""
|
||
@echo "PRE-CONDITIONS"
|
||
@echo " [ ] SSH key: ~/.ssh/id_ops"
|
||
@echo " [ ] SOPS key: ~/.config/sops/age/keys.txt (or SOPS_AGE_KEY)"
|
||
@echo " [ ] ops-bridge: bridge up state-hub-railiance01 k3s-api-railiance01"
|
||
@echo ""
|
||
@echo "S1 — Infrastructure Substrate (this repo)"
|
||
@echo " make tf-plan && make tf-apply # provision server (skip if exists)"
|
||
@echo " ssh tegwick@92.205.62.239 'cd ~/railiance-infra/ansible && ansible-playbook playbooks/bootstrap.yaml -c local --become -l Railiance01'"
|
||
@echo " make verify"
|
||
@echo ""
|
||
@echo "S2 — Cluster Runtime (railiance-cluster)"
|
||
@echo " ssh tegwick@92.205.62.239 'cd ~/railiance-cluster && make converge && make smoke'"
|
||
@echo ""
|
||
@echo "S3 — Platform Services (railiance-platform)"
|
||
@echo " ssh tegwick@92.205.62.239 'cd ~/railiance-platform && make forgejo-db-status'"
|
||
@echo ""
|
||
@echo "S4 — Developer Enablement (railiance-enablement)"
|
||
@echo " make -C ~/railiance-enablement check test"
|
||
@echo ""
|
||
@echo "S5 — Workloads and forge consumer checks"
|
||
@echo " make -C ~/railiance-forge gitea-status"
|
||
@echo " deploy each application through its owning rapp/release runbook"
|
||
@echo ""
|
||
@echo " Full runbook: docs/deploy-stack.md"
|
||
|
||
# ---- Utilities ----
|
||
doctor: ## Check tools and basic repo setup
|
||
@bash -ceu ' \
|
||
ok(){ printf "✔ %s\n" "$$1"; }; fail(){ printf "❌ %s\n" "$$1"; exit 1; }; \
|
||
command -v git >/dev/null && ok "git: $$(git --version)" || fail "git missing"; \
|
||
command -v ansible >/dev/null && ok "ansible: $$(ansible --version | head -1)"; \
|
||
command -v sops >/dev/null && ok "sops: $$(sops --version --check-for-updates)"; \
|
||
command -v age >/dev/null && ok "age: $$(age --version)"; \
|
||
command -v terraform >/dev/null && ok "terraform: $$(terraform -version | head -1)"; \
|
||
test -f keys/admin_ssh.pub && ok "keys/admin_ssh.pub present" || echo "ℹ add your SSH pubkey to keys/admin_ssh.pub"; \
|
||
python3 scripts/check_secret_paths.py --tracked >/dev/null && ok "declared secret paths encrypted" || fail "secret path check failed"; \
|
||
grep -q "age1" .sops.yaml && ok ".sops.yaml has an age recipient" || echo "ℹ add your age public key to .sops.yaml"; \
|
||
git config --get core.hooksPath >/dev/null && ok "git hooksPath: $$(git config --get core.hooksPath)" || echo "ℹ run: make hooks"; \
|
||
'
|
||
|
||
# ---- Inventory convenience ----
|
||
new-host: ## Add a new host quickly: make new-host NAME=core1 TYPE=cpx11 REGION=nbg1 ROLE=core
|
||
@[ -n "$(NAME)" ] || (echo "Usage: make new-host NAME=... [TYPE=...] [REGION=...] [ROLE=...] [IMG=...] [USER=...]" && exit 1)
|
||
@python3 scripts/new_host.py --name "$(NAME)" --type "$(TYPE)" --region "$(REGION)" --role "$(ROLE)" --image "$(IMG)" --user "$(USER)"
|
||
@echo "✔ Added host $(NAME) to inventory/servers.yaml"
|
||
|
||
remote-set: ## Set origin to your Gitea repo (GITEA/OWNER/REPO vars)
|
||
git remote remove origin 2>/dev/null || true
|
||
git remote add origin https://$(GITEA)/$(OWNER)/$(REPO).git
|
||
git branch -M main
|
||
git push -u origin main
|
||
@echo "✔ Remote set to https://$(GITEA)/$(OWNER)/$(REPO).git"
|
||
|
||
|
||
# ==== Convergence (Ansible) ====
|
||
ANS_DIR := ansible
|
||
INV_SCRIPT := $(ANS_DIR)/inventory_from_yaml.py
|
||
PLAY := $(ANS_DIR)/playbooks/bootstrap.yaml
|
||
# Inventory servers.yaml sets ansible_user. Override only when needed:
|
||
# make converge SSH_USER=tegwick
|
||
SSH_USER ?=
|
||
ANSIBLE_USER_FLAG := $(if $(SSH_USER),-u $(SSH_USER),)
|
||
|
||
# Load your SOPS key for decryption when running playbooks (optional if you use keys.txt)
|
||
export SOPS_AGE_KEY := $(shell cat ~/.config/sops/age/keys.txt 2>/dev/null)
|
||
|
||
ansible-help: ## Show common Ansible commands
|
||
@echo "Convergence targets:"
|
||
@echo " make ansible-inventory # show resolved inventory"
|
||
@echo " make ansible-ping # ping all hosts"
|
||
@echo " make converge # run baseline convergence on all hosts"
|
||
@echo " make converge-host HOST=web-01# run on a single host"
|
||
@echo " make converge-tags TAGS=base # run only tagged tasks"
|
||
@echo " make converge-firewall HOST=Railiance01 # UFW only (RAIL-HO-WP-0009)"
|
||
@echo " make converge-check # dry-run (check mode)"
|
||
@echo " make converge-diff # show config diffs"
|
||
@echo " make verify-host HOST=Railiance01 # read-only host verification"
|
||
@echo " make verify-refresh-host HOST=Railiance01 APPROVE_VERIFY_REFRESH=REFRESH-GOSS-Railiance01"
|
||
@echo " make goss-status # last on-host timer result"
|
||
|
||
ansible-inventory: ## Print the dynamic inventory Ansible will use
|
||
cd $(ANS_DIR) && ansible-inventory --list | head -200
|
||
|
||
ansible-ping: ## Quick connectivity check (SSH + Python availability)
|
||
cd $(ANS_DIR) && ansible all $(ANSIBLE_USER_FLAG) -m ping
|
||
|
||
status: ## Show live security state of all hosts (UFW, fail2ban, SSH hardening)
|
||
@echo "=== Connectivity ==="
|
||
cd $(ANS_DIR) && ansible all $(ANSIBLE_USER_FLAG) -m ping
|
||
@echo "=== UFW ==="
|
||
cd $(ANS_DIR) && ansible all $(ANSIBLE_USER_FLAG) -m shell -a "ufw status" --become
|
||
@echo "=== fail2ban ==="
|
||
cd $(ANS_DIR) && ansible all $(ANSIBLE_USER_FLAG) -m shell -a "systemctl is-active fail2ban"
|
||
@echo "=== SSH hardening ==="
|
||
cd $(ANS_DIR) && ansible all $(ANSIBLE_USER_FLAG) -m shell -a "grep -iE '^(PermitRootLogin|PasswordAuthentication)' /etc/ssh/sshd_config" --become
|
||
@echo ""
|
||
@echo "--- Hint: run 'make verify' for a structured pass/fail report ---"
|
||
|
||
verify: validate-handoff-readonly ## Read-only Goss verification of all hosts; writes TAP only on controller
|
||
@echo "Running read-only Goss baseline assertions..."
|
||
@cd $(ANS_DIR) && ansible-playbook playbooks/verify.yaml $(ANSIBLE_USER_FLAG) || \
|
||
(echo "One or more assertions FAILED — see reports/ for TAP output." && exit 1)
|
||
@echo "All assertions passed."
|
||
|
||
observe-railiance01: ## Timestamped host capacity observation for resource-control
|
||
@mkdir -p docs/evidence/resource-hosteurope-railiance01/observations
|
||
@out=$$(mktemp); \
|
||
python3 scripts/observe-host-capacity.py Railiance01 > $$out; \
|
||
stamp=$$(python3 -c "import json; print(json.load(open('$$out'))['observed_at'].replace(':',''))"); \
|
||
dest=docs/evidence/resource-hosteurope-railiance01/observations/$$stamp.json; \
|
||
mv $$out $$dest; \
|
||
ln -sfn $$stamp.json docs/evidence/resource-hosteurope-railiance01/observations/latest.json; \
|
||
echo "wrote $$dest"
|
||
|
||
verify-host: validate-handoff-readonly ## Read-only Goss verification: make verify-host HOST=Railiance01
|
||
@test -n "$(HOST)" || (echo "Usage: make verify-host HOST=Railiance01"; exit 1)
|
||
@echo "Running read-only Goss baseline assertions on $(HOST)..."
|
||
@cd $(ANS_DIR) && ansible-playbook playbooks/verify.yaml $(ANSIBLE_USER_FLAG) -l $(HOST) || \
|
||
(echo "One or more assertions FAILED — see reports/ for TAP output." && exit 1)
|
||
|
||
verify-refresh: ## Refresh Goss on all hosts after review (exact approval required)
|
||
@test "$(APPROVE_VERIFY_REFRESH)" = "REFRESH-GOSS-ALL" || \
|
||
(echo "Refusing host mutation: set APPROVE_VERIFY_REFRESH=REFRESH-GOSS-ALL after review"; exit 1)
|
||
cd $(ANS_DIR) && ansible-playbook playbooks/verify-refresh.yaml $(ANSIBLE_USER_FLAG)
|
||
|
||
verify-refresh-host: ## Refresh one host: HOST=... APPROVE_VERIFY_REFRESH=REFRESH-GOSS-<HOST>
|
||
@test -n "$(HOST)" || (echo "Usage: make verify-refresh-host HOST=Railiance01 APPROVE_VERIFY_REFRESH=REFRESH-GOSS-Railiance01"; exit 1)
|
||
@test "$(APPROVE_VERIFY_REFRESH)" = "REFRESH-GOSS-$(HOST)" || \
|
||
(echo "Refusing host mutation: set APPROVE_VERIFY_REFRESH=REFRESH-GOSS-$(HOST) after review"; exit 1)
|
||
cd $(ANS_DIR) && ansible-playbook playbooks/verify-refresh.yaml $(ANSIBLE_USER_FLAG) -l $(HOST)
|
||
|
||
goss-status: ## Fetch last on-host Goss timer result (fails if FAILED flag present)
|
||
cd $(ANS_DIR) && ansible-playbook playbooks/goss-status.yaml $(ANSIBLE_USER_FLAG)
|
||
|
||
converge: ## Converge all hosts to the baseline (idempotent)
|
||
cd $(ANS_DIR) && ansible-playbook $(PLAY) $(ANSIBLE_USER_FLAG)
|
||
|
||
converge-host: ## Converge a single host: make converge-host HOST=core-01
|
||
@test -n "$(HOST)" || (echo "Usage: make converge-host HOST=<name>"; exit 1)
|
||
cd $(ANS_DIR) && ansible-playbook $(PLAY) $(ANSIBLE_USER_FLAG) -l $(HOST)
|
||
|
||
converge-tags: ## Run only certain tags: make converge-tags TAGS="base,ufw"
|
||
@test -n "$(TAGS)" || (echo "Usage: make converge-tags TAGS=tag1,tag2"; exit 1)
|
||
cd $(ANS_DIR) && ansible-playbook $(PLAY) $(ANSIBLE_USER_FLAG) --tags "$(TAGS)"
|
||
|
||
converge-firewall: ## Apply only UFW tasks: make converge-firewall HOST=Railiance01
|
||
@test -n "$(HOST)" || (echo "Usage: make converge-firewall HOST=Railiance01"; exit 1)
|
||
cd $(ANS_DIR) && ansible-playbook playbooks/firewall.yaml $(ANSIBLE_USER_FLAG) -l $(HOST) --tags firewall \
|
||
$(if $(REEF_DECLARATION),-e reef_declaration_path="$(abspath $(REEF_DECLARATION))",)
|
||
|
||
validate-reef-exposure: ## Validate PORTS against REEF_DECLARATION without changing a host
|
||
@test -n "$(REEF_DECLARATION)" || (echo "Usage: make validate-reef-exposure REEF_DECLARATION=... PORTS=80,443"; exit 1)
|
||
python3 scripts/validate-reef-exposure.py --reef-declaration "$(REEF_DECLARATION)" --ports "$(or $(PORTS),80,443)"
|
||
|
||
converge-check: ## Dry-run (no changes), great for previews
|
||
cd $(ANS_DIR) && ansible-playbook $(PLAY) $(ANSIBLE_USER_FLAG) --check
|
||
|
||
converge-diff: ## Show file/templating diffs while applying changes
|
||
cd $(ANS_DIR) && ansible-playbook $(PLAY) $(ANSIBLE_USER_FLAG) --diff
|