generar_panel.py
#!/usr/bin/env python3
"""Genera grafana-actualizaciones.yaml (ConfigMap con el panel de Grafana).
El sidecar de Grafana carga cualquier ConfigMap con la etiqueta grafana_dashboard=1.
Uso: python3 ansible/kubernetes/generar_panel.py
"""
import json
import os
DS = {"type": "prometheus", "uid": "${datasource}"}
def q(expr, ref, fmt="table", instant=True, legend=None):
t = {"datasource": DS, "expr": expr, "refId": ref, "format": fmt, "instant": instant, "range": not instant}
if legend:
t["legendFormat"] = legend
return t
def stat(id_, titulo, expr, x, pasos, unidad="none", descripcion=""):
return {
"id": id_, "type": "stat", "title": titulo, "description": descripcion, "datasource": DS,
"gridPos": {"h": 4, "w": 6, "x": x, "y": 0},
"targets": [q(expr, "A", fmt="time_series")],
"options": {"colorMode": "background", "graphMode": "none", "reduceOptions": {"calcs": ["lastNotNull"]}},
"fieldConfig": {"defaults": {"unit": unidad, "noValue": "sin datos", "thresholds": {
"mode": "absolute", "steps": pasos}}, "overrides": []},
}
def version(componente, ref):
return q(f'max by (nodo, {componente}) (label_replace(nodo_version_info{{componente="{componente}"}}, '
f'"{componente}", "$1", "version", "(.*)"))', ref)
VERDE, AMARILLO, ROJO = "green", "yellow", "red"
panel = {
"uid": "actualizaciones-nodos",
"title": "Actualizaciones de los nodos",
"tags": ["mantenimiento", "seguridad"],
"timezone": "browser",
"refresh": "5m",
"time": {"from": "now-30d", "to": "now"},
"schemaVersion": 39,
"templating": {"list": [{"name": "datasource", "type": "datasource", "query": "prometheus",
"label": "Origen", "hide": 0}]},
"links": [
{"title": "Portal de nodos", "type": "link", "url": "http://203.0.113.121:3010",
"icon": "external link", "targetBlank": True, "tooltip": "Semaphore: lanzar actualizaciones"},
{"title": "Vulnerabilidades (Trivy)", "type": "link", "url": "/d/vulnerabilidades-imagenes",
"icon": "dashboard"},
],
"panels": [
stat(1, "De seguridad", 'sum(nodo_actualizaciones_pendientes{tipo="seguridad"})', 0,
[{"color": VERDE, "value": None}, {"color": ROJO, "value": 1}],
descripcion="Paquetes con actualización de seguridad pendiente, sumando todos los nodos"),
stat(2, "Normales", 'sum(nodo_actualizaciones_pendientes{tipo="normal"})', 6,
[{"color": VERDE, "value": None}, {"color": AMARILLO, "value": 1}]),
stat(3, "Piden reinicio", "sum(nodo_reinicio_pendiente)", 12,
[{"color": VERDE, "value": None}, {"color": AMARILLO, "value": 1}],
descripcion="Nodos con kernel o librerías nuevas que necesitan reiniciar"),
stat(4, "Revisión más antigua", "time() - min(nodo_actualizaciones_revisado_timestamp_seconds)", 18,
[{"color": VERDE, "value": None}, {"color": AMARILLO, "value": 8 * 3600}, {"color": ROJO, "value": 24 * 3600}],
unidad="s", descripcion="Cada nodo se revisa solo cada 6 h; si pasa de 24 h, algo falla"),
{
"id": 5, "type": "table", "title": "Nodos", "datasource": DS,
"gridPos": {"h": 9, "w": 24, "x": 0, "y": 4},
"targets": [
q('max by (nodo) (nodo_actualizaciones_pendientes{tipo="seguridad"})', "A"),
q('max by (nodo) (nodo_actualizaciones_pendientes{tipo="normal"})', "B"),
q("max by (nodo) (nodo_reinicio_pendiente)", "C"),
q("max by (nodo) (nodo_actualizaciones_automaticas)", "D"),
q("time() - max by (nodo) (nodo_actualizaciones_revisado_timestamp_seconds)", "E"),
version("tailscale", "F"), version("k3s", "G"), version("kernel", "H"), version("sistema", "I"),
],
"transformations": [
{"id": "merge", "options": {}},
{"id": "organize", "options": {
"excludeByName": {"Time": True, "Value #F": True, "Value #G": True, "Value #H": True, "Value #I": True},
"indexByName": {"nodo": 0, "Value #A": 1, "Value #B": 2, "Value #C": 3, "tailscale": 4,
"k3s": 5, "kernel": 6, "sistema": 7, "Value #D": 8, "Value #E": 9},
"renameByName": {"nodo": "Nodo", "Value #A": "Seguridad", "Value #B": "Normales",
"Value #C": "Reinicio", "Value #D": "Automáticas", "Value #E": "Revisado hace",
"tailscale": "Tailscale", "k3s": "k3s", "kernel": "Kernel", "sistema": "Sistema"}}},
{"id": "sortBy", "options": {"sort": [{"field": "Nodo"}]}},
],
"fieldConfig": {"defaults": {"custom": {"align": "auto"}}, "overrides": [
{"matcher": {"id": "byName", "options": "Seguridad"}, "properties": [
{"id": "custom.cellOptions", "value": {"type": "color-background"}},
{"id": "thresholds", "value": {"mode": "absolute", "steps": [
{"color": VERDE, "value": None}, {"color": ROJO, "value": 1}]}}]},
{"matcher": {"id": "byName", "options": "Normales"}, "properties": [
{"id": "custom.cellOptions", "value": {"type": "color-background"}},
{"id": "thresholds", "value": {"mode": "absolute", "steps": [
{"color": VERDE, "value": None}, {"color": AMARILLO, "value": 1}]}}]},
{"matcher": {"id": "byName", "options": "Reinicio"}, "properties": [
{"id": "mappings", "value": [{"type": "value", "options": {
"0": {"text": "no", "color": VERDE}, "1": {"text": "SÍ", "color": AMARILLO}}}]},
{"id": "custom.cellOptions", "value": {"type": "color-text"}}]},
{"matcher": {"id": "byName", "options": "Automáticas"}, "properties": [
{"id": "mappings", "value": [{"type": "value", "options": {
"0": {"text": "no", "color": AMARILLO}, "1": {"text": "sí", "color": VERDE}}}]},
{"id": "custom.cellOptions", "value": {"type": "color-text"}}]},
{"matcher": {"id": "byName", "options": "Revisado hace"}, "properties": [
{"id": "unit", "value": "s"}, {"id": "decimals", "value": 0}]},
]},
"options": {"showHeader": True, "cellHeight": "sm"},
},
{
"id": 6, "type": "table", "title": "Paquetes pendientes", "datasource": DS,
"gridPos": {"h": 10, "w": 14, "x": 0, "y": 13},
"targets": [q("max by (nodo, paquete, version, seguridad) (nodo_actualizacion_paquete)", "A")],
"transformations": [
{"id": "organize", "options": {
"excludeByName": {"Time": True, "Value": True},
"indexByName": {"nodo": 0, "paquete": 1, "version": 2, "seguridad": 3},
"renameByName": {"nodo": "Nodo", "paquete": "Paquete", "version": "Versión nueva",
"seguridad": "Seguridad"}}},
{"id": "sortBy", "options": {"sort": [{"field": "Seguridad", "desc": True}]}},
],
"fieldConfig": {"defaults": {}, "overrides": [
{"matcher": {"id": "byName", "options": "Seguridad"}, "properties": [
{"id": "mappings", "value": [{"type": "value", "options": {
"0": {"text": "no", "color": "text"}, "1": {"text": "SÍ", "color": ROJO}}}]},
{"id": "custom.cellOptions", "value": {"type": "color-text"}}]}]},
"options": {"showHeader": True, "cellHeight": "sm"},
},
{
"id": 7, "type": "text", "title": "Cómo lanzar las actualizaciones",
"gridPos": {"h": 10, "w": 10, "x": 14, "y": 13},
"options": {"mode": "markdown", "content": (
"**[Portal de nodos](http://203.0.113.121:3010)** (Semaphore, solo por Tailscale, con 2FA) → "
"*Nodos del clúster* → *Task Templates* → ▶ y elegir los **nodos** "
"(`all`, `workers`, `masters` o uno solo):\n\n"
"| Botón | Qué hace |\n|---|---|\n"
"| 1 · Revisar ahora | Refresca este panel. No instala nada |\n"
"| 2 · Simular actualización | Qué se instalaría. No instala ni reinicia |\n"
"| 3 · Actualizar sistema | Instala, nodo a nodo; reinicia con drain si hace falta |\n"
"| 4 · Actualizar Tailscale | Solo Tailscale |\n"
"| 5 · Reiniciar los que lo piden | Con drain, de uno en uno |\n"
"| 6 · Actualizar k3s | Pide la versión (`v1.X.Y+k3sN`): masters primero, luego workers |\n\n"
"El master-1 no se reinicia solo. Desde la terminal del master-1: "
"`./scripts/mantenimiento.sh <revisar|actualizar|tailscale|reiniciar|k3s>`.")},
},
{
"id": 8, "type": "timeseries", "title": "Pendientes por nodo", "datasource": DS,
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 23},
"targets": [q("sum by (nodo) (nodo_actualizaciones_pendientes)", "A", fmt="time_series",
instant=False, legend="{{nodo}}")],
"fieldConfig": {"defaults": {"custom": {"drawStyle": "line", "lineInterpolation": "stepAfter",
"fillOpacity": 10}, "decimals": 0}, "overrides": []},
"options": {"legend": {"displayMode": "list", "placement": "bottom"}},
},
],
}
aqui = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(aqui, "grafana-actualizaciones.yaml"), "w") as f:
f.write("# Generado por generar_panel.py: no editar a mano.\n")
f.write("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: panel-actualizaciones-nodos\n"
" namespace: monitoring\n labels:\n grafana_dashboard: \"1\"\ndata:\n")
f.write(" actualizaciones-nodos.json: |-\n")
for linea in json.dumps(panel, indent=2, ensure_ascii=False).splitlines():
f.write(f" {linea}\n")
print("✓ grafana-actualizaciones.yaml")