This commit is contained in:
0
app/automatizados/__init__.py
Normal file
0
app/automatizados/__init__.py
Normal file
220
app/automatizados/acciones.py
Normal file
220
app/automatizados/acciones.py
Normal file
@@ -0,0 +1,220 @@
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from datetime import datetime
|
||||
|
||||
from django.db import connection
|
||||
from django.utils import timezone
|
||||
from common.utils import clean_sql_string, clean_sql_int
|
||||
|
||||
|
||||
# =========================================================
|
||||
# Helper HTTP (stdlib) — evita dependencia extra en requirements
|
||||
# =========================================================
|
||||
def _http_get_json(url, timeout=8):
|
||||
"""GET a un servicio externo que responde JSON. Usa stdlib para no añadir deps."""
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'django-core-base/1.0'})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as response:
|
||||
raw = response.read().decode('utf-8')
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return {'raw': raw}
|
||||
|
||||
|
||||
def _http_get_text(url, timeout=8):
|
||||
"""GET a un servicio externo que responde texto plano."""
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'django-core-base/1.0'})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as response:
|
||||
return response.read().decode('utf-8').strip()
|
||||
|
||||
|
||||
# =========================================================
|
||||
# ACCIÓN 1 — Base de datos (INSERT histórico de ejecución)
|
||||
# set_parameterized
|
||||
# =========================================================
|
||||
def setEjecucion(params):
|
||||
"""
|
||||
Inserta una fila en automatizacion_ejecuciones registrando una ejecución.
|
||||
params esperados: nombre, descripcion, estado, origen, resultado, error
|
||||
"""
|
||||
query = """
|
||||
INSERT INTO automatizacion_ejecuciones
|
||||
(nombre, descripcion, estado, origen, resultado, error, fecha_inicio, fecha_fin, activo)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
"""
|
||||
|
||||
ahora = timezone.now()
|
||||
resultado_json = json.dumps(params.get('resultado')) if params.get('resultado') is not None else None
|
||||
|
||||
parameter_dict = [
|
||||
clean_sql_string(params.get('nombre')),
|
||||
clean_sql_string(params.get('descripcion')),
|
||||
clean_sql_string(params.get('estado') or 'ok'),
|
||||
clean_sql_string(params.get('origen') or 'manual'),
|
||||
resultado_json,
|
||||
clean_sql_string(params.get('error')) if params.get('error') else None,
|
||||
ahora,
|
||||
ahora,
|
||||
bool(params.get('activo', True)),
|
||||
]
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(query, parameter_dict)
|
||||
row = cursor.fetchone()
|
||||
nuevo_id = row[0] if row else None
|
||||
|
||||
return {'id': nuevo_id, 'fecha': ahora.isoformat()}
|
||||
|
||||
|
||||
# =========================================================
|
||||
# ACCIÓN 2 — Servicio externo: Cat Fact API
|
||||
# https://catfact.ninja/fact (gratis, sin auth)
|
||||
# =========================================================
|
||||
def getCatFact():
|
||||
url = 'https://catfact.ninja/fact'
|
||||
data = _http_get_json(url)
|
||||
return {
|
||||
'servicio': 'catfact.ninja',
|
||||
'url': url,
|
||||
'fact': data.get('fact'),
|
||||
'length': data.get('length'),
|
||||
}
|
||||
|
||||
|
||||
# =========================================================
|
||||
# ACCIÓN 3 — Servicio externo: GitHub Zen
|
||||
# https://api.github.com/zen (gratis, sin auth)
|
||||
# =========================================================
|
||||
def getGithubZen():
|
||||
url = 'https://api.github.com/zen'
|
||||
texto = _http_get_text(url)
|
||||
return {
|
||||
'servicio': 'api.github.com/zen',
|
||||
'url': url,
|
||||
'zen': texto,
|
||||
}
|
||||
|
||||
|
||||
# =========================================================
|
||||
# Orquestador — ejecuta las 3 acciones en secuencia
|
||||
# =========================================================
|
||||
def ejecutarAutomatizaciones(params):
|
||||
"""
|
||||
Ejecuta las 3 acciones automatizadas:
|
||||
1) Llamada a servicio externo: catfact.ninja
|
||||
2) Llamada a servicio externo: api.github.com/zen
|
||||
3) Persistencia en BD: inserta la ejecución en automatizacion_ejecuciones
|
||||
"""
|
||||
resultados = {'acciones': []}
|
||||
errores = []
|
||||
|
||||
# Acción 2 — catfact
|
||||
try:
|
||||
resultados['acciones'].append({'step': 1, 'ok': True, 'data': getCatFact()})
|
||||
except Exception as err:
|
||||
errores.append(f'catfact: {err}')
|
||||
resultados['acciones'].append({'step': 1, 'ok': False, 'error': str(err)})
|
||||
|
||||
# Acción 3 — github zen
|
||||
try:
|
||||
resultados['acciones'].append({'step': 2, 'ok': True, 'data': getGithubZen()})
|
||||
except Exception as err:
|
||||
errores.append(f'github_zen: {err}')
|
||||
resultados['acciones'].append({'step': 2, 'ok': False, 'error': str(err)})
|
||||
|
||||
# Acción 1 — persistir en BD
|
||||
estado_final = 'ok' if not errores else 'error'
|
||||
nombre = clean_sql_string(params.get('nombre')) or f'Ejecucion {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
|
||||
origen = clean_sql_string(params.get('origen')) or 'manual'
|
||||
|
||||
registro = setEjecucion({
|
||||
'nombre': nombre,
|
||||
'descripcion': params.get('descripcion') or 'Ejecución orquestada por /api/automatizados/ejecutar/',
|
||||
'estado': estado_final,
|
||||
'origen': origen,
|
||||
'resultado': resultados,
|
||||
'error': '; '.join(errores) if errores else None,
|
||||
'activo': True,
|
||||
})
|
||||
|
||||
resultados['acciones'].append({'step': 3, 'ok': True, 'data': registro})
|
||||
|
||||
return {
|
||||
'ejecucion_id': registro.get('id'),
|
||||
'estado': estado_final,
|
||||
'total_acciones': len(resultados['acciones']),
|
||||
'resultados': resultados,
|
||||
'errores': errores,
|
||||
}
|
||||
|
||||
|
||||
# =========================================================
|
||||
# Lectura — historial (get_parameterized)
|
||||
# =========================================================
|
||||
def getHistorial(params):
|
||||
"""
|
||||
Devuelve el histórico de ejecuciones. Permite filtrar por estado y limit.
|
||||
"""
|
||||
query = """
|
||||
SELECT id, nombre, descripcion, estado, origen, resultado, error,
|
||||
fecha_inicio, fecha_fin, activo
|
||||
FROM automatizacion_ejecuciones
|
||||
WHERE (%s = '' OR estado = %s)
|
||||
ORDER BY fecha_inicio DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
|
||||
estado = clean_sql_string(params.get('estado')) if params.get('estado') else ''
|
||||
limite = clean_sql_int(params.get('limit')) or 20
|
||||
|
||||
parameter_dict = [estado, estado, limite]
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(query, parameter_dict)
|
||||
columns = [col[0] for col in cursor.description]
|
||||
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||
|
||||
# Normalizamos el campo resultado (JSON serializado en SQLite)
|
||||
for row in rows:
|
||||
if isinstance(row.get('resultado'), str):
|
||||
try:
|
||||
row['resultado'] = json.loads(row['resultado'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return {'total': len(rows), 'data': rows}
|
||||
|
||||
|
||||
# =========================================================
|
||||
# Estado del módulo
|
||||
# =========================================================
|
||||
def getEstado():
|
||||
"""
|
||||
Devuelve el estado general del módulo de automatizaciones:
|
||||
cuántas ejecuciones hay, última ejecución, endpoints disponibles.
|
||||
"""
|
||||
query = """
|
||||
SELECT COUNT(*) AS total,
|
||||
SUM(CASE WHEN estado = 'ok' THEN 1 ELSE 0 END) AS ok,
|
||||
SUM(CASE WHEN estado = 'error' THEN 1 ELSE 0 END) AS errores,
|
||||
MAX(fecha_inicio) AS ultima_ejecucion
|
||||
FROM automatizacion_ejecuciones
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(query)
|
||||
columns = [col[0] for col in cursor.description]
|
||||
row = cursor.fetchone()
|
||||
resumen = dict(zip(columns, row)) if row else {}
|
||||
|
||||
return {
|
||||
'status': 'ok',
|
||||
'modulo': 'automatizados',
|
||||
'endpoints': [
|
||||
'POST /api/automatizados/ejecutar/',
|
||||
'POST /api/automatizados/historial/',
|
||||
'POST /api/automatizados/estado/',
|
||||
],
|
||||
'resumen': resumen,
|
||||
}
|
||||
6
app/automatizados/apps.py
Normal file
6
app/automatizados/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AutomatizadosConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'automatizados'
|
||||
47
app/automatizados/fixtures/semillas.json
Normal file
47
app/automatizados/fixtures/semillas.json
Normal file
@@ -0,0 +1,47 @@
|
||||
[
|
||||
{
|
||||
"model": "automatizados.automatizacionejecucion",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"nombre": "Ejecución inicial de prueba",
|
||||
"descripcion": "Seed de ejemplo para poblar la tabla de ejecuciones automatizadas",
|
||||
"estado": "ok",
|
||||
"origen": "seed",
|
||||
"resultado": {"acciones": 0, "detalle": "fixture de arranque"},
|
||||
"error": null,
|
||||
"fecha_inicio": "2026-04-16T10:00:00Z",
|
||||
"fecha_fin": "2026-04-16T10:00:05Z",
|
||||
"activo": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "automatizados.automatizacionejecucion",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"nombre": "Chequeo nocturno",
|
||||
"descripcion": "Ejemplo de ejecución recurrente programada",
|
||||
"estado": "ok",
|
||||
"origen": "jenkins",
|
||||
"resultado": {"acciones": 3, "detalle": "todas las llamadas OK"},
|
||||
"error": null,
|
||||
"fecha_inicio": "2026-04-16T03:00:00Z",
|
||||
"fecha_fin": "2026-04-16T03:00:12Z",
|
||||
"activo": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "automatizados.automatizacionejecucion",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"nombre": "Ejecución con error simulado",
|
||||
"descripcion": "Registro de referencia para estado=error",
|
||||
"estado": "error",
|
||||
"origen": "manual",
|
||||
"resultado": null,
|
||||
"error": "timeout al llamar servicio externo",
|
||||
"fecha_inicio": "2026-04-15T18:30:00Z",
|
||||
"fecha_fin": "2026-04-15T18:30:08Z",
|
||||
"activo": false
|
||||
}
|
||||
}
|
||||
]
|
||||
0
app/automatizados/migrations/__init__.py
Normal file
0
app/automatizados/migrations/__init__.py
Normal file
25
app/automatizados/models.py
Normal file
25
app/automatizados/models.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AutomatizacionEjecucion(models.Model):
|
||||
"""
|
||||
Registro histórico de ejecuciones automatizadas.
|
||||
Cada vez que el endpoint `ejecutar/` corre, se guarda una fila con el
|
||||
resultado consolidado de las acciones ejecutadas.
|
||||
"""
|
||||
nombre = models.CharField(max_length=255)
|
||||
descripcion = models.TextField(null=True, blank=True)
|
||||
estado = models.CharField(max_length=50, default='pendiente') # pendiente | ok | error
|
||||
origen = models.CharField(max_length=100, default='manual') # manual | jenkins | cron
|
||||
resultado = models.JSONField(null=True, blank=True)
|
||||
error = models.TextField(null=True, blank=True)
|
||||
fecha_inicio = models.DateTimeField(auto_now_add=True)
|
||||
fecha_fin = models.DateTimeField(null=True, blank=True)
|
||||
activo = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'automatizacion_ejecuciones'
|
||||
ordering = ['-fecha_inicio']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.nombre} [{self.estado}]'
|
||||
8
app/automatizados/urls.py
Normal file
8
app/automatizados/urls.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.urls import path
|
||||
from .views import AutomatizadosEjecutar, AutomatizadosHistorial, AutomatizadosEstado
|
||||
|
||||
urlpatterns = [
|
||||
path('ejecutar/', AutomatizadosEjecutar.as_view(), name='automatizados_ejecutar'),
|
||||
path('historial/', AutomatizadosHistorial.as_view(), name='automatizados_historial'),
|
||||
path('estado/', AutomatizadosEstado.as_view(), name='automatizados_estado'),
|
||||
]
|
||||
132
app/automatizados/views.py
Normal file
132
app/automatizados/views.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from django.http import JsonResponse
|
||||
|
||||
from general.utilidades.acciones import LogService
|
||||
from .acciones import ejecutarAutomatizaciones, getHistorial, getEstado
|
||||
|
||||
|
||||
class AutomatizadosEjecutar(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
path = '/automatizados/ejecutar/'
|
||||
|
||||
# --- BLOQUE 1: Inicio Log ---
|
||||
log_id = LogService.gestionar_log(self, request, path=path)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 2: Data Cleaning ---
|
||||
data = request.data
|
||||
status = 100
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
|
||||
|
||||
params = {
|
||||
'nombre': data.get('nombre'),
|
||||
'descripcion': data.get('descripcion'),
|
||||
'origen': data.get('origen') or 'manual',
|
||||
}
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
|
||||
status = 400
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 3: Action Call ---
|
||||
resultado = ejecutarAutomatizaciones(params)
|
||||
response = resultado
|
||||
status = 200
|
||||
# --- BLOQUE 4: Cierre Log ---
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, safe=False, status=status)
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
|
||||
status = 500
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
|
||||
|
||||
class AutomatizadosHistorial(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
path = '/automatizados/historial/'
|
||||
|
||||
# --- BLOQUE 1: Inicio Log ---
|
||||
log_id = LogService.gestionar_log(self, request, path=path)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 2: Data Cleaning ---
|
||||
data = request.data
|
||||
status = 100
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
|
||||
|
||||
params = {
|
||||
'estado': data.get('estado'),
|
||||
'limit': data.get('limit') or 20,
|
||||
}
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
|
||||
status = 400
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 3: Action Call ---
|
||||
resultado = getHistorial(params)
|
||||
response = resultado
|
||||
status = 200
|
||||
# --- BLOQUE 4: Cierre Log ---
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, safe=False, status=status)
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
|
||||
status = 500
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
|
||||
|
||||
class AutomatizadosEstado(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
path = '/automatizados/estado/'
|
||||
|
||||
# --- BLOQUE 1: Inicio Log ---
|
||||
log_id = LogService.gestionar_log(self, request, path=path)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 2: Data Cleaning ---
|
||||
data = request.data
|
||||
status = 100
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
|
||||
status = 400
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 3: Action Call ---
|
||||
resultado = getEstado()
|
||||
response = resultado
|
||||
status = 200
|
||||
# --- BLOQUE 4: Cierre Log ---
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, safe=False, status=status)
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
|
||||
status = 500
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
Reference in New Issue
Block a user