- Add apps/ directory with modular components: - captain.py: Main orchestrator - corp/, deck/, devops/, docker/, hst/: Domain-specific apps - Fix duplicate logger handlers in long sessions - Add flush=True to print statements for real-time output Note: flow-ui, mindlink, tzzr-cli are separate repos (not included) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
344 lines
14 KiB
Python
344 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
TZZR CORP App - Gestión del servidor CORP (92.112.181.188)
|
|
Servicios: Agentes Margaret/Jared/Mason/Feldman, Context Manager, Apps
|
|
"""
|
|
import subprocess
|
|
import json
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from typing import List, Dict, Optional
|
|
from datetime import datetime
|
|
|
|
SERVER = "root@92.112.181.188"
|
|
SSH_KEY = "~/.ssh/tzzr"
|
|
|
|
# Servicios conocidos en CORP
|
|
SERVICES = {
|
|
# Microservicios TZZR
|
|
"margaret": {"port": 5051, "type": "service", "desc": "Recepción de datos", "path": "/opt/margaret"},
|
|
"jared": {"port": 5052, "type": "service", "desc": "Automatización de flujos", "path": "/opt/jared"},
|
|
"mason": {"port": 5053, "type": "service", "desc": "Espacio de enriquecimiento", "path": "/opt/mason"},
|
|
"feldman": {"port": 5054, "type": "service", "desc": "Registro final + Merkle", "path": "/opt/feldman"},
|
|
|
|
# Aplicaciones
|
|
"nextcloud": {"port": 8080, "type": "app", "desc": "Cloud storage", "url": "nextcloud.tzzrcorp.me"},
|
|
"directus": {"port": 8055, "type": "app", "desc": "CMS", "url": "tzzrcorp.me"},
|
|
"vaultwarden": {"port": 8081, "type": "app", "desc": "Passwords", "url": "vault.tzzrcorp.me"},
|
|
"shlink": {"port": 8082, "type": "app", "desc": "URL shortener", "url": "shlink.tzzrcorp.me"},
|
|
"addy": {"port": 8083, "type": "app", "desc": "Email aliases", "url": "addy.tzzrcorp.me"},
|
|
"odoo": {"port": 8069, "type": "app", "desc": "ERP", "url": "erp.tzzrcorp.me"},
|
|
"ntfy": {"port": 8880, "type": "app", "desc": "Notifications", "url": "ntfy.tzzrcorp.me"},
|
|
|
|
# APIs y Automatización
|
|
"hsu": {"port": 5002, "type": "api", "desc": "HSU User Library", "url": "hsu.tzzrcorp.me"},
|
|
"windmill": {"port": 8000, "type": "automation", "desc": "Automatización de flujos", "url": "windmill.tzzrcorp.me"},
|
|
"context-manager": {"port": None, "type": "system", "desc": "Context Manager IA", "path": "/opt/context-manager"},
|
|
|
|
# Infraestructura
|
|
"postgres": {"port": 5432, "type": "db", "desc": "PostgreSQL 16"},
|
|
"redis": {"port": 6379, "type": "db", "desc": "Cache Redis"},
|
|
}
|
|
|
|
@dataclass
|
|
class Result:
|
|
success: bool
|
|
data: any
|
|
error: str = ""
|
|
|
|
def ssh(cmd: str, timeout: int = 60) -> Result:
|
|
"""Ejecuta comando en CORP"""
|
|
full_cmd = f'ssh -i {SSH_KEY} {SERVER} "{cmd}"'
|
|
try:
|
|
result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
|
return Result(success=result.returncode == 0, data=result.stdout.strip(), error=result.stderr.strip())
|
|
except Exception as e:
|
|
return Result(success=False, data="", error=str(e))
|
|
|
|
def get_all_containers() -> List[Dict]:
|
|
"""Lista todos los contenedores Docker"""
|
|
result = ssh("docker ps -a --format '{{.Names}}|{{.Status}}|{{.Ports}}|{{.Image}}'")
|
|
if not result.success:
|
|
return []
|
|
|
|
containers = []
|
|
for line in result.data.split('\n'):
|
|
if line:
|
|
parts = line.split('|')
|
|
containers.append({
|
|
"name": parts[0],
|
|
"status": parts[1] if len(parts) > 1 else "",
|
|
"ports": parts[2] if len(parts) > 2 else "",
|
|
"image": parts[3] if len(parts) > 3 else ""
|
|
})
|
|
return containers
|
|
|
|
def get_service_status(service: str) -> Dict:
|
|
"""Estado detallado de un servicio"""
|
|
info = SERVICES.get(service, {})
|
|
container = f"{service}-service" if info.get("type") == "agent" else service
|
|
|
|
status_result = ssh(f"docker ps --filter name={container} --format '{{{{.Status}}}}'")
|
|
|
|
health = "unknown"
|
|
if info.get("port"):
|
|
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/health 2>/dev/null | head -c 100")
|
|
health = "healthy" if health_result.success and health_result.data else "unhealthy"
|
|
|
|
return {
|
|
"service": service,
|
|
"container": container,
|
|
"status": status_result.data if status_result.success else "not found",
|
|
"health": health,
|
|
"port": info.get("port"),
|
|
"url": info.get("url"),
|
|
"desc": info.get("desc")
|
|
}
|
|
|
|
def restart_service(service: str) -> Result:
|
|
"""Reinicia un servicio"""
|
|
info = SERVICES.get(service, {})
|
|
container = f"{service}-service" if info.get("type") == "agent" else service
|
|
return ssh(f"docker restart {container}")
|
|
|
|
def get_logs(service: str, lines: int = 100) -> str:
|
|
"""Obtiene logs de un servicio"""
|
|
info = SERVICES.get(service, {})
|
|
container = f"{service}-service" if info.get("type") == "agent" else service
|
|
result = ssh(f"docker logs {container} --tail {lines} 2>&1")
|
|
return result.data if result.success else result.error
|
|
|
|
def query_agent(agent: str, endpoint: str, method: str = "GET", data: dict = None) -> Result:
|
|
"""Hace petición a un agente TZZR"""
|
|
info = SERVICES.get(agent)
|
|
if not info or info.get("type") != "agent":
|
|
return Result(success=False, data="", error="Agente no encontrado")
|
|
|
|
port = info["port"]
|
|
|
|
if method == "GET":
|
|
cmd = f"curl -s http://localhost:{port}{endpoint}"
|
|
else:
|
|
json_data = json.dumps(data) if data else "{}"
|
|
cmd = f"curl -s -X {method} -H 'Content-Type: application/json' -d '{json_data}' http://localhost:{port}{endpoint}"
|
|
|
|
result = ssh(cmd)
|
|
|
|
try:
|
|
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
|
except:
|
|
return Result(success=result.success, data=result.data)
|
|
|
|
def get_system_stats() -> Dict:
|
|
"""Estadísticas del sistema"""
|
|
stats = {}
|
|
|
|
mem_result = ssh("free -h | grep Mem | awk '{print $2,$3,$4}'")
|
|
if mem_result.success:
|
|
parts = mem_result.data.split()
|
|
stats["memory"] = {"total": parts[0], "used": parts[1], "available": parts[2]}
|
|
|
|
disk_result = ssh("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'")
|
|
if disk_result.success:
|
|
parts = disk_result.data.split()
|
|
stats["disk"] = {"total": parts[0], "used": parts[1], "available": parts[2], "percent": parts[3]}
|
|
|
|
containers_result = ssh("docker ps -q | wc -l")
|
|
stats["containers_running"] = int(containers_result.data) if containers_result.success else 0
|
|
|
|
return stats
|
|
|
|
def get_postgres_stats() -> Dict:
|
|
"""Estadísticas de PostgreSQL"""
|
|
result = ssh("sudo -u postgres psql -c \"SELECT datname, pg_size_pretty(pg_database_size(datname)) as size FROM pg_database WHERE datistemplate = false;\" -t")
|
|
|
|
databases = {}
|
|
if result.success:
|
|
for line in result.data.split('\n'):
|
|
if '|' in line:
|
|
parts = line.split('|')
|
|
databases[parts[0].strip()] = parts[1].strip()
|
|
|
|
return databases
|
|
|
|
def ingest_to_margaret(contenedor: dict, auth_key: str) -> Result:
|
|
"""Envía datos a Margaret para ingestión"""
|
|
json_data = json.dumps(contenedor)
|
|
cmd = f"curl -s -X POST -H 'Content-Type: application/json' -H 'X-Auth-Key: {auth_key}' -d '{json_data}' http://localhost:5051/ingest"
|
|
result = ssh(cmd)
|
|
|
|
try:
|
|
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
|
except:
|
|
return Result(success=result.success, data=result.data)
|
|
|
|
def execute_flow(flow_id: int, data: dict, auth_key: str) -> Result:
|
|
"""Ejecuta un flujo en Jared"""
|
|
json_data = json.dumps(data)
|
|
cmd = f"curl -s -X POST -H 'Content-Type: application/json' -H 'X-Auth-Key: {auth_key}' -d '{json_data}' http://localhost:5052/ejecutar/{flow_id}"
|
|
result = ssh(cmd)
|
|
|
|
try:
|
|
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
|
except:
|
|
return Result(success=result.success, data=result.data)
|
|
|
|
def get_pending_issues() -> Result:
|
|
"""Obtiene incidencias pendientes de Mason"""
|
|
result = ssh("curl -s http://localhost:5053/pendientes")
|
|
try:
|
|
return Result(success=result.success, data=json.loads(result.data) if result.data else [])
|
|
except:
|
|
return Result(success=result.success, data=result.data)
|
|
|
|
def verify_merkle(hash: str) -> Result:
|
|
"""Verifica registro en Feldman"""
|
|
result = ssh(f"curl -s http://localhost:5054/verify/{hash}")
|
|
try:
|
|
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
|
except:
|
|
return Result(success=result.success, data=result.data)
|
|
|
|
# CLI
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("""
|
|
TZZR CORP App - Servidor 92.112.181.188
|
|
=======================================
|
|
|
|
Uso: python app.py <comando> [argumentos]
|
|
|
|
Comandos Generales:
|
|
status Estado general del sistema
|
|
containers Lista todos los contenedores
|
|
stats Estadísticas del sistema
|
|
|
|
Gestión de Servicios:
|
|
service <name> Estado detallado de servicio
|
|
restart <service> Reiniciar servicio
|
|
logs <service> [lines] Ver logs
|
|
|
|
Agentes TZZR (Flujo de datos):
|
|
agents Estado de todos los agentes
|
|
agent <name> <endpoint> Query a agente (GET)
|
|
|
|
margaret Estado de Margaret (Secretaria)
|
|
jared Estado de Jared (Flujos)
|
|
mason Estado de Mason (Editor)
|
|
feldman Estado de Feldman (Contable)
|
|
|
|
flows Lista flujos predefinidos en Jared
|
|
pending Incidencias pendientes en Mason
|
|
verify <hash> Verificar hash en Feldman
|
|
|
|
Bases de Datos:
|
|
postgres Estadísticas PostgreSQL
|
|
|
|
Servicios disponibles:
|
|
Agentes: margaret, jared, mason, feldman
|
|
Apps: nextcloud, directus, vaultwarden, shlink, addy, odoo, ntfy
|
|
APIs: hsu, context-manager
|
|
DB: postgres, redis
|
|
""")
|
|
return
|
|
|
|
cmd = sys.argv[1]
|
|
|
|
if cmd == "status":
|
|
print("\n📊 CORP Status (92.112.181.188)")
|
|
print("=" * 40)
|
|
stats = get_system_stats()
|
|
print(f"💾 Memoria: {stats.get('memory', {}).get('used', '?')}/{stats.get('memory', {}).get('total', '?')}")
|
|
print(f"💿 Disco: {stats.get('disk', {}).get('used', '?')}/{stats.get('disk', {}).get('total', '?')} ({stats.get('disk', {}).get('percent', '?')})")
|
|
print(f"📦 Contenedores: {stats.get('containers_running', 0)}")
|
|
|
|
elif cmd == "containers":
|
|
containers = get_all_containers()
|
|
print(f"\n📦 Contenedores en CORP ({len(containers)} total):")
|
|
for c in containers:
|
|
icon = "✅" if "Up" in c["status"] else "❌"
|
|
print(f" {icon} {c['name']}: {c['status'][:30]}")
|
|
|
|
elif cmd == "service" and len(sys.argv) >= 3:
|
|
status = get_service_status(sys.argv[2])
|
|
print(f"\n🔧 {status['service']}")
|
|
print(f" Container: {status['container']}")
|
|
print(f" Status: {status['status']}")
|
|
print(f" Health: {status['health']}")
|
|
print(f" Desc: {status.get('desc', '')}")
|
|
if status.get('url'):
|
|
print(f" URL: https://{status['url']}")
|
|
|
|
elif cmd == "restart" and len(sys.argv) >= 3:
|
|
result = restart_service(sys.argv[2])
|
|
print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'reiniciado' if result.success else result.error}")
|
|
|
|
elif cmd == "logs" and len(sys.argv) >= 3:
|
|
lines = int(sys.argv[3]) if len(sys.argv) > 3 else 50
|
|
print(get_logs(sys.argv[2], lines))
|
|
|
|
elif cmd == "agents":
|
|
print("\n🤖 Agentes TZZR en CORP:")
|
|
print(" PACKET → MARGARET → JARED → MASON/FELDMAN")
|
|
print()
|
|
for agent in ["margaret", "jared", "mason", "feldman"]:
|
|
status = get_service_status(agent)
|
|
icon = "✅" if "Up" in status["status"] else "❌"
|
|
health = f"({status['health']})" if status['health'] != "unknown" else ""
|
|
print(f" {icon} {agent}: {status['status'][:20]} {health}")
|
|
print(f" └─ {SERVICES[agent]['desc']}")
|
|
|
|
elif cmd == "agent" and len(sys.argv) >= 4:
|
|
result = query_agent(sys.argv[2], sys.argv[3])
|
|
if result.success:
|
|
print(json.dumps(result.data, indent=2) if isinstance(result.data, dict) else result.data)
|
|
else:
|
|
print(f"❌ Error: {result.error}")
|
|
|
|
elif cmd in ["margaret", "jared", "mason", "feldman"]:
|
|
status = get_service_status(cmd)
|
|
print(f"\n🤖 {cmd.upper()} - {SERVICES[cmd]['desc']}")
|
|
print(f" Puerto: {SERVICES[cmd]['port']}")
|
|
print(f" Status: {status['status']}")
|
|
print(f" Health: {status['health']}")
|
|
|
|
# Endpoints específicos
|
|
contracts = query_agent(cmd, "/s-contract")
|
|
if contracts.success and isinstance(contracts.data, dict):
|
|
print(f" Versión: {contracts.data.get('version', 'N/A')}")
|
|
|
|
elif cmd == "flows":
|
|
result = query_agent("jared", "/flujos")
|
|
if result.success:
|
|
flows = result.data if isinstance(result.data, list) else []
|
|
print("\n📋 Flujos predefinidos en Jared:")
|
|
for f in flows:
|
|
print(f" • [{f.get('id')}] {f.get('nombre')}")
|
|
|
|
elif cmd == "pending":
|
|
result = get_pending_issues()
|
|
if result.success:
|
|
issues = result.data if isinstance(result.data, list) else []
|
|
print(f"\n⚠️ Incidencias pendientes en Mason: {len(issues)}")
|
|
for i in issues[:10]:
|
|
print(f" • {i.get('id')}: {i.get('tipo', 'N/A')} - {i.get('descripcion', '')[:40]}")
|
|
|
|
elif cmd == "verify" and len(sys.argv) >= 3:
|
|
result = verify_merkle(sys.argv[2])
|
|
if result.success:
|
|
print(json.dumps(result.data, indent=2))
|
|
else:
|
|
print(f"❌ Error: {result.error}")
|
|
|
|
elif cmd == "postgres":
|
|
dbs = get_postgres_stats()
|
|
print("\n🐘 PostgreSQL en CORP:")
|
|
for db, size in dbs.items():
|
|
print(f" • {db}: {size}")
|
|
|
|
else:
|
|
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|