Files
captain-claude/apps/docker/app.py
ARCHITECT d35f11e2f7 Add apps modules and improve captain_claude logging
- 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>
2026-01-02 01:13:30 +00:00

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()