n8n: vigilante contra reversiones de BD (alerta Telegram)
CronJob externo a n8n que lee la SQLite en solo-lectura por el hostPath y avisa si el workflow del autopost desaparece (firma de la reversión del 2026-07-16) o si el total de workflows se desploma. La reversión SUBE el nº de activos, así que un umbral de activos no la cazaría; el disparador es la desaparición del autopost. Cada 6h. Reutiliza telegram-notify-infisical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
# Vigilante de n8n contra reversiones de BD (como la del 2026-07-16, en que la
|
||||
# SQLite de n8n se revirtió a un snapshot de abril durante los reinicios de
|
||||
# kernel: desaparecieron los workflows de julio —incluido el autopost de Ghost
|
||||
# EN— y volvieron activados los bots viejos, sin que nadie lo notara en una
|
||||
# semana, hasta que la copia buena se purgó de los backups por la retención).
|
||||
#
|
||||
# Lee la SQLite EN SOLO LECTURA por el hostPath /data/n8n (independiente del pod
|
||||
# de n8n: funciona aunque n8n esté roto) y avisa por Telegram si el workflow del
|
||||
# autopost DESAPARECE (la firma de la reversión; ojo: la reversión SUBE el nº de
|
||||
# activos, así que un umbral de "activos" no la cazaría) o si el total de
|
||||
# workflows se desploma. Cada 6 h: caza dentro del día, deja margen de sobra
|
||||
# antes de que la retención de 7 días borre la última copia buena.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: n8n-monitor-script
|
||||
namespace: n8n
|
||||
data:
|
||||
n8n_monitor.py: |
|
||||
#!/usr/bin/env python3
|
||||
"""Vigilante de n8n: caza una reversión de BD. Sin deps externas."""
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
DB = os.environ.get("N8N_DB", "/n8n/database.sqlite")
|
||||
TOTAL_FLOOR = int(os.environ.get("TOTAL_FLOOR", "18"))
|
||||
AUTOPOST_MATCH = os.environ.get("AUTOPOST_MATCH", "%X Autopost%")
|
||||
|
||||
|
||||
def send_telegram(text):
|
||||
token = os.environ["TELEGRAM_BOT_TOKEN"]
|
||||
chat = os.environ["TELEGRAM_CHAT_ID"]
|
||||
data = urllib.parse.urlencode(
|
||||
{"chat_id": chat, "text": text, "disable_web_page_preview": "true"}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
f"https://api.telegram.org/bot{token}/sendMessage", data=data
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=20) as r:
|
||||
r.read()
|
||||
|
||||
|
||||
def main():
|
||||
if "--install-test" in sys.argv:
|
||||
send_telegram("✅ n8n workflow monitor instalado y funcionando (prueba).")
|
||||
print("mensaje de prueba enviado")
|
||||
return
|
||||
|
||||
try:
|
||||
con = sqlite3.connect(f"file:{DB}?mode=ro", uri=True, timeout=30)
|
||||
total = con.execute("SELECT COUNT(*) FROM workflow_entity").fetchone()[0]
|
||||
active = con.execute(
|
||||
"SELECT COUNT(*) FROM workflow_entity WHERE active=1"
|
||||
).fetchone()[0]
|
||||
ap = con.execute(
|
||||
"SELECT active FROM workflow_entity WHERE name LIKE ?", (AUTOPOST_MATCH,)
|
||||
).fetchone()
|
||||
con.close()
|
||||
except Exception as e: # noqa: BLE001
|
||||
send_telegram(
|
||||
f"🚨 n8n monitor: no pude leer la BD de n8n ({e}). ¿Pod/DB caídos?"
|
||||
)
|
||||
print("ERROR leyendo la BD:", e)
|
||||
sys.exit(1)
|
||||
|
||||
problems = []
|
||||
if ap is None:
|
||||
problems.append(
|
||||
"el workflow «ExclusionZone → X Autopost» ha DESAPARECIDO — "
|
||||
"firma de reversion de BD (como el 2026-07-16)"
|
||||
)
|
||||
if total < TOTAL_FLOOR:
|
||||
problems.append(
|
||||
f"solo {total} workflows (esperados >={TOTAL_FLOOR}) — posible perdida de datos"
|
||||
)
|
||||
|
||||
if problems:
|
||||
msg = (
|
||||
"🚨 ALERTA n8n:\n"
|
||||
+ "\n".join("• " + p for p in problems)
|
||||
+ f"\n\n(total={total}, activos={active})\n"
|
||||
+ "Runbook: k8s-manifests/backup-system/RESTORE.md (Escenario A)"
|
||||
)
|
||||
send_telegram(msg)
|
||||
print("ALERTA enviada:", problems)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"OK: total={total} activos={active} autopost={'ON' if ap[0] else 'OFF'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: n8n-workflow-monitor
|
||||
namespace: n8n
|
||||
spec:
|
||||
schedule: "0 */6 * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 1
|
||||
activeDeadlineSeconds: 120
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: n8n-workflow-monitor
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
# /data/n8n vive en chemavx-k8; hay que leerlo desde ahí.
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: chemavx-k8
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
containers:
|
||||
- name: monitor
|
||||
image: python:3.12-alpine@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df
|
||||
command: ["python3", "/scripts/n8n_monitor.py"]
|
||||
envFrom:
|
||||
# TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID (sincronizado de Infisical)
|
||||
- secretRef:
|
||||
name: telegram-notify-infisical
|
||||
env:
|
||||
- name: N8N_DB
|
||||
value: /n8n/database.sqlite
|
||||
- name: TOTAL_FLOOR
|
||||
value: "18"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
volumeMounts:
|
||||
- name: n8n-data
|
||||
mountPath: /n8n
|
||||
readOnly: true
|
||||
- name: script
|
||||
mountPath: /scripts
|
||||
volumes:
|
||||
- name: n8n-data
|
||||
hostPath:
|
||||
path: /data/n8n
|
||||
type: Directory
|
||||
- name: script
|
||||
configMap:
|
||||
name: n8n-monitor-script
|
||||
Reference in New Issue
Block a user