incus-contrib/bin/lab-status

437 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
readonly VERSION="0.1.0"
readonly SCRIPT_NAME="lab-status"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
readonly ENVS_DIR="${REPO_DIR}/envs"
# --- Defaults -----------------------------------------------------------------
QUIET=false
VERBOSE=false
ENV_NAME=""
NO_CHECK=false
JSON_OUTPUT=false
# Service arrays (parallel indexed)
declare -a SVC_NAMES=()
declare -a SVC_URLS=()
declare -a SVC_STATUSES=()
# --- Colors & logging --------------------------------------------------------
setup_colors() {
if [[ -t 1 ]] && [[ "${NO_COLOR:-}" != "1" ]] && [[ "${TERM:-}" != "dumb" ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' RESET=''
fi
}
info() { [[ "$QUIET" == true ]] && return; echo -e "${BLUE}[info]${RESET} $*"; }
success() { [[ "$QUIET" == true ]] && return; echo -e "${GREEN}[ok]${RESET} $*"; }
warn() { echo -e "${YELLOW}[warn]${RESET} $*" >&2; }
error() { echo -e "${RED}[error]${RESET} $*" >&2; }
detail() { [[ "$VERBOSE" != true ]] && return; echo -e "${DIM} $*${RESET}"; }
# --- Usage --------------------------------------------------------------------
usage() {
cat <<EOF
${BOLD}${SCRIPT_NAME}${RESET} v${VERSION} - Lab service status overview
${BOLD}USAGE${RESET}
${SCRIPT_NAME} [OPTIONS]
${BOLD}OPTIONS${RESET}
-e, --env NAME Environment name (auto-detects if only one exists)
--no-check Skip reachability checks, just show URLs
--json Machine-readable JSON output
-v, --verbose Show extra details
-q, --quiet Suppress info messages
-h, --help Show this help
${BOLD}DESCRIPTION${RESET}
Reads YAML configs from envs/<env>/ and checks whether each service
is reachable. Any HTTP response (even 401/403) counts as UP; connection
timeout or refused counts as DOWN.
Services detected: Proxmox, OC Server, Cluster Nodes, Aether, AWX,
HAProxy VIP, Grafana, Prometheus.
${BOLD}EXAMPLES${RESET}
# Show hetzner lab with reachability checks
${SCRIPT_NAME} --env hetzner
# Show beelink lab without checks
${SCRIPT_NAME} --env beelink --no-check
# JSON output for scripting
${SCRIPT_NAME} --env hetzner --json
EOF
}
# --- YAML helpers -------------------------------------------------------------
# Extract a value from a YAML section: yaml_val FILE SECTION KEY
yaml_val() {
local file="$1" section="$2" key="$3"
grep -A50 "^${section}:" "$file" \
| grep "^ *${key}:" | head -1 \
| sed "s/^ *${key}: *//" | tr -d '"' | sed 's/ *#.*//' || true
}
# Extract a top-level YAML value: yaml_val_top FILE KEY
yaml_val_top() {
local file="$1" key="$2"
grep "^${key}:" "$file" | head -1 \
| sed "s/^${key}: *//" | tr -d '"' | sed 's/ *#.*//' || true
}
# --- Service collection -------------------------------------------------------
add_service() {
local name="$1" url="$2"
SVC_NAMES+=("$name")
SVC_URLS+=("$url")
SVC_STATUSES+=("")
}
detect_env() {
if [[ -n "$ENV_NAME" ]]; then
if [[ ! -d "${ENVS_DIR}/${ENV_NAME}" ]]; then
error "Environment not found: ${ENV_NAME}"
local available
available=$(ls "$ENVS_DIR" 2>/dev/null | tr '\n' ' ')
[[ -n "$available" ]] && error "Available: ${available}"
exit 1
fi
return
fi
local envs=()
for d in "$ENVS_DIR"/*/; do
[[ -d "$d" ]] && envs+=("$(basename "$d")")
done
if [[ ${#envs[@]} -eq 1 ]]; then
ENV_NAME="${envs[0]}"
info "Auto-detected environment: ${ENV_NAME}"
elif [[ ${#envs[@]} -eq 0 ]]; then
error "No environments found in ${ENVS_DIR}/"
exit 1
else
error "Multiple environments found: ${envs[*]}"
error "Use --env NAME to select one"
exit 1
fi
}
collect_proxmox() {
local env_dir="${ENVS_DIR}/${ENV_NAME}"
local pve_file=""
if [[ -f "${env_dir}/proxmox.yaml" ]]; then
pve_file="${env_dir}/proxmox.yaml"
elif [[ -f "${env_dir}/proxmox.yaml.example" ]]; then
pve_file="${env_dir}/proxmox.yaml.example"
warn "Using proxmox.yaml.example (proxmox.yaml not found)"
else
detail "No proxmox.yaml found, skipping Proxmox"
return
fi
local host
host=$(yaml_val_top "$pve_file" "host")
if [[ -n "$host" ]]; then
host="${host%%/*}"
add_service "Proxmox" "https://${host}:8006"
detail "Proxmox from $(basename "$pve_file"): ${host}"
fi
}
collect_lab_vms() {
local env_dir="${ENVS_DIR}/${ENV_NAME}"
local lab_file=""
# Prefer lab-production.yaml over lab-cluster.yaml
if [[ -f "${env_dir}/lab-production.yaml" ]]; then
lab_file="${env_dir}/lab-production.yaml"
elif [[ -f "${env_dir}/lab-cluster.yaml" ]]; then
lab_file="${env_dir}/lab-cluster.yaml"
else
detail "No lab-*.yaml found, skipping cluster VMs"
return
fi
detail "Lab VMs from $(basename "$lab_file")"
local vm_data
vm_data=$(python3 -c "
import sys, re
try:
import yaml
with open(sys.argv[1]) as f:
data = yaml.safe_load(f)
except ImportError:
data = {'vms': []}
with open(sys.argv[1]) as f:
content = f.read()
in_vms = False
current = {}
for line in content.split('\n'):
if line.startswith('vms:'):
in_vms = True
continue
if in_vms:
if line.startswith(' - '):
if current:
data['vms'].append(current)
current = {}
line = line[4:]
elif line.startswith(' '):
line = line[4:]
elif line and not line.startswith(' ') and not line.startswith('#'):
if current:
data['vms'].append(current)
break
else:
continue
m = re.match(r'(\w+):\s*(.*)', line.strip())
if m:
current[m.group(1)] = m.group(2).strip('\"')
if current:
data['vms'].append(current)
for vm in data.get('vms', []):
name = vm.get('name', '')
app = vm.get('app', '')
ip = vm.get('ip', '').split('/')[0]
print(f'{name}|{app}|{ip}')
" "$lab_file" 2>/dev/null) || return
local node_idx=0
while IFS='|' read -r name app ip; do
[[ -z "$ip" ]] && continue
case "$app" in
operations-center)
add_service "OC Server" "https://${ip}:8443"
;;
incus)
node_idx=$((node_idx + 1))
local label
label=$(printf "Cluster Node %02d" "$node_idx")
add_service "$label" "https://${ip}:8443"
;;
esac
done <<< "$vm_data"
}
collect_haproxy() {
local env_dir="${ENVS_DIR}/${ENV_NAME}"
local file="${env_dir}/haproxy.yaml"
[[ -f "$file" ]] || return
local aether_url vip
aether_url=$(yaml_val "$file" "haproxy" "aether_url")
vip=$(yaml_val "$file" "haproxy" "vip")
if [[ -n "$aether_url" ]]; then
add_service "Aether" "$aether_url"
detail "Aether from haproxy.yaml: ${aether_url}"
fi
if [[ -n "$vip" ]]; then
vip="${vip%%/*}"
add_service "HAProxy VIP" "http://${vip}:80"
detail "HAProxy VIP from haproxy.yaml: ${vip}"
fi
}
collect_awx() {
local env_dir="${ENVS_DIR}/${ENV_NAME}"
local file="${env_dir}/awx.yaml"
[[ -f "$file" ]] || return
local ip
ip=$(yaml_val "$file" "awx" "ip")
[[ -z "$ip" ]] && return
ip="${ip%%/*}"
add_service "AWX" "http://${ip}:30080"
detail "AWX from awx.yaml: ${ip}"
}
collect_observability() {
local env_dir="${ENVS_DIR}/${ENV_NAME}"
local file="${env_dir}/observability.yaml"
[[ -f "$file" ]] || return
local forward_vip
forward_vip=$(yaml_val "$file" "observability" "forward_vip")
[[ -z "$forward_vip" ]] && return
forward_vip="${forward_vip%%/*}"
add_service "Grafana" "http://${forward_vip}:3000"
add_service "Prometheus" "http://${forward_vip}:9090"
detail "Observability from observability.yaml: forward_vip=${forward_vip}"
}
# --- Reachability checks -----------------------------------------------------
check_reachability() {
local url="$1" result_file="$2"
local http_code
http_code=$(curl -sk --connect-timeout 2 --max-time 3 \
-o /dev/null -w '%{http_code}' "$url" 2>/dev/null) || http_code="000"
if [[ "$http_code" != "000" ]]; then
echo "UP" > "$result_file"
else
echo "DOWN" > "$result_file"
fi
}
run_checks() {
local tmpdir
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/${SCRIPT_NAME}-XXXXXX")
# shellcheck disable=SC2064
trap "rm -rf '$tmpdir'" EXIT
local i
for i in "${!SVC_URLS[@]}"; do
check_reachability "${SVC_URLS[$i]}" "${tmpdir}/${i}" &
done
wait
for i in "${!SVC_URLS[@]}"; do
if [[ -f "${tmpdir}/${i}" ]]; then
SVC_STATUSES[$i]=$(cat "${tmpdir}/${i}")
else
SVC_STATUSES[$i]="DOWN"
fi
done
}
# --- Output -------------------------------------------------------------------
print_table() {
local max_name=7 max_url=3 # minimum: "Service" / "URL"
local i
for i in "${!SVC_NAMES[@]}"; do
[[ ${#SVC_NAMES[$i]} -gt $max_name ]] && max_name=${#SVC_NAMES[$i]}
[[ ${#SVC_URLS[$i]} -gt $max_url ]] && max_url=${#SVC_URLS[$i]}
done
local total_width=$((max_name + max_url + 14))
local sep
sep=$(printf '%*s' "$total_width" '' | tr ' ' '─')
echo
echo -e "${BOLD}Lab Environment: ${ENV_NAME}${RESET}"
echo "$sep"
printf "%-${max_name}s %-${max_url}s %s\n" "Service" "URL" "Status"
echo "$sep"
for i in "${!SVC_NAMES[@]}"; do
local status="${SVC_STATUSES[$i]:-}"
local status_colored
case "$status" in
UP) status_colored="${GREEN}UP${RESET}" ;;
DOWN) status_colored="${RED}DOWN${RESET}" ;;
*) status_colored="$status" ;;
esac
printf "%-${max_name}s %-${max_url}s " "${SVC_NAMES[$i]}" "${SVC_URLS[$i]}"
echo -e "$status_colored"
done
echo "$sep"
echo
}
print_json() {
local last=$((${#SVC_NAMES[@]} - 1))
echo "["
local i
for i in "${!SVC_NAMES[@]}"; do
local comma=","
[[ $i -eq $last ]] && comma=""
local status_json
if [[ -n "${SVC_STATUSES[$i]:-}" ]]; then
status_json="\"${SVC_STATUSES[$i]}\""
else
status_json="null"
fi
printf ' {"service": "%s", "url": "%s", "status": %s}%s\n' \
"${SVC_NAMES[$i]}" "${SVC_URLS[$i]}" "$status_json" "$comma"
done
echo "]"
}
# --- Argument parsing ---------------------------------------------------------
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-e|--env)
[[ $# -lt 2 ]] && { error "--env requires a value"; exit 1; }
ENV_NAME="$2"; shift
;;
--no-check) NO_CHECK=true ;;
--json) JSON_OUTPUT=true ;;
-v|--verbose) VERBOSE=true ;;
-q|--quiet) QUIET=true ;;
-h|--help) usage; exit 0 ;;
*)
error "Unknown option: $1"
echo "Run '${SCRIPT_NAME} --help' for usage."
exit 1
;;
esac
shift
done
}
# --- Main ---------------------------------------------------------------------
main() {
setup_colors
parse_args "$@"
detect_env
# Collect services from all config files
collect_proxmox
collect_lab_vms
collect_haproxy
collect_awx
collect_observability
if [[ ${#SVC_NAMES[@]} -eq 0 ]]; then
warn "No services found in envs/${ENV_NAME}/"
exit 0
fi
# Run reachability checks (parallel)
if [[ "$NO_CHECK" != true ]]; then
info "Checking ${#SVC_NAMES[@]} services..."
run_checks
fi
# Output
if [[ "$JSON_OUTPUT" == true ]]; then
print_json
else
print_table
fi
}
main "$@"