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
### 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
682 lines
27 KiB
Makefile
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
|