Implementar MARGARET - Log de entrada CORP
- app.py: API Flask con endpoints /health, /ingest, /query, /list - Dockerfile y docker-compose.yml para despliegue - init.sql para crear tabla margaret_log - Autenticacion via X-Auth-Key (h_instancia) - Almacenamiento en R2 y PostgreSQL Desplegado en CORP (92.112.181.188:5051)
This commit is contained in:
11
.env.example
Normal file
11
.env.example
Normal file
@@ -0,0 +1,11 @@
|
||||
H_INSTANCIA=your_h_instancia_here
|
||||
DB_HOST=172.17.0.1
|
||||
DB_PORT=5432
|
||||
DB_NAME=corp
|
||||
DB_USER=corp
|
||||
DB_PASSWORD=your_password
|
||||
R2_ENDPOINT=https://your-account.r2.cloudflarestorage.com
|
||||
R2_ACCESS_KEY=your_access_key
|
||||
R2_SECRET_KEY=your_secret_key
|
||||
R2_BUCKET=corp
|
||||
PORT=5051
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.h_instancia
|
||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app.py .
|
||||
|
||||
EXPOSE 5051
|
||||
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5051", "--workers", "2", "--timeout", "120", "app:app"]
|
||||
81
README.md
81
README.md
@@ -1,7 +1,6 @@
|
||||
# MARGARET
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**Log de entrada CORP - Sistema TZZR**
|
||||
|
||||
@@ -9,46 +8,72 @@
|
||||
|
||||
Secretaria de entrada para CORP (servidor empresarial). Variante de CLARA con funcionalidades adicionales.
|
||||
|
||||
## Posición en el Flujo
|
||||
## Posicion en el Flujo
|
||||
|
||||
```
|
||||
PACKET (App) ──► MARGARET ──► MASON ──► FELDMAN
|
||||
│
|
||||
└──► R2 (archivos)
|
||||
PACKET (App) --> MARGARET --> MASON --> FELDMAN
|
||||
|
|
||||
└--> R2 (archivos)
|
||||
```
|
||||
|
||||
## Comparación
|
||||
## Endpoints
|
||||
|
||||
| Metodo | Ruta | Descripcion |
|
||||
|--------|------|-------------|
|
||||
| GET | `/health` | Estado del servicio |
|
||||
| POST | `/ingest` | Recibir contenedor |
|
||||
| GET | `/query/<h_entrada>` | Consultar por hash |
|
||||
| GET | `/list` | Listar entradas |
|
||||
|
||||
## Autenticacion
|
||||
|
||||
Todas las rutas (excepto `/health`) requieren:
|
||||
```
|
||||
X-Auth-Key: {h_instancia}
|
||||
```
|
||||
|
||||
## Despliegue
|
||||
|
||||
```bash
|
||||
# En CORP
|
||||
cd /opt/margaret
|
||||
cp .env.example .env
|
||||
# Editar .env con credenciales
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Configuracion
|
||||
|
||||
Ver `.env.example` para variables requeridas:
|
||||
- `H_INSTANCIA`: Hash unico de CORP
|
||||
- `DB_*`: Credenciales PostgreSQL
|
||||
- `R2_*`: Credenciales Cloudflare R2
|
||||
|
||||
## Base de Datos
|
||||
|
||||
Ejecutar `init.sql` en PostgreSQL:
|
||||
```bash
|
||||
sudo -u postgres psql -d corp -f init.sql
|
||||
```
|
||||
|
||||
## Comparacion
|
||||
|
||||
| Aspecto | CLARA (DECK) | MARGARET (CORP) |
|
||||
|---------|--------------|-----------------|
|
||||
| Servidor | Personal | Empresarial |
|
||||
| Log | Inmutable | Inmutable |
|
||||
| Extras | - | + NOTARIO (certificación) |
|
||||
| Puerto | 5051 | 5051 |
|
||||
| Bucket R2 | deck | corp |
|
||||
|
||||
## Función
|
||||
## Funcion
|
||||
|
||||
1. Recibe contenedor de PACKET
|
||||
2. Envía archivos a R2
|
||||
3. Registra metadata + ubicación R2
|
||||
4. **NO añade información**
|
||||
2. Envia archivos a R2
|
||||
3. Registra metadata + ubicacion R2
|
||||
4. **NO agrega informacion**
|
||||
5. **NO procesa**
|
||||
6. **NO modifica**
|
||||
|
||||
## Identificador
|
||||
|
||||
```
|
||||
h_instancia = SHA-256(seed único de CORP)
|
||||
```
|
||||
|
||||
Mismo hash para:
|
||||
- Autenticación (`X-Auth-Key`)
|
||||
- Biblioteca privada
|
||||
- Prefijo R2
|
||||
|
||||
## Arquitectura
|
||||
|
||||
Ver documentación completa en [contratos-comunes/architecture](https://git.tzzr.me/tzzr/contratos-comunes/src/branch/main/architecture/01-clara-margaret.md)
|
||||
|
||||
---
|
||||
|
||||
*Componente del sistema TZZR*
|
||||
*Componente del sistema TZZR - Implementado 2025-12-24*
|
||||
|
||||
254
app.py
Normal file
254
app.py
Normal file
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MARGARET - Log de entrada CORP
|
||||
Servicio inmutable para recibir datos de PACKET
|
||||
"""
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime
|
||||
from flask import Flask, request, jsonify
|
||||
import psycopg2
|
||||
from psycopg2.extras import Json
|
||||
import boto3
|
||||
from botocore.client import Config
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Configuración desde variables de entorno
|
||||
H_INSTANCIA = os.getenv('H_INSTANCIA')
|
||||
DB_HOST = os.getenv('DB_HOST', 'localhost')
|
||||
DB_PORT = os.getenv('DB_PORT', '5432')
|
||||
DB_NAME = os.getenv('DB_NAME', 'corp')
|
||||
DB_USER = os.getenv('DB_USER', 'postgres')
|
||||
DB_PASSWORD = os.getenv('DB_PASSWORD')
|
||||
|
||||
# Cloudflare R2
|
||||
R2_ENDPOINT = os.getenv('R2_ENDPOINT')
|
||||
R2_ACCESS_KEY = os.getenv('R2_ACCESS_KEY')
|
||||
R2_SECRET_KEY = os.getenv('R2_SECRET_KEY')
|
||||
R2_BUCKET = os.getenv('R2_BUCKET', 'corp')
|
||||
|
||||
# Cliente S3 (compatible con R2)
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
endpoint_url=R2_ENDPOINT,
|
||||
aws_access_key_id=R2_ACCESS_KEY,
|
||||
aws_secret_access_key=R2_SECRET_KEY,
|
||||
config=Config(signature_version='s3v4'),
|
||||
region_name='auto'
|
||||
)
|
||||
|
||||
|
||||
def get_db_connection():
|
||||
"""Obtener conexión a PostgreSQL"""
|
||||
return psycopg2.connect(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
database=DB_NAME,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD
|
||||
)
|
||||
|
||||
|
||||
def verify_auth(request):
|
||||
"""Verificar autenticación mediante X-Auth-Key"""
|
||||
auth_key = request.headers.get('X-Auth-Key')
|
||||
if not auth_key or auth_key != H_INSTANCIA:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def upload_to_r2(file_data, file_path):
|
||||
"""Subir archivo a R2"""
|
||||
try:
|
||||
s3_client.put_object(
|
||||
Bucket=R2_BUCKET,
|
||||
Key=file_path,
|
||||
Body=file_data
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error subiendo a R2: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""Endpoint de salud"""
|
||||
return jsonify({
|
||||
"service": "margaret",
|
||||
"status": "ok",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
})
|
||||
|
||||
|
||||
@app.route('/ingest', methods=['POST'])
|
||||
def ingest():
|
||||
"""
|
||||
Endpoint principal para recibir contenedores de PACKET
|
||||
"""
|
||||
if not verify_auth(request):
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
|
||||
try:
|
||||
contenedor = request.get_json()
|
||||
|
||||
if not contenedor.get('id'):
|
||||
return jsonify({"error": "missing_id"}), 400
|
||||
|
||||
if not contenedor.get('archivo_hash'):
|
||||
return jsonify({"error": "missing_archivo_hash"}), 400
|
||||
|
||||
h_entrada = contenedor['archivo_hash']
|
||||
|
||||
conn = get_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute(
|
||||
"SELECT id FROM margaret_log WHERE h_entrada = %s",
|
||||
(h_entrada,)
|
||||
)
|
||||
|
||||
if cur.fetchone():
|
||||
cur.close()
|
||||
conn.close()
|
||||
return jsonify({"error": "hash_exists"}), 409
|
||||
|
||||
r2_paths = {}
|
||||
if 'archivos' in contenedor:
|
||||
for idx, archivo in enumerate(contenedor['archivos']):
|
||||
if 'data' in archivo:
|
||||
file_data = archivo['data']
|
||||
file_name = archivo.get('nombre', f'archivo_{idx}')
|
||||
r2_path = f"{H_INSTANCIA}/{h_entrada}/{file_name}"
|
||||
|
||||
if upload_to_r2(file_data, r2_path):
|
||||
r2_paths[file_name] = r2_path
|
||||
else:
|
||||
cur.close()
|
||||
conn.close()
|
||||
return jsonify({"error": "r2_upload_failed"}), 500
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO margaret_log (h_instancia, h_entrada, contenedor, r2_paths)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(H_INSTANCIA, h_entrada, Json(contenedor), Json(r2_paths))
|
||||
)
|
||||
|
||||
record_id = cur.fetchone()[0]
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
app.logger.info(f"Contenedor registrado: {record_id} - {h_entrada}")
|
||||
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"id": record_id,
|
||||
"h_entrada": h_entrada
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error en /ingest: {e}")
|
||||
return jsonify({"error": "internal_error", "detail": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/query/<h_entrada>', methods=['GET'])
|
||||
def query(h_entrada):
|
||||
if not verify_auth(request):
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, h_entrada, contenedor, r2_paths, created_at
|
||||
FROM margaret_log
|
||||
WHERE h_entrada = %s AND h_instancia = %s
|
||||
""",
|
||||
(h_entrada, H_INSTANCIA)
|
||||
)
|
||||
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if not row:
|
||||
return jsonify({"error": "not_found"}), 404
|
||||
|
||||
return jsonify({
|
||||
"id": row[0],
|
||||
"h_entrada": row[1],
|
||||
"contenedor": row[2],
|
||||
"r2_paths": row[3],
|
||||
"created_at": row[4].isoformat()
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error en /query: {e}")
|
||||
return jsonify({"error": "internal_error"}), 500
|
||||
|
||||
|
||||
@app.route('/list', methods=['GET'])
|
||||
def list_entries():
|
||||
if not verify_auth(request):
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
|
||||
try:
|
||||
limit = int(request.args.get('limit', 50))
|
||||
offset = int(request.args.get('offset', 0))
|
||||
|
||||
conn = get_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, h_entrada, created_at
|
||||
FROM margaret_log
|
||||
WHERE h_instancia = %s
|
||||
ORDER BY id DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
(H_INSTANCIA, limit, offset)
|
||||
)
|
||||
|
||||
rows = cur.fetchall()
|
||||
|
||||
cur.execute(
|
||||
"SELECT COUNT(*) FROM margaret_log WHERE h_instancia = %s",
|
||||
(H_INSTANCIA,)
|
||||
)
|
||||
total = cur.fetchone()[0]
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"entries": [
|
||||
{
|
||||
"id": row[0],
|
||||
"h_entrada": row[1],
|
||||
"created_at": row[2].isoformat()
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error en /list: {e}")
|
||||
return jsonify({"error": "internal_error"}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = int(os.getenv('PORT', 5051))
|
||||
app.run(host='0.0.0.0', port=port, debug=False)
|
||||
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
margaret:
|
||||
build: .
|
||||
container_name: margaret-service
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5051:5051"
|
||||
environment:
|
||||
- H_INSTANCIA=${H_INSTANCIA}
|
||||
- DB_HOST=${DB_HOST}
|
||||
- DB_PORT=${DB_PORT}
|
||||
- DB_NAME=${DB_NAME}
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- R2_ENDPOINT=${R2_ENDPOINT}
|
||||
- R2_ACCESS_KEY=${R2_ACCESS_KEY}
|
||||
- R2_SECRET_KEY=${R2_SECRET_KEY}
|
||||
- R2_BUCKET=${R2_BUCKET}
|
||||
- PORT=5051
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5051/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
23
init.sql
Normal file
23
init.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- MARGARET Log Table
|
||||
-- Deploy on CORP PostgreSQL
|
||||
|
||||
CREATE TABLE IF NOT EXISTS margaret_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
h_instancia VARCHAR(64) NOT NULL,
|
||||
h_entrada VARCHAR(64) NOT NULL,
|
||||
contenedor JSONB NOT NULL,
|
||||
r2_paths JSONB DEFAULT '{}',
|
||||
estado VARCHAR(20) DEFAULT 'recibido',
|
||||
procesado_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
CONSTRAINT margaret_log_h_entrada_unique UNIQUE (h_entrada)
|
||||
);
|
||||
|
||||
-- Grant permissions
|
||||
GRANT ALL PRIVILEGES ON TABLE margaret_log TO corp;
|
||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO corp;
|
||||
|
||||
-- Index for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_margaret_log_h_instancia ON margaret_log(h_instancia);
|
||||
CREATE INDEX IF NOT EXISTS idx_margaret_log_estado ON margaret_log(estado);
|
||||
CREATE INDEX IF NOT EXISTS idx_margaret_log_created_at ON margaret_log(created_at);
|
||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
flask==3.0.0
|
||||
gunicorn==21.2.0
|
||||
psycopg2-binary==2.9.9
|
||||
boto3==1.34.0
|
||||
Reference in New Issue
Block a user