437 lines
12 KiB
Bash
Executable File
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 "$@"
|