diff --git a/apps/captain.py b/apps/captain.py new file mode 100644 index 0000000..9782a1b --- /dev/null +++ b/apps/captain.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +""" +CAPTAIN CLAUDE - CLI Unificado para el Sistema TZZR +Punto de entrada principal para todas las apps +""" +import sys +import os +import subprocess + +APPS_DIR = os.path.dirname(os.path.abspath(__file__)) + +APPS = { + "devops": { + "path": f"{APPS_DIR}/devops/app.py", + "desc": "Gestión de despliegues y construcción", + "alias": ["deploy", "build"] + }, + "deck": { + "path": f"{APPS_DIR}/deck/app.py", + "desc": "Servidor DECK (72.62.1.113) - Agentes, Mail, Apps", + "alias": ["d"] + }, + "corp": { + "path": f"{APPS_DIR}/corp/app.py", + "desc": "Servidor CORP (92.112.181.188) - Margaret, Jared, Mason, Feldman", + "alias": ["c"] + }, + "hst": { + "path": f"{APPS_DIR}/hst/app.py", + "desc": "Servidor HST (72.62.2.84) - Directus, Imágenes", + "alias": ["h"] + }, + "docker": { + "path": f"{APPS_DIR}/docker/app.py", + "desc": "Gestión Docker multi-servidor", + "alias": ["dk"] + } +} + +def print_banner(): + print(""" +╔═══════════════════════════════════════════════════════════════╗ +║ CAPTAIN CLAUDE ║ +║ Sistema Multiagente TZZR ║ +╠═══════════════════════════════════════════════════════════════╣ +║ Servidor Central: 69.62.126.110 (Gitea, PostgreSQL) ║ +║ DECK: 72.62.1.113 │ CORP: 92.112.181.188 ║ +║ HST: 72.62.2.84 │ R2: Cloudflare Storage ║ +╚═══════════════════════════════════════════════════════════════╝ + """) + +def print_help(): + print_banner() + print("Uso: python captain.py [comando] [argumentos]\n") + print("Apps disponibles:") + print("─" * 60) + + for name, info in APPS.items(): + aliases = ", ".join(info["alias"]) if info["alias"] else "" + alias_str = f" (alias: {aliases})" if aliases else "" + print(f" {name}{alias_str}") + print(f" └─ {info['desc']}") + print() + + print("Ejemplos:") + print(" python captain.py devops deploy clara deck") + print(" python captain.py deck agents") + print(" python captain.py corp pending") + print(" python captain.py docker ps all") + print(" python captain.py hst directus") + print() + print("Atajos rápidos:") + print(" python captain.py d agents # DECK agents") + print(" python captain.py c flows # CORP flows") + print(" python captain.py dk dashboard # Docker dashboard") + +def resolve_app(name: str) -> str: + """Resuelve nombre o alias a nombre de app""" + if name in APPS: + return name + + for app_name, info in APPS.items(): + if name in info.get("alias", []): + return app_name + + return None + +def run_app(app: str, args: list): + """Ejecuta una app con los argumentos dados""" + app_name = resolve_app(app) + + if not app_name: + print(f"❌ App no encontrada: {app}") + print(f" Apps disponibles: {', '.join(APPS.keys())}") + return 1 + + app_path = APPS[app_name]["path"] + + if not os.path.exists(app_path): + print(f"❌ Archivo no encontrado: {app_path}") + return 1 + + cmd = ["python3", app_path] + args + + try: + result = subprocess.run(cmd) + return result.returncode + except KeyboardInterrupt: + print("\n⚠️ Interrumpido") + return 130 + except Exception as e: + print(f"❌ Error: {e}") + return 1 + +def quick_status(): + """Muestra estado rápido de todos los servidores""" + print_banner() + print("📊 Estado rápido del sistema:\n") + + servers = [ + ("DECK", "root@72.62.1.113"), + ("CORP", "root@92.112.181.188"), + ("HST", "root@72.62.2.84") + ] + + for name, host in servers: + cmd = f"ssh -i ~/.ssh/tzzr -o ConnectTimeout=3 {host} 'docker ps -q | wc -l' 2>/dev/null" + try: + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5) + if result.returncode == 0: + count = result.stdout.strip() + print(f" ✅ {name} ({host.split('@')[1]}): {count} contenedores") + else: + print(f" ❌ {name} ({host.split('@')[1]}): No responde") + except: + print(f" ❌ {name} ({host.split('@')[1]}): Timeout") + + print() + +def main(): + if len(sys.argv) < 2: + print_help() + return 0 + + cmd = sys.argv[1] + + # Comandos especiales + if cmd in ["-h", "--help", "help"]: + print_help() + return 0 + + if cmd in ["status", "s"]: + quick_status() + return 0 + + if cmd in ["version", "-v", "--version"]: + print("Captain Claude v1.0.0") + return 0 + + # Ejecutar app + args = sys.argv[2:] if len(sys.argv) > 2 else [] + return run_app(cmd, args) + +if __name__ == "__main__": + sys.exit(main()) diff --git a/apps/corp/app.py b/apps/corp/app.py new file mode 100644 index 0000000..972033a --- /dev/null +++ b/apps/corp/app.py @@ -0,0 +1,343 @@ +#!/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 [argumentos] + +Comandos Generales: + status Estado general del sistema + containers Lista todos los contenedores + stats Estadísticas del sistema + +Gestión de Servicios: + service Estado detallado de servicio + restart Reiniciar servicio + logs [lines] Ver logs + +Agentes TZZR (Flujo de datos): + agents Estado de todos los agentes + agent 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 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() diff --git a/apps/deck/app.py b/apps/deck/app.py new file mode 100644 index 0000000..946d22d --- /dev/null +++ b/apps/deck/app.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +""" +TZZR DECK App - Gestión del servidor DECK (72.62.1.113) +Servicios: Agentes TZZR, Mailcow, Nextcloud, Odoo, Vaultwarden, etc. +""" +import subprocess +import json +import sys +from dataclasses import dataclass +from typing import Optional, List, Dict +from datetime import datetime + +SERVER = "root@72.62.1.113" +SSH_KEY = "~/.ssh/tzzr" + +# Servicios conocidos en DECK +SERVICES = { + # Microservicios TZZR + "clara": {"port": 5051, "type": "service", "desc": "Log inmutable"}, + "alfred": {"port": 5052, "type": "service", "desc": "Automatización de flujos"}, + "mason": {"port": 5053, "type": "service", "desc": "Espacio de enriquecimiento"}, + "feldman": {"port": 5054, "type": "service", "desc": "Validador Merkle"}, + + # Aplicaciones + "nextcloud": {"port": 8084, "type": "app", "desc": "Cloud storage", "url": "cloud.tzzrdeck.me"}, + "odoo": {"port": 8069, "type": "app", "desc": "ERP", "url": "odoo.tzzrdeck.me", "container": "deck-odoo"}, + "vaultwarden": {"port": 8085, "type": "app", "desc": "Passwords", "url": "vault.tzzrdeck.me"}, + "directus": {"port": 8055, "type": "app", "desc": "CMS", "url": "directus.tzzrdeck.me"}, + "shlink": {"port": 8083, "type": "app", "desc": "URL shortener", "url": "shlink.tzzrdeck.me"}, + "ntfy": {"port": 8080, "type": "app", "desc": "Notifications", "url": "ntfy.tzzrdeck.me"}, + "filebrowser": {"port": 8082, "type": "app", "desc": "File manager", "url": "files.tzzrdeck.me"}, + + # Infraestructura + "postgres": {"port": 5432, "type": "db", "desc": "PostgreSQL con pgvector"}, + "redis": {"port": 6379, "type": "db", "desc": "Cache Redis"}, + "mailcow": {"port": 8180, "type": "mail", "desc": "Servidor correo", "url": "mail.tzzr.net"}, +} + +@dataclass +class Result: + success: bool + data: any + error: str = "" + +def ssh(cmd: str, timeout: int = 60) -> Result: + """Ejecuta comando en DECK""" + 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 = info.get("container", f"{service}-service" if info.get("type") == "service" else service) + + # Estado del contenedor + status_result = ssh(f"docker ps --filter name={container} --format '{{{{.Status}}}}'") + + # Logs recientes + logs_result = ssh(f"docker logs {container} --tail 5 2>&1") + + # Health check si tiene endpoint + health = "unknown" + if info.get("type") == "service": + health_result = ssh(f"curl -s http://localhost:{info.get('port')}/health 2>/dev/null") + 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"), + "logs": logs_result.data[:500] if logs_result.success else "" + } + +def restart_service(service: str) -> Result: + """Reinicia un servicio""" + info = SERVICES.get(service, {}) + container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service) + return ssh(f"docker restart {container}") + +def stop_service(service: str) -> Result: + """Detiene un servicio""" + info = SERVICES.get(service, {}) + container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service) + return ssh(f"docker stop {container}") + +def start_service(service: str) -> Result: + """Inicia un servicio""" + info = SERVICES.get(service, {}) + container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service) + return ssh(f"docker start {container}") + +def get_logs(service: str, lines: int = 100) -> str: + """Obtiene logs de un servicio""" + info = SERVICES.get(service, {}) + container = info.get("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 get_system_stats() -> Dict: + """Estadísticas del sistema""" + stats = {} + + # CPU y memoria + 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]} + + # Disco + 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]} + + # Contenedores + containers_result = ssh("docker ps -q | wc -l") + stats["containers_running"] = int(containers_result.data) if containers_result.success else 0 + + # Load average + load_result = ssh("cat /proc/loadavg | awk '{print $1,$2,$3}'") + if load_result.success: + parts = load_result.data.split() + stats["load"] = {"1m": parts[0], "5m": parts[1], "15m": parts[2]} + + return stats + +def query_service(service: str, endpoint: str, method: str = "GET", data: dict = None) -> Result: + """Hace petición a un servicio TZZR""" + info = SERVICES.get(service) + if not info or info.get("type") != "service": + return Result(success=False, data="", error="Servicio 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_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 get_mailcow_status() -> Dict: + """Estado de Mailcow""" + containers = ["postfix-mailcow", "dovecot-mailcow", "nginx-mailcow", "mysql-mailcow", "redis-mailcow"] + status = {} + + for c in containers: + result = ssh(f"docker ps --filter name={c} --format '{{{{.Status}}}}'") + status[c.replace("-mailcow", "")] = result.data if result.success and result.data else "stopped" + + return status + +# CLI +def main(): + if len(sys.argv) < 2: + print(""" +TZZR DECK App - Servidor 72.62.1.113 +==================================== + +Uso: python app.py [argumentos] + +Comandos Generales: + status Estado general del sistema + containers Lista todos los contenedores + stats Estadísticas del sistema + +Gestión de Servicios: + service Estado detallado de servicio + restart Reiniciar servicio + stop Detener servicio + start Iniciar servicio + logs [lines] Ver logs + +Servicios TZZR: + services Estado de todos los servicios + query Query a servicio (GET) + +Bases de Datos: + postgres Estadísticas PostgreSQL + +Mail: + mailcow Estado de Mailcow + +Servicios disponibles: + TZZR: clara, alfred, mason, feldman + Apps: nextcloud, odoo, vaultwarden, directus, shlink, ntfy, filebrowser + DB: postgres, redis + Mail: mailcow + """) + return + + cmd = sys.argv[1] + + if cmd == "status": + print("\n📊 DECK Status (72.62.1.113)") + 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)}") + print(f"⚡ Load: {stats.get('load', {}).get('1m', '?')}") + + elif cmd == "containers": + containers = get_all_containers() + print(f"\n📦 Contenedores en DECK ({len(containers)} total):") + for c in containers: + icon = "✅" if "Up" in c["status"] else "❌" + print(f" {icon} {c['name']}: {c['status'][:30]}") + + elif cmd == "stats": + stats = get_system_stats() + print(json.dumps(stats, indent=2)) + + 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']}") + if status.get('url'): + print(f" URL: https://{status['url']}") + if status.get('port'): + print(f" Puerto: {status['port']}") + + 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 == "stop" and len(sys.argv) >= 3: + result = stop_service(sys.argv[2]) + print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'detenido' if result.success else result.error}") + + elif cmd == "start" and len(sys.argv) >= 3: + result = start_service(sys.argv[2]) + print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'iniciado' 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 == "services": + print("\n⚙️ Servicios TZZR en DECK:") + for svc in ["clara", "alfred", "mason", "feldman"]: + status = get_service_status(svc) + icon = "✅" if "Up" in status["status"] else "❌" + health = f"({status['health']})" if status['health'] != "unknown" else "" + print(f" {icon} {svc}: {status['status'][:25]} {health} - {SERVICES[svc]['desc']}") + + elif cmd == "query" and len(sys.argv) >= 4: + result = query_service(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 == "postgres": + dbs = get_postgres_stats() + print("\n🐘 PostgreSQL en DECK:") + for db, size in dbs.items(): + print(f" • {db}: {size}") + + elif cmd == "mailcow": + status = get_mailcow_status() + print("\n📧 Mailcow Status:") + for service, st in status.items(): + icon = "✅" if "Up" in st else "❌" + print(f" {icon} {service}: {st[:30]}") + + else: + print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.") + +if __name__ == "__main__": + main() diff --git a/apps/devops/app.py b/apps/devops/app.py new file mode 100644 index 0000000..bc2feee --- /dev/null +++ b/apps/devops/app.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +""" +TZZR DevOps App - Gestión de despliegues y construcción +""" +import subprocess +import sys +from dataclasses import dataclass +from typing import Optional +import json + +# Configuración de servidores +SERVERS = { + "deck": {"host": "root@72.62.1.113", "name": "DECK"}, + "corp": {"host": "root@92.112.181.188", "name": "CORP"}, + "hst": {"host": "root@72.62.2.84", "name": "HST"}, + "local": {"host": None, "name": "LOCAL (69.62.126.110)"} +} + +SSH_KEY = "~/.ssh/tzzr" + +# Agentes conocidos +AGENTS = { + "deck": ["clara", "alfred", "mason", "feldman"], + "corp": ["margaret", "jared", "mason", "feldman"], + "hst": ["hst-api", "directus_hst", "directus_lumalia", "directus_personal"] +} + +@dataclass +class CommandResult: + success: bool + output: str + error: str = "" + +def ssh_cmd(server: str, command: str) -> CommandResult: + """Ejecuta comando SSH en servidor remoto""" + if server == "local": + full_cmd = command + else: + host = SERVERS[server]["host"] + full_cmd = f'ssh -i {SSH_KEY} {host} "{command}"' + + try: + result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=60) + return CommandResult( + success=result.returncode == 0, + output=result.stdout.strip(), + error=result.stderr.strip() + ) + except subprocess.TimeoutExpired: + return CommandResult(success=False, output="", error="Timeout") + except Exception as e: + return CommandResult(success=False, output="", error=str(e)) + +def deploy_agent(agent: str, server: str) -> CommandResult: + """Despliega un agente en el servidor especificado""" + print(f"🚀 Desplegando {agent} en {server}...") + + # Usar el script de deploy en DECK + cmd = f"/opt/scripts/deploy-agent.sh {agent} {server}" + result = ssh_cmd("deck", cmd) + + if result.success: + print(f"✅ {agent} desplegado exitosamente en {server}") + else: + print(f"❌ Error desplegando {agent}: {result.error}") + + return result + +def backup_postgres(server: str = "deck") -> CommandResult: + """Ejecuta backup de PostgreSQL""" + print(f"💾 Ejecutando backup en {server}...") + result = ssh_cmd(server, "/opt/scripts/backup_postgres.sh") + + if result.success: + print("✅ Backup completado") + else: + print(f"❌ Error: {result.error}") + + return result + +def sync_r2() -> CommandResult: + """Sincroniza backups a R2""" + print("☁️ Sincronizando con R2...") + result = ssh_cmd("deck", "/opt/scripts/sync_backups_r2.sh") + + if result.success: + print("✅ Sincronización completada") + else: + print(f"❌ Error: {result.error}") + + return result + +def onboard_user(email: str, username: str) -> CommandResult: + """Da de alta un nuevo usuario""" + print(f"👤 Creando usuario {username} ({email})...") + result = ssh_cmd("deck", f"/opt/scripts/onboard-user.sh {email} {username}") + + if result.success: + print(f"✅ Usuario {username} creado") + else: + print(f"❌ Error: {result.error}") + + return result + +def get_agent_status(server: str) -> dict: + """Obtiene estado de agentes en un servidor""" + agents = AGENTS.get(server, []) + status = {} + + for agent in agents: + result = ssh_cmd(server, f"docker ps --filter name={agent} --format '{{{{.Status}}}}'") + status[agent] = result.output if result.success else "unknown" + + return status + +def restart_agent(agent: str, server: str) -> CommandResult: + """Reinicia un agente específico""" + print(f"🔄 Reiniciando {agent} en {server}...") + result = ssh_cmd(server, f"docker restart {agent}-service 2>/dev/null || docker restart {agent}") + + if result.success: + print(f"✅ {agent} reiniciado") + else: + print(f"❌ Error: {result.error}") + + return result + +def get_logs(agent: str, server: str, lines: int = 50) -> CommandResult: + """Obtiene logs de un agente""" + container = f"{agent}-service" if agent in ["clara", "alfred", "mason", "feldman", "margaret", "jared"] else agent + return ssh_cmd(server, f"docker logs {container} --tail {lines} 2>&1") + +def list_deployments() -> dict: + """Lista todos los deployments activos""" + deployments = {} + + for server in ["deck", "corp", "hst"]: + result = ssh_cmd(server, "docker ps --format '{{.Names}}|{{.Status}}|{{.Ports}}'") + if result.success: + containers = [] + for line in result.output.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 "" + }) + deployments[server] = containers + + return deployments + +def git_pull_all(server: str) -> dict: + """Hace git pull en todos los proyectos de /opt""" + result = ssh_cmd(server, "for d in /opt/*/; do echo \"=== $d ===\"; cd $d && git pull 2>/dev/null || echo 'no git'; done") + return {"output": result.output, "success": result.success} + +# CLI +def main(): + if len(sys.argv) < 2: + print(""" +TZZR DevOps App +=============== + +Uso: python app.py [argumentos] + +Comandos: + deploy Despliega un agente + backup [server] Ejecuta backup PostgreSQL + sync Sincroniza backups a R2 + onboard Alta de usuario + status Estado de agentes + restart Reinicia agente + logs Ver logs de agente + list Lista todos los deployments + pull Git pull en todos los proyectos + +Servidores: deck, corp, hst, local +Agentes DECK: clara, alfred, mason, feldman +Agentes CORP: margaret, jared, mason, feldman + """) + return + + cmd = sys.argv[1] + + if cmd == "deploy" and len(sys.argv) >= 4: + deploy_agent(sys.argv[2], sys.argv[3]) + + elif cmd == "backup": + server = sys.argv[2] if len(sys.argv) > 2 else "deck" + backup_postgres(server) + + elif cmd == "sync": + sync_r2() + + elif cmd == "onboard" and len(sys.argv) >= 4: + onboard_user(sys.argv[2], sys.argv[3]) + + elif cmd == "status" and len(sys.argv) >= 3: + status = get_agent_status(sys.argv[2]) + print(f"\n📊 Estado de agentes en {sys.argv[2]}:") + for agent, st in status.items(): + icon = "✅" if "Up" in st else "❌" + print(f" {icon} {agent}: {st}") + + elif cmd == "restart" and len(sys.argv) >= 4: + restart_agent(sys.argv[2], sys.argv[3]) + + elif cmd == "logs" and len(sys.argv) >= 4: + result = get_logs(sys.argv[2], sys.argv[3]) + print(result.output) + + elif cmd == "list": + deployments = list_deployments() + for server, containers in deployments.items(): + print(f"\n📦 {server.upper()}:") + for c in containers[:10]: + print(f" • {c['name']}: {c['status']}") + if len(containers) > 10: + print(f" ... y {len(containers)-10} más") + + elif cmd == "pull" and len(sys.argv) >= 3: + result = git_pull_all(sys.argv[2]) + print(result["output"]) + + else: + print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.") + +if __name__ == "__main__": + main() diff --git a/apps/docker/app.py b/apps/docker/app.py new file mode 100644 index 0000000..a5b5cae --- /dev/null +++ b/apps/docker/app.py @@ -0,0 +1,425 @@ +#!/usr/bin/env python3 +""" +TZZR Docker App - Gestión unificada de Docker en todos los servidores +Servidores: DECK (72.62.1.113), CORP (92.112.181.188), HST (72.62.2.84) +""" +import subprocess +import json +import sys +from dataclasses import dataclass, field +from typing import List, Dict, Optional +from datetime import datetime + +# Configuración de servidores +SERVERS = { + "deck": { + "host": "root@72.62.1.113", + "name": "DECK", + "desc": "Producción - Agentes TZZR, Mail, Apps" + }, + "corp": { + "host": "root@92.112.181.188", + "name": "CORP", + "desc": "Aplicaciones - Margaret, Jared, Mason, Feldman" + }, + "hst": { + "host": "root@72.62.2.84", + "name": "HST", + "desc": "Contenido - Directus, Imágenes" + } +} + +SSH_KEY = "~/.ssh/tzzr" + +@dataclass +class Container: + name: str + status: str + image: str + ports: str = "" + created: str = "" + server: str = "" + + @property + def is_running(self) -> bool: + return "Up" in self.status + + @property + def icon(self) -> str: + return "✅" if self.is_running else "❌" + +@dataclass +class Result: + success: bool + data: any + error: str = "" + +def ssh(server: str, cmd: str, timeout: int = 60) -> Result: + """Ejecuta comando SSH""" + host = SERVERS[server]["host"] + full_cmd = f'ssh -i {SSH_KEY} {host} "{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 subprocess.TimeoutExpired: + return Result(success=False, data="", error="Timeout") + except Exception as e: + return Result(success=False, data="", error=str(e)) + +def get_containers(server: str, all: bool = False) -> List[Container]: + """Lista contenedores de un servidor""" + flag = "-a" if all else "" + result = ssh(server, f"docker ps {flag} --format '{{{{.Names}}}}|{{{{.Status}}}}|{{{{.Image}}}}|{{{{.Ports}}}}'") + + if not result.success: + return [] + + containers = [] + for line in result.data.split('\n'): + if line: + parts = line.split('|') + containers.append(Container( + name=parts[0], + status=parts[1] if len(parts) > 1 else "", + image=parts[2] if len(parts) > 2 else "", + ports=parts[3] if len(parts) > 3 else "", + server=server + )) + + return containers + +def get_all_containers(all: bool = False) -> Dict[str, List[Container]]: + """Lista contenedores de todos los servidores""" + result = {} + for server in SERVERS: + result[server] = get_containers(server, all) + return result + +def container_action(server: str, container: str, action: str) -> Result: + """Ejecuta acción en contenedor (start, stop, restart, rm)""" + return ssh(server, f"docker {action} {container}") + +def get_logs(server: str, container: str, lines: int = 100, follow: bool = False) -> str: + """Obtiene logs de contenedor""" + follow_flag = "-f" if follow else "" + result = ssh(server, f"docker logs {container} --tail {lines} {follow_flag} 2>&1", timeout=120 if follow else 60) + return result.data if result.success else result.error + +def inspect_container(server: str, container: str) -> Dict: + """Inspecciona contenedor""" + result = ssh(server, f"docker inspect {container}") + if result.success: + try: + return json.loads(result.data)[0] + except: + return {} + return {} + +def get_images(server: str) -> List[Dict]: + """Lista imágenes Docker""" + result = ssh(server, "docker images --format '{{.Repository}}|{{.Tag}}|{{.Size}}|{{.ID}}'") + + if not result.success: + return [] + + images = [] + for line in result.data.split('\n'): + if line: + parts = line.split('|') + images.append({ + "repository": parts[0], + "tag": parts[1] if len(parts) > 1 else "", + "size": parts[2] if len(parts) > 2 else "", + "id": parts[3] if len(parts) > 3 else "" + }) + + return images + +def get_networks(server: str) -> List[Dict]: + """Lista redes Docker""" + result = ssh(server, "docker network ls --format '{{.Name}}|{{.Driver}}|{{.Scope}}'") + + if not result.success: + return [] + + networks = [] + for line in result.data.split('\n'): + if line: + parts = line.split('|') + networks.append({ + "name": parts[0], + "driver": parts[1] if len(parts) > 1 else "", + "scope": parts[2] if len(parts) > 2 else "" + }) + + return networks + +def get_volumes(server: str) -> List[Dict]: + """Lista volúmenes Docker""" + result = ssh(server, "docker volume ls --format '{{.Name}}|{{.Driver}}'") + + if not result.success: + return [] + + volumes = [] + for line in result.data.split('\n'): + if line: + parts = line.split('|') + volumes.append({ + "name": parts[0], + "driver": parts[1] if len(parts) > 1 else "" + }) + + return volumes + +def docker_stats(server: str) -> List[Dict]: + """Estadísticas de contenedores""" + result = ssh(server, "docker stats --no-stream --format '{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}'") + + if not result.success: + return [] + + stats = [] + for line in result.data.split('\n'): + if line: + parts = line.split('|') + stats.append({ + "name": parts[0], + "cpu": parts[1] if len(parts) > 1 else "", + "memory": parts[2] if len(parts) > 2 else "", + "network": parts[3] if len(parts) > 3 else "" + }) + + return stats + +def prune(server: str, what: str = "all") -> Result: + """Limpia recursos Docker no usados""" + if what == "all": + return ssh(server, "docker system prune -f") + elif what == "images": + return ssh(server, "docker image prune -f") + elif what == "volumes": + return ssh(server, "docker volume prune -f") + elif what == "networks": + return ssh(server, "docker network prune -f") + return Result(success=False, data="", error="Tipo no válido") + +def compose_action(server: str, path: str, action: str) -> Result: + """Ejecuta docker compose en un directorio""" + return ssh(server, f"cd {path} && docker compose {action}") + +def exec_container(server: str, container: str, command: str) -> Result: + """Ejecuta comando dentro de contenedor""" + return ssh(server, f"docker exec {container} {command}") + +def find_container(name: str) -> List[tuple]: + """Busca contenedor por nombre en todos los servidores""" + results = [] + for server in SERVERS: + containers = get_containers(server, all=True) + for c in containers: + if name.lower() in c.name.lower(): + results.append((server, c)) + return results + +def get_system_df(server: str) -> Dict: + """Uso de disco de Docker""" + result = ssh(server, "docker system df --format '{{.Type}}|{{.Size}}|{{.Reclaimable}}'") + + if not result.success: + return {} + + df = {} + for line in result.data.split('\n'): + if line: + parts = line.split('|') + df[parts[0]] = { + "size": parts[1] if len(parts) > 1 else "", + "reclaimable": parts[2] if len(parts) > 2 else "" + } + + return df + +# CLI +def main(): + if len(sys.argv) < 2: + print(""" +TZZR Docker App - Gestión Multi-servidor +========================================= + +Uso: python app.py [argumentos] + +Servidores: deck, corp, hst (o 'all' para todos) + +Contenedores: + ps [server] Lista contenedores activos + ps -a [server] Lista todos los contenedores + start Iniciar contenedor + stop Detener contenedor + restart Reiniciar contenedor + rm Eliminar contenedor + logs Ver logs + inspect Inspeccionar contenedor + exec Ejecutar comando en contenedor + find Buscar contenedor en todos los servidores + +Recursos: + images Lista imágenes + networks Lista redes + volumes Lista volúmenes + stats Estadísticas de recursos + df Uso de disco Docker + +Mantenimiento: + prune [type] Limpiar recursos (all/images/volumes/networks) + +Compose: + up docker compose up -d + down docker compose down + build docker compose build + +Dashboard: + dashboard Vista general de todos los servidores + """) + return + + cmd = sys.argv[1] + + # PS - Lista contenedores + if cmd == "ps": + show_all = "-a" in sys.argv + server = None + for arg in sys.argv[2:]: + if arg != "-a" and arg in SERVERS: + server = arg + + if server: + containers = get_containers(server, show_all) + print(f"\n📦 {SERVERS[server]['name']} ({len(containers)} contenedores):") + for c in containers: + print(f" {c.icon} {c.name}: {c.status[:30]}") + else: + # Todos los servidores + all_containers = get_all_containers(show_all) + for srv, containers in all_containers.items(): + print(f"\n📦 {SERVERS[srv]['name']} ({len(containers)}):") + for c in containers[:15]: + print(f" {c.icon} {c.name}: {c.status[:25]}") + if len(containers) > 15: + print(f" ... y {len(containers)-15} más") + + # Acciones de contenedor + elif cmd in ["start", "stop", "restart", "rm"] and len(sys.argv) >= 4: + server, container = sys.argv[2], sys.argv[3] + result = container_action(server, container, cmd) + icon = "✅" if result.success else "❌" + action_name = {"start": "iniciado", "stop": "detenido", "restart": "reiniciado", "rm": "eliminado"} + print(f"{icon} {container} {action_name.get(cmd, cmd)}" if result.success else f"❌ Error: {result.error}") + + # Logs + elif cmd == "logs" and len(sys.argv) >= 4: + server, container = sys.argv[2], sys.argv[3] + lines = int(sys.argv[4]) if len(sys.argv) > 4 else 50 + print(get_logs(server, container, lines)) + + # Inspect + elif cmd == "inspect" and len(sys.argv) >= 4: + server, container = sys.argv[2], sys.argv[3] + info = inspect_container(server, container) + print(json.dumps(info, indent=2)[:3000]) + + # Exec + elif cmd == "exec" and len(sys.argv) >= 5: + server, container = sys.argv[2], sys.argv[3] + command = " ".join(sys.argv[4:]) + result = exec_container(server, container, command) + print(result.data if result.success else f"Error: {result.error}") + + # Find + elif cmd == "find" and len(sys.argv) >= 3: + name = sys.argv[2] + results = find_container(name) + if results: + print(f"\n🔍 Encontrados {len(results)} contenedores con '{name}':") + for server, c in results: + print(f" {c.icon} [{server}] {c.name}: {c.status[:25]}") + else: + print(f"❌ No se encontró '{name}'") + + # Images + elif cmd == "images" and len(sys.argv) >= 3: + server = sys.argv[2] + images = get_images(server) + print(f"\n🖼️ Imágenes en {server}:") + for img in images[:20]: + print(f" • {img['repository']}:{img['tag']} ({img['size']})") + + # Networks + elif cmd == "networks" and len(sys.argv) >= 3: + server = sys.argv[2] + networks = get_networks(server) + print(f"\n🌐 Redes en {server}:") + for net in networks: + print(f" • {net['name']} ({net['driver']})") + + # Volumes + elif cmd == "volumes" and len(sys.argv) >= 3: + server = sys.argv[2] + volumes = get_volumes(server) + print(f"\n💾 Volúmenes en {server}:") + for vol in volumes: + print(f" • {vol['name']}") + + # Stats + elif cmd == "stats" and len(sys.argv) >= 3: + server = sys.argv[2] + stats = docker_stats(server) + print(f"\n📊 Estadísticas en {server}:") + for s in stats[:15]: + print(f" {s['name'][:20]}: CPU {s['cpu']} | MEM {s['memory']}") + + # DF + elif cmd == "df" and len(sys.argv) >= 3: + server = sys.argv[2] + df = get_system_df(server) + print(f"\n💿 Uso de Docker en {server}:") + for type_, info in df.items(): + print(f" {type_}: {info['size']} (recuperable: {info['reclaimable']})") + + # Prune + elif cmd == "prune" and len(sys.argv) >= 3: + server = sys.argv[2] + what = sys.argv[3] if len(sys.argv) > 3 else "all" + result = prune(server, what) + print(f"{'✅' if result.success else '❌'} Limpieza completada" if result.success else f"Error: {result.error}") + + # Compose + elif cmd in ["up", "down", "build"] and len(sys.argv) >= 4: + server, path = sys.argv[2], sys.argv[3] + action = f"{cmd} -d" if cmd == "up" else cmd + result = compose_action(server, path, action) + print(f"{'✅' if result.success else '❌'} compose {cmd}" if result.success else f"Error: {result.error}") + + # Dashboard + elif cmd == "dashboard": + print("\n" + "=" * 60) + print("🐳 TZZR Docker Dashboard") + print("=" * 60) + + for server, info in SERVERS.items(): + containers = get_containers(server) + running = sum(1 for c in containers if c.is_running) + print(f"\n📦 {info['name']} ({info['host'].split('@')[1]})") + print(f" {info['desc']}") + print(f" Contenedores: {running}/{len(containers)} activos") + + # Top 5 contenedores + for c in containers[:5]: + print(f" {c.icon} {c.name}") + + else: + print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.") + +if __name__ == "__main__": + main() diff --git a/apps/hst/app.py b/apps/hst/app.py new file mode 100644 index 0000000..ef55f67 --- /dev/null +++ b/apps/hst/app.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +""" +TZZR HST App - Gestión del servidor HST (72.62.2.84) +Servicios: Directus (3 instancias), Servidor de imágenes, APIs +""" +import subprocess +import json +import sys +from dataclasses import dataclass +from typing import List, Dict + +SERVER = "root@72.62.2.84" +SSH_KEY = "~/.ssh/tzzr" + +# Servicios conocidos en HST +SERVICES = { + # Directus instances + "directus_hst": {"port": 8055, "type": "cms", "desc": "Directus HST principal", "url": "hst.tzrtech.org"}, + "directus_lumalia": {"port": 8056, "type": "cms", "desc": "Directus Lumalia", "url": "lumalia.tzrtech.org"}, + "directus_personal": {"port": 8057, "type": "cms", "desc": "Directus Personal", "url": "personal.tzrtech.org"}, + + # APIs + "hst-api": {"port": 5001, "type": "api", "desc": "HST Flask API"}, + "hst-images": {"port": 80, "type": "web", "desc": "Servidor NGINX imágenes", "url": "tzrtech.org"}, + + # Infraestructura + "postgres_hst": {"port": 5432, "type": "db", "desc": "PostgreSQL 15"}, + "filebrowser": {"port": 8081, "type": "app", "desc": "File Browser"}, +} + +@dataclass +class Result: + success: bool + data: any + error: str = "" + +def ssh(cmd: str, timeout: int = 60) -> Result: + """Ejecuta comando en HST""" + 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, {}) + + status_result = ssh(f"docker ps --filter name={service} --format '{{{{.Status}}}}'") + + health = "unknown" + if info.get("port") and info.get("type") in ["cms", "api"]: + if info.get("type") == "cms": + health_result = ssh(f"curl -s http://localhost:{info.get('port')}/server/health 2>/dev/null | head -c 50") + else: + health_result = ssh(f"curl -s http://localhost:{info.get('port')}/health 2>/dev/null | head -c 50") + health = "healthy" if health_result.success and health_result.data else "unhealthy" + + return { + "service": service, + "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""" + return ssh(f"docker restart {service}") + +def get_logs(service: str, lines: int = 100) -> str: + """Obtiene logs de un servicio""" + result = ssh(f"docker logs {service} --tail {lines} 2>&1") + return result.data if result.success else result.error + +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 query_directus(instance: str, endpoint: str, token: str = None) -> Result: + """Hace petición a una instancia de Directus""" + info = SERVICES.get(instance) + if not info or info.get("type") != "cms": + return Result(success=False, data="", error="Instancia Directus no encontrada") + + port = info["port"] + auth = f"-H 'Authorization: Bearer {token}'" if token else "" + + cmd = f"curl -s {auth} 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_directus_collections(instance: str) -> List[str]: + """Lista colecciones de una instancia Directus""" + result = query_directus(instance, "/collections") + if result.success and isinstance(result.data, dict): + collections = result.data.get("data", []) + return [c.get("collection") for c in collections if not c.get("collection", "").startswith("directus_")] + return [] + +def list_images(path: str = "/var/www/images") -> List[str]: + """Lista imágenes en el servidor""" + result = ssh(f"ls -la {path} 2>/dev/null | head -20") + return result.data.split('\n') if result.success else [] + +def get_postgres_databases() -> List[str]: + """Lista bases de datos PostgreSQL""" + result = ssh("docker exec postgres_hst psql -U postgres -c '\\l' -t 2>/dev/null") + if not result.success: + return [] + + databases = [] + for line in result.data.split('\n'): + if '|' in line: + db = line.split('|')[0].strip() + if db and db not in ['template0', 'template1']: + databases.append(db) + + return databases + +# CLI +def main(): + if len(sys.argv) < 2: + print(""" +TZZR HST App - Servidor 72.62.2.84 +================================== + +Uso: python app.py [argumentos] + +Comandos Generales: + status Estado general del sistema + containers Lista todos los contenedores + stats Estadísticas del sistema + +Gestión de Servicios: + service Estado detallado de servicio + restart Reiniciar servicio + logs [lines] Ver logs + +Directus (CMS): + directus Estado de las 3 instancias + collections Lista colecciones de instancia + query Query a Directus API + +Imágenes: + images [path] Lista imágenes + +Bases de Datos: + postgres Lista bases de datos + +Servicios disponibles: + CMS: directus_hst, directus_lumalia, directus_personal + API: hst-api + Web: hst-images + DB: postgres_hst + App: filebrowser + """) + return + + cmd = sys.argv[1] + + if cmd == "status": + print("\n📊 HST Status (72.62.2.84)") + 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 HST ({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" 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 == "directus": + print("\n📚 Instancias Directus en HST:") + for instance in ["directus_hst", "directus_lumalia", "directus_personal"]: + status = get_service_status(instance) + icon = "✅" if "Up" in status["status"] else "❌" + health = f"({status['health']})" if status['health'] != "unknown" else "" + url = f"https://{status['url']}" if status.get('url') else "" + print(f" {icon} {instance}: {status['status'][:20]} {health}") + print(f" └─ {url}") + + elif cmd == "collections" and len(sys.argv) >= 3: + collections = get_directus_collections(sys.argv[2]) + print(f"\n📋 Colecciones en {sys.argv[2]}:") + for c in collections: + print(f" • {c}") + + elif cmd == "query" and len(sys.argv) >= 4: + result = query_directus(sys.argv[2], sys.argv[3]) + if result.success: + print(json.dumps(result.data, indent=2)[:2000]) + else: + print(f"❌ Error: {result.error}") + + elif cmd == "images": + path = sys.argv[2] if len(sys.argv) > 2 else "/var/www/images" + images = list_images(path) + print(f"\n🖼️ Imágenes en {path}:") + for img in images[:20]: + print(f" {img}") + + elif cmd == "postgres": + dbs = get_postgres_databases() + print("\n🐘 Bases de datos PostgreSQL:") + for db in dbs: + print(f" • {db}") + + else: + print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.") + +if __name__ == "__main__": + main() diff --git a/captain_claude.py b/captain_claude.py index 1a41733..512636e 100755 --- a/captain_claude.py +++ b/captain_claude.py @@ -98,6 +98,11 @@ class CaptainClaude: self.cost_tracker = CostTracker() logger = logging.getLogger("captain-claude") logger.setLevel(logging.INFO) + # Evitar handlers duplicados en sesiones largas + if not logger.handlers: + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter('%(message)s')) + logger.addHandler(handler) self.logger = StructuredLogger(logger) self.output_dir = Path(output_dir) if output_dir else Path.cwd() / "captain_output" self.output_dir.mkdir(exist_ok=True) @@ -244,33 +249,33 @@ Continue and complete this work.""" """Execute a task using intelligent agent orchestration.""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - print(f"\n{'='*60}") - print("CAPTAIN CLAUDE by dadiaar") - print(f"{'='*60}") - print(f"Task: {task[:100]}...") - print(f"{'='*60}\n") + print(f"\n{'='*60}", flush=True) + print("CAPTAIN CLAUDE by dadiaar", flush=True) + print(f"{'='*60}", flush=True) + print(f"Task: {task[:100]}...", flush=True) + print(f"{'='*60}\n", flush=True) # Phase 1: Analyze task - print("[Captain] Analyzing task...") + print("[Captain] Analyzing task...", flush=True) plan = await self.analyze_task(task) - print(f"[Captain] Plan created: {len(plan.get('steps', []))} steps") + print(f"[Captain] Plan created: {len(plan.get('steps', []))} steps", flush=True) # Phase 2: Execute plan results = [] if plan.get("parallel_possible") and len(plan.get("agents_needed", [])) > 1: - print("[Captain] Executing agents in parallel...") + print("[Captain] Executing agents in parallel...", flush=True) parallel_result = await self.run_parallel( task, plan.get("agents_needed", ["coder"]) ) results.append(parallel_result) else: - print("[Captain] Executing agents sequentially...") + print("[Captain] Executing agents sequentially...", flush=True) sequential_results = await self.run_sequential(plan.get("steps", [])) results.extend(sequential_results) # Phase 3: Synthesize results - print("[Captain] Synthesizing results...") + print("[Captain] Synthesizing results...", flush=True) synthesis_prompt = f"""Synthesize these results into a coherent final output: Original task: {task} @@ -298,12 +303,12 @@ Provide a clear, actionable final result.""" with open(output_file, "w") as f: json.dump(final_result, f, indent=2, default=str) - print(f"\n{'='*60}") - print("EXECUTION COMPLETE") - print(f"{'='*60}") - print(f"Output saved: {output_file}") - print(f"Cost: {self.cost_tracker.summary()}") - print(f"{'='*60}\n") + print(f"\n{'='*60}", flush=True) + print("EXECUTION COMPLETE", flush=True) + print(f"{'='*60}", flush=True) + print(f"Output saved: {output_file}", flush=True) + print(f"Cost: {self.cost_tracker.summary()}", flush=True) + print(f"{'='*60}\n", flush=True) return final_result @@ -322,18 +327,18 @@ async def main(): """Interactive Captain Claude session.""" captain = CaptainClaude() - print("\n" + "="*60) - print("CAPTAIN CLAUDE by dadiaar") - print("Multi-Agent Orchestration System") - print("="*60) - print("\nCommands:") - print(" /execute - Full multi-agent execution") - print(" /chat - Chat with Captain") - print(" /agent - Chat with specific agent") - print(" /parallel - Run all agents in parallel") - print(" /cost - Show cost summary") - print(" /quit - Exit") - print("="*60 + "\n") + print("\n" + "="*60, flush=True) + print("CAPTAIN CLAUDE by dadiaar", flush=True) + print("Multi-Agent Orchestration System", flush=True) + print("="*60, flush=True) + print("\nCommands:", flush=True) + print(" /execute - Full multi-agent execution", flush=True) + print(" /chat - Chat with Captain", flush=True) + print(" /agent - Chat with specific agent", flush=True) + print(" /parallel - Run all agents in parallel", flush=True) + print(" /cost - Show cost summary", flush=True) + print(" /quit - Exit", flush=True) + print("="*60 + "\n", flush=True) while True: try: @@ -343,24 +348,24 @@ async def main(): continue if user_input.lower() == "/quit": - print("Goodbye!") + print("Goodbye!", flush=True) break if user_input.lower() == "/cost": - print(f"Cost: {captain.cost_tracker.summary()}") + print(f"Cost: {captain.cost_tracker.summary()}", flush=True) continue if user_input.startswith("/execute "): task = user_input[9:] result = await captain.execute(task) - print(f"\nFinal Output:\n{result['final_output']}\n") + print(f"\nFinal Output:\n{result['final_output']}\n", flush=True) continue if user_input.startswith("/parallel "): task = user_input[10:] agents = ["coder", "reviewer", "researcher"] result = await captain.run_parallel(task, agents) - print(f"\nParallel Results:\n{result}\n") + print(f"\nParallel Results:\n{result}\n", flush=True) continue if user_input.startswith("/agent "): @@ -368,24 +373,24 @@ async def main(): if len(parts) == 2: agent, message = parts response = await captain.chat(message, agent) - print(f"\n[{agent}]: {response}\n") + print(f"\n[{agent}]: {response}\n", flush=True) else: - print("Usage: /agent ") + print("Usage: /agent ", flush=True) continue if user_input.startswith("/chat ") or not user_input.startswith("/"): message = user_input[6:] if user_input.startswith("/chat ") else user_input response = await captain.chat(message) - print(f"\n[Captain]: {response}\n") + print(f"\n[Captain]: {response}\n", flush=True) continue - print("Unknown command. Use /quit to exit.") + print("Unknown command. Use /quit to exit.", flush=True) except KeyboardInterrupt: - print("\nGoodbye!") + print("\nGoodbye!", flush=True) break except Exception as e: - print(f"Error: {e}") + print(f"Error: {e}", flush=True) if __name__ == "__main__":