- 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>
426 lines
14 KiB
Python
426 lines
14 KiB
Python
#!/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 <comando> [argumentos]
|
|
|
|
Servidores: deck, corp, hst (o 'all' para todos)
|
|
|
|
Contenedores:
|
|
ps [server] Lista contenedores activos
|
|
ps -a [server] Lista todos los contenedores
|
|
start <server> <name> Iniciar contenedor
|
|
stop <server> <name> Detener contenedor
|
|
restart <server> <name> Reiniciar contenedor
|
|
rm <server> <name> Eliminar contenedor
|
|
logs <server> <name> Ver logs
|
|
inspect <server> <name> Inspeccionar contenedor
|
|
exec <server> <name> <cmd> Ejecutar comando en contenedor
|
|
find <name> Buscar contenedor en todos los servidores
|
|
|
|
Recursos:
|
|
images <server> Lista imágenes
|
|
networks <server> Lista redes
|
|
volumes <server> Lista volúmenes
|
|
stats <server> Estadísticas de recursos
|
|
df <server> Uso de disco Docker
|
|
|
|
Mantenimiento:
|
|
prune <server> [type] Limpiar recursos (all/images/volumes/networks)
|
|
|
|
Compose:
|
|
up <server> <path> docker compose up -d
|
|
down <server> <path> docker compose down
|
|
build <server> <path> 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()
|