- 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>
231 lines
7.3 KiB
Python
231 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
TZZR DevOps App - Gestión de despliegues y construcción
|
|
"""
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
import json
|
|
|
|
# Configuración de servidores
|
|
SERVERS = {
|
|
"deck": {"host": "root@72.62.1.113", "name": "DECK"},
|
|
"corp": {"host": "root@92.112.181.188", "name": "CORP"},
|
|
"hst": {"host": "root@72.62.2.84", "name": "HST"},
|
|
"local": {"host": None, "name": "LOCAL (69.62.126.110)"}
|
|
}
|
|
|
|
SSH_KEY = "~/.ssh/tzzr"
|
|
|
|
# Agentes conocidos
|
|
AGENTS = {
|
|
"deck": ["clara", "alfred", "mason", "feldman"],
|
|
"corp": ["margaret", "jared", "mason", "feldman"],
|
|
"hst": ["hst-api", "directus_hst", "directus_lumalia", "directus_personal"]
|
|
}
|
|
|
|
@dataclass
|
|
class CommandResult:
|
|
success: bool
|
|
output: str
|
|
error: str = ""
|
|
|
|
def ssh_cmd(server: str, command: str) -> CommandResult:
|
|
"""Ejecuta comando SSH en servidor remoto"""
|
|
if server == "local":
|
|
full_cmd = command
|
|
else:
|
|
host = SERVERS[server]["host"]
|
|
full_cmd = f'ssh -i {SSH_KEY} {host} "{command}"'
|
|
|
|
try:
|
|
result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=60)
|
|
return CommandResult(
|
|
success=result.returncode == 0,
|
|
output=result.stdout.strip(),
|
|
error=result.stderr.strip()
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return CommandResult(success=False, output="", error="Timeout")
|
|
except Exception as e:
|
|
return CommandResult(success=False, output="", error=str(e))
|
|
|
|
def deploy_agent(agent: str, server: str) -> CommandResult:
|
|
"""Despliega un agente en el servidor especificado"""
|
|
print(f"🚀 Desplegando {agent} en {server}...")
|
|
|
|
# Usar el script de deploy en DECK
|
|
cmd = f"/opt/scripts/deploy-agent.sh {agent} {server}"
|
|
result = ssh_cmd("deck", cmd)
|
|
|
|
if result.success:
|
|
print(f"✅ {agent} desplegado exitosamente en {server}")
|
|
else:
|
|
print(f"❌ Error desplegando {agent}: {result.error}")
|
|
|
|
return result
|
|
|
|
def backup_postgres(server: str = "deck") -> CommandResult:
|
|
"""Ejecuta backup de PostgreSQL"""
|
|
print(f"💾 Ejecutando backup en {server}...")
|
|
result = ssh_cmd(server, "/opt/scripts/backup_postgres.sh")
|
|
|
|
if result.success:
|
|
print("✅ Backup completado")
|
|
else:
|
|
print(f"❌ Error: {result.error}")
|
|
|
|
return result
|
|
|
|
def sync_r2() -> CommandResult:
|
|
"""Sincroniza backups a R2"""
|
|
print("☁️ Sincronizando con R2...")
|
|
result = ssh_cmd("deck", "/opt/scripts/sync_backups_r2.sh")
|
|
|
|
if result.success:
|
|
print("✅ Sincronización completada")
|
|
else:
|
|
print(f"❌ Error: {result.error}")
|
|
|
|
return result
|
|
|
|
def onboard_user(email: str, username: str) -> CommandResult:
|
|
"""Da de alta un nuevo usuario"""
|
|
print(f"👤 Creando usuario {username} ({email})...")
|
|
result = ssh_cmd("deck", f"/opt/scripts/onboard-user.sh {email} {username}")
|
|
|
|
if result.success:
|
|
print(f"✅ Usuario {username} creado")
|
|
else:
|
|
print(f"❌ Error: {result.error}")
|
|
|
|
return result
|
|
|
|
def get_agent_status(server: str) -> dict:
|
|
"""Obtiene estado de agentes en un servidor"""
|
|
agents = AGENTS.get(server, [])
|
|
status = {}
|
|
|
|
for agent in agents:
|
|
result = ssh_cmd(server, f"docker ps --filter name={agent} --format '{{{{.Status}}}}'")
|
|
status[agent] = result.output if result.success else "unknown"
|
|
|
|
return status
|
|
|
|
def restart_agent(agent: str, server: str) -> CommandResult:
|
|
"""Reinicia un agente específico"""
|
|
print(f"🔄 Reiniciando {agent} en {server}...")
|
|
result = ssh_cmd(server, f"docker restart {agent}-service 2>/dev/null || docker restart {agent}")
|
|
|
|
if result.success:
|
|
print(f"✅ {agent} reiniciado")
|
|
else:
|
|
print(f"❌ Error: {result.error}")
|
|
|
|
return result
|
|
|
|
def get_logs(agent: str, server: str, lines: int = 50) -> CommandResult:
|
|
"""Obtiene logs de un agente"""
|
|
container = f"{agent}-service" if agent in ["clara", "alfred", "mason", "feldman", "margaret", "jared"] else agent
|
|
return ssh_cmd(server, f"docker logs {container} --tail {lines} 2>&1")
|
|
|
|
def list_deployments() -> dict:
|
|
"""Lista todos los deployments activos"""
|
|
deployments = {}
|
|
|
|
for server in ["deck", "corp", "hst"]:
|
|
result = ssh_cmd(server, "docker ps --format '{{.Names}}|{{.Status}}|{{.Ports}}'")
|
|
if result.success:
|
|
containers = []
|
|
for line in result.output.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 ""
|
|
})
|
|
deployments[server] = containers
|
|
|
|
return deployments
|
|
|
|
def git_pull_all(server: str) -> dict:
|
|
"""Hace git pull en todos los proyectos de /opt"""
|
|
result = ssh_cmd(server, "for d in /opt/*/; do echo \"=== $d ===\"; cd $d && git pull 2>/dev/null || echo 'no git'; done")
|
|
return {"output": result.output, "success": result.success}
|
|
|
|
# CLI
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("""
|
|
TZZR DevOps App
|
|
===============
|
|
|
|
Uso: python app.py <comando> [argumentos]
|
|
|
|
Comandos:
|
|
deploy <agent> <server> Despliega un agente
|
|
backup [server] Ejecuta backup PostgreSQL
|
|
sync Sincroniza backups a R2
|
|
onboard <email> <username> Alta de usuario
|
|
status <server> Estado de agentes
|
|
restart <agent> <server> Reinicia agente
|
|
logs <agent> <server> Ver logs de agente
|
|
list Lista todos los deployments
|
|
pull <server> Git pull en todos los proyectos
|
|
|
|
Servidores: deck, corp, hst, local
|
|
Agentes DECK: clara, alfred, mason, feldman
|
|
Agentes CORP: margaret, jared, mason, feldman
|
|
""")
|
|
return
|
|
|
|
cmd = sys.argv[1]
|
|
|
|
if cmd == "deploy" and len(sys.argv) >= 4:
|
|
deploy_agent(sys.argv[2], sys.argv[3])
|
|
|
|
elif cmd == "backup":
|
|
server = sys.argv[2] if len(sys.argv) > 2 else "deck"
|
|
backup_postgres(server)
|
|
|
|
elif cmd == "sync":
|
|
sync_r2()
|
|
|
|
elif cmd == "onboard" and len(sys.argv) >= 4:
|
|
onboard_user(sys.argv[2], sys.argv[3])
|
|
|
|
elif cmd == "status" and len(sys.argv) >= 3:
|
|
status = get_agent_status(sys.argv[2])
|
|
print(f"\n📊 Estado de agentes en {sys.argv[2]}:")
|
|
for agent, st in status.items():
|
|
icon = "✅" if "Up" in st else "❌"
|
|
print(f" {icon} {agent}: {st}")
|
|
|
|
elif cmd == "restart" and len(sys.argv) >= 4:
|
|
restart_agent(sys.argv[2], sys.argv[3])
|
|
|
|
elif cmd == "logs" and len(sys.argv) >= 4:
|
|
result = get_logs(sys.argv[2], sys.argv[3])
|
|
print(result.output)
|
|
|
|
elif cmd == "list":
|
|
deployments = list_deployments()
|
|
for server, containers in deployments.items():
|
|
print(f"\n📦 {server.upper()}:")
|
|
for c in containers[:10]:
|
|
print(f" • {c['name']}: {c['status']}")
|
|
if len(containers) > 10:
|
|
print(f" ... y {len(containers)-10} más")
|
|
|
|
elif cmd == "pull" and len(sys.argv) >= 3:
|
|
result = git_pull_all(sys.argv[2])
|
|
print(result["output"])
|
|
|
|
else:
|
|
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|