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:
165
apps/captain.py
Normal file
165
apps/captain.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CAPTAIN CLAUDE - CLI Unificado para el Sistema TZZR
|
||||
Punto de entrada principal para todas las apps
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
APPS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
APPS = {
|
||||
"devops": {
|
||||
"path": f"{APPS_DIR}/devops/app.py",
|
||||
"desc": "Gestión de despliegues y construcción",
|
||||
"alias": ["deploy", "build"]
|
||||
},
|
||||
"deck": {
|
||||
"path": f"{APPS_DIR}/deck/app.py",
|
||||
"desc": "Servidor DECK (72.62.1.113) - Agentes, Mail, Apps",
|
||||
"alias": ["d"]
|
||||
},
|
||||
"corp": {
|
||||
"path": f"{APPS_DIR}/corp/app.py",
|
||||
"desc": "Servidor CORP (92.112.181.188) - Margaret, Jared, Mason, Feldman",
|
||||
"alias": ["c"]
|
||||
},
|
||||
"hst": {
|
||||
"path": f"{APPS_DIR}/hst/app.py",
|
||||
"desc": "Servidor HST (72.62.2.84) - Directus, Imágenes",
|
||||
"alias": ["h"]
|
||||
},
|
||||
"docker": {
|
||||
"path": f"{APPS_DIR}/docker/app.py",
|
||||
"desc": "Gestión Docker multi-servidor",
|
||||
"alias": ["dk"]
|
||||
}
|
||||
}
|
||||
|
||||
def print_banner():
|
||||
print("""
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ CAPTAIN CLAUDE ║
|
||||
║ Sistema Multiagente TZZR ║
|
||||
╠═══════════════════════════════════════════════════════════════╣
|
||||
║ Servidor Central: 69.62.126.110 (Gitea, PostgreSQL) ║
|
||||
║ DECK: 72.62.1.113 │ CORP: 92.112.181.188 ║
|
||||
║ HST: 72.62.2.84 │ R2: Cloudflare Storage ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
def print_help():
|
||||
print_banner()
|
||||
print("Uso: python captain.py <app> [comando] [argumentos]\n")
|
||||
print("Apps disponibles:")
|
||||
print("─" * 60)
|
||||
|
||||
for name, info in APPS.items():
|
||||
aliases = ", ".join(info["alias"]) if info["alias"] else ""
|
||||
alias_str = f" (alias: {aliases})" if aliases else ""
|
||||
print(f" {name}{alias_str}")
|
||||
print(f" └─ {info['desc']}")
|
||||
print()
|
||||
|
||||
print("Ejemplos:")
|
||||
print(" python captain.py devops deploy clara deck")
|
||||
print(" python captain.py deck agents")
|
||||
print(" python captain.py corp pending")
|
||||
print(" python captain.py docker ps all")
|
||||
print(" python captain.py hst directus")
|
||||
print()
|
||||
print("Atajos rápidos:")
|
||||
print(" python captain.py d agents # DECK agents")
|
||||
print(" python captain.py c flows # CORP flows")
|
||||
print(" python captain.py dk dashboard # Docker dashboard")
|
||||
|
||||
def resolve_app(name: str) -> str:
|
||||
"""Resuelve nombre o alias a nombre de app"""
|
||||
if name in APPS:
|
||||
return name
|
||||
|
||||
for app_name, info in APPS.items():
|
||||
if name in info.get("alias", []):
|
||||
return app_name
|
||||
|
||||
return None
|
||||
|
||||
def run_app(app: str, args: list):
|
||||
"""Ejecuta una app con los argumentos dados"""
|
||||
app_name = resolve_app(app)
|
||||
|
||||
if not app_name:
|
||||
print(f"❌ App no encontrada: {app}")
|
||||
print(f" Apps disponibles: {', '.join(APPS.keys())}")
|
||||
return 1
|
||||
|
||||
app_path = APPS[app_name]["path"]
|
||||
|
||||
if not os.path.exists(app_path):
|
||||
print(f"❌ Archivo no encontrado: {app_path}")
|
||||
return 1
|
||||
|
||||
cmd = ["python3", app_path] + args
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd)
|
||||
return result.returncode
|
||||
except KeyboardInterrupt:
|
||||
print("\n⚠️ Interrumpido")
|
||||
return 130
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return 1
|
||||
|
||||
def quick_status():
|
||||
"""Muestra estado rápido de todos los servidores"""
|
||||
print_banner()
|
||||
print("📊 Estado rápido del sistema:\n")
|
||||
|
||||
servers = [
|
||||
("DECK", "root@72.62.1.113"),
|
||||
("CORP", "root@92.112.181.188"),
|
||||
("HST", "root@72.62.2.84")
|
||||
]
|
||||
|
||||
for name, host in servers:
|
||||
cmd = f"ssh -i ~/.ssh/tzzr -o ConnectTimeout=3 {host} 'docker ps -q | wc -l' 2>/dev/null"
|
||||
try:
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
count = result.stdout.strip()
|
||||
print(f" ✅ {name} ({host.split('@')[1]}): {count} contenedores")
|
||||
else:
|
||||
print(f" ❌ {name} ({host.split('@')[1]}): No responde")
|
||||
except:
|
||||
print(f" ❌ {name} ({host.split('@')[1]}): Timeout")
|
||||
|
||||
print()
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print_help()
|
||||
return 0
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
# Comandos especiales
|
||||
if cmd in ["-h", "--help", "help"]:
|
||||
print_help()
|
||||
return 0
|
||||
|
||||
if cmd in ["status", "s"]:
|
||||
quick_status()
|
||||
return 0
|
||||
|
||||
if cmd in ["version", "-v", "--version"]:
|
||||
print("Captain Claude v1.0.0")
|
||||
return 0
|
||||
|
||||
# Ejecutar app
|
||||
args = sys.argv[2:] if len(sys.argv) > 2 else []
|
||||
return run_app(cmd, args)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
343
apps/corp/app.py
Normal file
343
apps/corp/app.py
Normal file
@@ -0,0 +1,343 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TZZR CORP App - Gestión del servidor CORP (92.112.181.188)
|
||||
Servicios: Agentes Margaret/Jared/Mason/Feldman, Context Manager, Apps
|
||||
"""
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime
|
||||
|
||||
SERVER = "root@92.112.181.188"
|
||||
SSH_KEY = "~/.ssh/tzzr"
|
||||
|
||||
# Servicios conocidos en CORP
|
||||
SERVICES = {
|
||||
# Microservicios TZZR
|
||||
"margaret": {"port": 5051, "type": "service", "desc": "Recepción de datos", "path": "/opt/margaret"},
|
||||
"jared": {"port": 5052, "type": "service", "desc": "Automatización de flujos", "path": "/opt/jared"},
|
||||
"mason": {"port": 5053, "type": "service", "desc": "Espacio de enriquecimiento", "path": "/opt/mason"},
|
||||
"feldman": {"port": 5054, "type": "service", "desc": "Registro final + Merkle", "path": "/opt/feldman"},
|
||||
|
||||
# Aplicaciones
|
||||
"nextcloud": {"port": 8080, "type": "app", "desc": "Cloud storage", "url": "nextcloud.tzzrcorp.me"},
|
||||
"directus": {"port": 8055, "type": "app", "desc": "CMS", "url": "tzzrcorp.me"},
|
||||
"vaultwarden": {"port": 8081, "type": "app", "desc": "Passwords", "url": "vault.tzzrcorp.me"},
|
||||
"shlink": {"port": 8082, "type": "app", "desc": "URL shortener", "url": "shlink.tzzrcorp.me"},
|
||||
"addy": {"port": 8083, "type": "app", "desc": "Email aliases", "url": "addy.tzzrcorp.me"},
|
||||
"odoo": {"port": 8069, "type": "app", "desc": "ERP", "url": "erp.tzzrcorp.me"},
|
||||
"ntfy": {"port": 8880, "type": "app", "desc": "Notifications", "url": "ntfy.tzzrcorp.me"},
|
||||
|
||||
# APIs y Automatización
|
||||
"hsu": {"port": 5002, "type": "api", "desc": "HSU User Library", "url": "hsu.tzzrcorp.me"},
|
||||
"windmill": {"port": 8000, "type": "automation", "desc": "Automatización de flujos", "url": "windmill.tzzrcorp.me"},
|
||||
"context-manager": {"port": None, "type": "system", "desc": "Context Manager IA", "path": "/opt/context-manager"},
|
||||
|
||||
# Infraestructura
|
||||
"postgres": {"port": 5432, "type": "db", "desc": "PostgreSQL 16"},
|
||||
"redis": {"port": 6379, "type": "db", "desc": "Cache Redis"},
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
success: bool
|
||||
data: any
|
||||
error: str = ""
|
||||
|
||||
def ssh(cmd: str, timeout: int = 60) -> Result:
|
||||
"""Ejecuta comando en CORP"""
|
||||
full_cmd = f'ssh -i {SSH_KEY} {SERVER} "{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 Exception as e:
|
||||
return Result(success=False, data="", error=str(e))
|
||||
|
||||
def get_all_containers() -> List[Dict]:
|
||||
"""Lista todos los contenedores Docker"""
|
||||
result = ssh("docker ps -a --format '{{.Names}}|{{.Status}}|{{.Ports}}|{{.Image}}'")
|
||||
if not result.success:
|
||||
return []
|
||||
|
||||
containers = []
|
||||
for line in result.data.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 "",
|
||||
"image": parts[3] if len(parts) > 3 else ""
|
||||
})
|
||||
return containers
|
||||
|
||||
def get_service_status(service: str) -> Dict:
|
||||
"""Estado detallado de un servicio"""
|
||||
info = SERVICES.get(service, {})
|
||||
container = f"{service}-service" if info.get("type") == "agent" else service
|
||||
|
||||
status_result = ssh(f"docker ps --filter name={container} --format '{{{{.Status}}}}'")
|
||||
|
||||
health = "unknown"
|
||||
if info.get("port"):
|
||||
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/health 2>/dev/null | head -c 100")
|
||||
health = "healthy" if health_result.success and health_result.data else "unhealthy"
|
||||
|
||||
return {
|
||||
"service": service,
|
||||
"container": container,
|
||||
"status": status_result.data if status_result.success else "not found",
|
||||
"health": health,
|
||||
"port": info.get("port"),
|
||||
"url": info.get("url"),
|
||||
"desc": info.get("desc")
|
||||
}
|
||||
|
||||
def restart_service(service: str) -> Result:
|
||||
"""Reinicia un servicio"""
|
||||
info = SERVICES.get(service, {})
|
||||
container = f"{service}-service" if info.get("type") == "agent" else service
|
||||
return ssh(f"docker restart {container}")
|
||||
|
||||
def get_logs(service: str, lines: int = 100) -> str:
|
||||
"""Obtiene logs de un servicio"""
|
||||
info = SERVICES.get(service, {})
|
||||
container = f"{service}-service" if info.get("type") == "agent" else service
|
||||
result = ssh(f"docker logs {container} --tail {lines} 2>&1")
|
||||
return result.data if result.success else result.error
|
||||
|
||||
def query_agent(agent: str, endpoint: str, method: str = "GET", data: dict = None) -> Result:
|
||||
"""Hace petición a un agente TZZR"""
|
||||
info = SERVICES.get(agent)
|
||||
if not info or info.get("type") != "agent":
|
||||
return Result(success=False, data="", error="Agente no encontrado")
|
||||
|
||||
port = info["port"]
|
||||
|
||||
if method == "GET":
|
||||
cmd = f"curl -s http://localhost:{port}{endpoint}"
|
||||
else:
|
||||
json_data = json.dumps(data) if data else "{}"
|
||||
cmd = f"curl -s -X {method} -H 'Content-Type: application/json' -d '{json_data}' http://localhost:{port}{endpoint}"
|
||||
|
||||
result = ssh(cmd)
|
||||
|
||||
try:
|
||||
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
||||
except:
|
||||
return Result(success=result.success, data=result.data)
|
||||
|
||||
def get_system_stats() -> Dict:
|
||||
"""Estadísticas del sistema"""
|
||||
stats = {}
|
||||
|
||||
mem_result = ssh("free -h | grep Mem | awk '{print $2,$3,$4}'")
|
||||
if mem_result.success:
|
||||
parts = mem_result.data.split()
|
||||
stats["memory"] = {"total": parts[0], "used": parts[1], "available": parts[2]}
|
||||
|
||||
disk_result = ssh("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'")
|
||||
if disk_result.success:
|
||||
parts = disk_result.data.split()
|
||||
stats["disk"] = {"total": parts[0], "used": parts[1], "available": parts[2], "percent": parts[3]}
|
||||
|
||||
containers_result = ssh("docker ps -q | wc -l")
|
||||
stats["containers_running"] = int(containers_result.data) if containers_result.success else 0
|
||||
|
||||
return stats
|
||||
|
||||
def get_postgres_stats() -> Dict:
|
||||
"""Estadísticas de PostgreSQL"""
|
||||
result = ssh("sudo -u postgres psql -c \"SELECT datname, pg_size_pretty(pg_database_size(datname)) as size FROM pg_database WHERE datistemplate = false;\" -t")
|
||||
|
||||
databases = {}
|
||||
if result.success:
|
||||
for line in result.data.split('\n'):
|
||||
if '|' in line:
|
||||
parts = line.split('|')
|
||||
databases[parts[0].strip()] = parts[1].strip()
|
||||
|
||||
return databases
|
||||
|
||||
def ingest_to_margaret(contenedor: dict, auth_key: str) -> Result:
|
||||
"""Envía datos a Margaret para ingestión"""
|
||||
json_data = json.dumps(contenedor)
|
||||
cmd = f"curl -s -X POST -H 'Content-Type: application/json' -H 'X-Auth-Key: {auth_key}' -d '{json_data}' http://localhost:5051/ingest"
|
||||
result = ssh(cmd)
|
||||
|
||||
try:
|
||||
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
||||
except:
|
||||
return Result(success=result.success, data=result.data)
|
||||
|
||||
def execute_flow(flow_id: int, data: dict, auth_key: str) -> Result:
|
||||
"""Ejecuta un flujo en Jared"""
|
||||
json_data = json.dumps(data)
|
||||
cmd = f"curl -s -X POST -H 'Content-Type: application/json' -H 'X-Auth-Key: {auth_key}' -d '{json_data}' http://localhost:5052/ejecutar/{flow_id}"
|
||||
result = ssh(cmd)
|
||||
|
||||
try:
|
||||
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
||||
except:
|
||||
return Result(success=result.success, data=result.data)
|
||||
|
||||
def get_pending_issues() -> Result:
|
||||
"""Obtiene incidencias pendientes de Mason"""
|
||||
result = ssh("curl -s http://localhost:5053/pendientes")
|
||||
try:
|
||||
return Result(success=result.success, data=json.loads(result.data) if result.data else [])
|
||||
except:
|
||||
return Result(success=result.success, data=result.data)
|
||||
|
||||
def verify_merkle(hash: str) -> Result:
|
||||
"""Verifica registro en Feldman"""
|
||||
result = ssh(f"curl -s http://localhost:5054/verify/{hash}")
|
||||
try:
|
||||
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
||||
except:
|
||||
return Result(success=result.success, data=result.data)
|
||||
|
||||
# CLI
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("""
|
||||
TZZR CORP App - Servidor 92.112.181.188
|
||||
=======================================
|
||||
|
||||
Uso: python app.py <comando> [argumentos]
|
||||
|
||||
Comandos Generales:
|
||||
status Estado general del sistema
|
||||
containers Lista todos los contenedores
|
||||
stats Estadísticas del sistema
|
||||
|
||||
Gestión de Servicios:
|
||||
service <name> Estado detallado de servicio
|
||||
restart <service> Reiniciar servicio
|
||||
logs <service> [lines] Ver logs
|
||||
|
||||
Agentes TZZR (Flujo de datos):
|
||||
agents Estado de todos los agentes
|
||||
agent <name> <endpoint> Query a agente (GET)
|
||||
|
||||
margaret Estado de Margaret (Secretaria)
|
||||
jared Estado de Jared (Flujos)
|
||||
mason Estado de Mason (Editor)
|
||||
feldman Estado de Feldman (Contable)
|
||||
|
||||
flows Lista flujos predefinidos en Jared
|
||||
pending Incidencias pendientes en Mason
|
||||
verify <hash> Verificar hash en Feldman
|
||||
|
||||
Bases de Datos:
|
||||
postgres Estadísticas PostgreSQL
|
||||
|
||||
Servicios disponibles:
|
||||
Agentes: margaret, jared, mason, feldman
|
||||
Apps: nextcloud, directus, vaultwarden, shlink, addy, odoo, ntfy
|
||||
APIs: hsu, context-manager
|
||||
DB: postgres, redis
|
||||
""")
|
||||
return
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
if cmd == "status":
|
||||
print("\n📊 CORP Status (92.112.181.188)")
|
||||
print("=" * 40)
|
||||
stats = get_system_stats()
|
||||
print(f"💾 Memoria: {stats.get('memory', {}).get('used', '?')}/{stats.get('memory', {}).get('total', '?')}")
|
||||
print(f"💿 Disco: {stats.get('disk', {}).get('used', '?')}/{stats.get('disk', {}).get('total', '?')} ({stats.get('disk', {}).get('percent', '?')})")
|
||||
print(f"📦 Contenedores: {stats.get('containers_running', 0)}")
|
||||
|
||||
elif cmd == "containers":
|
||||
containers = get_all_containers()
|
||||
print(f"\n📦 Contenedores en CORP ({len(containers)} total):")
|
||||
for c in containers:
|
||||
icon = "✅" if "Up" in c["status"] else "❌"
|
||||
print(f" {icon} {c['name']}: {c['status'][:30]}")
|
||||
|
||||
elif cmd == "service" and len(sys.argv) >= 3:
|
||||
status = get_service_status(sys.argv[2])
|
||||
print(f"\n🔧 {status['service']}")
|
||||
print(f" Container: {status['container']}")
|
||||
print(f" Status: {status['status']}")
|
||||
print(f" Health: {status['health']}")
|
||||
print(f" Desc: {status.get('desc', '')}")
|
||||
if status.get('url'):
|
||||
print(f" URL: https://{status['url']}")
|
||||
|
||||
elif cmd == "restart" and len(sys.argv) >= 3:
|
||||
result = restart_service(sys.argv[2])
|
||||
print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'reiniciado' if result.success else result.error}")
|
||||
|
||||
elif cmd == "logs" and len(sys.argv) >= 3:
|
||||
lines = int(sys.argv[3]) if len(sys.argv) > 3 else 50
|
||||
print(get_logs(sys.argv[2], lines))
|
||||
|
||||
elif cmd == "agents":
|
||||
print("\n🤖 Agentes TZZR en CORP:")
|
||||
print(" PACKET → MARGARET → JARED → MASON/FELDMAN")
|
||||
print()
|
||||
for agent in ["margaret", "jared", "mason", "feldman"]:
|
||||
status = get_service_status(agent)
|
||||
icon = "✅" if "Up" in status["status"] else "❌"
|
||||
health = f"({status['health']})" if status['health'] != "unknown" else ""
|
||||
print(f" {icon} {agent}: {status['status'][:20]} {health}")
|
||||
print(f" └─ {SERVICES[agent]['desc']}")
|
||||
|
||||
elif cmd == "agent" and len(sys.argv) >= 4:
|
||||
result = query_agent(sys.argv[2], sys.argv[3])
|
||||
if result.success:
|
||||
print(json.dumps(result.data, indent=2) if isinstance(result.data, dict) else result.data)
|
||||
else:
|
||||
print(f"❌ Error: {result.error}")
|
||||
|
||||
elif cmd in ["margaret", "jared", "mason", "feldman"]:
|
||||
status = get_service_status(cmd)
|
||||
print(f"\n🤖 {cmd.upper()} - {SERVICES[cmd]['desc']}")
|
||||
print(f" Puerto: {SERVICES[cmd]['port']}")
|
||||
print(f" Status: {status['status']}")
|
||||
print(f" Health: {status['health']}")
|
||||
|
||||
# Endpoints específicos
|
||||
contracts = query_agent(cmd, "/s-contract")
|
||||
if contracts.success and isinstance(contracts.data, dict):
|
||||
print(f" Versión: {contracts.data.get('version', 'N/A')}")
|
||||
|
||||
elif cmd == "flows":
|
||||
result = query_agent("jared", "/flujos")
|
||||
if result.success:
|
||||
flows = result.data if isinstance(result.data, list) else []
|
||||
print("\n📋 Flujos predefinidos en Jared:")
|
||||
for f in flows:
|
||||
print(f" • [{f.get('id')}] {f.get('nombre')}")
|
||||
|
||||
elif cmd == "pending":
|
||||
result = get_pending_issues()
|
||||
if result.success:
|
||||
issues = result.data if isinstance(result.data, list) else []
|
||||
print(f"\n⚠️ Incidencias pendientes en Mason: {len(issues)}")
|
||||
for i in issues[:10]:
|
||||
print(f" • {i.get('id')}: {i.get('tipo', 'N/A')} - {i.get('descripcion', '')[:40]}")
|
||||
|
||||
elif cmd == "verify" and len(sys.argv) >= 3:
|
||||
result = verify_merkle(sys.argv[2])
|
||||
if result.success:
|
||||
print(json.dumps(result.data, indent=2))
|
||||
else:
|
||||
print(f"❌ Error: {result.error}")
|
||||
|
||||
elif cmd == "postgres":
|
||||
dbs = get_postgres_stats()
|
||||
print("\n🐘 PostgreSQL en CORP:")
|
||||
for db, size in dbs.items():
|
||||
print(f" • {db}: {size}")
|
||||
|
||||
else:
|
||||
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
317
apps/deck/app.py
Normal file
317
apps/deck/app.py
Normal file
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TZZR DECK App - Gestión del servidor DECK (72.62.1.113)
|
||||
Servicios: Agentes TZZR, Mailcow, Nextcloud, Odoo, Vaultwarden, etc.
|
||||
"""
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
|
||||
SERVER = "root@72.62.1.113"
|
||||
SSH_KEY = "~/.ssh/tzzr"
|
||||
|
||||
# Servicios conocidos en DECK
|
||||
SERVICES = {
|
||||
# Microservicios TZZR
|
||||
"clara": {"port": 5051, "type": "service", "desc": "Log inmutable"},
|
||||
"alfred": {"port": 5052, "type": "service", "desc": "Automatización de flujos"},
|
||||
"mason": {"port": 5053, "type": "service", "desc": "Espacio de enriquecimiento"},
|
||||
"feldman": {"port": 5054, "type": "service", "desc": "Validador Merkle"},
|
||||
|
||||
# Aplicaciones
|
||||
"nextcloud": {"port": 8084, "type": "app", "desc": "Cloud storage", "url": "cloud.tzzrdeck.me"},
|
||||
"odoo": {"port": 8069, "type": "app", "desc": "ERP", "url": "odoo.tzzrdeck.me", "container": "deck-odoo"},
|
||||
"vaultwarden": {"port": 8085, "type": "app", "desc": "Passwords", "url": "vault.tzzrdeck.me"},
|
||||
"directus": {"port": 8055, "type": "app", "desc": "CMS", "url": "directus.tzzrdeck.me"},
|
||||
"shlink": {"port": 8083, "type": "app", "desc": "URL shortener", "url": "shlink.tzzrdeck.me"},
|
||||
"ntfy": {"port": 8080, "type": "app", "desc": "Notifications", "url": "ntfy.tzzrdeck.me"},
|
||||
"filebrowser": {"port": 8082, "type": "app", "desc": "File manager", "url": "files.tzzrdeck.me"},
|
||||
|
||||
# Infraestructura
|
||||
"postgres": {"port": 5432, "type": "db", "desc": "PostgreSQL con pgvector"},
|
||||
"redis": {"port": 6379, "type": "db", "desc": "Cache Redis"},
|
||||
"mailcow": {"port": 8180, "type": "mail", "desc": "Servidor correo", "url": "mail.tzzr.net"},
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
success: bool
|
||||
data: any
|
||||
error: str = ""
|
||||
|
||||
def ssh(cmd: str, timeout: int = 60) -> Result:
|
||||
"""Ejecuta comando en DECK"""
|
||||
full_cmd = f'ssh -i {SSH_KEY} {SERVER} "{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 Exception as e:
|
||||
return Result(success=False, data="", error=str(e))
|
||||
|
||||
def get_all_containers() -> List[Dict]:
|
||||
"""Lista todos los contenedores Docker"""
|
||||
result = ssh("docker ps -a --format '{{.Names}}|{{.Status}}|{{.Ports}}|{{.Image}}'")
|
||||
if not result.success:
|
||||
return []
|
||||
|
||||
containers = []
|
||||
for line in result.data.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 "",
|
||||
"image": parts[3] if len(parts) > 3 else ""
|
||||
})
|
||||
return containers
|
||||
|
||||
def get_service_status(service: str) -> Dict:
|
||||
"""Estado detallado de un servicio"""
|
||||
info = SERVICES.get(service, {})
|
||||
container = info.get("container", f"{service}-service" if info.get("type") == "service" else service)
|
||||
|
||||
# Estado del contenedor
|
||||
status_result = ssh(f"docker ps --filter name={container} --format '{{{{.Status}}}}'")
|
||||
|
||||
# Logs recientes
|
||||
logs_result = ssh(f"docker logs {container} --tail 5 2>&1")
|
||||
|
||||
# Health check si tiene endpoint
|
||||
health = "unknown"
|
||||
if info.get("type") == "service":
|
||||
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/health 2>/dev/null")
|
||||
health = "healthy" if health_result.success and health_result.data else "unhealthy"
|
||||
|
||||
return {
|
||||
"service": service,
|
||||
"container": container,
|
||||
"status": status_result.data if status_result.success else "not found",
|
||||
"health": health,
|
||||
"port": info.get("port"),
|
||||
"url": info.get("url"),
|
||||
"logs": logs_result.data[:500] if logs_result.success else ""
|
||||
}
|
||||
|
||||
def restart_service(service: str) -> Result:
|
||||
"""Reinicia un servicio"""
|
||||
info = SERVICES.get(service, {})
|
||||
container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service)
|
||||
return ssh(f"docker restart {container}")
|
||||
|
||||
def stop_service(service: str) -> Result:
|
||||
"""Detiene un servicio"""
|
||||
info = SERVICES.get(service, {})
|
||||
container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service)
|
||||
return ssh(f"docker stop {container}")
|
||||
|
||||
def start_service(service: str) -> Result:
|
||||
"""Inicia un servicio"""
|
||||
info = SERVICES.get(service, {})
|
||||
container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service)
|
||||
return ssh(f"docker start {container}")
|
||||
|
||||
def get_logs(service: str, lines: int = 100) -> str:
|
||||
"""Obtiene logs de un servicio"""
|
||||
info = SERVICES.get(service, {})
|
||||
container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service)
|
||||
result = ssh(f"docker logs {container} --tail {lines} 2>&1")
|
||||
return result.data if result.success else result.error
|
||||
|
||||
def get_system_stats() -> Dict:
|
||||
"""Estadísticas del sistema"""
|
||||
stats = {}
|
||||
|
||||
# CPU y memoria
|
||||
mem_result = ssh("free -h | grep Mem | awk '{print $2,$3,$4}'")
|
||||
if mem_result.success:
|
||||
parts = mem_result.data.split()
|
||||
stats["memory"] = {"total": parts[0], "used": parts[1], "available": parts[2]}
|
||||
|
||||
# Disco
|
||||
disk_result = ssh("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'")
|
||||
if disk_result.success:
|
||||
parts = disk_result.data.split()
|
||||
stats["disk"] = {"total": parts[0], "used": parts[1], "available": parts[2], "percent": parts[3]}
|
||||
|
||||
# Contenedores
|
||||
containers_result = ssh("docker ps -q | wc -l")
|
||||
stats["containers_running"] = int(containers_result.data) if containers_result.success else 0
|
||||
|
||||
# Load average
|
||||
load_result = ssh("cat /proc/loadavg | awk '{print $1,$2,$3}'")
|
||||
if load_result.success:
|
||||
parts = load_result.data.split()
|
||||
stats["load"] = {"1m": parts[0], "5m": parts[1], "15m": parts[2]}
|
||||
|
||||
return stats
|
||||
|
||||
def query_service(service: str, endpoint: str, method: str = "GET", data: dict = None) -> Result:
|
||||
"""Hace petición a un servicio TZZR"""
|
||||
info = SERVICES.get(service)
|
||||
if not info or info.get("type") != "service":
|
||||
return Result(success=False, data="", error="Servicio no encontrado")
|
||||
|
||||
port = info["port"]
|
||||
|
||||
if method == "GET":
|
||||
cmd = f"curl -s http://localhost:{port}{endpoint}"
|
||||
else:
|
||||
json_data = json.dumps(data) if data else "{}"
|
||||
cmd = f"curl -s -X {method} -H 'Content-Type: application/json' -d '{json_data}' http://localhost:{port}{endpoint}"
|
||||
|
||||
result = ssh(cmd)
|
||||
|
||||
try:
|
||||
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
||||
except:
|
||||
return Result(success=result.success, data=result.data)
|
||||
|
||||
def get_postgres_stats() -> Dict:
|
||||
"""Estadísticas de PostgreSQL"""
|
||||
result = ssh("sudo -u postgres psql -c \"SELECT datname, pg_size_pretty(pg_database_size(datname)) as size FROM pg_database WHERE datistemplate = false;\" -t")
|
||||
|
||||
databases = {}
|
||||
if result.success:
|
||||
for line in result.data.split('\n'):
|
||||
if '|' in line:
|
||||
parts = line.split('|')
|
||||
databases[parts[0].strip()] = parts[1].strip()
|
||||
|
||||
return databases
|
||||
|
||||
def get_mailcow_status() -> Dict:
|
||||
"""Estado de Mailcow"""
|
||||
containers = ["postfix-mailcow", "dovecot-mailcow", "nginx-mailcow", "mysql-mailcow", "redis-mailcow"]
|
||||
status = {}
|
||||
|
||||
for c in containers:
|
||||
result = ssh(f"docker ps --filter name={c} --format '{{{{.Status}}}}'")
|
||||
status[c.replace("-mailcow", "")] = result.data if result.success and result.data else "stopped"
|
||||
|
||||
return status
|
||||
|
||||
# CLI
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("""
|
||||
TZZR DECK App - Servidor 72.62.1.113
|
||||
====================================
|
||||
|
||||
Uso: python app.py <comando> [argumentos]
|
||||
|
||||
Comandos Generales:
|
||||
status Estado general del sistema
|
||||
containers Lista todos los contenedores
|
||||
stats Estadísticas del sistema
|
||||
|
||||
Gestión de Servicios:
|
||||
service <name> Estado detallado de servicio
|
||||
restart <service> Reiniciar servicio
|
||||
stop <service> Detener servicio
|
||||
start <service> Iniciar servicio
|
||||
logs <service> [lines] Ver logs
|
||||
|
||||
Servicios TZZR:
|
||||
services Estado de todos los servicios
|
||||
query <name> <endpoint> Query a servicio (GET)
|
||||
|
||||
Bases de Datos:
|
||||
postgres Estadísticas PostgreSQL
|
||||
|
||||
Mail:
|
||||
mailcow Estado de Mailcow
|
||||
|
||||
Servicios disponibles:
|
||||
TZZR: clara, alfred, mason, feldman
|
||||
Apps: nextcloud, odoo, vaultwarden, directus, shlink, ntfy, filebrowser
|
||||
DB: postgres, redis
|
||||
Mail: mailcow
|
||||
""")
|
||||
return
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
if cmd == "status":
|
||||
print("\n📊 DECK Status (72.62.1.113)")
|
||||
print("=" * 40)
|
||||
stats = get_system_stats()
|
||||
print(f"💾 Memoria: {stats.get('memory', {}).get('used', '?')}/{stats.get('memory', {}).get('total', '?')}")
|
||||
print(f"💿 Disco: {stats.get('disk', {}).get('used', '?')}/{stats.get('disk', {}).get('total', '?')} ({stats.get('disk', {}).get('percent', '?')})")
|
||||
print(f"📦 Contenedores: {stats.get('containers_running', 0)}")
|
||||
print(f"⚡ Load: {stats.get('load', {}).get('1m', '?')}")
|
||||
|
||||
elif cmd == "containers":
|
||||
containers = get_all_containers()
|
||||
print(f"\n📦 Contenedores en DECK ({len(containers)} total):")
|
||||
for c in containers:
|
||||
icon = "✅" if "Up" in c["status"] else "❌"
|
||||
print(f" {icon} {c['name']}: {c['status'][:30]}")
|
||||
|
||||
elif cmd == "stats":
|
||||
stats = get_system_stats()
|
||||
print(json.dumps(stats, indent=2))
|
||||
|
||||
elif cmd == "service" and len(sys.argv) >= 3:
|
||||
status = get_service_status(sys.argv[2])
|
||||
print(f"\n🔧 {status['service']}")
|
||||
print(f" Container: {status['container']}")
|
||||
print(f" Status: {status['status']}")
|
||||
print(f" Health: {status['health']}")
|
||||
if status.get('url'):
|
||||
print(f" URL: https://{status['url']}")
|
||||
if status.get('port'):
|
||||
print(f" Puerto: {status['port']}")
|
||||
|
||||
elif cmd == "restart" and len(sys.argv) >= 3:
|
||||
result = restart_service(sys.argv[2])
|
||||
print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'reiniciado' if result.success else result.error}")
|
||||
|
||||
elif cmd == "stop" and len(sys.argv) >= 3:
|
||||
result = stop_service(sys.argv[2])
|
||||
print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'detenido' if result.success else result.error}")
|
||||
|
||||
elif cmd == "start" and len(sys.argv) >= 3:
|
||||
result = start_service(sys.argv[2])
|
||||
print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'iniciado' if result.success else result.error}")
|
||||
|
||||
elif cmd == "logs" and len(sys.argv) >= 3:
|
||||
lines = int(sys.argv[3]) if len(sys.argv) > 3 else 50
|
||||
print(get_logs(sys.argv[2], lines))
|
||||
|
||||
elif cmd == "services":
|
||||
print("\n⚙️ Servicios TZZR en DECK:")
|
||||
for svc in ["clara", "alfred", "mason", "feldman"]:
|
||||
status = get_service_status(svc)
|
||||
icon = "✅" if "Up" in status["status"] else "❌"
|
||||
health = f"({status['health']})" if status['health'] != "unknown" else ""
|
||||
print(f" {icon} {svc}: {status['status'][:25]} {health} - {SERVICES[svc]['desc']}")
|
||||
|
||||
elif cmd == "query" and len(sys.argv) >= 4:
|
||||
result = query_service(sys.argv[2], sys.argv[3])
|
||||
if result.success:
|
||||
print(json.dumps(result.data, indent=2) if isinstance(result.data, dict) else result.data)
|
||||
else:
|
||||
print(f"❌ Error: {result.error}")
|
||||
|
||||
elif cmd == "postgres":
|
||||
dbs = get_postgres_stats()
|
||||
print("\n🐘 PostgreSQL en DECK:")
|
||||
for db, size in dbs.items():
|
||||
print(f" • {db}: {size}")
|
||||
|
||||
elif cmd == "mailcow":
|
||||
status = get_mailcow_status()
|
||||
print("\n📧 Mailcow Status:")
|
||||
for service, st in status.items():
|
||||
icon = "✅" if "Up" in st else "❌"
|
||||
print(f" {icon} {service}: {st[:30]}")
|
||||
|
||||
else:
|
||||
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
230
apps/devops/app.py
Normal file
230
apps/devops/app.py
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/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()
|
||||
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()
|
||||
273
apps/hst/app.py
Normal file
273
apps/hst/app.py
Normal file
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TZZR HST App - Gestión del servidor HST (72.62.2.84)
|
||||
Servicios: Directus (3 instancias), Servidor de imágenes, APIs
|
||||
"""
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict
|
||||
|
||||
SERVER = "root@72.62.2.84"
|
||||
SSH_KEY = "~/.ssh/tzzr"
|
||||
|
||||
# Servicios conocidos en HST
|
||||
SERVICES = {
|
||||
# Directus instances
|
||||
"directus_hst": {"port": 8055, "type": "cms", "desc": "Directus HST principal", "url": "hst.tzrtech.org"},
|
||||
"directus_lumalia": {"port": 8056, "type": "cms", "desc": "Directus Lumalia", "url": "lumalia.tzrtech.org"},
|
||||
"directus_personal": {"port": 8057, "type": "cms", "desc": "Directus Personal", "url": "personal.tzrtech.org"},
|
||||
|
||||
# APIs
|
||||
"hst-api": {"port": 5001, "type": "api", "desc": "HST Flask API"},
|
||||
"hst-images": {"port": 80, "type": "web", "desc": "Servidor NGINX imágenes", "url": "tzrtech.org"},
|
||||
|
||||
# Infraestructura
|
||||
"postgres_hst": {"port": 5432, "type": "db", "desc": "PostgreSQL 15"},
|
||||
"filebrowser": {"port": 8081, "type": "app", "desc": "File Browser"},
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
success: bool
|
||||
data: any
|
||||
error: str = ""
|
||||
|
||||
def ssh(cmd: str, timeout: int = 60) -> Result:
|
||||
"""Ejecuta comando en HST"""
|
||||
full_cmd = f'ssh -i {SSH_KEY} {SERVER} "{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 Exception as e:
|
||||
return Result(success=False, data="", error=str(e))
|
||||
|
||||
def get_all_containers() -> List[Dict]:
|
||||
"""Lista todos los contenedores Docker"""
|
||||
result = ssh("docker ps -a --format '{{.Names}}|{{.Status}}|{{.Ports}}|{{.Image}}'")
|
||||
if not result.success:
|
||||
return []
|
||||
|
||||
containers = []
|
||||
for line in result.data.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 "",
|
||||
"image": parts[3] if len(parts) > 3 else ""
|
||||
})
|
||||
return containers
|
||||
|
||||
def get_service_status(service: str) -> Dict:
|
||||
"""Estado detallado de un servicio"""
|
||||
info = SERVICES.get(service, {})
|
||||
|
||||
status_result = ssh(f"docker ps --filter name={service} --format '{{{{.Status}}}}'")
|
||||
|
||||
health = "unknown"
|
||||
if info.get("port") and info.get("type") in ["cms", "api"]:
|
||||
if info.get("type") == "cms":
|
||||
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/server/health 2>/dev/null | head -c 50")
|
||||
else:
|
||||
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/health 2>/dev/null | head -c 50")
|
||||
health = "healthy" if health_result.success and health_result.data else "unhealthy"
|
||||
|
||||
return {
|
||||
"service": service,
|
||||
"status": status_result.data if status_result.success else "not found",
|
||||
"health": health,
|
||||
"port": info.get("port"),
|
||||
"url": info.get("url"),
|
||||
"desc": info.get("desc")
|
||||
}
|
||||
|
||||
def restart_service(service: str) -> Result:
|
||||
"""Reinicia un servicio"""
|
||||
return ssh(f"docker restart {service}")
|
||||
|
||||
def get_logs(service: str, lines: int = 100) -> str:
|
||||
"""Obtiene logs de un servicio"""
|
||||
result = ssh(f"docker logs {service} --tail {lines} 2>&1")
|
||||
return result.data if result.success else result.error
|
||||
|
||||
def get_system_stats() -> Dict:
|
||||
"""Estadísticas del sistema"""
|
||||
stats = {}
|
||||
|
||||
mem_result = ssh("free -h | grep Mem | awk '{print $2,$3,$4}'")
|
||||
if mem_result.success:
|
||||
parts = mem_result.data.split()
|
||||
stats["memory"] = {"total": parts[0], "used": parts[1], "available": parts[2]}
|
||||
|
||||
disk_result = ssh("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'")
|
||||
if disk_result.success:
|
||||
parts = disk_result.data.split()
|
||||
stats["disk"] = {"total": parts[0], "used": parts[1], "available": parts[2], "percent": parts[3]}
|
||||
|
||||
containers_result = ssh("docker ps -q | wc -l")
|
||||
stats["containers_running"] = int(containers_result.data) if containers_result.success else 0
|
||||
|
||||
return stats
|
||||
|
||||
def query_directus(instance: str, endpoint: str, token: str = None) -> Result:
|
||||
"""Hace petición a una instancia de Directus"""
|
||||
info = SERVICES.get(instance)
|
||||
if not info or info.get("type") != "cms":
|
||||
return Result(success=False, data="", error="Instancia Directus no encontrada")
|
||||
|
||||
port = info["port"]
|
||||
auth = f"-H 'Authorization: Bearer {token}'" if token else ""
|
||||
|
||||
cmd = f"curl -s {auth} http://localhost:{port}{endpoint}"
|
||||
result = ssh(cmd)
|
||||
|
||||
try:
|
||||
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
||||
except:
|
||||
return Result(success=result.success, data=result.data)
|
||||
|
||||
def get_directus_collections(instance: str) -> List[str]:
|
||||
"""Lista colecciones de una instancia Directus"""
|
||||
result = query_directus(instance, "/collections")
|
||||
if result.success and isinstance(result.data, dict):
|
||||
collections = result.data.get("data", [])
|
||||
return [c.get("collection") for c in collections if not c.get("collection", "").startswith("directus_")]
|
||||
return []
|
||||
|
||||
def list_images(path: str = "/var/www/images") -> List[str]:
|
||||
"""Lista imágenes en el servidor"""
|
||||
result = ssh(f"ls -la {path} 2>/dev/null | head -20")
|
||||
return result.data.split('\n') if result.success else []
|
||||
|
||||
def get_postgres_databases() -> List[str]:
|
||||
"""Lista bases de datos PostgreSQL"""
|
||||
result = ssh("docker exec postgres_hst psql -U postgres -c '\\l' -t 2>/dev/null")
|
||||
if not result.success:
|
||||
return []
|
||||
|
||||
databases = []
|
||||
for line in result.data.split('\n'):
|
||||
if '|' in line:
|
||||
db = line.split('|')[0].strip()
|
||||
if db and db not in ['template0', 'template1']:
|
||||
databases.append(db)
|
||||
|
||||
return databases
|
||||
|
||||
# CLI
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("""
|
||||
TZZR HST App - Servidor 72.62.2.84
|
||||
==================================
|
||||
|
||||
Uso: python app.py <comando> [argumentos]
|
||||
|
||||
Comandos Generales:
|
||||
status Estado general del sistema
|
||||
containers Lista todos los contenedores
|
||||
stats Estadísticas del sistema
|
||||
|
||||
Gestión de Servicios:
|
||||
service <name> Estado detallado de servicio
|
||||
restart <service> Reiniciar servicio
|
||||
logs <service> [lines] Ver logs
|
||||
|
||||
Directus (CMS):
|
||||
directus Estado de las 3 instancias
|
||||
collections <instance> Lista colecciones de instancia
|
||||
query <instance> <path> Query a Directus API
|
||||
|
||||
Imágenes:
|
||||
images [path] Lista imágenes
|
||||
|
||||
Bases de Datos:
|
||||
postgres Lista bases de datos
|
||||
|
||||
Servicios disponibles:
|
||||
CMS: directus_hst, directus_lumalia, directus_personal
|
||||
API: hst-api
|
||||
Web: hst-images
|
||||
DB: postgres_hst
|
||||
App: filebrowser
|
||||
""")
|
||||
return
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
if cmd == "status":
|
||||
print("\n📊 HST Status (72.62.2.84)")
|
||||
print("=" * 40)
|
||||
stats = get_system_stats()
|
||||
print(f"💾 Memoria: {stats.get('memory', {}).get('used', '?')}/{stats.get('memory', {}).get('total', '?')}")
|
||||
print(f"💿 Disco: {stats.get('disk', {}).get('used', '?')}/{stats.get('disk', {}).get('total', '?')} ({stats.get('disk', {}).get('percent', '?')})")
|
||||
print(f"📦 Contenedores: {stats.get('containers_running', 0)}")
|
||||
|
||||
elif cmd == "containers":
|
||||
containers = get_all_containers()
|
||||
print(f"\n📦 Contenedores en HST ({len(containers)} total):")
|
||||
for c in containers:
|
||||
icon = "✅" if "Up" in c["status"] else "❌"
|
||||
print(f" {icon} {c['name']}: {c['status'][:30]}")
|
||||
|
||||
elif cmd == "service" and len(sys.argv) >= 3:
|
||||
status = get_service_status(sys.argv[2])
|
||||
print(f"\n🔧 {status['service']}")
|
||||
print(f" Status: {status['status']}")
|
||||
print(f" Health: {status['health']}")
|
||||
print(f" Desc: {status.get('desc', '')}")
|
||||
if status.get('url'):
|
||||
print(f" URL: https://{status['url']}")
|
||||
|
||||
elif cmd == "restart" and len(sys.argv) >= 3:
|
||||
result = restart_service(sys.argv[2])
|
||||
print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'reiniciado' if result.success else result.error}")
|
||||
|
||||
elif cmd == "logs" and len(sys.argv) >= 3:
|
||||
lines = int(sys.argv[3]) if len(sys.argv) > 3 else 50
|
||||
print(get_logs(sys.argv[2], lines))
|
||||
|
||||
elif cmd == "directus":
|
||||
print("\n📚 Instancias Directus en HST:")
|
||||
for instance in ["directus_hst", "directus_lumalia", "directus_personal"]:
|
||||
status = get_service_status(instance)
|
||||
icon = "✅" if "Up" in status["status"] else "❌"
|
||||
health = f"({status['health']})" if status['health'] != "unknown" else ""
|
||||
url = f"https://{status['url']}" if status.get('url') else ""
|
||||
print(f" {icon} {instance}: {status['status'][:20]} {health}")
|
||||
print(f" └─ {url}")
|
||||
|
||||
elif cmd == "collections" and len(sys.argv) >= 3:
|
||||
collections = get_directus_collections(sys.argv[2])
|
||||
print(f"\n📋 Colecciones en {sys.argv[2]}:")
|
||||
for c in collections:
|
||||
print(f" • {c}")
|
||||
|
||||
elif cmd == "query" and len(sys.argv) >= 4:
|
||||
result = query_directus(sys.argv[2], sys.argv[3])
|
||||
if result.success:
|
||||
print(json.dumps(result.data, indent=2)[:2000])
|
||||
else:
|
||||
print(f"❌ Error: {result.error}")
|
||||
|
||||
elif cmd == "images":
|
||||
path = sys.argv[2] if len(sys.argv) > 2 else "/var/www/images"
|
||||
images = list_images(path)
|
||||
print(f"\n🖼️ Imágenes en {path}:")
|
||||
for img in images[:20]:
|
||||
print(f" {img}")
|
||||
|
||||
elif cmd == "postgres":
|
||||
dbs = get_postgres_databases()
|
||||
print("\n🐘 Bases de datos PostgreSQL:")
|
||||
for db in dbs:
|
||||
print(f" • {db}")
|
||||
|
||||
else:
|
||||
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user