Jenkins + Minio + Prometheus + Grafana v1
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
CLOUDFLARE_TUNNEL_TOKEN=<token>
|
||||
|
||||
# MySQL connection
|
||||
MYSQL_ROOT_PASSWORD=<password>
|
||||
MYSQL_USER=<user>
|
||||
MYSQL_PASSWORD=<password>
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# Backups
|
||||
/backups/
|
||||
|
||||
# Cache de Python
|
||||
__pycache__/
|
||||
|
||||
# Variables de entorno
|
||||
.env
|
||||
|
||||
# Certificados y claves
|
||||
/data/certs/
|
||||
*.key
|
||||
*.pem
|
||||
*.crt
|
||||
|
||||
# Grafana (estado runtime)
|
||||
/data/grafana/data/
|
||||
|
||||
# Jenkins home (volumen)
|
||||
/data/jenkins_home/
|
||||
|
||||
# MinIO (motor de storage y objetos)
|
||||
/data/minio/
|
||||
|
||||
# MySQL (datadir del motor, no el init)
|
||||
/data/mysql/data/
|
||||
|
||||
# Nginx Proxy Manager (estado, keys, certs)
|
||||
/data/npm/
|
||||
|
||||
# Prometheus (TimeSeriesDataBase, no el config)
|
||||
/data/prometheus/data/
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
===========================================================================
|
||||
[2026-09-01 13:27:18] ALERTA GRAFANA - ESTADO: FIRING
|
||||
Título: [FIRING:1] TestAlert Grafana
|
||||
|
||||
[Alerta #1] Status: firing
|
||||
- Nombre : TestAlert
|
||||
- Cliente : N/A
|
||||
- Sucursal : N/A
|
||||
- SCO : Grafana
|
||||
- Severidad : N/A
|
||||
- Resumen : Notification test
|
||||
- Inicio : 2026-09-01T13:27:18.650618266Z
|
||||
|
||||
RAW JSON: {"receiver": "webhook", "status": "firing", "alerts": [{"status": "firing", "labels": {"alertname": "TestAlert", "instance": "Grafana"}, "annotations": {"summary": "Notification test"}, "startsAt": "2026-09-01T13:27:18.650618266Z", "endsAt": "0001-01-01T00:00:00Z", "generatorURL": "", "fingerprint": "57c6d9296de2ad39", "silenceURL": "http://laoficina1782.com:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTestAlert&matcher=instance%3DGrafana", "dashboardURL": "", "panelURL": "", "values": null, "valueString": "[ metric='foo' labels={instance=bar} value=10 ]"}], "groupLabels": {"alertname": "TestAlert", "instance": "Grafana"}, "commonLabels": {"alertname": "TestAlert", "instance": "Grafana"}, "commonAnnotations": {"summary": "Notification test"}, "externalURL": "http://laoficina1782.com:3000/", "appVersion": "13.1.1", "version": "1", "groupKey": "webhook-57c6d9296de2ad39-1788269238", "truncatedAlerts": 0, "orgId": 1, "title": "[FIRING:1] TestAlert Grafana ", "state": "alerting", "message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = TestAlert\n - instance = Grafana\nAnnotations:\n - summary = Notification test\nSilence: http://laoficina1782.com:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DTestAlert&matcher=instance%3DGrafana\n"}
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM mcr.microsoft.com/playwright/python:v1.45.0-jammy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# copiamos e instalamos dependencias
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# copiamos el resto del código
|
||||
COPY . .
|
||||
|
||||
# gunicorn con 2 workers y 4 threads por worker
|
||||
# gunicorn -k gthread -w 2 --threads 4 -b 0.0.0.0:5000 central_api:app
|
||||
CMD ["gunicorn", "-k", "gthread", "-w", "2", "--threads", "4", "-b", "0.0.0.0:5000", "central_api:app"]
|
||||
@@ -0,0 +1,660 @@
|
||||
import os
|
||||
import io
|
||||
import requests
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from flask import Flask, request, jsonify, send_file, after_this_request
|
||||
from pydantic import BaseModel, ValidationError, Field
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
import mysql.connector
|
||||
from mysql.connector import Error
|
||||
from mysql.connector import pooling
|
||||
import copy
|
||||
import uuid
|
||||
import concurrent.futures
|
||||
import threading
|
||||
import time
|
||||
from flask import render_template_string
|
||||
import json
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# auxpara compartir multiples workers de Gunicorn
|
||||
def set_job_status(job_id, status_dict):
|
||||
try:
|
||||
with open(f"/tmp/{job_id}.json", "w") as f:
|
||||
json.dump(status_dict, f)
|
||||
except Exception as e:
|
||||
logger.error(f"Error guardando status JSON: {e}")
|
||||
|
||||
def get_job_status(job_id):
|
||||
try:
|
||||
path = f"/tmp/{job_id}.json"
|
||||
if os.path.exists(path):
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
pass
|
||||
return {'status': 'not_found'}
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Pydantic para validar los campos
|
||||
class TransactionSchema(BaseModel):
|
||||
cliente: str
|
||||
sco_number: str
|
||||
sucursal: str
|
||||
pos_transaction_id: int
|
||||
|
||||
started_at: datetime
|
||||
end_transaction_at: Optional[datetime] = None
|
||||
closed_at: datetime
|
||||
|
||||
status: str
|
||||
|
||||
item_count: int = 0
|
||||
total_amount: float = 0.0
|
||||
paid_amount: float = 0.0
|
||||
change_amount: float = 0.0
|
||||
amounts_reconstructed: bool = False
|
||||
|
||||
payment_type: Optional[str] = None
|
||||
split_payment: bool = False
|
||||
|
||||
age_verification_requested: bool = False
|
||||
age_verification_value: Optional[int] = Field(default=None, gt=0)
|
||||
bag_item_count: int = 0
|
||||
|
||||
# config mysql con .env general
|
||||
DB_HOST = os.environ.get("DB_HOST")
|
||||
DB_PORT = int(os.environ.get("DB_PORT", "3306"))
|
||||
DB_USER = os.environ.get("DB_USER")
|
||||
DB_PASSWORD = os.environ.get("DB_PASSWORD")
|
||||
DB_NAME = os.environ.get("DB_NAME")
|
||||
|
||||
if not all([DB_HOST, DB_USER, DB_PASSWORD, DB_NAME]):
|
||||
raise ValueError("Faltan credenciales de MySQL.")
|
||||
|
||||
# manejo de db pool dentro de la instancia
|
||||
db_pool = None
|
||||
|
||||
dbconfig = {
|
||||
"host": DB_HOST,
|
||||
"port": DB_PORT,
|
||||
"user": DB_USER,
|
||||
"password": DB_PASSWORD,
|
||||
"database": DB_NAME
|
||||
}
|
||||
|
||||
max_retries = 5
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
db_pool = pooling.MySQLConnectionPool(
|
||||
pool_name="sco_pool",
|
||||
pool_size=10,
|
||||
pool_reset_session=True,
|
||||
**dbconfig
|
||||
)
|
||||
logger.info("Connection pool de MySQL creado con éxito.")
|
||||
break
|
||||
except Error as e:
|
||||
logger.error(f"Error al crear el pool de conexiones (Intento {attempt + 1}/{max_retries}): {e}")
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(3)
|
||||
else:
|
||||
logger.error("No se pudo conectar a MySQL después de varios intentos.")
|
||||
|
||||
def get_db_pool():
|
||||
return db_pool
|
||||
|
||||
# Pantalla de carga HTML (PRUEBA grafica)
|
||||
LOADING_HTML = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Generando Reporte...</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #111217; color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||
.spinner { border: 4px solid rgba(255,255,255,0.1); width: 60px; height: 60px; border-radius: 50%; border-left-color: #3274d9; animation: spin 1s linear infinite; margin-bottom: 25px; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
.text { font-size: 1.4rem; font-weight: 400; text-align: center; }
|
||||
.subtext { font-size: 1rem; color: #8e8e8e; margin-top: 15px; text-align: center; max-width: 400px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="spinner"></div>
|
||||
<div class="text">Construyendo su reporte PDF...</div>
|
||||
<div class="subtext">Estamos recolectando y procesando los datos de Grafana. Esto tomará entre 1 y 4 minutos dependiendo del tamaño. Por favor, <b>no cierre esta ventana</b>.</div>
|
||||
<script>
|
||||
const jobId = "{{job_id}}";
|
||||
const checkStatus = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/export/status/${jobId}`);
|
||||
const data = await res.json();
|
||||
if (data.status === 'done') {
|
||||
// Redirigir a la descarga real
|
||||
window.location.href = `/api/v1/export/download/${jobId}`;
|
||||
|
||||
// Actualizamos el mensaje
|
||||
document.body.innerHTML = '<div class="spinner" style="border-left-color: #4caf50; animation: none; transform: rotate(45deg);"></div><div class="text" style="color:#4caf50;">¡Reporte generado con éxito!</div><div class="subtext">Tu descarga ha comenzado. Esta pestaña se cerrará automáticamente...</div>';
|
||||
|
||||
// Esperamos 2.5 segundos para asegurar la descarga y cerramos la ventana
|
||||
setTimeout(() => {
|
||||
window.close(); // Intenta cerrar la ventana actual
|
||||
|
||||
// Fallback de seguridad: si Chrome bloquea el cierre (por no haber sido abierta vía target="_blank" JS), regresamos a Grafana.
|
||||
setTimeout(() => {
|
||||
if (document.referrer) {
|
||||
window.location.href = document.referrer;
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
}, 1500);
|
||||
}, 2500);
|
||||
|
||||
} else if (data.status === 'error') {
|
||||
document.body.innerHTML = '<div class="text" style="color:#e02f44;">Ocurrió un error al generar el reporte:</div><div class="subtext">' + (data.error || 'Fallo desconocido') + '</div>';
|
||||
} else {
|
||||
setTimeout(checkStatus, 5000); // Polling cada 5 seg
|
||||
}
|
||||
} catch (e) {
|
||||
setTimeout(checkStatus, 5000);
|
||||
}
|
||||
};
|
||||
setTimeout(checkStatus, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def background_export_task(job_id, dashboard_uid, dashboard_name, cliente, grafana_vars, time_from, time_to, tz, grafana_session):
|
||||
logger.info(f"[{job_id}] Iniciando hilo de trabajo en segundo plano (Modo Playwright Headless)...")
|
||||
set_job_status(job_id, {'status': 'processing'})
|
||||
|
||||
try:
|
||||
# URL hacia la UI real de Grafana
|
||||
target_url = f"http://grafana:3000/d/{dashboard_uid}?orgId=1"
|
||||
|
||||
# Inyectamos dinámicamente TODAS las variables que empiezan con var-
|
||||
for var_key, var_values in grafana_vars.items():
|
||||
for val in var_values:
|
||||
target_url += f"&{var_key}={val}"
|
||||
|
||||
# Forzamos timezone
|
||||
target_url += "&kiosk=1&hideNav=1&timezone=browser"
|
||||
|
||||
if time_from: target_url += f"&from={time_from}"
|
||||
if time_to: target_url += f"&to={time_to}"
|
||||
|
||||
with sync_playwright() as p:
|
||||
# Apagamos GPU y WebGL para evitar limites de hardware
|
||||
browser = p.chromium.launch(headless=True, args=['--disable-gpu', '--disable-dev-shm-usage', '--disable-webgl'])
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
ignore_https_errors=True,
|
||||
timezone_id=tz,
|
||||
)
|
||||
|
||||
if grafana_session:
|
||||
context.add_cookies([{
|
||||
'name': 'grafana_session',
|
||||
'value': grafana_session,
|
||||
'domain': 'grafana', # host interno del contenedor
|
||||
'path': '/'
|
||||
}])
|
||||
logger.info(f"[{job_id}] Clonación de Cookie exitosa.")
|
||||
else:
|
||||
logger.warning(f"[{job_id}] ERROR: No se recibió cookie de sesión desde. Petición rechazada por Grafana.")
|
||||
|
||||
page = context.new_page()
|
||||
|
||||
# Bloquear Websockets
|
||||
page.route("**/*api/live/ws*", lambda route: route.abort())
|
||||
page.on("pageerror", lambda err: logger.error(f"[{job_id}] Playwright Page Error: {err}"))
|
||||
|
||||
# escuchamos dinamicamente las querys
|
||||
active_queries = [0]
|
||||
|
||||
def handle_request(request):
|
||||
if "api/ds/query" in request.url:
|
||||
active_queries[0] += 1
|
||||
|
||||
def handle_request_finished(request):
|
||||
if "api/ds/query" in request.url:
|
||||
active_queries[0] = max(0, active_queries[0] - 1)
|
||||
|
||||
def handle_request_failed(request):
|
||||
if "api/ds/query" in request.url:
|
||||
active_queries[0] = max(0, active_queries[0] - 1)
|
||||
|
||||
page.on("request", handle_request)
|
||||
page.on("requestfinished", handle_request_finished)
|
||||
page.on("requestfailed", handle_request_failed)
|
||||
|
||||
page.goto(target_url, timeout=0, wait_until='load')
|
||||
|
||||
logger.info(f"[{job_id}] Validando permisos de acceso al dashboard...")
|
||||
# Dar tiempo a que React termine sus peticiones
|
||||
try:
|
||||
page.wait_for_load_state("networkidle", timeout=10000)
|
||||
except:
|
||||
pass
|
||||
|
||||
page.wait_for_timeout(2000)
|
||||
|
||||
if "/login" in page.url or page.get_by_placeholder("email or username").is_visible():
|
||||
error_msg = "Acceso Denegado - No tienes permisos para exportar este Dashboard o tu sesion caducó."
|
||||
logger.error(f"[{job_id}] {error_msg}")
|
||||
raise Exception(error_msg)
|
||||
|
||||
# check "Forbidden" o "Access denied" en el contenido de la página
|
||||
page_text = page.locator("body").inner_text().lower()
|
||||
if "failed to load dashboard" in page_text or "forbidden" in page_text or "access denied" in page_text or "not found" in page_text:
|
||||
error_msg = "Error 404 - El dashboard solicitado no existe o fue eliminado."
|
||||
logger.error(f"[{job_id}] Intento de acceso a dashboard sin permisos.")
|
||||
raise Exception(error_msg)
|
||||
|
||||
logger.info(f"[{job_id}] Acceso concedido. Esperando 10s para la primera fila de paneles...")
|
||||
page.wait_for_timeout(10000)
|
||||
|
||||
try:
|
||||
# # Localizamos ROWS colapsadas y las abrimos
|
||||
page.evaluate("""
|
||||
const buttons = document.querySelectorAll('[aria-expanded="false"]');
|
||||
buttons.forEach(btn => {
|
||||
const testId = btn.getAttribute('data-testid') || '';
|
||||
const className = btn.className || '';
|
||||
const parentClass = (btn.parentElement && btn.parentElement.className) || '';
|
||||
const ariaLabel = btn.getAttribute('aria-label') || '';
|
||||
|
||||
// Es una fila si cumple alguna de estas heurísticas de Grafana:
|
||||
const isRow = testId.includes('dashboard-row') ||
|
||||
className.includes('row') ||
|
||||
parentClass.includes('row') ||
|
||||
ariaLabel.toLowerCase().includes('row') ||
|
||||
ariaLabel.toLowerCase().includes('fila') ||
|
||||
btn.offsetWidth > 500; // Las barras de fila ocupan mucho ancho
|
||||
|
||||
if (isRow) {
|
||||
btn.setAttribute('data-pdf-row', 'true');
|
||||
}
|
||||
});
|
||||
|
||||
// Soporte legacy para Grafana 8
|
||||
document.querySelectorAll('.dashboard-row--collapsed').forEach(div => {
|
||||
div.setAttribute('data-pdf-row', 'true');
|
||||
});
|
||||
""")
|
||||
|
||||
# Recolectamos todas las filas
|
||||
row_elements = page.locator('[data-pdf-row="true"]').all()
|
||||
rows_to_click = []
|
||||
|
||||
for el in row_elements:
|
||||
if el.is_visible():
|
||||
box = el.bounding_box()
|
||||
if box:
|
||||
rows_to_click.append({
|
||||
'el': el,
|
||||
'y': box['y'],
|
||||
'text': el.inner_text().strip()[:40]
|
||||
})
|
||||
|
||||
# ORDENAMOS de mayor a menor (important)
|
||||
rows_to_click.sort(key=lambda item: item['y'], reverse=True)
|
||||
|
||||
logger.info(f"[{job_id}] Se encontraron {len(rows_to_click)} filas dinamicas para expandir.")
|
||||
|
||||
for item in rows_to_click:
|
||||
logger.info(f"[{job_id}] Expandiendo fila interactiva: {item['text']}")
|
||||
item['el'].scroll_into_view_if_needed()
|
||||
page.wait_for_timeout(500)
|
||||
item['el'].click(force=True)
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[{job_id}] Error secundario al hacer click en filas: {e}")
|
||||
|
||||
# 5 segundos para que Grafana renderize
|
||||
page.wait_for_timeout(5000)
|
||||
|
||||
total_height = page.evaluate("document.body.scrollHeight")
|
||||
|
||||
# salto de 1080px para que las querys carguen tranquilas
|
||||
viewport_height = 1080
|
||||
current_y = total_height
|
||||
|
||||
while current_y > 0:
|
||||
page.evaluate(f"window.scrollTo(0, {current_y})")
|
||||
|
||||
logger.info(f"[{job_id}] Scrolleamos a {current_y}px...")
|
||||
# 2s iniciales para que grafana/react reaccione
|
||||
page.wait_for_timeout(2000)
|
||||
|
||||
# Espera reactiva ---> vigila hasta que las queries activas terminen.
|
||||
wait_time = 0
|
||||
while active_queries[0] > 0 and wait_time < 15000:
|
||||
page.wait_for_timeout(1000)
|
||||
wait_time += 1000
|
||||
if wait_time % 5000 == 0:
|
||||
logger.info(f"[{job_id}] Waiting DB... Queries actives: {active_queries[0]}")
|
||||
|
||||
current_y -= viewport_height
|
||||
|
||||
page.evaluate("window.scrollTo(0, 0)")
|
||||
logger.info(f"[{job_id}] Tope alcanzado. Esperando ultima resolución.")
|
||||
page.wait_for_timeout(2000)
|
||||
wait_time = 0
|
||||
|
||||
# tiempo de seguridad por precaucion
|
||||
while active_queries[0] > 0 and wait_time < 45000:
|
||||
page.wait_for_timeout(1000)
|
||||
wait_time += 1000
|
||||
if wait_time % 5000 == 0:
|
||||
logger.info(f"[{job_id}] Tope - Still waiting DB... Queries actives: {active_queries[0]}")
|
||||
|
||||
logger.info(f"[{job_id}] Cambiando a Viewport original de la pagina de: {total_height}px.")
|
||||
final_height = min(total_height, 25000)
|
||||
page.set_viewport_size({'width': 1920, 'height': final_height})
|
||||
|
||||
page.evaluate("window.scrollTo(0, 0)")
|
||||
|
||||
logger.info(f"[{job_id}] 15000ms para que grafana procese...")
|
||||
page.wait_for_timeout(15000)
|
||||
|
||||
# checkeamos si NO ESTAMOS EN LOGIN == tenemos cookies validas?
|
||||
if "/login" in page.url or page.get_by_placeholder("email or username").is_visible():
|
||||
error_msg = "Acceso Denegado - Sesión caducada o rechazada en el último momento."
|
||||
logger.error(f"[{job_id}] {error_msg}")
|
||||
raise Exception(error_msg)
|
||||
|
||||
# sacamos titulo para que no diga: " - Dashboards - Grafana"
|
||||
page.evaluate("document.title = document.title.replace(' - Dashboards - Grafana', '');")
|
||||
|
||||
# Eliminar todos los (href) para que no sean clickeables en el PDF generado
|
||||
logger.info(f"[{job_id}] Eliminando enlaces interactivos del HTML...")
|
||||
page.evaluate("""
|
||||
document.querySelectorAll('a[href]').forEach(el => {
|
||||
el.removeAttribute('href');
|
||||
});
|
||||
""")
|
||||
|
||||
logger.info(f"[{job_id}] Generando PDF...")
|
||||
pdf_filepath = f"/tmp/{job_id}.pdf"
|
||||
|
||||
# Forzamos a Chrome a creer que sigue en una pantalla usando CSS @media print
|
||||
# Esto evita que Grafana oculte el reloj + botones
|
||||
page.emulate_media(media="screen")
|
||||
|
||||
page.pdf(
|
||||
path=pdf_filepath,
|
||||
print_background=True,
|
||||
width="1920px",
|
||||
height=f"{final_height}px",
|
||||
margin={"top": "0", "right": "0", "bottom": "0", "left": "0"},
|
||||
page_ranges="1"
|
||||
)
|
||||
|
||||
browser.close()
|
||||
|
||||
logger.info(f"[{job_id}] Work Done! Guardado en: {pdf_filepath}")
|
||||
|
||||
import re
|
||||
# convertimos a lowercase el 'dashboard_name' para que el archivo salga mejor
|
||||
safe_dashboard_name = re.sub(r'[^a-z0-9]+', '-', dashboard_name.lower()).strip('-')
|
||||
filename_final = f"{safe_dashboard_name}.pdf"
|
||||
|
||||
set_job_status(job_id, {'status': 'done', 'filepath': pdf_filepath, 'filename': filename_final})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{job_id}] FATAL ERROR en Playwright: {str(e)}")
|
||||
set_job_status(job_id, {'status': 'error', 'error': str(e)})
|
||||
|
||||
@app.route('/api/v1/export/image', methods=['GET'])
|
||||
def export_image_trigger():
|
||||
|
||||
# Capturar TODAS las variables (empiezan con var-)
|
||||
grafana_vars = {}
|
||||
for key in request.args.keys():
|
||||
if key.startswith('var-'):
|
||||
grafana_vars[key] = request.args.getlist(key)
|
||||
|
||||
cliente = request.args.get('cliente')
|
||||
if not cliente and 'var-cliente' in grafana_vars:
|
||||
cliente = grafana_vars['var-cliente'][0]
|
||||
|
||||
dashboard_uid = request.args.get('dashboard_uid')
|
||||
dashboard_name = request.args.get('dashboard_name', 'Masivo')
|
||||
time_from = request.args.get('from')
|
||||
time_to = request.args.get('to')
|
||||
tz = request.args.get('tz', 'America/Argentina/Buenos_Aires')
|
||||
if "${" in tz or not tz:
|
||||
tz = 'America/Argentina/Buenos_Aires'
|
||||
|
||||
grafana_session = request.cookies.get('grafana_session')
|
||||
|
||||
if not cliente or not dashboard_uid:
|
||||
return jsonify({'error': 'Faltan parametros obligatorios'}), 400
|
||||
|
||||
# Crear el ticket asincrono
|
||||
job_id = uuid.uuid4().hex
|
||||
set_job_status(job_id, {'status': 'starting'})
|
||||
|
||||
# Lanzar el thread
|
||||
thread = threading.Thread(
|
||||
target=background_export_task,
|
||||
args=(job_id, dashboard_uid, dashboard_name, cliente, grafana_vars, time_from, time_to, tz, grafana_session)
|
||||
)
|
||||
thread.start()
|
||||
|
||||
# Responder INMEDIATAMENTE a el usuario asi no falla cloudflare
|
||||
html_content = LOADING_HTML.replace("{{job_id}}", job_id)
|
||||
return render_template_string(html_content)
|
||||
|
||||
@app.route('/api/v1/export/status/<job_id>', methods=['GET'])
|
||||
def export_status(job_id):
|
||||
status_info = get_job_status(job_id)
|
||||
return jsonify(status_info)
|
||||
|
||||
@app.route('/api/v1/export/download/<job_id>', methods=['GET'])
|
||||
def export_download(job_id):
|
||||
status_info = get_job_status(job_id)
|
||||
if status_info.get('status') != 'done':
|
||||
return "Archivo no encontrado o no está listo", 404
|
||||
|
||||
filepath = status_info.get('filepath')
|
||||
download_name = status_info.get('filename', 'reporte.pdf')
|
||||
|
||||
try:
|
||||
# delete del archivo after download
|
||||
@after_this_request
|
||||
def remove_file(response):
|
||||
try:
|
||||
os.remove(filepath)
|
||||
os.remove(f"/tmp/{job_id}.json")
|
||||
logger.info(f"[{job_id}] Limpieza temporal exitosa tras descarga.")
|
||||
except Exception as e:
|
||||
logger.error(f"[{job_id}] Error borrando temp files: {e}")
|
||||
return response
|
||||
|
||||
return send_file(
|
||||
filepath,
|
||||
mimetype='application/pdf',
|
||||
as_attachment=True,
|
||||
download_name=download_name
|
||||
)
|
||||
except Exception as e:
|
||||
return str(e), 500
|
||||
|
||||
@app.route('/api/v1/transactions', methods=['POST'])
|
||||
def receive_transaction():
|
||||
data = request.get_json()
|
||||
|
||||
if not data or not isinstance(data, list):
|
||||
return jsonify({"error": "Se esperaba un array JSON de transacciones"}), 400
|
||||
|
||||
# Pydantic validation
|
||||
valid_transactions = []
|
||||
failed_messages = []
|
||||
|
||||
for i, item in enumerate(data):
|
||||
try:
|
||||
valid_transactions.append(TransactionSchema(**item))
|
||||
except ValidationError as e:
|
||||
logger.warning(f"JSON rechazado en índice {i}: {e.errors()}")
|
||||
tx_id = item.get("pos_transaction_id", f"índice {i}")
|
||||
failed_messages.append(f"La transaccion con id {tx_id} no cumple el formato esperado: {str(e.errors())}")
|
||||
|
||||
if not valid_transactions:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"data": "Todas las transacciones fallaron la validación de formato.\n" + "\n".join(failed_messages)
|
||||
}), 400
|
||||
|
||||
pool = get_db_pool()
|
||||
if not pool:
|
||||
return jsonify({"error": "Error interno del servidor"}), 500
|
||||
|
||||
# Extraemos una conexion disponible del pool
|
||||
try:
|
||||
connection = pool.get_connection()
|
||||
except Error as e:
|
||||
logger.error(f"Error obteniendo conexión del pool: {e}")
|
||||
return jsonify({"error": "Error de base de datos"}), 500
|
||||
|
||||
cursor = connection.cursor()
|
||||
|
||||
insert_query = """
|
||||
INSERT INTO transactions (
|
||||
cliente, sco_number, sucursal, pos_transaction_id, started_at, end_transaction_at,
|
||||
closed_at, status, item_count, total_amount, paid_amount, change_amount,
|
||||
amounts_reconstructed, payment_type, split_payment, age_verification_requested,
|
||||
age_verification_value, bag_item_count
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
started_at = VALUES(started_at),
|
||||
end_transaction_at = VALUES(end_transaction_at),
|
||||
closed_at = VALUES(closed_at),
|
||||
status = VALUES(status),
|
||||
item_count = VALUES(item_count),
|
||||
total_amount = VALUES(total_amount),
|
||||
paid_amount = VALUES(paid_amount),
|
||||
change_amount = VALUES(change_amount),
|
||||
amounts_reconstructed = VALUES(amounts_reconstructed),
|
||||
payment_type = VALUES(payment_type),
|
||||
split_payment = VALUES(split_payment),
|
||||
age_verification_requested = VALUES(age_verification_requested),
|
||||
age_verification_value = VALUES(age_verification_value),
|
||||
bag_item_count = VALUES(bag_item_count)
|
||||
"""
|
||||
|
||||
values_list = []
|
||||
for transaction in valid_transactions:
|
||||
# Forzamos lower case en Python también como doble capa de seguridad
|
||||
values_list.append((
|
||||
transaction.cliente.lower(),
|
||||
transaction.sco_number,
|
||||
transaction.sucursal,
|
||||
transaction.pos_transaction_id,
|
||||
transaction.started_at,
|
||||
transaction.end_transaction_at,
|
||||
transaction.closed_at,
|
||||
transaction.status,
|
||||
transaction.item_count,
|
||||
transaction.total_amount,
|
||||
transaction.paid_amount,
|
||||
transaction.change_amount,
|
||||
transaction.amounts_reconstructed,
|
||||
transaction.payment_type,
|
||||
transaction.split_payment,
|
||||
transaction.age_verification_requested,
|
||||
transaction.age_verification_value,
|
||||
transaction.bag_item_count
|
||||
))
|
||||
|
||||
try:
|
||||
cursor.executemany(insert_query, values_list)
|
||||
connection.commit()
|
||||
logger.info(f"Bulk Upsert exitoso: {len(valid_transactions)} guardadas. {len(failed_messages)} fallidas.")
|
||||
|
||||
response_data = "Batch procesado correctamente."
|
||||
if failed_messages:
|
||||
response_data = "\n".join(failed_messages)
|
||||
|
||||
return jsonify({
|
||||
"status": "ok",
|
||||
"data": response_data
|
||||
}), 200
|
||||
except Error as e:
|
||||
logger.error(f"Error ejecutando query MySQL: {e}")
|
||||
connection.rollback()
|
||||
return jsonify({"error": "Error al guardar en la base de datos"}), 500
|
||||
finally:
|
||||
if cursor:
|
||||
cursor.close()
|
||||
if connection:
|
||||
# no cerramos la conexión - la devolvemos al pool
|
||||
connection.close()
|
||||
|
||||
ALERTS_LOG_FILE = os.environ.get("ALERTS_LOG_FILE", "/data/alerts/alerts.txt")
|
||||
|
||||
@app.route('/api/v1/alerts/webhook', methods=['POST'])
|
||||
def receive_grafana_alert():
|
||||
try:
|
||||
data = request.get_json(force=True)
|
||||
if not data:
|
||||
return jsonify({"status": "error", "message": "No JSON payload received"}), 400
|
||||
|
||||
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
status = data.get("status", "unknown").upper()
|
||||
title = data.get("title", data.get("groupLabels", {}).get("alertname", "Sin Titulo"))
|
||||
alerts = data.get("alerts", [])
|
||||
|
||||
# Asegurar que el directorio exista
|
||||
log_path = Path(ALERTS_LOG_FILE)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(str(log_path), "a", encoding="utf-8") as f:
|
||||
f.write(f"\n{'='*75}\n")
|
||||
f.write(f"[{now_str}] ALERTA GRAFANA - ESTADO: {status}\n")
|
||||
f.write(f"Título: {title}\n")
|
||||
|
||||
if alerts:
|
||||
for idx, alert in enumerate(alerts, 1):
|
||||
labels = alert.get("labels", {})
|
||||
annotations = alert.get("annotations", {})
|
||||
starts_at = alert.get("startsAt", "")
|
||||
ends_at = alert.get("endsAt", "")
|
||||
alert_status = alert.get("status", status)
|
||||
|
||||
f.write(f"\n [Alerta #{idx}] Status: {alert_status}\n")
|
||||
f.write(f" - Nombre : {labels.get('alertname', 'N/A')}\n")
|
||||
f.write(f" - Cliente : {labels.get('cliente', 'N/A')}\n")
|
||||
f.write(f" - Sucursal : {labels.get('sucursal', 'N/A')}\n")
|
||||
f.write(f" - SCO : {labels.get('sco_number', labels.get('instance', 'N/A'))}\n")
|
||||
f.write(f" - Severidad : {labels.get('severity', 'N/A')}\n")
|
||||
f.write(f" - Resumen : {annotations.get('summary', annotations.get('description', 'N/A'))}\n")
|
||||
if starts_at:
|
||||
f.write(f" - Inicio : {starts_at}\n")
|
||||
if ends_at and ends_at != "0001-01-01T00:00:00Z":
|
||||
f.write(f" - Fin : {ends_at}\n")
|
||||
else:
|
||||
f.write(f" Detalle: {data.get('message', 'Sin descripción adicional')}\n")
|
||||
|
||||
f.write(f"\nRAW JSON: {json.dumps(data, ensure_ascii=False)}\n")
|
||||
f.flush()
|
||||
|
||||
logger.info(f"Alerta de Grafana guardada en {ALERTS_LOG_FILE} [{status}] {title}")
|
||||
return jsonify({"status": "ok", "message": "Alerta registrada correctamente"}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error procesando webhook de alerta: {e}")
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
@@ -0,0 +1,7 @@
|
||||
Flask==3.1.3
|
||||
mysql-connector-python==26.7.0
|
||||
gunicorn==21.2.0
|
||||
pydantic>=2.0.0
|
||||
requests==2.31.0
|
||||
Pillow==10.2.0
|
||||
playwright==1.45.0
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM jenkins/jenkins:2.541.3-jdk21
|
||||
|
||||
USER root
|
||||
RUN apt-get update && apt-get install -y curl git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
USER jenkins
|
||||
|
||||
RUN jenkins-plugin-cli --plugins \
|
||||
configuration-as-code \
|
||||
credentials-binding \
|
||||
git \
|
||||
workflow-aggregator \
|
||||
job-dsl \
|
||||
pipeline-utility-steps
|
||||
|
||||
COPY jenkins.yaml /usr/share/jenkins/ref/jenkins.yaml
|
||||
ENV CASC_JENKINS_CONFIG=/usr/share/jenkins/ref/jenkins.yaml
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/bin/sh
|
||||
|
||||
### DOWNLOAD MC
|
||||
if [ ! -f /usr/local/bin/mc ]; then
|
||||
curl -L https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
|
||||
chmod +x /usr/local/bin/mc
|
||||
fi
|
||||
|
||||
systemctl stop scoserver.service
|
||||
systemctl stop scodocker.service
|
||||
docker rmi cpi-server:latest gotenberg/gotenberg:8-chromium jumpmind/symmetricds:3.15.16 html-converter:latest mysql:8.0.32
|
||||
|
||||
### UPDATE SOFTWARE
|
||||
SERVER=minio-api.laoficina1782.com
|
||||
CUSTOMER="${1:?Uso: $0 <customer> <sucursal> <sco_number>}"
|
||||
mc --config-dir /tmp/mc-config alias set minio "https://$SERVER" minioadmin LIGTNbUgQr
|
||||
mc --config-dir /tmp/mc-config mirror --json --remove --overwrite --exclude "backend/docker/data/**" "minio/$CUSTOMER" /opt/sco/
|
||||
find /opt/sco/ -type d -exec chmod 755 {} \;
|
||||
find /opt/sco/ -type f -exec chmod 644 {} \;
|
||||
chmod -R 755 /opt/sco/frontend/
|
||||
|
||||
### TMP FOLDER
|
||||
cp /usr/lib/tmpfiles.d/fs-tmp.conf /etc/tmpfiles.d/
|
||||
echo "D /tmp 1777 root root -" > /etc/tmpfiles.d/fs-tmp.conf
|
||||
|
||||
### SERVICE
|
||||
find /opt/sco/resources/services/ -type f -exec sh -c 'cp "$1" "/etc/systemd/system/$(basename "$1")"' _ {} \;
|
||||
find /opt/sco/resources/services/ -type f -exec sh -c 'systemctl enable "$(basename "$1")"' _ {} \;
|
||||
systemctl daemon-reload
|
||||
|
||||
### AUTOSTART
|
||||
rsync -a --delete /opt/sco/resources/autostart/ /home/sco/.config/autostart/
|
||||
chown -R sco:users /home/sco/.config/autostart/*
|
||||
chmod -R 644 /home/sco/.config/autostart/*
|
||||
|
||||
### CRONTAB
|
||||
crontab -u root /opt/sco/resources/crontab/root.txt
|
||||
crontab -u sco /opt/sco/resources/crontab/sco.txt
|
||||
|
||||
### PYTHON
|
||||
find /opt/sco/resources/python/ -type d -iname "packages" | while read packages_dir; do
|
||||
parent_dir=$(dirname "$packages_dir")
|
||||
req_file="$parent_dir/requirements.txt"
|
||||
if [ -f "$req_file" ]; then
|
||||
pip install -q --no-index --find-links="$packages_dir" -r "$req_file"
|
||||
fi
|
||||
done
|
||||
|
||||
### DOCKER IMAGES
|
||||
mkdir -p /tmp/images/
|
||||
find /opt/sco/resources/images/ -type f -name '*.gz' -exec sh -c 'gunzip -c "$1" > /tmp/images/"$(basename "${1%.gz}")"' _ {} \;
|
||||
find /tmp/images/ -type f -iname "*.tar" -exec docker load -i {} \;
|
||||
|
||||
### INIT CPI-SERVER DATABASES AND LOGS FOLDERS
|
||||
mkdir -p /opt/sco/backend/docker/data/cpi-server/database
|
||||
mkdir -p /opt/sco/backend/docker/data/cpi-server/logs/devices
|
||||
mkdir -p /opt/sco/backend/docker/data/cpi-server/logs/service
|
||||
touch /opt/sco/backend/docker/data/cpi-server/database/configuration.db
|
||||
touch /opt/sco/backend/docker/data/cpi-server/database/connectivity.db
|
||||
touch /opt/sco/backend/docker/data/cpi-server/database/Identity.db
|
||||
touch /opt/sco/backend/docker/data/cpi-server/database/transaction.db
|
||||
touch /opt/sco/backend/docker/data/cpi-server/database/trialdata.db
|
||||
|
||||
### INIT SYMETRIC CONFIGURATION FILE
|
||||
mkdir -p /opt/sco/backend/docker/data/symmetric
|
||||
if [ ! -f /opt/sco/backend/docker/data/symmetric/engine.properties ]; then
|
||||
LOCAL_NODE=$(hostname)
|
||||
LOCAL_IP=$(ip route get 1.1.1.1 | grep -oP '(?<=src\s)\d+(\.\d+){3}')
|
||||
cat <<EOF > /opt/sco/backend/docker/data/symmetric/engine.properties
|
||||
engine.name=$LOCAL_NODE
|
||||
group.id=fullmesh
|
||||
external.id=$LOCAL_NODE
|
||||
db.driver=com.mysql.cj.jdbc.Driver
|
||||
db.url=jdbc:mysql://$LOCAL_IP:3306/sco
|
||||
db.user=root
|
||||
db.password=U2qs&U6LA9U2
|
||||
registration.url=
|
||||
sync.url=http://$LOCAL_IP:31415/sync/$LOCAL_NODE
|
||||
start.pulled.job=true
|
||||
job.purge.period.time.ms=7200000
|
||||
EOF
|
||||
fi
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/bin/sh
|
||||
|
||||
### DOWNLOAD MC
|
||||
if [ ! -f /usr/local/bin/mc ]; then
|
||||
curl -L https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
|
||||
chmod +x /usr/local/bin/mc
|
||||
fi
|
||||
|
||||
### VALIDATE PARAMETERS
|
||||
SERVER=minio-api.laoficina1782.com
|
||||
CUSTOMER="${1:?Uso: $0 <customer> <sucursal> <sco_number>}"
|
||||
SUCURSAL="${2:?Uso: $0 <customer> <sucursal> <sco_number>}"
|
||||
SCO_NUMBER="${3:?Uso: $0 <customer> <sucursal> <sco_number>}"
|
||||
|
||||
### STOP SERVICES
|
||||
systemctl stop scovirtual.service
|
||||
systemctl stop scoserver.service
|
||||
systemctl stop scodocker.service
|
||||
|
||||
### UPDATE SOFTWARE
|
||||
mc --config-dir /tmp/mc-config alias set minio "https://$SERVER" minioadmin LIGTNbUgQr
|
||||
mc --config-dir /tmp/mc-config mirror --json --remove --overwrite --exclude "backend/docker/data/**" "minio/$CUSTOMER" /opt/sco/
|
||||
find /opt/sco/ -type d -exec chmod 755 {} \;
|
||||
find /opt/sco/ -type f -exec chmod 644 {} \;
|
||||
chmod -R 755 /opt/sco/frontend/
|
||||
|
||||
### ENV FILE FOR MONITORING+REPORTING
|
||||
cat <<EOF > /opt/sco/backend/docker/.env
|
||||
SUCURSAL=$SUCURSAL
|
||||
SCO_NUMBER=$SCO_NUMBER
|
||||
SCO_CLIENT=$CUSTOMER
|
||||
EOF
|
||||
chmod 644 /opt/sco/backend/docker/.env
|
||||
|
||||
### SCO-AGENT DATA DIRS
|
||||
mkdir -p /opt/sco/backend/docker/data/sco-agent/outbox
|
||||
mkdir -p /opt/sco/backend/docker/data/sco-agent/state
|
||||
mkdir -p /opt/sco/backend/docker/data/sco-agent/prom
|
||||
chmod -R 777 /opt/sco/backend/docker/data/sco-agent/
|
||||
|
||||
### TMP FOLDER
|
||||
cp /usr/lib/tmpfiles.d/fs-tmp.conf /etc/tmpfiles.d/
|
||||
echo "D /tmp 1777 root root -" > /etc/tmpfiles.d/fs-tmp.conf
|
||||
|
||||
### SERVICE
|
||||
find /opt/sco/resources/services/ -type f -exec sh -c 'cp "$1" "/etc/systemd/system/$(basename "$1")"' _ {} \;
|
||||
find /opt/sco/resources/services/ -type f -exec sh -c 'systemctl enable "$(basename "$1")"' _ {} \;
|
||||
systemctl daemon-reload
|
||||
|
||||
### AUTOSTART
|
||||
rsync -a --delete /opt/sco/resources/autostart/ /home/sco/.config/autostart/
|
||||
chown -R sco:users /home/sco/.config/autostart/*
|
||||
chmod -R 644 /home/sco/.config/autostart/*
|
||||
|
||||
### CRONTAB
|
||||
crontab -u root /opt/sco/resources/crontab/root.txt
|
||||
crontab -u sco /opt/sco/resources/crontab/sco.txt
|
||||
|
||||
### PYTHON
|
||||
find /opt/sco/resources/python/ -type d -iname "packages" | while read packages_dir; do
|
||||
parent_dir=$(dirname "$packages_dir")
|
||||
req_file="$parent_dir/requirements.txt"
|
||||
if [ -f "$req_file" ]; then
|
||||
pip install -q --no-index --find-links="$packages_dir" -r "$req_file"
|
||||
fi
|
||||
done
|
||||
|
||||
### DOCKER IMAGES
|
||||
docker rmi -f $(docker images -aq)
|
||||
mkdir -p /tmp/images/
|
||||
find /opt/sco/resources/images/ -type f -name '*.gz' -exec sh -c 'gunzip -c "$1" > /tmp/images/"$(basename "${1%.gz}")"' _ {} \;
|
||||
find /tmp/images/ -type f -iname "*.tar" -exec docker load -i {} \;
|
||||
|
||||
### INIT CPI-SERVER DATABASES AND LOGS FOLDERS
|
||||
mkdir -p /opt/sco/backend/docker/data/cpi-server/database
|
||||
mkdir -p /opt/sco/backend/docker/data/cpi-server/logs/devices
|
||||
mkdir -p /opt/sco/backend/docker/data/cpi-server/logs/service
|
||||
touch /opt/sco/backend/docker/data/cpi-server/database/configuration.db
|
||||
touch /opt/sco/backend/docker/data/cpi-server/database/connectivity.db
|
||||
touch /opt/sco/backend/docker/data/cpi-server/database/Identity.db
|
||||
touch /opt/sco/backend/docker/data/cpi-server/database/transaction.db
|
||||
touch /opt/sco/backend/docker/data/cpi-server/database/trialdata.db
|
||||
|
||||
### INIT SYMETRIC CONFIGURATION FILE
|
||||
mkdir -p /opt/sco/backend/docker/data/symmetric
|
||||
if [ ! -f /opt/sco/backend/docker/data/symmetric/engine.properties ]; then
|
||||
LOCAL_NODE=$(hostname)
|
||||
LOCAL_IP=$(ip route get 1.1.1.1 | grep -oP '(?<=src\s)\d+(\.\d+){3}')
|
||||
cat <<EOF > /opt/sco/backend/docker/data/symmetric/engine.properties
|
||||
engine.name=$LOCAL_NODE
|
||||
group.id=fullmesh
|
||||
external.id=$LOCAL_NODE
|
||||
db.driver=com.mysql.cj.jdbc.Driver
|
||||
db.url=jdbc:mysql://$LOCAL_IP:3306/sco
|
||||
db.user=root
|
||||
db.password=U2qs&U6LA9U2
|
||||
registration.url=
|
||||
sync.url=http://$LOCAL_IP:31415/sync/$LOCAL_NODE
|
||||
start.pulled.job=true
|
||||
job.purge.period.time.ms=7200000
|
||||
EOF
|
||||
fi
|
||||
|
||||
### REBOOT
|
||||
nohup sh -c 'sleep 5; shutdown -r now' >/dev/null 2>&1 &
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if ! docker ps --format '{{.Names}}' | grep -qx sco-agent; then
|
||||
echo "El contenedor sco-agent no esta corriendo en este equipo" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker exec sco-agent python -m sco_agent.history
|
||||
@@ -0,0 +1,617 @@
|
||||
jenkins:
|
||||
numExecutors: 2
|
||||
nodeMonitors:
|
||||
- "architecture"
|
||||
- "clock"
|
||||
- diskSpace:
|
||||
freeSpaceThreshold: "0"
|
||||
freeSpaceWarningThreshold: "0"
|
||||
- "swapSpace"
|
||||
- tmpSpace:
|
||||
freeSpaceThreshold: "0"
|
||||
freeSpaceWarningThreshold: "0"
|
||||
- "responseTime"
|
||||
authorizationStrategy:
|
||||
loggedInUsersCanDoAnything:
|
||||
allowAnonymousRead: false
|
||||
securityRealm:
|
||||
local:
|
||||
allowsSignup: false
|
||||
users:
|
||||
- id: admin
|
||||
password: H0l4mund0!
|
||||
# Agentes permanentes (WebSocket): cada SCO conecta outbound a Jenkins via wss://.
|
||||
# El deploy (Jenkinsfile.sco) targetea por nombre exacto de nodo (parámetro NODO),
|
||||
# no por label, por eso los nodos no llevan labelString.
|
||||
nodes:
|
||||
- permanent:
|
||||
name: "ralph-caguas-sco04"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-caguas-sco05"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-caguas-sco06"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-caguas-sco07"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-caguas-sco08"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-caguas-sco09"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-cayey-sco01"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-cayey-sco02"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-cayey-sco03"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-cayey-sco04"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-deleste-humacao-sco01"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-deleste-humacao-sco02"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-fajardo-sco07"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-fajardo-sco08"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-fajardo-sco09"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-fajardo-sco10"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-fajardo-sco11"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-fajardo-sco12"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-gurabo-sco03"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-gurabo-sco04"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-gurabo-sco05"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-gurabo-sco06"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-humacao-sco07"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-humacao-sco08"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-humacao-sco09"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-humacao-sco10"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-humacao-sco11"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-humacao-sco12"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-juntos-sco04"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-juntos-sco05"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-juntos-sco06"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-juntos-sco07"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-las-piedras-sco08"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-las-piedras-sco09"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-las-piedras-sco10"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-las-piedras-sco11"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-las-piedras-sco12"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-montehiedra-sco13"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-montehiedra-sco14"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-montehiedra-sco15"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-montehiedra-sco16"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-montehiedra-sco17"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-montehiedra-sco18"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-montehiedra-sco19"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-montehiedra-sco20"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-naguabo-sco04"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-naguabo-sco05"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-naguabo-sco06"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-naguabo-sco07"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-yabucoa-sco02"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-yabucoa-sco03"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-yabucoa-sco04"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-yabucoa-sco05"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-yabucoa-sco06"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-yabucoa-sco07"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-san-lorenzo-sco03"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-san-lorenzo-sco04"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-san-lorenzo-sco05"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-san-lorenzo-sco06"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-rio-grande-sco01"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-rio-grande-sco02"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-rio-grande-sco03"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-rio-grande-sco04"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-rio-grande-sco05"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "ralph-rio-grande-sco06"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "laboratorio-girboy-sco81"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "laboratorio-girboy-sco82"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "laboratorio-girboy-sco83"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
- permanent:
|
||||
name: "hisense-santiago-sco01"
|
||||
remoteFS: "/opt/jenkins-agent"
|
||||
numExecutors: 1
|
||||
retentionStrategy: "always"
|
||||
launcher:
|
||||
inbound:
|
||||
webSocket: true
|
||||
|
||||
|
||||
unclassified:
|
||||
location:
|
||||
url: https://jenkins.laoficina1782.com/
|
||||
security:
|
||||
globalJobDslSecurityConfiguration:
|
||||
useScriptSecurity: false
|
||||
jobs:
|
||||
- script: |
|
||||
pipelineJob('deploy-sco') {
|
||||
description('Deploy de install.sh a un nodo SCO puntual, para el cliente seleccionado.')
|
||||
definition {
|
||||
cps {
|
||||
script(new File('/opt/jenkins/Jenkinsfile.sco').text)
|
||||
sandbox(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
- script: |
|
||||
pipelineJob('deploy-sco-reports') {
|
||||
description('Deploy de install.sh.reports a un nodo SCO puntual, para el cliente+sucursal+sco seleccionado.')
|
||||
definition {
|
||||
cps {
|
||||
script(new File('/opt/jenkins/Jenkinsfile.sco.reports').text)
|
||||
sandbox(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
- script: |
|
||||
pipelineJob('history-sco') {
|
||||
description('Reprocesa el historial de logs de un nodo SCO puntual via sco-agent.')
|
||||
definition {
|
||||
cps {
|
||||
script(new File('/opt/jenkins/Jenkinsfile.sco.reports.history').text)
|
||||
sandbox(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
pipeline {
|
||||
agent none
|
||||
parameters {
|
||||
string(
|
||||
name: 'NODO',
|
||||
defaultValue: '',
|
||||
description: 'Nombre exacto del nodo a deployar'
|
||||
)
|
||||
choice(
|
||||
name: 'CLIENTE',
|
||||
choices: [
|
||||
'SELECCIONAR_CLIENTE',
|
||||
'cencosud-jumbo',
|
||||
'censosud-staisabel',
|
||||
'changomas-hiper',
|
||||
'changomas-super',
|
||||
'dibal',
|
||||
'hisense',
|
||||
'lsco',
|
||||
'mbs-econo',
|
||||
'mbs-famcoop',
|
||||
'mbs-ralphs',
|
||||
'mbs-supermax',
|
||||
'modatelas',
|
||||
'walmart-chile-acuenta',
|
||||
'walmart-chile-lider',
|
||||
'wm-mexico-bodega',
|
||||
'wm-mexico-walmart'
|
||||
],
|
||||
description: 'Cliente para el cual se ejecuta el deploy'
|
||||
)
|
||||
}
|
||||
stages {
|
||||
stage('Registro de parámetros') {
|
||||
agent none
|
||||
when { expression { params.CLIENTE == null } }
|
||||
steps {
|
||||
echo 'Primer build: Jenkins está registrando los parámetros de este Jenkinsfile. No se ejecuta ningún deploy. Volvé a correr el job con "Build with Parameters".'
|
||||
script { currentBuild.result = 'NOT_BUILT' }
|
||||
}
|
||||
}
|
||||
stage('Preparar scripts') {
|
||||
agent { label 'built-in' }
|
||||
when { expression { params.CLIENTE != null } }
|
||||
steps {
|
||||
sh 'cp /opt/deploy/install.sh .'
|
||||
stash name: 'scripts', includes: 'install.sh'
|
||||
}
|
||||
}
|
||||
stage('Deploy') {
|
||||
agent { label 'built-in' }
|
||||
when { expression { params.CLIENTE != null } }
|
||||
steps {
|
||||
script {
|
||||
if (!params.NODO?.trim()) {
|
||||
error "El parámetro NODO es obligatorio"
|
||||
}
|
||||
if (params.CLIENTE == 'SELECCIONAR_CLIENTE') {
|
||||
error "El parámetro CLIENTE es obligatorio"
|
||||
}
|
||||
def target = params.NODO.trim()
|
||||
node(target) {
|
||||
unstash 'scripts'
|
||||
sh "bash install.sh '${params.CLIENTE.trim()}'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
pipeline {
|
||||
agent none
|
||||
parameters {
|
||||
string(
|
||||
name: 'NODO',
|
||||
defaultValue: '',
|
||||
description: 'Nombre exacto del nodo a deployar'
|
||||
)
|
||||
choice(
|
||||
name: 'CLIENTE',
|
||||
choices: [
|
||||
'SELECCIONAR_CLIENTE',
|
||||
'cencosud-jumbo',
|
||||
'censosud-staisabel',
|
||||
'changomas-hiper',
|
||||
'changomas-super',
|
||||
'dibal',
|
||||
'hisense',
|
||||
'lsco',
|
||||
'mbs-econo',
|
||||
'mbs-famcoop',
|
||||
'mbs-ralphs',
|
||||
'mbs-supermax',
|
||||
'modatelas',
|
||||
'walmart-chile-acuenta',
|
||||
'walmart-chile-lider',
|
||||
'wm-mexico-bodega',
|
||||
'wm-mexico-walmart'
|
||||
],
|
||||
description: 'Cliente para el cual se ejecuta el deploy'
|
||||
)
|
||||
string(
|
||||
name: 'SUCURSAL',
|
||||
defaultValue: '',
|
||||
description: 'Sucursal del equipo'
|
||||
)
|
||||
string(
|
||||
name: 'SCO_NUMBER',
|
||||
defaultValue: '',
|
||||
description: 'Número de SCO (ej. sco05)'
|
||||
)
|
||||
}
|
||||
stages {
|
||||
stage('Registro de parámetros') {
|
||||
agent none
|
||||
when { expression { params.CLIENTE == null } }
|
||||
steps {
|
||||
echo 'Primer build: Jenkins está registrando los parámetros de este Jenkinsfile. No se ejecuta ningún deploy. Volvé a correr el job con "Build with Parameters".'
|
||||
script { currentBuild.result = 'NOT_BUILT' }
|
||||
}
|
||||
}
|
||||
stage('Preparar scripts') {
|
||||
agent { label 'built-in' }
|
||||
when { expression { params.CLIENTE != null } }
|
||||
steps {
|
||||
sh 'cp /opt/deploy/install.sh.reports .'
|
||||
stash name: 'scripts', includes: 'install.sh.reports'
|
||||
}
|
||||
}
|
||||
stage('Deploy') {
|
||||
agent { label 'built-in' }
|
||||
when { expression { params.CLIENTE != null } }
|
||||
steps {
|
||||
script {
|
||||
if (!params.NODO?.trim()) {
|
||||
error "El parámetro NODO es obligatorio"
|
||||
}
|
||||
if (params.CLIENTE == 'SELECCIONAR_CLIENTE') {
|
||||
error "El parámetro CLIENTE es obligatorio"
|
||||
}
|
||||
def target = params.NODO.trim()
|
||||
node(target) {
|
||||
unstash 'scripts'
|
||||
sh "bash install.sh.reports '${params.CLIENTE.trim()}' '${params.SUCURSAL.trim()}' '${params.SCO_NUMBER.trim()}'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
parameters {
|
||||
string(name: 'NODO', defaultValue: '', description: 'Nombre exacto del agente SCO (ej: ralph-yabucoa-sco05)')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Registro de parámetros') {
|
||||
agent none
|
||||
when { expression { params.NODO == null } }
|
||||
steps {
|
||||
echo 'Primer build: Jenkins está registrando los parámetros de este Jenkinsfile. No se ejecuta nada. Volvé a correr el job con "Build with Parameters".'
|
||||
script { currentBuild.result = 'NOT_BUILT' }
|
||||
}
|
||||
}
|
||||
stage('Preparar scripts') {
|
||||
agent { label 'built-in' }
|
||||
when { expression { params.NODO != null } }
|
||||
steps {
|
||||
sh 'cp /opt/history/history.sh .'
|
||||
stash name: 'scripts', includes: 'history.sh'
|
||||
}
|
||||
}
|
||||
stage('Reprocesar historial') {
|
||||
agent { label 'built-in' }
|
||||
when { expression { params.NODO != null } }
|
||||
steps {
|
||||
script {
|
||||
if (!params.NODO?.trim()) {
|
||||
error "El parámetro NODO es obligatorio"
|
||||
}
|
||||
def target = params.NODO.trim()
|
||||
node(target) {
|
||||
unstash 'scripts'
|
||||
timeout(time: 30, unit: 'MINUTES') {
|
||||
sh 'bash history.sh'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
cliente VARCHAR(50) NOT NULL,
|
||||
sco_number VARCHAR(50) NOT NULL,
|
||||
sucursal VARCHAR(100) NOT NULL,
|
||||
pos_transaction_id INT NOT NULL,
|
||||
started_at DATETIME NOT NULL,
|
||||
end_transaction_at DATETIME NULL,
|
||||
closed_at DATETIME NOT NULL,
|
||||
status ENUM('completed', 'canceled') NOT NULL,
|
||||
item_count INT NOT NULL DEFAULT 0,
|
||||
total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
payment_type VARCHAR(50) NULL,
|
||||
payment_amount DECIMAL(10,2) NULL,
|
||||
split_payment BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
age_verification_requested BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
age_verification_value INT NULL,
|
||||
bag_item_count INT NOT NULL DEFAULT 0,
|
||||
|
||||
-- restrict lower case
|
||||
CONSTRAINT chk_cliente_lower CHECK (cliente = LOWER(cliente)),
|
||||
|
||||
-- anti-duplicados para el UPSERT
|
||||
UNIQUE KEY uq_cliente_sucursal_sco_pos (cliente, sucursal, sco_number, pos_transaction_id, (DATE(started_at))),
|
||||
|
||||
-- index para acelerar Grafana
|
||||
KEY idx_cliente_sucursal_sco_started (cliente, sucursal, sco_number, started_at)
|
||||
);
|
||||
@@ -0,0 +1,70 @@
|
||||
sco_registered{sucursal="santiago", sco_number="sco1", cliente="econo"} 1
|
||||
sco_registered{sucursal="santiago", sco_number="sco2", cliente="econo"} 1
|
||||
sco_registered{sucursal="concepcion", sco_number="sco1", cliente="wallmart"} 1
|
||||
sco_registered{sucursal="ciudadela", sco_number="sco1", cliente="econo"} 1
|
||||
sco_registered{sucursal="santiago", sco_number="sco01", cliente="hisense"} 1
|
||||
sco_registered{sucursal="caguas", sco_number="sco04", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="caguas", sco_number="sco05", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="caguas", sco_number="sco06", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="caguas", sco_number="sco07", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="caguas", sco_number="sco08", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="caguas", sco_number="sco09", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="cayey", sco_number="sco01", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="cayey", sco_number="sco02", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="cayey", sco_number="sco03", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="cayey", sco_number="sco04", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="deleste-humacao", sco_number="sco01", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="deleste-humacao", sco_number="sco02", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="fajardo", sco_number="sco07", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="fajardo", sco_number="sco08", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="fajardo", sco_number="sco09", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="fajardo", sco_number="sco10", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="fajardo", sco_number="sco11", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="fajardo", sco_number="sco12", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="gurabo", sco_number="sco03", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="gurabo", sco_number="sco04", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="gurabo", sco_number="sco05", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="gurabo", sco_number="sco06", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="humacao", sco_number="sco07", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="humacao", sco_number="sco08", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="humacao", sco_number="sco09", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="humacao", sco_number="sco10", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="humacao", sco_number="sco11", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="humacao", sco_number="sco12", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="juntos", sco_number="sco04", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="juntos", sco_number="sco05", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="juntos", sco_number="sco06", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="juntos", sco_number="sco07", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="las-piedras", sco_number="sco08", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="las-piedras", sco_number="sco09", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="las-piedras", sco_number="sco10", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="las-piedras", sco_number="sco11", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="montehiedra", sco_number="sco13", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="montehiedra", sco_number="sco14", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="montehiedra", sco_number="sco15", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="montehiedra", sco_number="sco16", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="montehiedra", sco_number="sco17", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="montehiedra", sco_number="sco18", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="montehiedra", sco_number="sco19", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="montehiedra", sco_number="sco20", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="naguabo", sco_number="sco04", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="naguabo", sco_number="sco05", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="naguabo", sco_number="sco06", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="naguabo", sco_number="sco07", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="yabucoa", sco_number="sco02", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="yabucoa", sco_number="sco03", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="yabucoa", sco_number="sco04", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="yabucoa", sco_number="sco05", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="yabucoa", sco_number="sco06", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="yabucoa", sco_number="sco07", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="san-lorenzo", sco_number="sco03", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="san-lorenzo", sco_number="sco04", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="san-lorenzo", sco_number="sco05", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="san-lorenzo", sco_number="sco06", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="rio-grande", sco_number="sco01", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="rio-grande", sco_number="sco02", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="rio-grande", sco_number="sco03", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="rio-grande", sco_number="sco04", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="rio-grande", sco_number="sco05", cliente="mbs-ralphs"} 1
|
||||
sco_registered{sucursal="rio-grande", sco_number="sco06", cliente="mbs-ralphs"} 1
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'prometheus-central'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
- job_name: 'server-central-node-exporter'
|
||||
static_configs:
|
||||
- targets: ['node-exporter:9100']
|
||||
@@ -0,0 +1,197 @@
|
||||
services:
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: minio
|
||||
command: server /data --console-address ":9001"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: LIGTNbUgQr
|
||||
ports:
|
||||
- "9000:9000" # API
|
||||
- "9001:9001" # Consola web
|
||||
volumes:
|
||||
- ./data/minio/data:/data
|
||||
#- ./data/certs:/root/.minio/certs:ro
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
jenkins:
|
||||
build:
|
||||
context: ./data/jenkins
|
||||
dockerfile: Dockerfile
|
||||
container_name: jenkins-server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- JAVA_OPTS=-Djenkins.install.runSetupWizard=false
|
||||
expose:
|
||||
- "8080"
|
||||
volumes:
|
||||
- ./data/jenkins_home:/var/jenkins_home
|
||||
- ./data/jenkins/jenkins.yaml:/usr/share/jenkins/ref/jenkins.yaml:ro
|
||||
- ./data/jenkins/deploy:/opt/deploy:ro
|
||||
- ./data/jenkins/history:/opt/history:ro
|
||||
- ./data/jenkins/pipelines/Jenkinsfile.sco:/opt/jenkins/Jenkinsfile.sco:ro
|
||||
- ./data/jenkins/pipelines/Jenkinsfile.sco.reports:/opt/jenkins/Jenkinsfile.sco.reports:ro
|
||||
- ./data/jenkins/pipelines/Jenkinsfile.sco.reports.history:/opt/jenkins/Jenkinsfile.sco.reports.history:ro
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
npm:
|
||||
image: jc21/nginx-proxy-manager:latest
|
||||
container_name: nginx-proxy-manager
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "81:81"
|
||||
volumes:
|
||||
- ./data/npm/data:/data
|
||||
- ./data/npm/letsencrypt:/etc/letsencrypt
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
prometheus-central:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus_central
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
# Archivo de configuracion alojado en data
|
||||
- ./data/prometheus/config/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
# Carpeta donde se guarda el historial de metricas
|
||||
- ./data/prometheus/data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.enable-remote-write-receiver'
|
||||
- '--query.lookback-delta=2m'
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
prom-label-proxy:
|
||||
image: quay.io/prometheuscommunity/prom-label-proxy:latest
|
||||
container_name: prom-label-proxy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8081:8080"
|
||||
command:
|
||||
- "-insecure-listen-address=0.0.0.0:8080"
|
||||
- "-upstream=http://prometheus-central:9090"
|
||||
- "-label=cliente"
|
||||
- "-enable-label-apis"
|
||||
depends_on:
|
||||
- prometheus-central
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:13.1.1
|
||||
container_name: grafana_central
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SERVER_DOMAIN=laoficina1782.com
|
||||
- GF_SESSION_COOKIE_DOMAIN=.laoficina1782.com
|
||||
- GF_SECURITY_ALLOW_EMBEDDING=true
|
||||
- GF_SECURITY_COOKIE_SAMESITE=none
|
||||
- GF_SECURITY_COOKIE_SECURE=true
|
||||
- GF_DATAPROXY_CONCURRENT_QUERIES=4
|
||||
- GF_DATAPROXY_TIMEOUT=900
|
||||
- GF_DATAPROXY_DIAL_TIMEOUT=900
|
||||
- GF_DATAPROXY_KEEPALIVE=900
|
||||
volumes:
|
||||
- ./data/grafana/data:/var/lib/grafana
|
||||
depends_on:
|
||||
- prometheus-central
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
node-exporter:
|
||||
image: prom/node-exporter:latest
|
||||
container_name: node-exporter
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- proxy
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/rootfs:ro
|
||||
- ./data/node-exporter/textfile_inventory:/etc/prometheus/textfile_inventory
|
||||
command:
|
||||
- '--path.procfs=/host/proc'
|
||||
- '--path.sysfs=/host/sys'
|
||||
- '--path.rootfs=/rootfs'
|
||||
- '--collector.textfile.directory=/etc/prometheus/textfile_inventory'
|
||||
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
container_name: cloudflared
|
||||
restart: unless-stopped
|
||||
command: tunnel --no-autoupdate run
|
||||
environment:
|
||||
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
mysql-db:
|
||||
image: mysql:8.0
|
||||
container_name: mysql-db
|
||||
restart: unless-stopped
|
||||
# sin exponer puertos, lo maneja ng
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: sco_telemetry
|
||||
MYSQL_USER: ${MYSQL_USER}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
volumes:
|
||||
- ./data/mysql/data:/var/lib/mysql
|
||||
# script de inicializacion
|
||||
- ./data/mysql/init:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
central-api:
|
||||
build:
|
||||
# Compilamos desde la estructura data/api-central
|
||||
context: ./data/central-api
|
||||
dockerfile: Dockerfile
|
||||
container_name: central-api
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "5000"
|
||||
environment:
|
||||
- DB_HOST=mysql-db
|
||||
- DB_PORT=3306
|
||||
- DB_USER=${MYSQL_USER}
|
||||
- DB_PASSWORD=${MYSQL_PASSWORD}
|
||||
- DB_NAME=sco_telemetry
|
||||
- ALERTS_LOG_FILE=/data/alerts/alerts.txt
|
||||
volumes:
|
||||
- ./data/alerts:/data/alerts
|
||||
depends_on:
|
||||
mysql-db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
# interfaz UI para probar registros
|
||||
adminer:
|
||||
image: adminer
|
||||
container_name: adminer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,150 @@
|
||||
# Backup de dashboards de Grafana
|
||||
|
||||
Este servidor corre Grafana en `http://localhost:3000` (puerto expuesto: 3000).
|
||||
El backup se hace vía la API HTTP de Grafana (no toca los volúmenes/DB de Docker
|
||||
directamente), exportando cada dashboard como JSON.
|
||||
|
||||
## Archivos involucrados
|
||||
|
||||
| Ruta | Qué es |
|
||||
|----------------------------------------------------------------|-------------------------------------------|
|
||||
| `/opt/server/scripts/backup-grafana-dashboards.sh` | Script de backup (root:root, permisos 700 porque contiene la contraseña de Grafana) |
|
||||
| `/opt/server/backups/grafana_dashboards/` | Carpeta con los `.tar.gz` generados |
|
||||
|
||||
**No está agregado a crontab** (a diferencia de `backup-jenkins-home.sh`). Se
|
||||
ejecuta manualmente con `sudo` cuando se necesite.
|
||||
|
||||
## Cómo correr el backup manualmente
|
||||
|
||||
```bash
|
||||
sudo /opt/server/scripts/backup-grafana-dashboards.sh
|
||||
```
|
||||
|
||||
Esto genera `/opt/server/backups/grafana_dashboards/grafana_dashboards_YYYYMMDD.tar.gz`
|
||||
y conserva solo los últimos 4 backups (los más viejos se borran automáticamente,
|
||||
`KEEP=4` dentro del script).
|
||||
|
||||
### Convención de nombres dentro del tar.gz
|
||||
|
||||
Cada dashboard se guarda como:
|
||||
|
||||
```
|
||||
<título_sanitizado>__<uid>.json
|
||||
```
|
||||
|
||||
Ejemplo:
|
||||
|
||||
```
|
||||
Monitoreo__adpjcx6.json
|
||||
Node_Exporter_Full__rYdddlPWk.json
|
||||
Node_Exporter_Full_completo__rYdddlPWk3.json
|
||||
MySQL_Overview__MQWgroiiz.json
|
||||
```
|
||||
|
||||
El título se sanitiza (solo `A-Za-z0-9`, resto reemplazado por `_`), y el `uid`
|
||||
real de Grafana queda al final por si dos títulos sanitizados coinciden.
|
||||
|
||||
### Ver contenido de un backup sin extraerlo
|
||||
|
||||
```bash
|
||||
sudo tar tzf /opt/server/backups/grafana_dashboards/grafana_dashboards_<fecha>.tar.gz
|
||||
```
|
||||
|
||||
## Cómo restaurar un dashboard
|
||||
|
||||
Cada archivo `.json` dentro del tar tiene esta forma (respuesta cruda de la API
|
||||
`GET /api/dashboards/uid/{uid}`):
|
||||
|
||||
```json
|
||||
{
|
||||
"meta": { ... },
|
||||
"dashboard": { ... } <- esto es lo que Grafana necesita para importar
|
||||
}
|
||||
```
|
||||
|
||||
**IMPORTANTE:** para importar, Grafana necesita únicamente el contenido del
|
||||
campo `dashboard`, sin el wrapper `meta`. Si se sube/pega el JSON completo
|
||||
(`meta` + `dashboard`) da el error:
|
||||
|
||||
```
|
||||
Invalid or unknown dashboard schema
|
||||
```
|
||||
|
||||
### Opción A — Restaurar vía API (rápido, scripteable)
|
||||
|
||||
```bash
|
||||
sudo bash -c '
|
||||
LATEST=$(ls -1t /opt/server/backups/grafana_dashboards/grafana_dashboards_*.tar.gz | head -1)
|
||||
mkdir -p /tmp/restore
|
||||
tar xzf "$LATEST" -C /tmp/restore ./<archivo>.json
|
||||
|
||||
jq "{dashboard: (.dashboard + {id: null}), overwrite: true}" \
|
||||
/tmp/restore/<archivo>.json > /tmp/restore/payload.json
|
||||
|
||||
curl -sf -X POST -H "Content-Type: application/json" \
|
||||
-u admin:<password_de_grafana> \
|
||||
http://localhost:3000/api/dashboards/db \
|
||||
-d @/tmp/restore/payload.json
|
||||
'
|
||||
```
|
||||
|
||||
Notas:
|
||||
- `id: null` — el `id` interno viejo puede no existir más (ej. si se borró el
|
||||
dashboard); ponerlo en `null` hace que Grafana cree uno nuevo.
|
||||
- El `uid` se mantiene igual, así el dashboard vuelve con la misma URL que tenía.
|
||||
- `overwrite: true` evita el error 412 si el uid ya existiera.
|
||||
- Respuesta esperada: `{"status":"success", "uid": "...", "url": "...", ...}`.
|
||||
- Limpiar `/tmp/restore` después (`sudo rm -rf /tmp/restore`).
|
||||
|
||||
### Opción B — Restaurar a mano desde la UI
|
||||
|
||||
1. Extraer solo el campo `dashboard` (sin el wrapper `meta`):
|
||||
|
||||
```bash
|
||||
sudo bash -c '
|
||||
LATEST=$(ls -1t /opt/server/backups/grafana_dashboards/grafana_dashboards_*.tar.gz | head -1)
|
||||
mkdir -p /tmp/restore
|
||||
tar xzf "$LATEST" -C /tmp/restore ./<archivo>.json
|
||||
jq ".dashboard" /tmp/restore/<archivo>.json > /tmp/restore/import.json
|
||||
'
|
||||
```
|
||||
|
||||
2. Bajar `/tmp/restore/import.json` a tu equipo si vas a subirlo como archivo
|
||||
(`scp jenkins-srv:/tmp/restore/import.json .`), o simplemente abrirlo con
|
||||
`cat`/editor para copiar el contenido.
|
||||
|
||||
3. En la UI de Grafana (`http://172.19.13.204:3000`):
|
||||
**Dashboards → New → Import → Upload JSON file** (o pegar el contenido en
|
||||
"Import via panel json").
|
||||
|
||||
4. Confirmar el nombre/folder propuesto y click **Import**.
|
||||
|
||||
5. Si Grafana avisa que el `uid` ya existe, ofrece la opción de sobrescribir
|
||||
en el mismo diálogo.
|
||||
|
||||
6. Limpiar `/tmp/restore` cuando termines.
|
||||
|
||||
## Encontrar rápido el uid/título de un dashboard sin extraer todo el tar
|
||||
|
||||
```bash
|
||||
for f in $(tar tzf <backup>.tar.gz | grep '\.json$'); do
|
||||
echo -n "$f -> "
|
||||
tar xzOf <backup>.tar.gz "$f" | jq -r '.dashboard.title'
|
||||
done
|
||||
```
|
||||
|
||||
(Con la convención de nombres actual esto ya no es necesario para uso normal,
|
||||
pero sirve como referencia si en algún momento se cambia el formato del script.)
|
||||
|
||||
## Credenciales
|
||||
|
||||
El script usa el usuario admin de Grafana (`admin`) y su contraseña, embebidas
|
||||
en `/opt/server/scripts/backup-grafana-dashboards.sh`. Por eso el script tiene
|
||||
permisos `700` (solo root puede leerlo/ejecutarlo), a diferencia de otros
|
||||
scripts en `/opt/server/scripts/` que son `755`.
|
||||
|
||||
## Probado el (2026-08-05)
|
||||
|
||||
Ciclo completo validado: crear dashboard de prueba → backup → borrar dashboard
|
||||
→ restaurar (API y manual) → verificar que el contenido vuelve idéntico. Ambos
|
||||
métodos de restauración funcionan.
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
GRAFANA_URL=http://localhost:3000
|
||||
GRAFANA_USER=admin
|
||||
GRAFANA_PASS=holamundo
|
||||
BACKUP_DIR=/opt/server/backups/grafana_dashboards
|
||||
KEEP=4
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
STAMP=$(date +%Y%m%d)
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
curl -sf -u "$GRAFANA_USER:$GRAFANA_PASS" "$GRAFANA_URL/api/search?type=dash-db" \
|
||||
| jq -r '.[].uid' \
|
||||
| while read -r uid; do
|
||||
dashboard_json=$(curl -sf -u "$GRAFANA_USER:$GRAFANA_PASS" "$GRAFANA_URL/api/dashboards/uid/$uid")
|
||||
title=$(jq -r '.dashboard.title' <<< "$dashboard_json")
|
||||
safe_title=$(echo "$title" | tr -c 'A-Za-z0-9' '_' | sed 's/_\+/_/g; s/^_//; s/_$//')
|
||||
echo "$dashboard_json" > "$TMP_DIR/${safe_title}__${uid}.json"
|
||||
done
|
||||
|
||||
tar czf "$BACKUP_DIR/grafana_dashboards_$STAMP.tar.gz" -C "$TMP_DIR" .
|
||||
|
||||
ls -1t "$BACKUP_DIR"/grafana_dashboards_*.tar.gz | tail -n +$((KEEP + 1)) | xargs -r rm --
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SRC_DIR=/opt/server/data
|
||||
BACKUP_DIR=/opt/server/backups/jenkins_home
|
||||
KEEP=4
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
STAMP=$(date +%Y%m%d)
|
||||
tar czf "$BACKUP_DIR/jenkins_home_$STAMP.tar.gz" -C "$SRC_DIR" jenkins_home
|
||||
|
||||
ls -1t "$BACKUP_DIR"/jenkins_home_*.tar.gz | tail -n +$((KEEP + 1)) | xargs -r rm --
|
||||
Reference in New Issue
Block a user