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:
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