Notificaciones vía Telegram, pausa nocturna y unidades systemd
Reemplaza ntfy.sh por Telegram Bot API (preferencia del usuario, no reduce el costo — el costo real es de la API de Claude/Sonnet, no del canal). Texto plano sin parse_mode: el motivo que devuelve Claude es texto libre y puede traer caracteres que rompen el parser Markdown de Telegram. Token en TELEGRAM_BOT_TOKEN_FILE (dueño devops, no root-only -- el servicio corre como devops por el login de suscripción, mismo criterio ya aplicado a GITEA_TOKEN_FILE). Validado en vivo: entrega confirmada con el mismo código de notify_send(). Agrega pausa nocturna (ACTIVE_HOURS_START/END, default 06:00-23:00 hora local) para no acumular costo de API sin necesidad: el script chequea el horario ANTES de tocar kubectl/docker/curl o invocar claude -p, no solo antes de notificar. Nota documentada: esta ventana no se solapa con COMMERCE_BACKEND_WINDOW_* (00:00-06:00), por lo que medusa-deploy queda efectivamente inalcanzable para auto-restart en la práctica -- pendiente de decisión aparte. Agrega unidades systemd de usuario (lab-agent/systemd/*.service, *.timer) con límites de CPU/memoria: OnCalendar cada 15 min dentro del horario activo, corre como devops (systemctl --user), sin habilitar todavía -- requiere confirmación explícita tras ver el costo mensual estimado (~US$60-65, documentado en agente-monitoreo.md).
This commit is contained in:
1 parent
61a951f576
commit
c273056771
5 files changed
+131
-22
No files matched your search
+29
-4
@@ -118,6 +118,30 @@ Si no existe, el agente sigue funcionando pero pierde la señal de
|
|||||||
"runner atascado" vía API de Gitea Actions (solo detecta el contenedor
|
"runner atascado" vía API de Gitea Actions (solo detecta el contenedor
|
||||||
caído/reiniciando).
|
caído/reiniciando).
|
||||||
|
|
||||||
|
**Notificaciones (Telegram, reemplaza a ntfy.sh desde 2026-08-26):**
|
||||||
|
creá el bot con [@BotFather](https://t.me/BotFather) (`/newbot`) y
|
||||||
|
obtené tu `chat_id` (mandale un mensaje al bot primero, después
|
||||||
|
`https://api.telegram.org/bot<TOKEN>/getUpdates`). El token va en un
|
||||||
|
archivo aparte, dueño `devops` — **no** root-only pese al patrón
|
||||||
|
original: el servicio corre como `devops` (necesita su login de
|
||||||
|
suscripción para `claude -p`), así que un archivo `root:root 600` le
|
||||||
|
sería ilegible, mismo criterio ya aplicado a `GITEA_TOKEN_FILE`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p "$(dirname "$TELEGRAM_BOT_TOKEN_FILE")"
|
||||||
|
echo "<tu-token-de-botfather>" > "$TELEGRAM_BOT_TOKEN_FILE"
|
||||||
|
chmod 600 "$TELEGRAM_BOT_TOKEN_FILE"
|
||||||
|
# y en lab-agent.env: TELEGRAM_CHAT_ID=<tu-chat-id> (no es secreto)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Horario activo:** `ACTIVE_HOURS_START`/`ACTIVE_HOURS_END` (hora local,
|
||||||
|
default `06:00`–`23:00`). Fuera de esa ventana el script sale antes de
|
||||||
|
tocar kubectl/docker/curl o invocar `claude -p` — no gasta nada, no
|
||||||
|
solo omite la notificación. **Ojo:** esta ventana no se solapa con
|
||||||
|
`COMMERCE_BACKEND_WINDOW_*` (`00:00`–`06:00`) — mientras el agente está
|
||||||
|
pausado, `medusa-deploy` nunca es elegible para auto-restart en la
|
||||||
|
práctica. Ver `docs/arquitectura/agente-monitoreo.md`.
|
||||||
|
|
||||||
## Probar
|
## Probar
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -132,8 +156,9 @@ ejecuta de verdad (confirmado con `kubectl get events` y la anotación
|
|||||||
`restartedAt`), y una segunda corrida sobre el mismo recurso caído
|
`restartedAt`), y una segunda corrida sobre el mismo recurso caído
|
||||||
respeta el bloqueo (`notify_only`, sin reintento). Dos bugs reales se
|
respeta el bloqueo (`notify_only`, sin reintento). Dos bugs reales se
|
||||||
encontraron y corrigieron en esa prueba — ver el detalle en
|
encontraron y corrigieron en esa prueba — ver el detalle en
|
||||||
`docs/arquitectura/agente-monitoreo.md`.
|
`docs/arquitectura/agente-monitoreo.md`. La notificación real se
|
||||||
|
validó con ntfy.sh en esa prueba; el cambio a Telegram debe repetir
|
||||||
|
al menos la validación de entrega antes de habilitar el timer.
|
||||||
|
|
||||||
**No actives el timer de systemd (Paso 4) todavía** — requiere tu
|
**No actives el timer de systemd todavía** — requiere tu confirmación
|
||||||
confirmación explícita por separado, aunque la prueba end-to-end ya
|
explícita por separado, después de ver el costo mensual estimado.
|
||||||
pasó.
|
|
||||||
@@ -62,10 +62,25 @@ GITEA_TOKEN_FILE=/DATA/AppData/k3d-lab/lab-agent/secrets/gitea_token
|
|||||||
CLAUDE_MODEL=claude-sonnet-5
|
CLAUDE_MODEL=claude-sonnet-5
|
||||||
CLAUDE_MAX_BUDGET_USD=0.50
|
CLAUDE_MAX_BUDGET_USD=0.50
|
||||||
|
|
||||||
# --- Notificaciones (Paso 3 — dejar vacío hasta tener un canal real) ---
|
# --- Notificaciones vía Telegram Bot API (reemplaza a ntfy.sh, 2026-08-26) ---
|
||||||
# Ej. self-hosted: https://ntfy.cruzcloud.net/lab-agent
|
# Token del bot: creado con @BotFather, va en un archivo aparte (ver
|
||||||
# ntfy.sh público: https://ntfy.sh/<topic-privado-dificil-de-adivinar>
|
# README.md). NO root-only pese al patrón original — el servicio corre
|
||||||
NTFY_URL=
|
# como `devops` (necesita el login de suscripción de ese usuario para
|
||||||
|
# claude -p), así que un archivo root:root 600 sería ilegible para él.
|
||||||
|
# Mismo criterio ya aplicado a GITEA_TOKEN_FILE.
|
||||||
|
TELEGRAM_BOT_TOKEN_FILE=/DATA/AppData/k3d-lab/lab-agent/secrets/telegram_bot_token
|
||||||
|
# chat_id NO es secreto (no autentica nada sin el token) — va acá directo.
|
||||||
|
TELEGRAM_CHAT_ID=
|
||||||
|
|
||||||
|
# --- Horario activo (hora LOCAL del host) ---
|
||||||
|
# Fuera de esta ventana el script aborta ANTES de tocar kubectl/docker/
|
||||||
|
# curl o invocar claude -p — pausa nocturna para no acumular costo de
|
||||||
|
# API. OJO: esta ventana NO se solapa con COMMERCE_BACKEND_WINDOW_*
|
||||||
|
# (00:00-06:00) — mientras el agente está pausado, medusa-deploy nunca
|
||||||
|
# es elegible para auto-restart en la práctica, aunque la regla siga
|
||||||
|
# activa en el código. Ver docs/arquitectura/agente-monitoreo.md.
|
||||||
|
ACTIVE_HOURS_START=06:00
|
||||||
|
ACTIVE_HOURS_END=23:00
|
||||||
|
|
||||||
# --- Pruebas controladas (Paso 5) ---
|
# --- Pruebas controladas (Paso 5) ---
|
||||||
# DRY_RUN=1: calcula elegibilidad y decisión igual que en producción,
|
# DRY_RUN=1: calcula elegibilidad y decisión igual que en producción,
|
||||||
|
|||||||
+49
-14
@@ -51,7 +51,18 @@ source "$ENV_FILE"
|
|||||||
: "${GITEA_TOKEN_FILE:=/DATA/AppData/k3d-lab/lab-agent/secrets/gitea_token}"
|
: "${GITEA_TOKEN_FILE:=/DATA/AppData/k3d-lab/lab-agent/secrets/gitea_token}"
|
||||||
: "${CLAUDE_MODEL:=claude-sonnet-5}"
|
: "${CLAUDE_MODEL:=claude-sonnet-5}"
|
||||||
: "${CLAUDE_MAX_BUDGET_USD:=0.50}"
|
: "${CLAUDE_MAX_BUDGET_USD:=0.50}"
|
||||||
: "${NTFY_URL:=}"
|
# Notificaciones vía Telegram Bot API (reemplaza a ntfy.sh, 2026-08-26).
|
||||||
|
# TELEGRAM_BOT_TOKEN_FILE: secreto, NO versionado. TELEGRAM_CHAT_ID no es
|
||||||
|
# secreto (solo identifica a quién mandar, no autentica nada por sí solo).
|
||||||
|
: "${TELEGRAM_BOT_TOKEN_FILE:=}"
|
||||||
|
: "${TELEGRAM_CHAT_ID:=}"
|
||||||
|
# Horario activo (hora LOCAL del host): fuera de esta ventana el script
|
||||||
|
# aborta ANTES de tocar kubectl/docker/curl o invocar claude -p — pausa
|
||||||
|
# nocturna para no acumular costo de API sin necesidad. Ver
|
||||||
|
# docs/arquitectura/agente-monitoreo.md para el tradeoff con la ventana
|
||||||
|
# de medusa-deploy (00:00-06:00).
|
||||||
|
: "${ACTIVE_HOURS_START:=06:00}"
|
||||||
|
: "${ACTIVE_HOURS_END:=23:00}"
|
||||||
: "${DRY_RUN:=1}"
|
: "${DRY_RUN:=1}"
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
@@ -77,6 +88,26 @@ trap 'log "ERROR inesperado en línea $LINENO (comando: $BASH_COMMAND)"' ERR
|
|||||||
|
|
||||||
log "=== lab-agent inicio (DRY_RUN=${DRY_RUN}) ==="
|
log "=== lab-agent inicio (DRY_RUN=${DRY_RUN}) ==="
|
||||||
|
|
||||||
|
in_time_window() { # start end (HH:MM, hora local) -> 0 si "ahora" cae adentro
|
||||||
|
local start="$1" end="$2" now_hm
|
||||||
|
now_hm="$(date +%H:%M)"
|
||||||
|
if [[ "$start" < "$end" ]]; then
|
||||||
|
[[ ( "$now_hm" > "$start" || "$now_hm" == "$start" ) && "$now_hm" < "$end" ]]
|
||||||
|
else
|
||||||
|
[[ "$now_hm" > "$start" || "$now_hm" == "$start" || "$now_hm" < "$end" ]]
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pausa nocturna: si estamos fuera del horario activo, salir ANTES de tocar
|
||||||
|
# kubectl/docker/curl o invocar claude -p — ni siquiera se gasta el costo
|
||||||
|
# de diagnóstico, no solo el de la llamada a claude. Sin notificación (sería
|
||||||
|
# spam cada 15 min durante toda la pausa) — solo queda en el log del run.
|
||||||
|
if ! in_time_window "$ACTIVE_HOURS_START" "$ACTIVE_HOURS_END"; then
|
||||||
|
log "fuera de horario activo (${ACTIVE_HOURS_START}-${ACTIVE_HOURS_END} hora local) — se omite esta corrida sin costo de API"
|
||||||
|
log "=== lab-agent fin (omitido por horario) ==="
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
for c in "$KUBECTL_BIN" docker curl jq flock "$CLAUDE_BIN" date; do
|
for c in "$KUBECTL_BIN" docker curl jq flock "$CLAUDE_BIN" date; do
|
||||||
command -v "$c" >/dev/null 2>&1 || { log "FATAL: falta comando requerido '$c'"; exit 1; }
|
command -v "$c" >/dev/null 2>&1 || { log "FATAL: falta comando requerido '$c'"; exit 1; }
|
||||||
done
|
done
|
||||||
@@ -141,13 +172,25 @@ log_action() { # resource decision result reason
|
|||||||
|
|
||||||
notify_send() { # title message
|
notify_send() { # title message
|
||||||
local title="$1" message="$2"
|
local title="$1" message="$2"
|
||||||
if [[ -z "$NTFY_URL" ]]; then
|
if [[ ! -r "$TELEGRAM_BOT_TOKEN_FILE" || -z "$TELEGRAM_CHAT_ID" ]]; then
|
||||||
log "NOTIFY (sin canal configurado aún — Paso 3) [$title]"
|
log "NOTIFY (Telegram no configurado — falta TELEGRAM_BOT_TOKEN_FILE/TELEGRAM_CHAT_ID) [$title]"
|
||||||
log "$message"
|
log "$message"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if ! curl -s -m 10 -H "Title: ${title}" -d "$message" "$NTFY_URL" >/dev/null 2>&1; then
|
local token text resp
|
||||||
log "WARN: fallo al enviar notificación a NTFY_URL"
|
token="$(<"$TELEGRAM_BOT_TOKEN_FILE")"
|
||||||
|
# Texto plano, sin parse_mode: el "reason" de claude es texto libre y
|
||||||
|
# puede traer _, *, [, ] etc. que rompen el parser Markdown de Telegram
|
||||||
|
# (400 Bad Request) — más vale entrega confiable que formato.
|
||||||
|
text="[${title}]
|
||||||
|
${message}"
|
||||||
|
text="${text:0:4000}"
|
||||||
|
resp="$(curl -s -m 10 -X POST "https://api.telegram.org/bot${token}/sendMessage" \
|
||||||
|
--data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \
|
||||||
|
--data-urlencode "text=${text}" 2>&1)"
|
||||||
|
token=""
|
||||||
|
if [[ "$resp" != *'"ok":true'* ]]; then
|
||||||
|
log "WARN: fallo al enviar notificación a Telegram: ${resp}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,15 +204,7 @@ DEPLOY_JSON="$("$KUBECTL_BIN" -n "$NAMESPACE" get deployment -o json 2>/dev/null
|
|||||||
mapfile -t DETECTED_DEPLOYMENTS < <(jq -r '.items[].metadata.name' <<<"$DEPLOY_JSON")
|
mapfile -t DETECTED_DEPLOYMENTS < <(jq -r '.items[].metadata.name' <<<"$DEPLOY_JSON")
|
||||||
|
|
||||||
in_commerce_backend_window() {
|
in_commerce_backend_window() {
|
||||||
local now_hm start end
|
in_time_window "$COMMERCE_BACKEND_WINDOW_START" "$COMMERCE_BACKEND_WINDOW_END"
|
||||||
now_hm="$(date +%H:%M)"
|
|
||||||
start="$COMMERCE_BACKEND_WINDOW_START"
|
|
||||||
end="$COMMERCE_BACKEND_WINDOW_END"
|
|
||||||
if [[ "$start" < "$end" ]]; then
|
|
||||||
[[ ( "$now_hm" > "$start" || "$now_hm" == "$start" ) && "$now_hm" < "$end" ]]
|
|
||||||
else
|
|
||||||
[[ "$now_hm" > "$start" || "$now_hm" == "$start" || "$now_hm" < "$end" ]]
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# devuelve: "eligible" | "not_eligible:<motivo>"
|
# devuelve: "eligible" | "not_eligible:<motivo>"
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=lab-agent: diagnostico/correccion del lab GitOps cruzcloud
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=/media/HDD-Storage/AppData/k3d-lab-v3/lab-cruzcloud/scripts/lab-agent
|
||||||
|
ExecStart=/media/HDD-Storage/AppData/k3d-lab-v3/lab-cruzcloud/scripts/lab-agent/lab-agent.sh --live
|
||||||
|
TimeoutStartSec=300
|
||||||
|
|
||||||
|
# Limites de recursos (pedido explicito de gobernanza, nunca irrestricto).
|
||||||
|
# El propio script casi siempre corta antes por la pausa nocturna o por
|
||||||
|
# no tener nada elegible, asi que en la practica rara vez se acerca a
|
||||||
|
# estos topes -- estan para contener un caso patologico (claude -p
|
||||||
|
# colgado, kubectl en loop), no para el uso normal.
|
||||||
|
CPUQuota=100%
|
||||||
|
MemoryMax=1G
|
||||||
|
MemorySwapMax=256M
|
||||||
|
Nice=10
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Dispara lab-agent.service cada 15 min, 06:00-23:00 hora local
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# Horas 06..22, minutos :00/:15/:30/:45 -> ultima corrida real 22:45.
|
||||||
|
# El propio script vuelve a chequear ACTIVE_HOURS_START/END al arrancar
|
||||||
|
# (defensa en profundidad si esta unidad se edita a mano sin tocar el
|
||||||
|
# .env, o si corre manualmente fuera de horario).
|
||||||
|
OnCalendar=*-*-* 06..22:00/15:00
|
||||||
|
AccuracySec=30s
|
||||||
|
Persistent=false
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Reference in new issue
Block a user