Jenkins + Minio + Prometheus + Grafana v1

This commit is contained in:
2026-09-01 13:42:14 +00:00
commit 05a197b0d0
21 changed files with 2256 additions and 0 deletions
+15
View File
@@ -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"}
+14
View File
@@ -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"]
+660
View File
@@ -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)
+7
View File
@@ -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
+18
View File
@@ -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
+82
View File
@@ -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
+105
View File
@@ -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 &
+9
View File
@@ -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
+617
View File
@@ -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)
}
}
}
+70
View File
@@ -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'
}
}
}
}
}
}
}
+28
View File
@@ -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
+11
View File
@@ -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']