check_cluster.sh
#!/usr/bin/env bash
# Revisión rápida de salud del clúster. Solo lectura: no cambia nada.
#
# Uso: ./scripts/check_cluster.sh
# Requiere kubectl apuntando al clúster y, para la parte de keepalived,
# acceso SSH (usuario $SSH_USER, por defecto admin) a los masters.
set -uo pipefail
SSH_USER="${SSH_USER:-admin}"
VIP="192.0.2.250"
MASTERS=(192.0.2.112 192.0.2.139 192.0.2.129)
ok() { printf ' \e[32m✔\e[0m %s\n' "$*"; }
warn() { printf ' \e[33m⚠\e[0m %s\n' "$*"; }
bad() { printf ' \e[31m✘\e[0m %s\n' "$*"; }
section() { printf '\n\e[1m%s\e[0m\n' "$*"; }
section "Nodos"
kubectl get nodes --no-headers | while read -r name status _; do
[[ "$status" == "Ready" ]] && ok "$name" || bad "$name: $status"
done
section "API por la VIP ($VIP)"
if curl -sk --max-time 3 "https://$VIP:6443/healthz" -o /dev/null -w '%{http_code}' | grep -qE '200|401'; then
ok "responde"
else
bad "no responde"
fi
section "keepalived en los masters"
KEEPALIVED_STATE="journalctl -u keepalived -n 200 --no-pager 2>/dev/null | grep -oiE 'entering (master|backup|fault) state' | tail -1"
for ip in "${MASTERS[@]}"; do
if ip -4 -o addr show | grep -qF " $ip/"; then
state=$(bash -c "$KEEPALIVED_STATE") # este mismo equipo: sin SSH
else
state=$(ssh -o BatchMode=yes -o ConnectTimeout=4 "$SSH_USER@$ip" "$KEEPALIVED_STATE" 2>/dev/null)
fi
case "${state,,}" in
*master*) ok "$ip MASTER (tiene la VIP)" ;;
*backup*) ok "$ip BACKUP" ;;
*fault*) bad "$ip FAULT: no puede tomar la VIP" ;;
*) warn "$ip: estado desconocido (sin acceso SSH o sin logs)" ;;
esac
done
section "Pods que no están Running/Completed"
pods=$(kubectl get pods -A --no-headers | grep -vE 'Running|Completed')
[[ -z "$pods" ]] && ok "ninguno" || echo "$pods" | sed 's/^/ /'
section "Volúmenes Longhorn no sanos"
vols=$(kubectl -n longhorn-system get volumes.longhorn.io --no-headers \
-o custom-columns='N:.metadata.name,PVC:.status.kubernetesStatus.pvcName,S:.status.state,R:.status.robustness' \
| awk '$3=="attached" && $4!="healthy"')
[[ -z "$vols" ]] && ok "todos los volúmenes en uso están healthy" || echo "$vols" | sed 's/^/ /'
section "StorageClass por defecto"
n=$(kubectl get sc -o jsonpath='{range .items[*]}{.metadata.annotations.storageclass\.kubernetes\.io/is-default-class}{"\n"}{end}' | grep -c true)
[[ "$n" -eq 1 ]] && ok "una sola" || warn "$n StorageClasses marcadas como default"
section "Velero"
# backups.velero.io: «backup» a secas es ambiguo (Longhorn tiene backups.longhorn.io) y
# kubectl elegía el de Longhorn, vacío en este namespace: salía «no existe ningún Backup».
last=$(kubectl -n velero get backups.velero.io --sort-by=.metadata.creationTimestamp --no-headers \
-o custom-columns='N:.metadata.name,P:.status.phase' 2>/dev/null | tail -1)
if [[ -z "$last" ]]; then
bad "no existe ningún Backup"
elif [[ "$last" == *Completed ]]; then
ok "último: $last"
else
warn "último: $last"
fi
section "Cargas pausadas a propósito"
kubectl get ds -A -o jsonpath='{range .items[?(@.spec.template.spec.nodeSelector.paused-by)]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}' \
| sed 's/^/ ⏸ DaemonSet /'
kubectl get deploy -A -o jsonpath='{range .items[?(@.spec.replicas==0)]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}' \
| sed 's/^/ ⏸ Deployment a 0 réplicas: /'
echo