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>
This commit is contained in:
425
apps/docker/app.py
Normal file
425
apps/docker/app.py
Normal file
@@ -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 <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()
|
||||
Reference in New Issue
Block a user