diff --git a/tools/lib/ppf-common.sh b/tools/lib/ppf-common.sh new file mode 100644 index 0000000..d85caed --- /dev/null +++ b/tools/lib/ppf-common.sh @@ -0,0 +1,158 @@ +#!/bin/bash +# ppf-common.sh -- shared library for PPF operations toolkit +# Source this file; do not execute directly. + +set -eu + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- +PPF_DIR="${PPF_DIR:-$HOME/git/ppf}" +ANSIBLE_DIR="/opt/ansible" +ANSIBLE_VENV="${ANSIBLE_DIR}/venv/bin/activate" + +# --------------------------------------------------------------------------- +# Host topology +# --------------------------------------------------------------------------- +MASTER="odin" +WORKERS="cassius edge sentinel" +ALL_HOSTS="odin cassius edge sentinel" + +# Container names per role +MASTER_CONTAINER="ppf" +WORKER_CONTAINER="ppf-worker" + +# --------------------------------------------------------------------------- +# Colors (respects NO_COLOR -- https://no-color.org) +# --------------------------------------------------------------------------- +if [ -z "${NO_COLOR:-}" ] && [ -t 1 ]; then + C_RST='\033[0m' + C_DIM='\033[2m' + C_BOLD='\033[1m' + C_RED='\033[38;5;167m' + C_GREEN='\033[38;5;114m' + C_YELLOW='\033[38;5;180m' + C_BLUE='\033[38;5;110m' + C_CYAN='\033[38;5;116m' +else + C_RST='' C_DIM='' C_BOLD='' C_RED='' C_GREEN='' + C_YELLOW='' C_BLUE='' C_CYAN='' +fi + +# --------------------------------------------------------------------------- +# Output helpers +# --------------------------------------------------------------------------- +log_ok() { printf "${C_GREEN} ✓${C_RST} %s\n" "$*"; } +log_err() { printf "${C_RED} ✗${C_RST} %s\n" "$*" >&2; } +log_warn() { printf "${C_YELLOW} ⚠${C_RST} %s\n" "$*"; } +log_info() { printf "${C_BLUE} ●${C_RST} %s\n" "$*"; } +log_dim() { printf "${C_DIM} %s${C_RST}\n" "$*"; } + +die() { log_err "$@"; exit 1; } + +# Section header +section() { + printf "\n${C_BOLD}${C_CYAN} %s${C_RST}\n" "$*" +} + +# --------------------------------------------------------------------------- +# Host resolution helpers +# --------------------------------------------------------------------------- +is_master() { [ "$1" = "$MASTER" ]; } +is_worker() { + local h + for h in $WORKERS; do [ "$h" = "$1" ] && return 0; done + return 1 +} + +container_name() { + if is_master "$1"; then echo "$MASTER_CONTAINER"; else echo "$WORKER_CONTAINER"; fi +} + +# Expand target aliases into host list +# "all" -> all hosts +# "workers" -> worker hosts +# "odin" -> just odin +# Multiple args are concatenated with comma +resolve_targets() { + local targets="" + local arg + for arg in "$@"; do + case "$arg" in + all) targets="${targets:+$targets }$ALL_HOSTS" ;; + workers) targets="${targets:+$targets }$WORKERS" ;; + master) targets="${targets:+$targets }$MASTER" ;; + *) targets="${targets:+$targets }$arg" ;; + esac + done + # Deduplicate while preserving order + echo "$targets" | tr ' ' '\n' | awk '!seen[$0]++' | tr '\n' ' ' | sed 's/ $//' +} + +# Convert space-separated host list to comma-separated for ansible +hosts_csv() { + echo "$*" | tr ' ' ',' +} + +# --------------------------------------------------------------------------- +# Ansible wrapper +# --------------------------------------------------------------------------- +# Runs ansible with venv activated and ANSIBLE_REMOTE_TMP set. +# Usage: ansible_cmd +ansible_cmd() { + ( + # shellcheck disable=SC1090 + . "$ANSIBLE_VENV" + cd "$ANSIBLE_DIR" + ANSIBLE_REMOTE_TMP=/tmp/.ansible ansible "$@" + ) +} + +# --------------------------------------------------------------------------- +# Remote podman/compose wrappers +# --------------------------------------------------------------------------- +# Run a podman command on a remote host as the podman user. +# Uses dynamic UID discovery. +# Usage: podman_cmd HOST "podman subcommand..." +podman_cmd() { + local host="$1"; shift + local cmd="$*" + ansible_cmd "$host" -m raw -a \ + "uid=\$(id -u podman) && sudo -u podman XDG_RUNTIME_DIR=/run/user/\$uid $cmd" +} + +# Run a podman-compose subcommand on a remote host. +# Usage: compose_cmd HOST "subcommand [args]" +compose_cmd() { + local host="$1"; shift + local cmd="$*" + ansible_cmd "$host" -m raw -a \ + "uid=\$(id -u podman) && cd /home/podman/ppf && sudo -u podman XDG_RUNTIME_DIR=/run/user/\$uid podman-compose $cmd" +} + +# --------------------------------------------------------------------------- +# Validation +# --------------------------------------------------------------------------- +validate_syntax() { + local errors=0 + local f + section "Validating Python syntax" + for f in "$PPF_DIR"/*.py; do + [ -f "$f" ] || continue + if python3 -m py_compile "$f" 2>/dev/null; then + log_dim "$(basename "$f")" + else + log_err "$(basename "$f")" + errors=$((errors + 1)) + fi + done + if [ "$errors" -gt 0 ]; then + die "$errors file(s) failed syntax check" + fi + log_ok "All files valid" +} + +# --------------------------------------------------------------------------- +# Version +# --------------------------------------------------------------------------- +PPF_TOOLS_VERSION="1.0.0"