Makefile
Rama main · réplica de lectura del repositorio original.
SHELL := /bin/bash
default: help;
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
current_dir := $(notdir $(patsubst %/,%,$(dir $(mkfile_path))))
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
# ======================================================================================
##@ Prepare
SOPS_VERSION ?= v3.13.3
# Interpreter used to build ./venv, defaulting to the system python3 (not pinned).
# 'make prepare' recreates the venv whenever it was built for a different Python
# version than the current one (e.g. after an OS upgrade bumps python3). Override
# only for a one-off, e.g. 'make prepare PYTHON=python3.12'.
PYTHON ?= python3
check-tools: ## Verify sops & age are installed; offer to install them if missing
@missing=""; \
for tool in sops age; do \
command -v $$tool >/dev/null 2>&1 || missing="$$missing $$tool"; \
done; \
if [ -z "$$missing" ]; then \
echo "Required tools found: sops, age."; \
exit 0; \
fi; \
echo "Missing required tool(s):$$missing"; \
if [ ! -t 0 ]; then \
echo "Non-interactive shell; cannot prompt. Install manually:" >&2; \
echo " age -> sudo apt install age" >&2; \
echo " sops -> https://github.com/getsops/sops/releases ($(SOPS_VERSION) .deb)" >&2; \
exit 1; \
fi; \
read -r -p "Install the missing tool(s) now? [y/N] " ans; \
case "$$ans" in \
[yY]|[yY][eE][sS]) ;; \
*) echo "Aborted. Install the tool(s) and re-run 'make prepare'."; exit 1 ;; \
esac; \
for tool in $$missing; do \
case "$$tool" in \
age) \
echo ">> Installing age via apt ..."; \
sudo apt-get update && sudo apt-get install -y age || exit 1; \
;; \
sops) \
ver="$(SOPS_VERSION)"; arch=$$(dpkg --print-architecture); \
deb="sops_$${ver#v}_$${arch}.deb"; \
url="https://github.com/getsops/sops/releases/download/$${ver}/$${deb}"; \
echo ">> Installing sops $${ver} ($${arch}) from GitHub releases ..."; \
tmp=$$(mktemp -d); \
if command -v curl >/dev/null 2>&1; then \
curl -fLo "$$tmp/$$deb" "$$url"; \
elif command -v wget >/dev/null 2>&1; then \
wget -qO "$$tmp/$$deb" "$$url"; \
else \
echo "Error: need curl or wget to download sops." >&2; rm -rf "$$tmp"; exit 1; \
fi && sudo apt-get install -y "$$tmp/$$deb" || { rm -rf "$$tmp"; echo "Error: sops install failed." >&2; exit 1; }; \
rm -rf "$$tmp"; \
;; \
esac; \
done; \
for tool in sops age; do \
command -v $$tool >/dev/null 2>&1 || { echo "Error: $$tool still not found after install." >&2; exit 1; }; \
done; \
echo "All required tools installed."
prepare: check-tools ## Prepare the ansible environment (recreates ./venv if it's incompatible with the current Python)
@command -v $(PYTHON) >/dev/null 2>&1 || { \
echo "Error: '$(PYTHON)' not found; install it or pass PYTHON=..., e.g. 'make prepare PYTHON=python3'." >&2; exit 1; }
@cur=$$($(PYTHON) -c 'import sys; print("%d.%d" % sys.version_info[:2])'); \
if [ -f ./venv/pyvenv.cfg ]; then \
venvver=$$(sed -n 's/^version *= *\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' ./venv/pyvenv.cfg); \
if [ "$$venvver" != "$$cur" ]; then \
echo ">> ./venv was built for Python $${venvver:-unknown}, current $(PYTHON) is $$cur; recreating"; \
rm -rf ./venv; \
fi; \
elif [ -d ./venv ]; then \
echo ">> ./venv is incomplete (no pyvenv.cfg); recreating"; \
rm -rf ./venv; \
fi; \
if [ ! -d ./venv ]; then \
echo ">> creating ./venv with $(PYTHON) ($$cur)"; \
$(PYTHON) -m venv ./venv; \
fi
@source venv/bin/activate && pip install -r ./requirements.txt
@source venv/bin/activate && ansible-galaxy collection install community.sops
@echo
@echo "Don't forget to activate the Venv with 'source venv/bin/activate'"
INV ?= inventory/vagrant.yaml
VER ?= 13
##@ Run
enroll: ## run the enroll tag on a specific host; vars: INV, HOST, ANSIBLE_PASS (optional), ANSIBLE_USER
@echo "Running with inventory: $(INV)" && \
. ./venv/bin/activate && \
if [ -z "$(HOST)" ]; then \
echo "Error: HOST variable is required!"; \
exit 1; \
fi && \
if [ -z "$(ANSIBLE_USER)" ]; then \
echo "Error: ANSIBLE_USER variable is required!"; \
exit 1; \
fi && \
if [ -z "$(INV)" ]; then \
echo "Error: Missing required parameter 'INV'" >&2; \
exit 1; \
fi && \
HOST_VAL="-l $(HOST)" && \
EXTRA_VARS="ansible_user=$$ANSIBLE_USER" && \
if [ -n "$$ANSIBLE_PASS" ]; then \
EXTRA_VARS="$$EXTRA_VARS ansible_ssh_pass=$$ANSIBLE_PASS ansible_become_pass=$$ANSIBLE_PASS"; \
fi && \
ansible-playbook -i $(INV) -u $(ANSIBLE_USER) -t enroll $$HOST_VAL \
--extra-vars "$$EXTRA_VARS" \
--become autobott.yaml
run: ## run playbook, env Vars: INV=inventory_path, HOST=<host>, TAG=<tag>
@if [ -z "$(INV)" ]; then \
echo "Error: Missing required parameter 'INV'" >&2; \
exit 1; \
fi && \
echo "Running with inventory: $(INV)" && \
. ./venv/bin/activate && \
TAG_VAL=$$( [ -n "$$TAG" ] && echo "-t $$TAG" || echo "" ) && \
HOST_VAL=$$( [ -n "$(HOST)" ] && echo "-l $(HOST)" || echo "" ) && \
echo "Using tag: $$TAG_VAL" && \
echo "Using host: $$HOST_VAL" && \
ansible-playbook -i $(INV) $$TAG_VAL $$HOST_VAL autobott.yaml
run-verbose: ## run playbook, env Vars: INV=inventory_path, HOST=<host>, TAG=<tag>
@if [ -z "$(INV)" ]; then \
echo "Error: Missing required parameter 'INV'" >&2; \
exit 1; \
fi && \
echo "Running with inventory: $(INV)" && \
. ./venv/bin/activate && \
TAG_VAL=$$( [ -n "$$TAG" ] && echo "-t $$TAG" || echo "" ) && \
HOST_VAL=$$( [ -n "$(HOST)" ] && echo "-l $(HOST)" || echo "" ) && \
echo "Using tag: $$TAG_VAL" && \
echo "Using host: $$HOST_VAL" && \
ansible-playbook -vvv -i $(INV) $$TAG_VAL $$HOST_VAL autobott.yaml
# 'run-local' reads INV/HOST/TAG (and optional EXTRA_ARGS / SOPS_AGE_KEY_FILE) from a
# git-ignored env file so you don't retype them on every 'make run'. Copy
# run-local.env.example -> run-local.env and edit it. Override the file: RUN_LOCAL_FILE=<path>.
# Any of INV/HOST/TAG/EXTRA_ARGS/SOPS_AGE_KEY_FILE passed on the command line
# (e.g. 'make run-local TAG=role_docker') takes precedence over the env file.
RUN_LOCAL_FILE ?= run-local.env
run-local: ## run playbook with INV/HOST/TAG from a git-ignored env file (default: run-local.env)
@if [ ! -f "$(RUN_LOCAL_FILE)" ]; then \
echo "Error: '$(RUN_LOCAL_FILE)' not found." >&2; \
echo " Create it from the template: cp run-local.env.example $(RUN_LOCAL_FILE)" >&2; \
exit 1; \
fi
@_INV_CLI="$$INV"; _HOST_CLI="$$HOST"; _TAG_CLI="$$TAG"; _EXTRA_CLI="$$EXTRA_ARGS"; _SOPS_CLI="$(SOPS_AGE_KEY_FILE_CLI)"; \
unset SOPS_AGE_KEY_FILE; \
set -a; . "$(CURDIR)/$(RUN_LOCAL_FILE)"; set +a; \
INV="$${_INV_CLI:-$$INV}"; HOST="$${_HOST_CLI:-$$HOST}"; TAG="$${_TAG_CLI:-$$TAG}"; \
EXTRA_ARGS="$${_EXTRA_CLI:-$$EXTRA_ARGS}"; SOPS_AGE_KEY_FILE="$${_SOPS_CLI:-$$SOPS_AGE_KEY_FILE}"; \
if [ -z "$$INV" ]; then \
echo "Error: 'INV' is not set in $(RUN_LOCAL_FILE)" >&2; \
exit 1; \
fi; \
if [ -z "$$SOPS_AGE_KEY_FILE" ]; then \
if [ -d "$$INV" ]; then SOPS_AGE_KEY_FILE="$$INV/sops_key"; \
else SOPS_AGE_KEY_FILE="$$(dirname "$$INV")/sops_key"; fi; \
fi; \
export SOPS_AGE_KEY_FILE; \
echo "Running with inventory: $$INV" && \
echo "Using sops key: $$SOPS_AGE_KEY_FILE" && \
. ./venv/bin/activate && \
TAG_VAL=$$( [ -n "$$TAG" ] && echo "-t $$TAG" || echo "" ) && \
HOST_VAL=$$( [ -n "$$HOST" ] && echo "-l $$HOST" || echo "" ) && \
echo "Using tag: $$TAG_VAL" && \
echo "Using host: $$HOST_VAL" && \
ansible-playbook -i "$$INV" $$TAG_VAL $$HOST_VAL $$EXTRA_ARGS autobott.yaml
##@ Secrets
# The age private key sops uses lives in the inventory's own directory, as
# <inventory-dir>/sops_key. INV may be the inventory DIRECTORY (key goes inside
# it) or an inventory FILE (key goes in the file's directory). Or set
# SOPS_AGE_KEY_FILE=<path> to point directly at a key file.
sops_key_dir := $(if $(wildcard $(INV)/.),$(abspath $(INV))/,$(dir $(abspath $(INV))))
export SOPS_AGE_KEY_FILE ?= $(sops_key_dir)sops_key
# The default export above always sets SOPS_AGE_KEY_FILE (derived from the sample
# INV), so run-local can't tell it apart from a user-supplied key. Capture the
# value ONLY when it actually came from the command line, so a bare 'make
# run-local' derives the key from the env-file INV instead of this default.
SOPS_AGE_KEY_FILE_CLI := $(if $(filter command line,$(origin SOPS_AGE_KEY_FILE)),$(SOPS_AGE_KEY_FILE),)
age-key: check-tools ## create the age key for an EXTERNAL inventory (<inv-dir>/sops_key); requires INV (not the in-repo sample), never overwrites
@if [ ! -e "$(INV)" ]; then \
echo "Error: inventory '$(INV)' not found — pass INV=<inventory_path>." >&2; exit 1; \
fi
@if [ -n "$(filter $(ROOT_DIR)/%,$(abspath $(INV)))" ]; then \
echo "Error: refusing to create a key for an in-repo inventory." >&2; \
echo " age-key is for external inventories: INV=<path outside this repo>." >&2; \
echo " The bundled inventory/sops_key is committed as an example." >&2; \
exit 1; \
fi
@if [ -f "$(SOPS_AGE_KEY_FILE)" ]; then \
echo "Error: key already exists at $(SOPS_AGE_KEY_FILE) — refusing to overwrite." >&2; exit 1; \
fi
@echo "Creating age key at $(SOPS_AGE_KEY_FILE) ..."
@mkdir -p "$(dir $(SOPS_AGE_KEY_FILE))"
@age-keygen -o "$(SOPS_AGE_KEY_FILE)"
@chmod 600 "$(SOPS_AGE_KEY_FILE)"
@echo "Done. Add this public key to your .sops.yaml recipients:"
@grep 'public key:' "$(SOPS_AGE_KEY_FILE)"
seal-secrets: ## first-time encrypt a plaintext secrets.sops.yaml in place (migration); vars: INV=inventory_path, HOST=hostname
@if [ ! -e "$(INV)" ]; then echo "Error: inventory '$(INV)' not found (pass INV=<inventory_path>)" >&2; exit 1; fi
@if [ -z "$(HOST)" ]; then echo "Error: Missing required parameter 'HOST'" >&2; exit 1; fi
cd $(sops_key_dir) && sops -e -i host_vars/$(HOST)/secrets.sops.yaml
edit-secrets: ## edit a host's sops secrets in your editor; vars: INV=inventory_path, HOST=hostname
@if [ ! -e "$(INV)" ]; then echo "Error: inventory '$(INV)' not found (pass INV=<inventory_path>)" >&2; exit 1; fi
@if [ -z "$(HOST)" ]; then echo "Error: Missing required parameter 'HOST'" >&2; exit 1; fi
@if [ ! -f "$(sops_key_dir)host_vars/$(HOST)/secrets.sops.yaml" ]; then echo "Error: sops file not found: $(sops_key_dir)host_vars/$(HOST)/secrets.sops.yaml" >&2; exit 1; fi
@if ! sops filestatus "$(sops_key_dir)host_vars/$(HOST)/secrets.sops.yaml" 2>/dev/null | grep -q '"encrypted":true'; then echo "Error: $(sops_key_dir)host_vars/$(HOST)/secrets.sops.yaml is not sops-encrypted — seal it first: make seal-secrets INV=$(INV) HOST=$(HOST)" >&2; exit 1; fi
cd $(sops_key_dir) && sops host_vars/$(HOST)/secrets.sops.yaml
edit-secrets-kate: ## edit a host's sops secrets in Kate (blocking GUI editor); vars: INV=inventory_path, HOST=hostname
@if [ ! -e "$(INV)" ]; then echo "Error: inventory '$(INV)' not found (pass INV=<inventory_path>)" >&2; exit 1; fi
@if [ -z "$(HOST)" ]; then echo "Error: Missing required parameter 'HOST'" >&2; exit 1; fi
@if [ ! -f "$(sops_key_dir)host_vars/$(HOST)/secrets.sops.yaml" ]; then echo "Error: sops file not found: $(sops_key_dir)host_vars/$(HOST)/secrets.sops.yaml" >&2; exit 1; fi
@if ! sops filestatus "$(sops_key_dir)host_vars/$(HOST)/secrets.sops.yaml" 2>/dev/null | grep -q '"encrypted":true'; then echo "Error: $(sops_key_dir)host_vars/$(HOST)/secrets.sops.yaml is not sops-encrypted — seal it first: make seal-secrets INV=$(INV) HOST=$(HOST)" >&2; exit 1; fi
cd $(sops_key_dir) && EDITOR='kate -b' sops host_vars/$(HOST)/secrets.sops.yaml
view-secrets: ## decrypt & print a host's sops secrets; vars: INV=inventory_path, HOST=hostname
@if [ ! -e "$(INV)" ]; then echo "Error: inventory '$(INV)' not found (pass INV=<inventory_path>)" >&2; exit 1; fi
@if [ -z "$(HOST)" ]; then echo "Error: Missing required parameter 'HOST'" >&2; exit 1; fi
cd $(sops_key_dir) && sops -d host_vars/$(HOST)/secrets.sops.yaml
rekey: ## re-encrypt all sops secrets after editing .sops.yaml recipients; vars: INV=inventory_path
@if [ ! -e "$(INV)" ]; then echo "Error: inventory '$(INV)' not found (pass INV=<inventory_path>)" >&2; exit 1; fi
cd $(sops_key_dir) && find host_vars -name 'secrets.sops.yaml' -exec sops updatekeys -y {} \;
##@ Vagrant
vagrant-base: ## Bake the base images for all debian versions (only needed once)
@cd vagrant/bake-base/ && ./bake-base-box.sh 13
# TODO: Debian 14 (forky) placeholder -- uncomment once its base box exists:
# @cd vagrant/bake-base/ && ./bake-base-box.sh 14
# fix included ssh key permissions
fix-ssh-key-perm:
@stat $(ROOT_DIR)/vagrant/autobott-key > /dev/null
@echo "changing permissions of key: $(ROOT_DIR)/vagrant/autobott-key"
@chmod 600 $(ROOT_DIR)/vagrant/autobott-key
vagrant-up: fix-ssh-key-perm ## start the vagrant environment and bootstrap provisioning, vars: VER=<13|13-k3s> (default 13)
@source ./venv/bin/activate && cd vagrant && vagrant up ansible-autobott2-linux-debian-$(VER)
.PHONY: check-vagrant-running
check-vagrant-running: # fail early (with a clear message) if the target VM (VER) isn't running
@state=$$(VAGRANT_CWD=vagrant vagrant status --machine-readable ansible-autobott2-linux-debian-$(VER) 2>/dev/null | awk -F, '$$3 == "state" { print $$4 }'); \
if [ "$$state" != "running" ]; then \
echo "Error: Vagrant VM 'ansible-autobott2-linux-debian-$(VER)' is '$${state:-unknown}', not running." >&2; \
echo " Start it first: make vagrant-up VER=$(VER)" >&2; \
exit 1; \
fi
vagrant-run: check-vagrant-running ## run playbook on vagrant, vars: TAG=<tag> (default all), VER=<13|13-k3s> (default 13)
@ssh-add ./vagrant/autobott-key # used in sftp connections
@mkdir -p logs
@. ./venv/bin/activate && \
if [ -n "$$TAG" ]; then \
TAG_VAL="-t $$TAG"; \
else \
TAG_VAL=""; \
fi && \
TIMESTAMP="$$(date +%Y%m%d-%H%M%S)" && \
RAW_LOG="/tmp/ansible-vagrant-$$TIMESTAMP.log" && \
LOG_FILE="logs/vagrant-changed-$$TIMESTAMP.log" && \
echo "Using tag: $$TAG_VAL" && \
echo "Using debian version: $(VER)" && \
echo "Logging to: $$LOG_FILE" && \
ANSIBLE_FORCE_COLOR=1 ansible-playbook autobott.yaml \
-i inventory/vagrant.yaml \
-i vagrant/.vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory \
--extra-vars "ansible_ssh_user='ans'" \
-l ansible-autobott2-linux-debian-$(VER) \
$$TAG_VAL 2>&1 | tee >(sed 's/\x1b\[[0-9;]*m//g' | awk '/^TASK \[/{task=$$0; in_dep=0; next} /\[DEPRECATION WARNING\]/{print task; in_dep=1; print; next} in_dep && /^(ok:|changed:|skipping:|failed:|PLAY )/{in_dep=0} in_dep{print; next} /^changed:/{print task; print}' > "$$LOG_FILE"); \
wait
vagrant-run-verbose: ## run playbook on vagrant in verbose mode, vars: TAG=<tag>, VER=<13|13-k3s> (default 13)
@ssh-add ./vagrant/autobott-key # used in sftp connections
@. ./venv/bin/activate && \
if [ -n "$$TAG" ]; then \
TAG_VAL="-t $$TAG"; \
else \
TAG_VAL=""; \
fi && \
echo "Using tag: $$TAG_VAL" && \
echo "Using debian version: $(VER)" && \
ansible-playbook autobott.yaml -vvv \
-i vagrant/.vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory \
-i inventory/vagrant.yaml \
--extra-vars "ansible_ssh_user='ans'" \
-l ansible-autobott2-linux-debian-$(VER) \
$$TAG_VAL
vagrant-run-short: ## run short playbook on vagrant: only run tags that generally require updates or config changes, vars: VER=<13|13-k3s> (default 13)
@ssh-add ./vagrant/autobott-key # used in sftp connections
@. ./venv/bin/activate && \
ansible-playbook autobott.yaml \
-i vagrant/.vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory \
-i inventory/vagrant.yaml \
--extra-vars "ansible_ssh_user='ans'" \
-l ansible-autobott2-linux-debian-$(VER) \
-t linux-upgrade
vagrant-test: ## run validation tests, vars: VER=<13|13-k3s> (default 13)
@ssh-add ./vagrant/autobott-key # used in sftp connections
@. ./venv/bin/activate && \
ansible-playbook test.yaml \
-i vagrant/.vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory \
-i inventory/vagrant.yaml \
--extra-vars "ansible_ssh_user='ans'" \
-l ansible-autobott2-linux-debian-$(VER)
vagrant-destroy: ## Delete all vagrant VMs
@cd vagrant && vagrant destroy -f
vagrant-snapshot-save: ## take a snapshot of the vagrant state
@cd vagrant && \
vagrant snapshot save ansible-autobott2-linux-debian-13 automated-snapshot --force
vagrant-snapshot-restore: ## restore to snapshot of the vagrant state
@cd vagrant && \
vagrant snapshot restore ansible-autobott2-linux-debian-13 automated-snapshot
##@ Release
.PHONY: check-git-clean
check-git-clean: # check if git repo is clean
@git diff --quiet
.PHONY: check-branch
check-branch:
@current_branch=$$(git symbolic-ref --short HEAD) && \
if [ "$$current_branch" != "main" ]; then \
echo "Error: You are on branch '$$current_branch'. Please switch to 'main'."; \
exit 1; \
fi
check-autobott-version:
@[ "${version}" ] || ( echo ">> version is not set, usage: make tag version=\"v1.2.3\" "; exit 1 )
@AUTOBOT_VERSION=$$(grep -E '^autobot_version:' ./roles/base/enroll/defaults/main.yaml | awk '{print $$2}') && \
if [ "$$AUTOBOT_VERSION" != "$(version)" ]; then \
echo "Error: autobot_version ($$AUTOBOT_VERSION) does not match the release version ($(version))"; \
exit 1; \
else \
echo "autobot_version ($$AUTOBOT_VERSION) matches release version ($(version))"; \
fi
.PHONY: tag
tag: check-branch check-git-clean check-autobott-version ## tag a release and push it; the release workflow then publishes it on GitHub. Usage: make tag version="v1.2.3"
@[ "${version}" ] || ( echo ">> version is not set, usage: make tag version=\"v1.2.3\" "; exit 1 )
@git tag -d $(version) || true
@git tag -a $(version) -m "Release version: $(version)"
@git push --delete origin $(version) || true
@git push origin $(version) || true
##@ Test
lint: ## run strict ansible lint (same as CI)
@. ./venv/bin/activate && \
cd roles && \
ansible-lint -s -v
lint-fix: ## auto-fix ansible lint violations where possible
@. ./venv/bin/activate && \
cd roles && \
ansible-lint -s -v --fix
##@ Help
.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)