ansible/Makefile
ilia c3e6caf9e8
All checks were successful
CI / skip-ci-check (push) Successful in 1m18s
CI / lint-and-test (push) Successful in 1m23s
CI / ansible-validation (push) Successful in 3m2s
CI / secret-scanning (push) Successful in 1m19s
CI / dependency-scan (push) Successful in 1m24s
CI / sast-scan (push) Successful in 2m32s
CI / license-check (push) Successful in 1m23s
CI / vault-check (push) Successful in 2m22s
CI / playbook-test (push) Successful in 2m25s
CI / container-scan (push) Successful in 1m51s
CI / sonar-analysis (push) Successful in 2m32s
CI / workflow-summary (push) Successful in 1m17s
refactor-servers-workstations-shell-monitoring (#4)
### Summary

This PR refactors the playbook layout to reduce duplication and make host intent clearer (servers vs workstations), splits monitoring by host type, and restores full Zsh setup for developers while keeping servers aliases-only.

### Key changes

- **New playbooks**
  - `playbooks/servers.yml`: baseline for server-class hosts (no desktop apps)
  - `playbooks/workstations.yml`: baseline for dev/desktop/local + **desktop apps only on `desktop` group**

- **Monitoring split**
  - `roles/monitoring_server`: server monitoring + intrusion prevention (includes `fail2ban`, sysstat)
  - `roles/monitoring_desktop`: desktop-oriented monitoring tooling
  - Updated playbooks to use the correct monitoring role per host type

- **Shell role: server-safe + developer-friendly**
  - `roles/shell` now supports two modes:
    - `shell_mode: minimal` (default): aliases-only, does not overwrite `.zshrc`
    - `shell_mode: full`: installs Oh My Zsh + Powerlevel10k + plugins and deploys a managed `.zshrc`
  - `playbooks/development.yml` and `playbooks/workstations.yml` use `shell_mode: full`
  - `playbooks/servers.yml` remains **aliases-only**

- **Applications**
  - Applications role runs only on `desktop` group (via `workstations.yml`)
  - Removed Brave installs/repo management
  - Added **CopyQ** to desktop apps (`applications_desktop_packages`)

- **Docs + architecture**
  - Added canonical doc tree under `project-docs/` (overview/architecture/standards/workflow/decisions)
  - Consolidated architecture docs: `docs/reference/architecture.md` is now a pointer to `project-docs/architecture.md`
  - Fixed broken doc links by adding the missing referenced pages under `docs/`

### Behavior changes (important)

- Desktop GUI apps install **only** on the `desktop` inventory group (not on servers, not on dev VMs unless they are in `desktop`).
- Dev/workstation Zsh is now provisioned in **full mode** (managed `.zshrc` + p10k).

### How to test (local CI parity)

```bash
make test
npm test
```

Optional dry runs (interactive sudo may be required):

```bash
make check
make check-local
```

### Rollout guidance

- Apply to a single host first:
  - Workstations: `make workstations HOST=<devhost>`
  - Servers: `make servers HOST=<serverhost>`
- Then expand to group runs.

Reviewed-on: #4
2026-01-01 22:11:24 -05:00

682 lines
27 KiB
Makefile

.PHONY: help bootstrap lint test check dev datascience inventory inventory-all local servers workstations clean status tailscale tailscale-check tailscale-dev tailscale-status create-vault create-vm monitoring
.DEFAULT_GOAL := help
## Colors for output
BOLD := \033[1m
RED := \033[31m
GREEN := \033[32m
YELLOW := \033[33m
BLUE := \033[34m
RESET := \033[0m
# Playbook paths
PLAYBOOK_SITE := playbooks/site.yml
PLAYBOOK_DEV := playbooks/development.yml
PLAYBOOK_LOCAL := playbooks/local.yml
PLAYBOOK_SERVERS := playbooks/servers.yml
PLAYBOOK_WORKSTATIONS := playbooks/workstations.yml
PLAYBOOK_MAINTENANCE := playbooks/maintenance.yml
PLAYBOOK_TAILSCALE := playbooks/tailscale.yml
PLAYBOOK_PROXMOX := playbooks/infrastructure/proxmox-vm.yml
PLAYBOOK_PROXMOX_INFO := playbooks/app/proxmox_info.yml
# Collection and requirement paths
COLLECTIONS_REQ := collections/requirements.yml
PYTHON_REQ := requirements.txt
# Inventory paths
INVENTORY := inventories/production
INVENTORY_HOSTS := $(INVENTORY)/hosts
# Common ansible-playbook command with options
ANSIBLE_PLAYBOOK := ansible-playbook -i $(INVENTORY)
ANSIBLE_ARGS := --vault-password-file ~/.ansible-vault-pass
# Note: sudo passwords are in vault files as ansible_become_password
## Auto-detect current host to exclude from remote operations
CURRENT_IP := $(shell hostname -I | awk '{print $$1}')
# NOTE: inventory parsing may require vault secrets. Keep this best-effort and silent in CI.
CURRENT_HOST := $(shell ansible-inventory --list --vault-password-file ~/.ansible-vault-pass 2>/dev/null | jq -r '._meta.hostvars | to_entries[] | select(.value.ansible_host == "$(CURRENT_IP)") | .key' 2>/dev/null | head -1)
EXCLUDE_CURRENT := $(if $(CURRENT_HOST),--limit '!$(CURRENT_HOST)',)
help: ## Show this help message
@echo "$(BOLD)Ansible Development Environment$(RESET)"
@echo ""
@echo "$(BOLD)Available commands:$(RESET)"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(BLUE)%-15s$(RESET) %s\n", $$1, $$2}'
@echo ""
@echo "$(BOLD)Examples:$(RESET)"
@echo " make bootstrap # Set up dependencies"
@echo " make check # Dry run all hosts"
@echo " make apply # Run on all dev hosts"
@echo " make dev HOST=dev01 # Run on specific host"
@echo " make local # Run local playbook"
@echo " make maintenance # Run maintenance on all hosts"
@echo " make maintenance GROUP=dev # Run maintenance on dev group"
@echo " make maintenance HOST=dev01 # Run maintenance on specific host"
@echo " make maintenance CHECK=true # Dry-run maintenance on all hosts"
@echo " make maintenance VERBOSE=true # Run with verbose output"
@echo " make maintenance-verbose GROUP=dev # Verbose maintenance on dev group"
@echo ""
bootstrap: ## Install all project dependencies from requirements files
@echo "$(BOLD)Installing Project Dependencies$(RESET)"
@echo ""
@echo "$(YELLOW)Python Requirements ($(PYTHON_REQ)):$(RESET)"
@if [ -f "$(PYTHON_REQ)" ]; then \
if command -v pipx >/dev/null 2>&1; then \
printf " %-30s " "Installing with pipx"; \
if pipx install -r $(PYTHON_REQ) >/dev/null 2>&1; then \
echo "$(GREEN)✓ Installed$(RESET)"; \
else \
echo "$(YELLOW)⚠ Some packages may have failed$(RESET)"; \
fi; \
elif command -v pip3 >/dev/null 2>&1; then \
printf " %-30s " "Installing with pip3 --user"; \
if pip3 install --user -r $(PYTHON_REQ) >/dev/null 2>&1; then \
echo "$(GREEN)✓ Installed$(RESET)"; \
else \
printf " %-30s " "Trying with --break-system-packages"; \
if pip3 install --break-system-packages -r $(PYTHON_REQ) >/dev/null 2>&1; then \
echo "$(GREEN)✓ Installed$(RESET)"; \
else \
echo "$(RED)✗ Failed$(RESET)"; \
fi; \
fi; \
else \
printf " %-30s " "Python packages"; \
echo "$(YELLOW)⚠ Skipped (pip3/pipx not found)$(RESET)"; \
fi; \
else \
printf " %-30s " "$(PYTHON_REQ)"; \
echo "$(RED)✗ File not found$(RESET)"; \
fi
@echo ""
@echo "$(YELLOW)Node.js Dependencies (package.json):$(RESET)"
@if [ -f "package.json" ] && command -v npm >/dev/null 2>&1; then \
printf " %-30s " "Installing npm packages"; \
if npm install >/dev/null 2>&1; then \
echo "$(GREEN)✓ Installed$(RESET)"; \
else \
echo "$(RED)✗ Failed$(RESET)"; \
fi; \
else \
printf " %-30s " "npm packages"; \
echo "$(YELLOW)⚠ Skipped (package.json or npm not found)$(RESET)"; \
fi
@echo ""
@echo "$(YELLOW)Ansible Collections ($(COLLECTIONS_REQ)):$(RESET)"
@if [ -f "$(COLLECTIONS_REQ)" ]; then \
ansible-galaxy collection install -r $(COLLECTIONS_REQ) 2>&1 | grep -E "(Installing|Skipping|ERROR)" | while read line; do \
if echo "$$line" | grep -q "Installing"; then \
collection=$$(echo "$$line" | awk '{print $$2}' | sed 's/:.*//'); \
printf " $(GREEN)✓ %-30s$(RESET) Installed\n" "$$collection"; \
elif echo "$$line" | grep -q "Skipping"; then \
collection=$$(echo "$$line" | awk '{print $$2}' | sed 's/,.*//'); \
printf " $(BLUE)- %-30s$(RESET) Already installed\n" "$$collection"; \
elif echo "$$line" | grep -q "ERROR"; then \
printf " $(RED)✗ Error: $$line$(RESET)\n"; \
fi; \
done || ansible-galaxy collection install -r $(COLLECTIONS_REQ); \
else \
printf " %-30s " "$(COLLECTIONS_REQ)"; \
echo "$(RED)✗ File not found$(RESET)"; \
fi
@echo ""
lint: ## Run ansible-lint on all playbooks and roles
@echo "$(YELLOW)Running ansible-lint...$(RESET)"
ansible-lint
@echo "$(GREEN)✓ Linting completed$(RESET)"
test-syntax: ## Run comprehensive syntax and validation checks
@echo "$(BOLD)Comprehensive Testing$(RESET)"
@echo ""
@echo "$(YELLOW)Playbook Syntax:$(RESET)"
@for playbook in $(PLAYBOOK_DEV) $(PLAYBOOK_LOCAL) $(PLAYBOOK_MAINTENANCE) $(PLAYBOOK_TAILSCALE); do \
if [ -f "$$playbook" ]; then \
printf " %-25s " "$$playbook"; \
if ansible-playbook "$$playbook" --syntax-check >/dev/null 2>&1; then \
echo "$(GREEN)✓ OK$(RESET)"; \
else \
echo "$(RED)✗ FAIL$(RESET)"; \
fi; \
fi; \
done
@echo ""
@echo "$(YELLOW)Infrastructure Playbooks:$(RESET)"
@for playbook in playbooks/infrastructure/*.yml; do \
if [ -f "$$playbook" ]; then \
printf " %-25s " "$$playbook"; \
if ansible-playbook "$$playbook" --syntax-check >/dev/null 2>&1; then \
echo "$(GREEN)✓ OK$(RESET)"; \
else \
echo "$(RED)✗ FAIL$(RESET)"; \
fi; \
fi; \
done
@echo ""
@echo "$(YELLOW)App Project Playbooks:$(RESET)"
@for playbook in playbooks/app/site.yml playbooks/app/provision_vms.yml playbooks/app/configure_app.yml playbooks/app/ssh_client_config.yml; do \
if [ -f "$$playbook" ]; then \
printf " %-25s " "$$playbook"; \
if ansible-playbook "$$playbook" --syntax-check >/dev/null 2>&1; then \
echo "$(GREEN)✓ OK$(RESET)"; \
else \
echo "$(RED)✗ FAIL$(RESET)"; \
fi; \
fi; \
done
@echo ""
@echo "$(YELLOW)Role Test Playbooks:$(RESET)"
@for test_playbook in roles/*/tests/test.yml; do \
if [ -f "$$test_playbook" ]; then \
role_name=$$(echo "$$test_playbook" | cut -d'/' -f2); \
printf " %-25s " "roles/$$role_name/test"; \
if ansible-playbook "$$test_playbook" --syntax-check >/dev/null 2>&1; then \
echo "$(GREEN)✓ OK$(RESET)"; \
else \
echo "$(RED)✗ FAIL$(RESET)"; \
fi; \
fi; \
done
@echo ""
@echo "$(YELLOW)Markdown Validation:$(RESET)"
@if [ -f "package.json" ] && command -v npm >/dev/null 2>&1; then \
printf " %-25s " "Markdown syntax"; \
if npm run test:markdown >/dev/null 2>&1; then \
echo "$(GREEN)✓ OK$(RESET)"; \
else \
echo "$(YELLOW)⚠ Issues$(RESET)"; \
fi; \
else \
printf " %-25s " "Markdown syntax"; \
echo "$(YELLOW)⚠ npm/package.json not available$(RESET)"; \
fi
@echo ""
@echo "$(YELLOW)Documentation Links:$(RESET)"
@if [ -f "package.json" ] && command -v npm >/dev/null 2>&1; then \
printf " %-25s " "Link validation"; \
if npm run test:links >/dev/null 2>&1; then \
echo "$(GREEN)✓ OK$(RESET)"; \
else \
echo "$(YELLOW)⚠ Issues$(RESET)"; \
fi; \
else \
printf " %-25s " "Link validation"; \
echo "$(YELLOW)⚠ npm/package.json not available$(RESET)"; \
fi
@echo ""
@echo "$(YELLOW)Configuration Validation:$(RESET)"
@for yaml_file in inventories/production/group_vars/all/main.yml; do \
if [ -f "$$yaml_file" ]; then \
printf " %-25s " "$$yaml_file (YAML)"; \
if python3 -c "import yaml" >/dev/null 2>&1; then \
if python3 -c "import yaml; yaml.safe_load(open('$$yaml_file'))" >/dev/null 2>&1; then \
echo "$(GREEN)✓ OK$(RESET)"; \
else \
echo "$(RED)✗ FAIL$(RESET)"; \
fi; \
else \
echo "$(YELLOW)⚠ Skipped (PyYAML not installed)$(RESET)"; \
fi; \
fi; \
done
@printf " %-25s " "ansible.cfg (INI)"; \
if python3 -c "import configparser; c=configparser.ConfigParser(); c.read('ansible.cfg')" >/dev/null 2>&1; then \
echo "$(GREEN)✓ OK$(RESET)"; \
else \
echo "$(RED)✗ FAIL$(RESET)"; \
fi
@echo ""
test: ## Run all tests (lint + syntax check if available)
@if command -v ansible-lint >/dev/null 2>&1; then \
echo "$(YELLOW)Running ansible-lint...$(RESET)"; \
ansible-lint 2>/dev/null && echo "$(GREEN)✓ Linting completed$(RESET)" || echo "$(YELLOW)⚠ Linting had issues$(RESET)"; \
echo ""; \
fi
@$(MAKE) test-syntax
check: auto-fallback ## Dry-run the development playbook (--check mode)
@echo "$(YELLOW)Running dry-run on development hosts...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_DEV) --check --diff
check-local: ## Dry-run the local playbook
@echo "$(YELLOW)Running dry-run on localhost...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_LOCAL) --check --diff -K
site: ## Run the complete site playbook
@echo "$(YELLOW)Running complete site deployment...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_SITE)
local: ## Run the local playbook on localhost
@echo "$(YELLOW)Applying local playbook...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_LOCAL) -K
servers: ## Run baseline server playbook (usage: make servers [GROUP=services] [HOST=host1])
@echo "$(YELLOW)Applying server baseline...$(RESET)"
@EXTRA=""; \
if [ -n "$(HOST)" ]; then \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_SERVERS) --limit $(HOST); \
elif [ -n "$(GROUP)" ]; then \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_SERVERS) -e target_group=$(GROUP); \
else \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_SERVERS); \
fi
workstations: ## Run workstation baseline (usage: make workstations [GROUP=dev] [HOST=dev01])
@echo "$(YELLOW)Applying workstation baseline...$(RESET)"
@EXTRA=""; \
if [ -n "$(HOST)" ]; then \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_WORKSTATIONS) --limit $(HOST); \
elif [ -n "$(GROUP)" ]; then \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_WORKSTATIONS) -e target_group=$(GROUP); \
else \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_WORKSTATIONS); \
fi
# Host-specific targets
dev: ## Run on specific host (usage: make dev HOST=dev01 [SUDO=true] [SSH_PASS=true])
ifndef HOST
@echo "$(RED)Error: HOST parameter required$(RESET)"
@echo "Usage: make dev HOST=dev01 [SUDO=true] [SSH_PASS=true]"
@exit 1
endif
@echo "$(YELLOW)Running on host: $(HOST)$(RESET)"
@SSH_FLAGS=""; \
SUDO_FLAGS=""; \
if [ "$(SSH_PASS)" = "true" ]; then \
SSH_FLAGS="-k"; \
fi; \
if [ "$(SUDO)" = "true" ]; then \
SUDO_FLAGS="-K"; \
fi; \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_DEV) --limit $(HOST) $(ANSIBLE_ARGS) $$SSH_FLAGS $$SUDO_FLAGS
# Data science role
datascience: ## Install data science stack (usage: make datascience HOST=server01)
ifndef HOST
@echo "$(RED)Error: HOST parameter required$(RESET)"
@echo "Usage: make datascience HOST=server01"
@exit 1
endif
@echo "$(YELLOW)Installing data science stack on: $(HOST)$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_DEV) --limit $(HOST) --tags datascience
# Inventory system
inventory: ## Show installed software on host (usage: make inventory HOST=dev01)
ifndef HOST
@echo "$(RED)Error: HOST parameter required$(RESET)"
@echo "Usage: make inventory HOST=dev01"
@exit 1
endif
@echo "$(BOLD)Software Inventory for: $(HOST)$(RESET)"
@echo ""
@ip=$$(ansible-inventory --list | jq -r "._meta.hostvars.$(HOST).ansible_host // empty" 2>/dev/null); \
user=$$(ansible-inventory --list | jq -r "._meta.hostvars.$(HOST).ansible_user // empty" 2>/dev/null); \
if [ -n "$$ip" ] && [ "$$ip" != "null" ] && [ -n "$$user" ] && [ "$$user" != "null" ]; then \
scp -q scripts/inventory.sh $$user@$$ip:/tmp/inventory.sh 2>/dev/null && \
ssh $$user@$$ip 'bash /tmp/inventory.sh; rm /tmp/inventory.sh' 2>/dev/null; \
else \
echo "$(RED)Could not determine IP or user for $(HOST)$(RESET)"; \
fi
inventory-all: ## Show installed software on all hosts
@echo "$(BOLD)Software Inventory - All Hosts$(RESET)\n"
@for host in $$(ansible all --list-hosts 2>/dev/null | grep -v "hosts" | tr -d ' '); do \
echo "$(YELLOW)=== $$host ===$(RESET)"; \
ansible $$host -m script -a "scripts/inventory.sh" 2>/dev/null | sed -n '/CHANGED/,$$p' | tail -n +3 || echo "$(RED)Failed to connect$(RESET)"; \
echo ""; \
done
# Tag-based execution
security: ## Run only security-related roles
@echo "$(YELLOW)Running security roles...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_DEV) --tags security
# Unified maintenance target with intelligent parameter detection
maintenance: ## Run maintenance (usage: make maintenance [GROUP=dev] [HOST=dev01] [SERIAL=1] [CHECK=true] [VERBOSE=true])
@$(MAKE) _maintenance-run
_maintenance-run:
@# Determine target and build command
@TARGET="all"; \
ANSIBLE_CMD="$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_MAINTENANCE)"; \
DESCRIPTION="all hosts"; \
NEED_SUDO=""; \
\
if [ -n "$(HOST)" ]; then \
TARGET="host $(HOST)"; \
ANSIBLE_CMD="$$ANSIBLE_CMD --limit $(HOST)"; \
DESCRIPTION="host $(HOST)"; \
if [ "$(HOST)" = "localhost" ]; then \
NEED_SUDO="-K"; \
fi; \
elif [ -n "$(GROUP)" ]; then \
TARGET="$(GROUP) group"; \
ANSIBLE_CMD="$$ANSIBLE_CMD -e target_group=$(GROUP)"; \
DESCRIPTION="$(GROUP) group"; \
if [ "$(GROUP)" = "local" ]; then \
NEED_SUDO="-K"; \
fi; \
else \
NEED_SUDO="-K"; \
fi; \
\
if [ -n "$(SERIAL)" ]; then \
ANSIBLE_CMD="$$ANSIBLE_CMD -e maintenance_serial=$(SERIAL)"; \
DESCRIPTION="$$DESCRIPTION (serial=$(SERIAL))"; \
fi; \
\
if [ "$(CHECK)" = "true" ]; then \
ANSIBLE_CMD="$$ANSIBLE_CMD --check --diff"; \
echo "$(YELLOW)Dry-run maintenance on $$DESCRIPTION...$(RESET)"; \
else \
echo "$(YELLOW)Running maintenance on $$DESCRIPTION...$(RESET)"; \
fi; \
\
if [ "$(VERBOSE)" = "true" ]; then \
ANSIBLE_CMD="$$ANSIBLE_CMD -v"; \
echo "$(BLUE)Running with verbose output...$(RESET)"; \
fi; \
\
if [ -n "$(GROUP)" ] && [ "$(GROUP)" != "dev" ] && [ "$(GROUP)" != "local" ]; then \
echo "$(BLUE)Available groups: dev, gitea, portainer, homepage, ansible, local$(RESET)"; \
fi; \
\
$$ANSIBLE_CMD $$NEED_SUDO
# Legacy/convenience aliases
maintenance-dev: ## Run maintenance on dev group (legacy alias)
@$(MAKE) maintenance GROUP=dev
maintenance-all: ## Run maintenance on all hosts (legacy alias)
@$(MAKE) maintenance
maintenance-check: ## Dry-run maintenance (legacy alias, usage: make maintenance-check [GROUP=dev])
@$(MAKE) maintenance CHECK=true GROUP=$(GROUP)
maintenance-verbose: ## Run maintenance with verbose output (usage: make maintenance-verbose [GROUP=dev])
@$(MAKE) maintenance VERBOSE=true GROUP=$(GROUP)
docker: ## Install/configure Docker only
@echo "$(YELLOW)Running Docker setup...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_DEV) --tags docker
shell: ## Configure shell (usage: make shell [HOST=dev02] [SUDO=true])
ifdef HOST
@echo "$(YELLOW)Running shell configuration on host: $(HOST)$(RESET)"
@if [ "$(SUDO)" = "true" ]; then \
$(ANSIBLE_PLAYBOOK) playbooks/shell.yml --limit $(HOST) $(ANSIBLE_ARGS) -K; \
else \
$(ANSIBLE_PLAYBOOK) playbooks/shell.yml --limit $(HOST) $(ANSIBLE_ARGS); \
fi
else
@echo "$(YELLOW)Running shell configuration on all dev hosts...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_DEV) --tags shell
endif
shell-all: ## Configure shell on all hosts (usage: make shell-all)
@echo "$(YELLOW)Running shell configuration on all hosts...$(RESET)"
$(ANSIBLE_PLAYBOOK) playbooks/shell.yml $(ANSIBLE_ARGS)
apps: ## Install applications only
@echo "$(YELLOW)Installing applications...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_WORKSTATIONS) --tags apps
# Connectivity targets
ping: auto-fallback ## Ping hosts with colored output (usage: make ping [GROUP=dev] [HOST=dev01])
ifdef HOST
@echo "$(YELLOW)Pinging host: $(HOST)$(RESET)"
@ansible $(HOST) -m ping --one-line | while read line; do \
if echo "$$line" | grep -q "SUCCESS"; then \
echo "$(GREEN)$$line$(RESET)"; \
elif echo "$$line" | grep -q "UNREACHABLE"; then \
echo "$(RED)$$line$(RESET)"; \
else \
echo "$(YELLOW)? $$line$(RESET)"; \
fi; \
done
else ifdef GROUP
@echo "$(YELLOW)Pinging $(GROUP) group...$(RESET)"
@ansible $(GROUP) -m ping --one-line | while read line; do \
if echo "$$line" | grep -q "SUCCESS"; then \
echo "$(GREEN)✓ $$line$(RESET)"; \
elif echo "$$line" | grep -q "UNREACHABLE"; then \
echo "$(RED)✗ $$line$(RESET)"; \
else \
echo "$(YELLOW)? $$line$(RESET)"; \
fi; \
done
else
@echo "$(YELLOW)Pinging all hosts...$(RESET)"
@if [ -n "$(CURRENT_HOST)" ]; then \
echo "$(BLUE)Auto-excluding current host: $(CURRENT_HOST) ($(CURRENT_IP))$(RESET)"; \
fi
@echo ""
@ansible all -m ping --one-line $(EXCLUDE_CURRENT) 2>/dev/null | grep -E "(SUCCESS|UNREACHABLE)" > /tmp/ping_results.tmp; \
success_count=$$(grep -c "SUCCESS" /tmp/ping_results.tmp 2>/dev/null || echo 0); \
fail_count=$$(grep -c "UNREACHABLE" /tmp/ping_results.tmp 2>/dev/null || echo 0); \
while read line; do \
host=$$(echo "$$line" | cut -d' ' -f1); \
if echo "$$line" | grep -q "SUCCESS"; then \
printf "$(GREEN) ✓ %-20s$(RESET) Connected\n" "$$host"; \
elif echo "$$line" | grep -q "UNREACHABLE"; then \
reason=$$(echo "$$line" | grep -o "Permission denied.*" | head -1 || echo "Connection failed"); \
printf "$(RED) ✗ %-20s$(RESET) $$reason\n" "$$host"; \
fi; \
done < /tmp/ping_results.tmp; \
rm -f /tmp/ping_results.tmp; \
echo ""; \
printf "$(BOLD)Summary:$(RESET) $(GREEN)$$success_count connected$(RESET), $(RED)$$fail_count failed$(RESET)\n"
endif
facts: ## Gather facts from all hosts
@echo "$(YELLOW)Gathering facts...$(RESET)"
ansible all -m setup --tree /tmp/facts $(EXCLUDE_CURRENT)
show-current: ## Show current host that will be auto-excluded
@echo "$(BOLD)Current Host Detection:$(RESET)"
@echo " Current IP: $(BLUE)$(CURRENT_IP)$(RESET)"
@echo " Current Host: $(BLUE)$(CURRENT_HOST)$(RESET)"
@echo " Exclude Flag: $(BLUE)$(EXCLUDE_CURRENT)$(RESET)"
clean: ## Clean up ansible artifacts
@echo "$(YELLOW)Cleaning up artifacts...$(RESET)"
rm -rf .ansible/facts/
find . -name "*.retry" -delete
@echo "$(GREEN)✓ Cleanup completed$(RESET)"
# Debug targets
debug: ## Run with debug output enabled
@echo "$(YELLOW)Running with debug output...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_DEV) -e "ansible_debug_output=true"
verbose: ## Run with verbose output
@echo "$(YELLOW)Running with verbose output...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_DEV) -vv
# Quick development workflow
quick: test check ## Quick test and check before applying
@echo "$(GREEN)✓ Ready to apply changes$(RESET)"
# Tailscale management
tailscale: ## Install Tailscale on all machines
@echo "$(YELLOW)Installing Tailscale on all machines...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_TAILSCALE)
@echo "$(GREEN)✓ Tailscale installation complete$(RESET)"
tailscale-check: ## Check Tailscale installation (dry-run)
@echo "$(YELLOW)Checking Tailscale installation...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_TAILSCALE) --check --diff
@echo "$(GREEN)✓ Tailscale check complete$(RESET)"
tailscale-dev: ## Install Tailscale on dev machines only
@echo "$(YELLOW)Installing Tailscale on dev machines...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_TAILSCALE) --limit dev
@echo "$(GREEN)✓ Tailscale installation on dev machines complete$(RESET)"
tailscale-status: ## Check Tailscale status on all machines
@echo "$(BOLD)Tailscale Status$(RESET)"
@if [ -n "$(CURRENT_HOST)" ]; then \
echo "$(BLUE)Auto-excluding current host: $(CURRENT_HOST) ($(CURRENT_IP))$(RESET)"; \
fi
@echo ""
@ansible all -m shell -a "tailscale status --json | jq -r '.Self.DNSName + \" (\" + .Self.TailscaleIPs[0] + \") - \" + .BackendState'" --become $(EXCLUDE_CURRENT) 2>/dev/null | while read line; do \
host=$$(echo "$$line" | cut -d' ' -f1); \
status=$$(echo "$$line" | grep -o "Running\|Stopped\|NeedsLogin" || echo "Unknown"); \
ip=$$(echo "$$line" | grep -o "100\.[0-9.]*" || echo "No IP"); \
if echo "$$line" | grep -q "SUCCESS"; then \
result=$$(echo "$$line" | cut -d'>' -f2 | tr -d ' "'); \
printf " $(GREEN)✓ %-20s$(RESET) %s\n" "$$host" "$$result"; \
elif echo "$$line" | grep -q "FAILED\|UNREACHABLE"; then \
printf " $(RED)✗ %-20s$(RESET) Not available\n" "$$host"; \
fi; \
done || ansible all -m shell -a "tailscale status" --become $(EXCLUDE_CURRENT) 2>/dev/null | grep -E "(SUCCESS|FAILED)" | while read line; do \
host=$$(echo "$$line" | cut -d' ' -f1); \
if echo "$$line" | grep -q "SUCCESS"; then \
printf " $(GREEN)✓ %-20s$(RESET) Connected\n" "$$host"; \
else \
printf " $(RED)✗ %-20s$(RESET) Failed\n" "$$host"; \
fi; \
done
# Vault management
edit-vault: ## Edit encrypted host vars (usage: make edit-vault HOST=dev01)
ifndef HOST
@echo "$(RED)Error: HOST parameter required$(RESET)"
@echo "Usage: make edit-vault HOST=dev01"
@exit 1
endif
ansible-vault edit host_vars/$(HOST).yml
edit-group-vault: ## Edit encrypted group vars (usage: make edit-group-vault)
ansible-vault edit inventories/production/group_vars/all/vault.yml
copy-ssh-key: ## Copy SSH key to specific host (usage: make copy-ssh-key HOST=giteaVM)
ifndef HOST
@echo "$(RED)Error: HOST parameter required$(RESET)"
@echo "Usage: make copy-ssh-key HOST=giteaVM"
@exit 1
endif
@echo "$(YELLOW)Copying SSH key to $(HOST)...$(RESET)"
@ip=$$(ansible-inventory --list | jq -r "._meta.hostvars.$(HOST).ansible_host // empty" 2>/dev/null); \
user=$$(ansible-inventory --list | jq -r "._meta.hostvars.$(HOST).ansible_user // empty" 2>/dev/null); \
if [ -n "$$ip" ] && [ "$$ip" != "null" ] && [ -n "$$user" ] && [ "$$user" != "null" ]; then \
echo "Target: $$user@$$ip"; \
ssh-copy-id $$user@$$ip; \
else \
echo "$(RED)Could not determine IP or user for $(HOST)$(RESET)"; \
echo "Check your inventory and host_vars"; \
fi
create-vault: ## Create encrypted vault file for secrets (passwords, auth keys, etc.)
@echo "$(YELLOW)Creating vault file for storing secrets...$(RESET)"
ansible-vault create group_vars/all/vault.yml
@echo "$(GREEN)✓ Vault file created. Add your secrets here (e.g. vault_tailscale_auth_key)$(RESET)"
create-vm: ## Create Ansible controller VM on Proxmox
@echo "$(YELLOW)Creating Ansible controller VM on Proxmox...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_PROXMOX) --ask-vault-pass
@echo "$(GREEN)✓ VM creation complete$(RESET)"
monitoring: ## Install monitoring tools on all machines
@echo "$(YELLOW)Installing monitoring tools...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_DEV) --tags monitoring
@echo "$(GREEN)✓ Monitoring installation complete$(RESET)"
proxmox-info: ## Show Proxmox VM/LXC info (usage: make proxmox-info [PROJECT=projectA] [ALL=true] [TYPE=lxc|qemu|all])
@echo "$(YELLOW)Querying Proxmox guest info...$(RESET)"
@EXTRA=""; \
if [ -n "$(PROJECT)" ]; then EXTRA="$$EXTRA -e app_project=$(PROJECT)"; fi; \
if [ "$(ALL)" = "true" ]; then EXTRA="$$EXTRA -e proxmox_info_all=true"; fi; \
if [ -n "$(TYPE)" ]; then EXTRA="$$EXTRA -e proxmox_info_type=$(TYPE)"; fi; \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_PROXMOX_INFO) $$EXTRA
app-provision: ## Provision app project containers/VMs on Proxmox (usage: make app-provision PROJECT=projectA)
ifndef PROJECT
@echo "$(RED)Error: PROJECT parameter required$(RESET)"
@echo "Usage: make app-provision PROJECT=projectA"
@exit 1
endif
@echo "$(YELLOW)Provisioning app project guests on Proxmox: $(PROJECT)$(RESET)"
$(ANSIBLE_PLAYBOOK) playbooks/app/provision_vms.yml -e app_project=$(PROJECT)
app-configure: ## Configure OS + app on project guests (usage: make app-configure PROJECT=projectA)
ifndef PROJECT
@echo "$(RED)Error: PROJECT parameter required$(RESET)"
@echo "Usage: make app-configure PROJECT=projectA"
@exit 1
endif
@echo "$(YELLOW)Configuring app project guests: $(PROJECT)$(RESET)"
$(ANSIBLE_PLAYBOOK) playbooks/app/configure_app.yml -e app_project=$(PROJECT)
app: ## Provision + configure app project (usage: make app PROJECT=projectA)
ifndef PROJECT
@echo "$(RED)Error: PROJECT parameter required$(RESET)"
@echo "Usage: make app PROJECT=projectA"
@exit 1
endif
@echo "$(YELLOW)Provisioning + configuring app project: $(PROJECT)$(RESET)"
$(ANSIBLE_PLAYBOOK) playbooks/app/site.yml -e app_project=$(PROJECT)
# Timeshift snapshot and rollback
timeshift-snapshot: ## Create Timeshift snapshot (usage: make timeshift-snapshot HOST=dev02)
ifndef HOST
@echo "$(RED)Error: HOST parameter required$(RESET)"
@echo "Usage: make timeshift-snapshot HOST=dev02"
@exit 1
endif
@echo "$(YELLOW)Creating Timeshift snapshot on $(HOST)...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_DEV) --limit $(HOST) --tags timeshift,snapshot
@echo "$(GREEN)✓ Snapshot created$(RESET)"
timeshift-list: ## List Timeshift snapshots (usage: make timeshift-list HOST=dev02)
ifndef HOST
@echo "$(RED)Error: HOST parameter required$(RESET)"
@echo "Usage: make timeshift-list HOST=dev02"
@exit 1
endif
@echo "$(YELLOW)Listing Timeshift snapshots on $(HOST)...$(RESET)"
@$(ANSIBLE_PLAYBOOK) playbooks/timeshift.yml --limit $(HOST) -e "timeshift_action=list" $(ANSIBLE_ARGS)
timeshift-restore: ## Restore from Timeshift snapshot (usage: make timeshift-restore HOST=dev02 SNAPSHOT=2025-12-17_21-30-00)
ifndef HOST
@echo "$(RED)Error: HOST parameter required$(RESET)"
@echo "Usage: make timeshift-restore HOST=dev02 SNAPSHOT=2025-12-17_21-30-00"
@exit 1
endif
ifndef SNAPSHOT
@echo "$(RED)Error: SNAPSHOT parameter required$(RESET)"
@echo "Usage: make timeshift-restore HOST=dev02 SNAPSHOT=2025-12-17_21-30-00"
@echo "$(YELLOW)Available snapshots:$(RESET)"
@$(MAKE) timeshift-list HOST=$(HOST)
@exit 1
endif
@echo "$(RED)WARNING: This will restore the system to snapshot $(SNAPSHOT)$(RESET)"
@echo "$(YELLOW)This action cannot be undone. Continue? [y/N]$(RESET)"
@read -r confirm && [ "$$confirm" = "y" ] || exit 1
@echo "$(YELLOW)Restoring snapshot $(SNAPSHOT) on $(HOST)...$(RESET)"
@$(ANSIBLE_PLAYBOOK) playbooks/timeshift.yml --limit $(HOST) -e "timeshift_action=restore timeshift_snapshot=$(SNAPSHOT)" $(ANSIBLE_ARGS)
@echo "$(GREEN)✓ Snapshot restored$(RESET)"
test-connectivity: ## Test host connectivity with detailed diagnostics and recommendations
@echo "$(YELLOW)Testing host connectivity...$(RESET)"
@if [ -f "test_connectivity.py" ]; then \
python3 test_connectivity.py --hosts-file $(INVENTORY_HOSTS); \
else \
echo "$(RED)Error: test_connectivity.py not found$(RESET)"; \
exit 1; \
fi
auto-fallback: ## Automatically switch to fallback IPs when primary IPs fail
@echo "$(YELLOW)Auto-fallback: Testing and switching to working IPs...$(RESET)"
@if [ -f "auto-fallback.sh" ]; then \
chmod +x auto-fallback.sh && ./auto-fallback.sh; \
else \
echo "$(RED)Error: auto-fallback.sh not found$(RESET)"; \
exit 1; \
fi