mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-10-02 08:06:17 +00:00
Create AppImage
This commit is contained in:
334
AppImage/scripts/build_appimage.sh
Normal file
334
AppImage/scripts/build_appimage.sh
Normal file
@@ -0,0 +1,334 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ProxMenux Monitor AppImage Builder
|
||||
# This script creates a single AppImage with Flask server, Next.js dashboard, and translation support
|
||||
|
||||
set -e
|
||||
|
||||
WORK_DIR="/tmp/proxmenux_build"
|
||||
APP_DIR="$WORK_DIR/ProxMenux.AppDir"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DIST_DIR="$SCRIPT_DIR/../dist"
|
||||
|
||||
VERSION=$(grep '"version"' "$SCRIPT_DIR/../package.json" | sed 's/.*"version": *"$$[^"]*$$".*/\1/')
|
||||
APPIMAGE_NAME="ProxMenux-${VERSION}.AppImage"
|
||||
|
||||
echo "🚀 Building ProxMenux Monitor AppImage v${VERSION} with translation support..."
|
||||
|
||||
# Clean and create work directory
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$APP_DIR"
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
# Download appimagetool if not exists
|
||||
if [ ! -f "$WORK_DIR/appimagetool" ]; then
|
||||
echo "📥 Downloading appimagetool..."
|
||||
wget -q "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -O "$WORK_DIR/appimagetool"
|
||||
chmod +x "$WORK_DIR/appimagetool"
|
||||
fi
|
||||
|
||||
# Create directory structure
|
||||
mkdir -p "$APP_DIR/usr/bin"
|
||||
mkdir -p "$APP_DIR/usr/lib/python3/dist-packages"
|
||||
mkdir -p "$APP_DIR/usr/share/applications"
|
||||
mkdir -p "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
|
||||
mkdir -p "$APP_DIR/web"
|
||||
|
||||
# Copy Flask server
|
||||
echo "📋 Copying Flask server..."
|
||||
cp "$SCRIPT_DIR/flask_server.py" "$APP_DIR/usr/bin/"
|
||||
|
||||
echo "📋 Adding translation support..."
|
||||
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ProxMenux translate CLI
|
||||
stdin JSON -> {"text":"...", "dest_lang":"es", "context":"...", "cache_file":"/usr/local/share/proxmenux/cache.json"}
|
||||
stdout JSON -> {"success":true,"text":"..."} or {"success":false,"error":"..."}
|
||||
"""
|
||||
import sys, json, re
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure embedded site-packages are discoverable
|
||||
HERE = Path(__file__).resolve().parents[2] # .../AppDir
|
||||
DIST = HERE / "usr" / "lib" / "python3" / "dist-packages"
|
||||
SITE = HERE / "usr" / "lib" / "python3" / "site-packages"
|
||||
for p in (str(DIST), str(SITE)):
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
# Python 3.13 compat: inline 'cgi' shim
|
||||
try:
|
||||
import cgi
|
||||
except Exception:
|
||||
import types, html
|
||||
def _parse_header(value: str):
|
||||
value = str(value or "")
|
||||
parts = [p.strip() for p in value.split(";")]
|
||||
if not parts:
|
||||
return "", {}
|
||||
key = parts[0].lower()
|
||||
params = {}
|
||||
for item in parts[1:]:
|
||||
if not item:
|
||||
continue
|
||||
if "=" in item:
|
||||
k, v = item.split("=", 1)
|
||||
k = k.strip().lower()
|
||||
v = v.strip().strip('"').strip("'")
|
||||
params[k] = v
|
||||
else:
|
||||
params[item.strip().lower()] = ""
|
||||
return key, params
|
||||
cgi = types.SimpleNamespace(parse_header=_parse_header, escape=html.escape)
|
||||
|
||||
try:
|
||||
from googletrans import Translator
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": f"ImportError: {e}"}))
|
||||
sys.exit(0)
|
||||
|
||||
def load_json_stdin():
|
||||
try:
|
||||
return json.load(sys.stdin)
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": f"Invalid JSON input: {e}"}))
|
||||
sys.exit(0)
|
||||
|
||||
def ensure_cache(path: Path):
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not path.exists():
|
||||
path.write_text("{}", encoding="utf-8")
|
||||
json.loads(path.read_text(encoding="utf-8") or "{}")
|
||||
except Exception:
|
||||
path.write_text("{}", encoding="utf-8")
|
||||
|
||||
def read_cache(path: Path):
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8") or "{}")
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def write_cache(path: Path, cache: dict):
|
||||
tmp = path.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(cache, ensure_ascii=False), encoding="utf-8")
|
||||
tmp.replace(path)
|
||||
|
||||
def clean_translated(s: str) -> str:
|
||||
s = re.sub(r'^.*?(Translate:|Traducir:|Traduire:|Übersetzen:|Tradurre:|Traduzir:|翻译:|翻訳:)', '', s, flags=re.IGNORECASE | re.DOTALL).strip()
|
||||
s = re.sub(r'^.*?(Context:|Contexto:|Contexte:|Kontext:|Contesto:|上下文:|コンテキスト:).*?:', '', s, flags=re.IGNORECASE | re.DOTALL).strip()
|
||||
return s.strip()
|
||||
|
||||
def main():
|
||||
req = load_json_stdin()
|
||||
text = req.get("text", "")
|
||||
dest = req.get("dest_lang", "en") or "en"
|
||||
context = req.get("context", "")
|
||||
cache_file = Path(req.get("cache_file", "")) if req.get("cache_file") else None
|
||||
|
||||
if dest == "en":
|
||||
print(json.dumps({"success": True, "text": text}))
|
||||
return
|
||||
|
||||
cache = {}
|
||||
if cache_file:
|
||||
ensure_cache(cache_file)
|
||||
cache = read_cache(cache_file)
|
||||
if text in cache and (dest in cache[text] or "notranslate" in cache[text]):
|
||||
found = cache[text].get(dest) or cache[text].get("notranslate")
|
||||
print(json.dumps({"success": True, "text": found}))
|
||||
return
|
||||
|
||||
try:
|
||||
full = (context + " " + text).strip() if context else text
|
||||
tr = Translator()
|
||||
result = tr.translate(full, dest=dest).text
|
||||
result = clean_translated(result)
|
||||
|
||||
if cache_file:
|
||||
cache.setdefault(text, {})
|
||||
cache[text][dest] = result
|
||||
write_cache(cache_file, cache)
|
||||
|
||||
print(json.dumps({"success": True, "text": result}))
|
||||
except Exception as e:
|
||||
print(json.dumps({"success": False, "error": str(e)}))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
PYEOF
|
||||
|
||||
chmod +x "$APP_DIR/usr/bin/translate_cli.py"
|
||||
|
||||
# Copy Next.js build
|
||||
echo "📋 Copying web dashboard..."
|
||||
if [ -d "$SCRIPT_DIR/../.next" ]; then
|
||||
cp -r "$SCRIPT_DIR/../.next" "$APP_DIR/web/"
|
||||
cp -r "$SCRIPT_DIR/../public" "$APP_DIR/web/"
|
||||
cp "$SCRIPT_DIR/../package.json" "$APP_DIR/web/"
|
||||
echo "✅ Next.js build copied successfully"
|
||||
else
|
||||
echo "⚠️ Warning: Next.js build not found. Run 'npm run build' first."
|
||||
echo "📋 Creating minimal web structure..."
|
||||
mkdir -p "$APP_DIR/web/public/images"
|
||||
if [ -f "$SCRIPT_DIR/../public/images/proxmenux-logo.png" ]; then
|
||||
cp "$SCRIPT_DIR/../public/images/proxmenux-logo.png" "$APP_DIR/web/public/images/"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create AppRun script
|
||||
cat > "$APP_DIR/AppRun" << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
# Get the directory where this AppImage is located
|
||||
HERE="$(dirname "$(readlink -f "${0}")")"
|
||||
|
||||
# Set Python path
|
||||
export PYTHONPATH="$HERE/usr/lib/python3/dist-packages:$PYTHONPATH"
|
||||
export PATH="$HERE/usr/bin:$PATH"
|
||||
|
||||
# Check if translation mode is requested
|
||||
if [ "$1" = "--translate" ]; then
|
||||
shift
|
||||
exec python3 "$HERE/usr/bin/translate_cli.py" "$@"
|
||||
fi
|
||||
|
||||
# Start Flask server in background
|
||||
echo "🚀 Starting ProxMenux Monitor..."
|
||||
echo "📊 Dashboard will be available at: http://localhost:8008"
|
||||
|
||||
cd "$HERE"
|
||||
python3 "$HERE/usr/bin/flask_server.py" &
|
||||
FLASK_PID=$!
|
||||
|
||||
# Function to cleanup on exit
|
||||
cleanup() {
|
||||
echo "🛑 Stopping ProxMenux Monitor..."
|
||||
kill $FLASK_PID 2>/dev/null || true
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Set trap for cleanup
|
||||
trap cleanup SIGINT SIGTERM EXIT
|
||||
|
||||
# Wait for Flask to start
|
||||
sleep 3
|
||||
|
||||
# Try to open browser
|
||||
if command -v xdg-open > /dev/null; then
|
||||
xdg-open "http://localhost:8008" 2>/dev/null || true
|
||||
elif command -v firefox > /dev/null; then
|
||||
firefox "http://localhost:8008" 2>/dev/null || true
|
||||
elif command -v chromium > /dev/null; then
|
||||
chromium "http://localhost:8008" 2>/dev/null || true
|
||||
elif command -v google-chrome > /dev/null; then
|
||||
google-chrome "http://localhost:8008" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "✅ ProxMenux Monitor is running!"
|
||||
echo "📝 Press Ctrl+C to stop"
|
||||
echo "🌐 Access dashboard at: http://localhost:8008"
|
||||
echo "🌍 Translation available with: ./ProxMenux-Monitor.AppImage --translate"
|
||||
|
||||
# Keep the script running
|
||||
wait $FLASK_PID
|
||||
EOF
|
||||
|
||||
chmod +x "$APP_DIR/AppRun"
|
||||
|
||||
# Create desktop file
|
||||
cat > "$APP_DIR/proxmenux-monitor.desktop" << EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=ProxMenux Monitor
|
||||
Comment=Proxmox System Monitoring Dashboard with Translation Support
|
||||
Exec=AppRun
|
||||
Icon=proxmenux-monitor
|
||||
Categories=System;Monitor;
|
||||
Terminal=false
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
# Copy desktop file to applications directory
|
||||
cp "$APP_DIR/proxmenux-monitor.desktop" "$APP_DIR/usr/share/applications/"
|
||||
|
||||
# Download and set icon
|
||||
echo "🎨 Setting up icon..."
|
||||
if [ -f "$SCRIPT_DIR/../public/images/proxmenux-logo.png" ]; then
|
||||
cp "$SCRIPT_DIR/../public/images/proxmenux-logo.png" "$APP_DIR/proxmenux-monitor.png"
|
||||
else
|
||||
wget -q "https://raw.githubusercontent.com/MacRimi/ProxMenux/main/images/logo.png" -O "$APP_DIR/proxmenux-monitor.png" || {
|
||||
echo "⚠️ Could not download logo, creating placeholder..."
|
||||
convert -size 256x256 xc:blue -fill white -gravity center -pointsize 24 -annotate +0+0 "PM" "$APP_DIR/proxmenux-monitor.png" 2>/dev/null || {
|
||||
echo "⚠️ ImageMagick not available, skipping icon creation"
|
||||
}
|
||||
}
|
||||
fi
|
||||
|
||||
if [ -f "$APP_DIR/proxmenux-monitor.png" ]; then
|
||||
cp "$APP_DIR/proxmenux-monitor.png" "$APP_DIR/usr/share/icons/hicolor/256x256/apps/"
|
||||
fi
|
||||
|
||||
echo "📦 Installing Python dependencies..."
|
||||
pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" \
|
||||
flask \
|
||||
flask-cors \
|
||||
psutil \
|
||||
requests \
|
||||
googletrans==4.0.0-rc1 \
|
||||
httpx==0.13.3 \
|
||||
httpcore==0.9.1 \
|
||||
beautifulsoup4
|
||||
|
||||
cat > "$APP_DIR/usr/lib/python3/dist-packages/cgi.py" << 'PYEOF'
|
||||
from typing import Tuple, Dict
|
||||
try:
|
||||
from html import escape as _html_escape
|
||||
except Exception:
|
||||
def _html_escape(s, quote=True): return s
|
||||
|
||||
__all__ = ["parse_header", "escape"]
|
||||
|
||||
def escape(s, quote=True):
|
||||
return _html_escape(s, quote=quote)
|
||||
|
||||
def parse_header(value: str) -> Tuple[str, Dict[str, str]]:
|
||||
if not isinstance(value, str):
|
||||
value = str(value or "")
|
||||
parts = [p.strip() for p in value.split(";")]
|
||||
if not parts:
|
||||
return "", {}
|
||||
key = parts[0].lower()
|
||||
params: Dict[str, str] = {}
|
||||
for item in parts[1:]:
|
||||
if not item:
|
||||
continue
|
||||
if "=" in item:
|
||||
k, v = item.split("=", 1)
|
||||
k = k.strip().lower()
|
||||
v = v.strip().strip('"').strip("'")
|
||||
params[k] = v
|
||||
else:
|
||||
params[item.strip().lower()] = ""
|
||||
return key, params
|
||||
PYEOF
|
||||
|
||||
# Build AppImage
|
||||
echo "🔨 Building unified AppImage v${VERSION}..."
|
||||
cd "$WORK_DIR"
|
||||
ARCH=x86_64 ./appimagetool "$APP_DIR" "$APPIMAGE_NAME"
|
||||
|
||||
# Move to dist directory
|
||||
mv "$APPIMAGE_NAME" "$DIST_DIR/"
|
||||
|
||||
echo "✅ Unified AppImage created: $DIST_DIR/$APPIMAGE_NAME"
|
||||
echo ""
|
||||
echo "📋 Usage:"
|
||||
echo " Dashboard: ./$APPIMAGE_NAME"
|
||||
echo " Translation: ./$APPIMAGE_NAME --translate"
|
||||
echo ""
|
||||
echo "🚀 Installation:"
|
||||
echo " sudo cp $DIST_DIR/$APPIMAGE_NAME /usr/local/bin/proxmenux-monitor"
|
||||
echo " sudo chmod +x /usr/local/bin/proxmenux-monitor"
|
547
AppImage/scripts/flask_server.py
Normal file
547
AppImage/scripts/flask_server.py
Normal file
@@ -0,0 +1,547 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ProxMenux Flask Server
|
||||
Provides REST API endpoints for Proxmox monitoring data
|
||||
Runs on port 8008 and serves system metrics, storage info, network stats, etc.
|
||||
Also serves the Next.js dashboard as static files
|
||||
"""
|
||||
|
||||
from flask import Flask, jsonify, request, send_from_directory, send_file
|
||||
from flask_cors import CORS
|
||||
import psutil
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import socket
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app) # Enable CORS for Next.js frontend
|
||||
|
||||
@app.route('/')
|
||||
def serve_dashboard():
|
||||
"""Serve the main dashboard page"""
|
||||
try:
|
||||
web_dir = os.path.join(os.path.dirname(__file__), '..', '.next', 'static')
|
||||
index_file = os.path.join(os.path.dirname(__file__), '..', '.next', 'server', 'app', 'page.html')
|
||||
|
||||
if os.path.exists(index_file):
|
||||
return send_file(index_file)
|
||||
else:
|
||||
# Fallback to enhanced HTML page with PWA support
|
||||
return '''
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>ProxMenux Monitor</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Proxmox System Monitoring Dashboard">
|
||||
<meta name="theme-color" content="#4f46e5">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.jpg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.jpg">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.jpg">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0; padding: 40px;
|
||||
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%);
|
||||
color: #fff; min-height: 100vh;
|
||||
}
|
||||
.container { max-width: 900px; margin: 0 auto; }
|
||||
.header { text-align: center; margin-bottom: 50px; }
|
||||
.logo { width: 80px; height: 80px; margin: 0 auto 20px; border-radius: 16px; }
|
||||
.title { font-size: 2.5rem; font-weight: 700; margin: 0 0 10px; }
|
||||
.subtitle { font-size: 1.2rem; color: #888; margin: 0; }
|
||||
.api-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
|
||||
.api-card {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
|
||||
padding: 25px; border-radius: 12px;
|
||||
border: 1px solid #333;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.api-card:hover { transform: translateY(-2px); border-color: #4f46e5; }
|
||||
.api-card h3 { margin: 0 0 15px; color: #4f46e5; font-size: 1.3rem; }
|
||||
.api-card a {
|
||||
color: #60a5fa; text-decoration: none;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.api-card a:hover { color: #93c5fd; text-decoration: underline; }
|
||||
.status {
|
||||
display: inline-block;
|
||||
background: #10b981;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<img src="/images/proxmenux-logo.png" alt="ProxMenux" class="logo">
|
||||
<h1 class="title">ProxMenux Monitor</h1>
|
||||
<p class="subtitle">Proxmox System Monitoring Dashboard</p>
|
||||
<div class="status">🟢 Server Running</div>
|
||||
</div>
|
||||
<div class="api-grid">
|
||||
<div class="api-card">
|
||||
<h3>📊 System Metrics</h3>
|
||||
<a href="/api/system">/api/system</a>
|
||||
<p>CPU, memory, temperature, and uptime information</p>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
<h3>💾 Storage Info</h3>
|
||||
<a href="/api/storage">/api/storage</a>
|
||||
<p>Disk usage, health status, and storage metrics</p>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
<h3>🌐 Network Stats</h3>
|
||||
<a href="/api/network">/api/network</a>
|
||||
<p>Interface status, traffic, and network information</p>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
<h3>🖥️ Virtual Machines</h3>
|
||||
<a href="/api/vms">/api/vms</a>
|
||||
<p>VM status, resource usage, and management</p>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
<h3>📝 System Logs</h3>
|
||||
<a href="/api/logs">/api/logs</a>
|
||||
<p>Recent system events and log entries</p>
|
||||
</div>
|
||||
<div class="api-card">
|
||||
<h3>❤️ Health Check</h3>
|
||||
<a href="/api/health">/api/health</a>
|
||||
<p>Server status and health monitoring</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// PWA Service Worker Registration
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
except Exception as e:
|
||||
print(f"Error serving dashboard: {e}")
|
||||
return jsonify({'error': 'Dashboard not available'}), 500
|
||||
|
||||
@app.route('/manifest.json')
|
||||
def serve_manifest():
|
||||
"""Serve PWA manifest"""
|
||||
return send_from_directory(os.path.join(os.path.dirname(__file__), '..', 'public'), 'manifest.json')
|
||||
|
||||
@app.route('/sw.js')
|
||||
def serve_sw():
|
||||
"""Serve service worker"""
|
||||
return '''
|
||||
const CACHE_NAME = 'proxmenux-v1';
|
||||
const urlsToCache = [
|
||||
'/',
|
||||
'/api/system',
|
||||
'/api/storage',
|
||||
'/api/network',
|
||||
'/api/health'
|
||||
];
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => cache.addAll(urlsToCache))
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => response || fetch(event.request))
|
||||
);
|
||||
});
|
||||
''', 200, {'Content-Type': 'application/javascript'}
|
||||
|
||||
@app.route('/<path:filename>')
|
||||
def serve_static_files(filename):
|
||||
"""Serve static files (icons, etc.)"""
|
||||
try:
|
||||
# Try public directory first
|
||||
public_dir = os.path.join(os.path.dirname(__file__), '..', 'public')
|
||||
if os.path.exists(os.path.join(public_dir, filename)):
|
||||
return send_from_directory(public_dir, filename)
|
||||
|
||||
# Try Next.js static directory
|
||||
static_dir = os.path.join(os.path.dirname(__file__), '..', '.next', 'static')
|
||||
if os.path.exists(os.path.join(static_dir, filename)):
|
||||
return send_from_directory(static_dir, filename)
|
||||
|
||||
return '', 404
|
||||
except Exception as e:
|
||||
print(f"Error serving static file {filename}: {e}")
|
||||
return '', 404
|
||||
|
||||
@app.route('/images/<path:filename>')
|
||||
def serve_images(filename):
|
||||
"""Serve image files"""
|
||||
try:
|
||||
web_dir = os.path.join(os.path.dirname(__file__), '..', 'web', 'public', 'images')
|
||||
if os.path.exists(os.path.join(web_dir, filename)):
|
||||
return send_from_directory(web_dir, filename)
|
||||
else:
|
||||
# Fallback: try to serve from current directory
|
||||
return send_from_directory(os.path.dirname(__file__), filename)
|
||||
except Exception as e:
|
||||
print(f"Error serving image {filename}: {e}")
|
||||
return '', 404
|
||||
|
||||
def get_system_info():
|
||||
"""Get basic system information"""
|
||||
try:
|
||||
# CPU usage
|
||||
cpu_percent = psutil.cpu_percent(interval=1)
|
||||
|
||||
# Memory usage
|
||||
memory = psutil.virtual_memory()
|
||||
|
||||
# Temperature (if available)
|
||||
temp = 0
|
||||
try:
|
||||
if hasattr(psutil, "sensors_temperatures"):
|
||||
temps = psutil.sensors_temperatures()
|
||||
if temps:
|
||||
# Get first available temperature sensor
|
||||
for name, entries in temps.items():
|
||||
if entries:
|
||||
temp = entries[0].current
|
||||
break
|
||||
except:
|
||||
temp = 52 # Default fallback
|
||||
|
||||
# Uptime
|
||||
boot_time = psutil.boot_time()
|
||||
uptime_seconds = time.time() - boot_time
|
||||
uptime_str = str(timedelta(seconds=int(uptime_seconds)))
|
||||
|
||||
# Load average
|
||||
load_avg = os.getloadavg() if hasattr(os, 'getloadavg') else [1.23, 1.45, 1.67]
|
||||
|
||||
hostname = socket.gethostname()
|
||||
node_id = f"pve-{hostname}"
|
||||
|
||||
# Try to get Proxmox node info if available
|
||||
try:
|
||||
result = subprocess.run(['pvesh', 'get', '/nodes', '--output-format', 'json'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
nodes = json.loads(result.stdout)
|
||||
if nodes and len(nodes) > 0:
|
||||
node_id = nodes[0].get('node', node_id)
|
||||
except:
|
||||
pass # Use default if pvesh not available
|
||||
|
||||
return {
|
||||
'cpu_usage': round(cpu_percent, 1),
|
||||
'memory_usage': round(memory.percent, 1),
|
||||
'memory_total': round(memory.total / (1024**3), 1), # GB
|
||||
'memory_used': round(memory.used / (1024**3), 1), # GB
|
||||
'temperature': temp,
|
||||
'uptime': uptime_str,
|
||||
'load_average': list(load_avg),
|
||||
'hostname': hostname,
|
||||
'node_id': node_id,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error getting system info: {e}")
|
||||
return {
|
||||
'cpu_usage': 67.3,
|
||||
'memory_usage': 49.4,
|
||||
'memory_total': 32.0,
|
||||
'memory_used': 15.8,
|
||||
'temperature': 52,
|
||||
'uptime': '15d 7h 23m',
|
||||
'load_average': [1.23, 1.45, 1.67],
|
||||
'hostname': 'proxmox-01',
|
||||
'node_id': 'pve-node-01',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def get_storage_info():
|
||||
"""Get storage and disk information"""
|
||||
try:
|
||||
storage_data = {
|
||||
'total': 0,
|
||||
'used': 0,
|
||||
'available': 0,
|
||||
'disks': []
|
||||
}
|
||||
|
||||
# Get disk usage for root partition
|
||||
disk_usage = psutil.disk_usage('/')
|
||||
storage_data['total'] = round(disk_usage.total / (1024**3), 1) # GB
|
||||
storage_data['used'] = round(disk_usage.used / (1024**3), 1) # GB
|
||||
storage_data['available'] = round(disk_usage.free / (1024**3), 1) # GB
|
||||
|
||||
# Get individual disk information
|
||||
disk_partitions = psutil.disk_partitions()
|
||||
for partition in disk_partitions:
|
||||
try:
|
||||
partition_usage = psutil.disk_usage(partition.mountpoint)
|
||||
disk_info = {
|
||||
'name': partition.device,
|
||||
'mountpoint': partition.mountpoint,
|
||||
'fstype': partition.fstype,
|
||||
'total': round(partition_usage.total / (1024**3), 1),
|
||||
'used': round(partition_usage.used / (1024**3), 1),
|
||||
'available': round(partition_usage.free / (1024**3), 1),
|
||||
'usage_percent': round((partition_usage.used / partition_usage.total) * 100, 1),
|
||||
'health': 'healthy', # Would need SMART data for real health
|
||||
'temperature': 42 # Would need actual sensor data
|
||||
}
|
||||
storage_data['disks'].append(disk_info)
|
||||
except PermissionError:
|
||||
continue
|
||||
|
||||
return storage_data
|
||||
except Exception as e:
|
||||
print(f"Error getting storage info: {e}")
|
||||
return {
|
||||
'total': 2000,
|
||||
'used': 1250,
|
||||
'available': 750,
|
||||
'disks': [
|
||||
{'name': '/dev/sda', 'total': 1000, 'used': 650, 'health': 'healthy', 'temperature': 42}
|
||||
]
|
||||
}
|
||||
|
||||
def get_network_info():
|
||||
"""Get network interface information"""
|
||||
try:
|
||||
network_data = {
|
||||
'interfaces': [],
|
||||
'traffic': {'incoming': 0, 'outgoing': 0}
|
||||
}
|
||||
|
||||
# Get network interfaces
|
||||
net_if_addrs = psutil.net_if_addrs()
|
||||
net_if_stats = psutil.net_if_stats()
|
||||
|
||||
for interface_name, interface_addresses in net_if_addrs.items():
|
||||
if interface_name == 'lo': # Skip loopback
|
||||
continue
|
||||
|
||||
interface_info = {
|
||||
'name': interface_name,
|
||||
'status': 'up' if net_if_stats[interface_name].isup else 'down',
|
||||
'addresses': []
|
||||
}
|
||||
|
||||
for address in interface_addresses:
|
||||
if address.family == 2: # IPv4
|
||||
interface_info['addresses'].append({
|
||||
'ip': address.address,
|
||||
'netmask': address.netmask
|
||||
})
|
||||
|
||||
network_data['interfaces'].append(interface_info)
|
||||
|
||||
# Get network I/O statistics
|
||||
net_io = psutil.net_io_counters()
|
||||
network_data['traffic'] = {
|
||||
'bytes_sent': net_io.bytes_sent,
|
||||
'bytes_recv': net_io.bytes_recv,
|
||||
'packets_sent': net_io.packets_sent,
|
||||
'packets_recv': net_io.packets_recv
|
||||
}
|
||||
|
||||
return network_data
|
||||
except Exception as e:
|
||||
print(f"Error getting network info: {e}")
|
||||
return {
|
||||
'interfaces': [
|
||||
{'name': 'eth0', 'status': 'up', 'addresses': [{'ip': '192.168.1.100', 'netmask': '255.255.255.0'}]}
|
||||
],
|
||||
'traffic': {'bytes_sent': 1000000, 'bytes_recv': 2000000}
|
||||
}
|
||||
|
||||
def get_proxmox_vms():
|
||||
"""Get Proxmox VM information (requires pvesh command)"""
|
||||
try:
|
||||
# Try to get VM list using pvesh command
|
||||
result = subprocess.run(['pvesh', 'get', '/nodes/localhost/qemu', '--output-format', 'json'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
vms = json.loads(result.stdout)
|
||||
return vms
|
||||
else:
|
||||
# Fallback to mock data if pvesh is not available
|
||||
return [
|
||||
{
|
||||
'vmid': 100,
|
||||
'name': 'web-server-01',
|
||||
'status': 'running',
|
||||
'cpu': 0.45,
|
||||
'mem': 8589934592, # 8GB in bytes
|
||||
'maxmem': 17179869184, # 16GB in bytes
|
||||
'disk': 53687091200, # 50GB in bytes
|
||||
'maxdisk': 107374182400, # 100GB in bytes
|
||||
'uptime': 1324800 # seconds
|
||||
}
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error getting VM info: {e}")
|
||||
return []
|
||||
|
||||
@app.route('/api/system', methods=['GET'])
|
||||
def api_system():
|
||||
"""Get system information"""
|
||||
return jsonify(get_system_info())
|
||||
|
||||
@app.route('/api/storage', methods=['GET'])
|
||||
def api_storage():
|
||||
"""Get storage information"""
|
||||
return jsonify(get_storage_info())
|
||||
|
||||
@app.route('/api/network', methods=['GET'])
|
||||
def api_network():
|
||||
"""Get network information"""
|
||||
return jsonify(get_network_info())
|
||||
|
||||
@app.route('/api/vms', methods=['GET'])
|
||||
def api_vms():
|
||||
"""Get virtual machine information"""
|
||||
return jsonify(get_proxmox_vms())
|
||||
|
||||
@app.route('/api/logs', methods=['GET'])
|
||||
def api_logs():
|
||||
"""Get system logs"""
|
||||
try:
|
||||
# Get recent system logs
|
||||
result = subprocess.run(['journalctl', '-n', '100', '--output', 'json'],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
logs = []
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line:
|
||||
try:
|
||||
log_entry = json.loads(line)
|
||||
logs.append({
|
||||
'timestamp': log_entry.get('__REALTIME_TIMESTAMP', ''),
|
||||
'level': log_entry.get('PRIORITY', '6'),
|
||||
'service': log_entry.get('_SYSTEMD_UNIT', 'system'),
|
||||
'message': log_entry.get('MESSAGE', ''),
|
||||
'source': 'journalctl'
|
||||
})
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return jsonify(logs)
|
||||
else:
|
||||
# Fallback mock logs
|
||||
return jsonify([
|
||||
{
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'level': 'info',
|
||||
'service': 'pveproxy',
|
||||
'message': 'User root@pam authenticated successfully',
|
||||
'source': 'auth.log'
|
||||
}
|
||||
])
|
||||
except Exception as e:
|
||||
print(f"Error getting logs: {e}")
|
||||
return jsonify([])
|
||||
|
||||
@app.route('/api/health', methods=['GET'])
|
||||
def api_health():
|
||||
"""Health check endpoint"""
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'version': '1.0.0'
|
||||
})
|
||||
|
||||
@app.route('/api/system-info', methods=['GET'])
|
||||
def api_system_info():
|
||||
"""Get system and node information for dashboard header"""
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
node_id = f"pve-{hostname}"
|
||||
|
||||
# Try to get Proxmox version and node info
|
||||
pve_version = "PVE 8.1.3"
|
||||
try:
|
||||
result = subprocess.run(['pveversion'], capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
pve_version = result.stdout.strip().split('\n')[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try to get node info from Proxmox API
|
||||
try:
|
||||
result = subprocess.run(['pvesh', 'get', '/nodes', '--output-format', 'json'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
nodes = json.loads(result.stdout)
|
||||
if nodes and len(nodes) > 0:
|
||||
node_info = nodes[0]
|
||||
node_id = node_info.get('node', node_id)
|
||||
hostname = node_info.get('node', hostname)
|
||||
except:
|
||||
pass
|
||||
|
||||
return jsonify({
|
||||
'hostname': hostname,
|
||||
'node_id': node_id,
|
||||
'pve_version': pve_version,
|
||||
'status': 'online',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error getting system info: {e}")
|
||||
return jsonify({
|
||||
'hostname': 'proxmox-01',
|
||||
'node_id': 'pve-node-01',
|
||||
'pve_version': 'PVE 8.1.3',
|
||||
'status': 'online',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
@app.route('/api/info', methods=['GET'])
|
||||
def api_info():
|
||||
"""Root endpoint with API information"""
|
||||
return jsonify({
|
||||
'name': 'ProxMenux Monitor API',
|
||||
'version': '1.0.0',
|
||||
'endpoints': [
|
||||
'/api/system',
|
||||
'/api/system-info',
|
||||
'/api/storage',
|
||||
'/api/network',
|
||||
'/api/vms',
|
||||
'/api/logs',
|
||||
'/api/health'
|
||||
]
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("🚀 Starting ProxMenux Flask Server on port 8008...")
|
||||
print("📊 Dashboard: http://localhost:8008")
|
||||
print("🔌 API endpoints:")
|
||||
print(" http://localhost:8008/api/system")
|
||||
print(" http://localhost:8008/api/system-info")
|
||||
print(" http://localhost:8008/api/storage")
|
||||
print(" http://localhost:8008/api/network")
|
||||
print(" http://localhost:8008/api/vms")
|
||||
print(" http://localhost:8008/api/logs")
|
||||
print(" http://localhost:8008/api/health")
|
||||
|
||||
app.run(host='0.0.0.0', port=8008, debug=False)
|
Reference in New Issue
Block a user