181 lines
6.8 KiB
Python
181 lines
6.8 KiB
Python
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',
|
|
}
|