From bd1f682948cbbabd5305e55b5a67a6ab04d0cd61 Mon Sep 17 00:00:00 2001 From: rgcosta Date: Tue, 31 Mar 2026 10:52:56 +0000 Subject: [PATCH] Add pve-metrics/homelab-api.py --- pve-metrics/homelab-api.py | 102 +++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 pve-metrics/homelab-api.py diff --git a/pve-metrics/homelab-api.py b/pve-metrics/homelab-api.py new file mode 100644 index 0000000..1ba61be --- /dev/null +++ b/pve-metrics/homelab-api.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +import json +import subprocess +import os +from http.server import BaseHTTPRequestHandler, HTTPServer + +# --- CONFIGURATION FLAGS --- +PORT = 8080 +IFACE = "bond0" + +# Toggle features True/False depending on your setup +ENABLE_ZFS = False +ZPOOL = "rpool" + +ENABLE_PVE_STATS = True # Set to False if not running on Proxmox +# --------------------------- + +def run_cmd(cmd): + """Runs a shell command and silently returns None if it fails.""" + try: + return subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL).decode('utf-8').strip() + except Exception: + return None + +class MetricsHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path == '/api/homelab': + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + + data = {} + + try: + # 1. Network & CPU (Includes 1-second delay for accuracy) + rx1 = int(run_cmd(f"cat /sys/class/net/{IFACE}/statistics/rx_bytes") or 0) + tx1 = int(run_cmd(f"cat /sys/class/net/{IFACE}/statistics/tx_bytes") or 0) + + cpu_idle_str = run_cmd("vmstat 1 2 | tail -1 | awk '{print $15}'") + cpu_idle = float(cpu_idle_str) if cpu_idle_str else 100.0 + data["cpu"] = round(100.0 - cpu_idle, 1) + + rx2 = int(run_cmd(f"cat /sys/class/net/{IFACE}/statistics/rx_bytes") or 0) + tx2 = int(run_cmd(f"cat /sys/class/net/{IFACE}/statistics/tx_bytes") or 0) + + data["rx_mb"] = round((rx2 - rx1) / (1024 * 1024), 2) + data["tx_mb"] = round((tx2 - tx1) / (1024 * 1024), 2) + + # 2. RAM Usage + ram_str = run_cmd("free | grep Mem | awk '{print $3/$2 * 100.0}'") + data["ram"] = round(float(ram_str), 1) if ram_str else 0.0 + + # 3. Load Average (Pure Python) + load1, load5, load15 = os.getloadavg() + data["load_avg"] = [round(load1, 2), round(load5, 2), round(load15, 2)] + + # 4. Uptime in Days (Pure Python) + with open('/proc/uptime', 'r') as f: + uptime_seconds = float(f.readline().split()[0]) + data["uptime_days"] = round(uptime_seconds / 86400, 1) + + # 5. Root Disk Usage (Pure Python, lightning fast) + st = os.statvfs('/') + total = st.f_blocks * st.f_frsize + free = st.f_bavail * st.f_frsize + data["root_disk_pct"] = round(((total - free) / total) * 100.0, 1) + + # 6. CPU Temperature (Reads native thermal zone) + try: + with open('/sys/class/thermal/thermal_zone0/temp', 'r') as f: + data["temp_c"] = round(int(f.read()) / 1000.0, 1) + except Exception: + data["temp_c"] = "N/A" # Fails safely if hardware doesn't expose it + + # 7. ZFS Array (Optional) + if ENABLE_ZFS: + zfs_alloc = run_cmd(f"zpool list -H -p -o allocated {ZPOOL}") + zfs_size = run_cmd(f"zpool list -H -p -o size {ZPOOL}") + if zfs_alloc and zfs_size: + data["zfs_pct"] = round((float(zfs_alloc) / float(zfs_size)) * 100.0, 1) + else: + data["zfs_pct"] = "Error" + + # 8. Proxmox VM/LXC Counts (Optional) + if ENABLE_PVE_STATS: + data["vms_running"] = int(run_cmd("qm list | grep running | wc -l") or 0) + data["lxcs_running"] = int(run_cmd("pct list | grep running | wc -l") or 0) + + # Send payload + self.wfile.write(json.dumps(data).encode()) + + except Exception as e: + self.wfile.write(json.dumps({"error": str(e)}).encode()) + else: + self.send_response(404) + self.end_headers() + +if __name__ == '__main__': + server = HTTPServer(('0.0.0.0', PORT), MetricsHandler) + print(f"Starting homelab API on port {PORT}...") + server.serve_forever() \ No newline at end of file