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