portal.sh
#!/usr/bin/env bash
# Portal de actualizaciones de los nodos (Semaphore UI) en el master-1. Como root.
#
# ./scripts/portal.sh instalar primera vez: secretos, usuario admin, claves, arranque y botones
# ./scripts/portal.sh botones crea o completa los botones (pide la contraseña del admin)
# ./scripts/portal.sh repo pone el portal al día con main (tras mergear cambios en ansible/)
# ./scripts/portal.sh claves vuelve a copiar la clave SSH, known_hosts y kubeconfig
# ./scripts/portal.sh estado | registros | parar | arrancar
#
# Web: http://203.0.113.121:3010 (solo por Tailscale). Ver ansible/README.md.
set -euo pipefail
RAIZ="$(cd "$(dirname "$0")/.." && pwd)"
COMPOSE=(docker compose -f "$RAIZ/ansible/semaphore/compose.yaml")
CONF=/root/.config/semaphore
ENV_FILE="$CONF/semaphore.env"
CLAVES_ORIGEN=/root/.config/mantenimiento
REPO=/opt/portal-nodos/repo
URL="http://${PORTAL_IP:-203.0.113.121}:${PORTAL_PUERTO:-3010}"
UID_SEMAPHORE=1001
umask 077
[ "$(id -u)" -eq 0 ] || { echo "Lánzalo como root" >&2; exit 1; }
copiar_claves() {
install -d -m 700 -o "$UID_SEMAPHORE" -g "$UID_SEMAPHORE" "$CONF/claves"
for f in mantenimiento_ed25519 known_hosts kubeconfig; do
[ -r "$CLAVES_ORIGEN/$f" ] || { echo "Falta $CLAVES_ORIGEN/$f (ver ansible/README.md)" >&2; exit 1; }
install -m 600 -o "$UID_SEMAPHORE" -g "$UID_SEMAPHORE" "$CLAVES_ORIGEN/$f" "$CONF/claves/$f"
done
echo "✓ claves copiadas a $CONF/claves (solo el usuario del contenedor)"
}
# Copia de main, independiente de la rama en la que esté /root/k3s: el portal
# solo ejecuta lo que ya se ha revisado y mergeado.
poner_repo() {
# El contenedor (uid 1001) tiene que poder leerla: no es secreta, es main.
local umask_antes; umask_antes=$(umask); umask 022
git -C "$RAIZ" fetch -q origin main
if [ ! -d "$REPO/.git" ]; then
install -d -m 755 "$(dirname "$REPO")"
git clone -q --no-checkout "$RAIZ" "$REPO" # clon completo: Semaphore no ve /root/k3s
fi
git -C "$REPO" fetch -q "$RAIZ" refs/remotes/origin/main
git -C "$REPO" checkout -q --detach FETCH_HEAD
chmod -R go+rX,go-w "$REPO"
umask "$umask_antes"
echo "✓ portal en main: $(git -C "$REPO" log -1 --format='%h %s')"
}
esperar() {
for _ in $(seq 60); do
curl -fs "$URL/api/ping" >/dev/null 2>&1 && return 0
sleep 2
done
echo "Semaphore no responde en $URL: ./scripts/portal.sh registros" >&2; exit 1
}
case "${1:-}" in
instalar)
install -d -m 700 "$CONF"
if [ ! -f "$ENV_FILE" ]; then
echo "Usuario administrador del portal (el 2FA se activa después, en la web)."
read -rp " Usuario: " usuario
read -rsp " Contraseña (mín. 12 caracteres): " clave; echo
read -rsp " Repite la contraseña: " clave2; echo
[ "$clave" = "$clave2" ] || { echo "No coinciden" >&2; exit 1; }
[ "${#clave}" -ge 12 ] || { echo "Demasiado corta" >&2; exit 1; }
{
echo "SEMAPHORE_COOKIE_HASH=$(head -c 32 /dev/urandom | base64)"
echo "SEMAPHORE_COOKIE_ENCRYPTION=$(head -c 32 /dev/urandom | base64)"
echo "SEMAPHORE_ACCESS_KEY_ENCRYPTION=$(head -c 32 /dev/urandom | base64)"
echo "# Avisos por Telegram (opcional): rellena y ./scripts/portal.sh arrancar"
echo "#SEMAPHORE_TELEGRAM_ALERT=true"
echo "#SEMAPHORE_TELEGRAM_CHAT="
echo "#SEMAPHORE_TELEGRAM_TOKEN="
echo "SEMAPHORE_ADMIN=$usuario"
echo "SEMAPHORE_ADMIN_NAME=$usuario"
echo "SEMAPHORE_ADMIN_EMAIL=$usuario@localhost"
echo "SEMAPHORE_ADMIN_PASSWORD=$clave"
} > "$ENV_FILE"
chmod 600 "$ENV_FILE"
fi
copiar_claves
poner_repo
"${COMPOSE[@]}" up -d --build
esperar
if grep -q '^SEMAPHORE_ADMIN_PASSWORD=' "$ENV_FILE"; then
usuario="$(sed -n 's/^SEMAPHORE_ADMIN=//p' "$ENV_FILE")"
clave="$(sed -n 's/^SEMAPHORE_ADMIN_PASSWORD=//p' "$ENV_FILE")"
# El admin ya existe en la base de datos: la contraseña sale del disco pase lo
# que pase con los botones (si fallan: ./scripts/portal.sh botones).
sed -i '/^SEMAPHORE_ADMIN_PASSWORD=/d' "$ENV_FILE"
PORTAL_URL="$URL" PORTAL_USUARIO="$usuario" PORTAL_CLAVE="$clave" \
python3 "$RAIZ/ansible/semaphore/configurar.py" \
|| { echo "✘ No se crearon los botones: ./scripts/portal.sh botones" >&2; exit 1; }
unset clave
fi
echo
echo "✓ Portal en $URL"
echo " Entra, y en tu usuario (arriba a la derecha) activa la verificación en dos pasos (TOTP)."
;;
botones)
# Crea o completa el proyecto y los botones (se puede repetir: no duplica).
read -rp "Usuario admin del portal: " usuario
read -rsp "Contraseña: " clave; echo
read -rp "Código 2FA de la app (6 dígitos; vacío si no tienes 2FA): " codigo
PORTAL_URL="$URL" PORTAL_USUARIO="$usuario" PORTAL_CLAVE="$clave" PORTAL_CODIGO="$codigo" \
python3 "$RAIZ/ansible/semaphore/configurar.py"
;;
repo) poner_repo ;;
claves) copiar_claves ;;
estado) "${COMPOSE[@]}" ps; curl -fs "$URL/api/ping" && echo " ← $URL" ;;
registros) "${COMPOSE[@]}" logs --tail 50 ;;
parar) "${COMPOSE[@]}" stop ;;
arrancar) "${COMPOSE[@]}" up -d; esperar; echo "✓ $URL" ;;
*) sed -n '2,10p' "$0"; exit 64 ;;
esac