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:
ARCHITECT
2025-12-24 09:31:46 +00:00
parent cad1163cd8
commit 1c3eace6bc
8 changed files with 391 additions and 28 deletions

11
.env.example Normal file
View 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
View File

@@ -0,0 +1,5 @@
.env
__pycache__/
*.pyc
*.pyo
.h_instancia

14
Dockerfile Normal file
View 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"]

View File

@@ -1,7 +1,6 @@
# MARGARET
![Estado](https://img.shields.io/badge/Estado-PLANIFICADO-yellow)
![Estado](https://img.shields.io/badge/Estado-IMPLEMENTADO-brightgreen)
**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
View 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
View 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
View 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
View File

@@ -0,0 +1,4 @@
flask==3.0.0
gunicorn==21.2.0
psycopg2-binary==2.9.9
boto3==1.34.0