import os import glob import subprocess import json from config import RUNNING_ON_MAIN_SERVER, VM_HOST, VM_PORT, VM_KEY, VM_USER def _run(cmd, timeout=20): try: r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) return r.stdout.strip(), r.stderr.strip() except Exception as e: return '', str(e) # ──────────────────────────────────────────────────────────────── # BACKUPS # ──────────────────────────────────────────────────────────────── def get_local_backups(): stdout, _ = _run("ls -t /root/backups/myapps-backup-*.tar.gz 2>/dev/null | head -20") files = [] if stdout: for line in stdout.split('\n'): line = line.strip() if line: files.append(os.path.basename(line)) return files def get_vm_backups(): vm_backups = [] if RUNNING_ON_MAIN_SERVER: try: cmd = ( f"ssh -i {VM_KEY} -p {VM_PORT} " f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 " f"{VM_USER}@{VM_HOST} " f"'ls -t /backups/main-server/myapps-backup-*.tar.gz 2>/dev/null | head -20'" ) stdout, _ = _run(cmd, timeout=20) if stdout: for line in stdout.split('\n'): line = line.strip() if line and '.tar.gz' in line: vm_backups.append(os.path.basename(line)) except Exception as e: print(f"[backups] VM backup fetch error: {e}") else: backup_dir = '/backups/main-server' if os.path.exists(backup_dir): files = glob.glob(f'{backup_dir}/myapps-backup-*.tar.gz') files.sort(key=os.path.getmtime, reverse=True) vm_backups = [os.path.basename(f) for f in files[:20]] return vm_backups # ──────────────────────────────────────────────────────────────── # ROOT CONTAINERS # ──────────────────────────────────────────────────────────────── def get_containers(): """Root app containers only (filtered).""" stdout, _ = _run( "docker ps -a --format '{{.Names}}|{{.Status}}|{{.Image}}|{{.Ports}}' 2>/dev/null | " "grep -E 'frappe|nextcloud|mautic|n8n|odoo'" ) containers = [] if stdout: for line in stdout.split('\n'): if '|' not in line: continue parts = line.split('|') containers.append({ 'name': parts[0].strip(), 'status': parts[1].strip(), 'image': parts[2].strip(), 'ports': parts[3].strip() if len(parts) > 3 else '', 'owner': 'root', }) return containers def get_all_root_containers(): """ALL root docker containers (unfiltered).""" stdout, _ = _run( "docker ps -a --format '{{.Names}}|{{.Status}}|{{.Image}}|{{.Ports}}' 2>/dev/null" ) containers = [] if stdout: for line in stdout.split('\n'): if '|' not in line: continue parts = line.split('|') containers.append({ 'name': parts[0].strip(), 'status': parts[1].strip(), 'image': parts[2].strip(), 'ports': parts[3].strip() if len(parts) > 3 else '', 'owner': 'root', }) return containers # ──────────────────────────────────────────────────────────────── # CONTAINER STATS # ──────────────────────────────────────────────────────────────── def get_container_stats(docker_socket=None): """One-shot stats snapshot. Returns dict keyed by container name.""" if docker_socket: cmd = ( f"DOCKER_HOST=unix://{docker_socket} " f"docker stats --no-stream --format " f"'{{{{.Name}}}}|{{{{.CPUPerc}}}}|{{{{.MemUsage}}}}|{{{{.MemPerc}}}}|{{{{.NetIO}}}}|{{{{.BlockIO}}}}' 2>/dev/null" ) else: cmd = ( "docker stats --no-stream --format " "'{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}' 2>/dev/null" ) stdout, _ = _run(cmd, timeout=30) stats = {} if stdout: for line in stdout.split('\n'): if '|' not in line: continue parts = line.split('|') if len(parts) < 6: continue name = parts[0].strip() stats[name] = { 'cpu': parts[1].strip(), 'mem': parts[2].strip(), 'mem_pct': parts[3].strip(), 'net': parts[4].strip(), 'block': parts[5].strip(), } return stats def get_all_stats(): """Stats for root + all rootless-user containers combined.""" all_stats = get_container_stats() try: import pwd for pw in pwd.getpwall(): if pw.pw_uid < 1000 or pw.pw_name == 'nobody': continue sock = f"/run/user/{pw.pw_uid}/docker.sock" if os.path.exists(sock): user_stats = get_container_stats(docker_socket=sock) all_stats.update(user_stats) except Exception as e: print(f"[stats] Error: {e}") return all_stats def get_system_info(): """Host-level system stats.""" cpu_out, _ = _run("top -bn1 | grep 'Cpu(s)' | awk '{print $2+$4}'") mem_out, _ = _run("free -m | awk 'NR==2{printf \"%s/%sMB\", $3, $2}'") mem_pct, _ = _run("free | awk 'NR==2{printf \"%.0f\", $3/$2*100}'") disk_out, _ = _run("df -h / | awk 'NR==2{printf \"%s/%s\", $3, $2}'") disk_pct, _ = _run("df / | awk 'NR==2{print $5}' | tr -d '%'") load_out, _ = _run("cat /proc/loadavg | awk '{print $1, $2, $3}'") uptime_out, _ = _run("uptime -p") docker_v, _ = _run("docker --version | cut -d' ' -f3 | tr -d ','") return { 'cpu_pct': cpu_out or '0', 'memory': mem_out or 'N/A', 'mem_pct': mem_pct or '0', 'disk': disk_out or 'N/A', 'disk_pct': disk_pct or '0', 'load': load_out or 'N/A', 'uptime': uptime_out or 'N/A', 'docker_v': docker_v or 'N/A', }