#!/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()