mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-22 13:36:17 +00:00
Update AppImage
This commit is contained in:
@@ -85,6 +85,7 @@ cp "$SCRIPT_DIR/health_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠
|
||||
cp "$SCRIPT_DIR/health_persistence.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ health_persistence.py not found"
|
||||
cp "$SCRIPT_DIR/flask_health_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_health_routes.py not found"
|
||||
cp "$SCRIPT_DIR/flask_proxmenux_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_proxmenux_routes.py not found"
|
||||
cp "$SCRIPT_DIR/flask_terminal_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_terminal_routes.py not found"
|
||||
|
||||
echo "📋 Adding translation support..."
|
||||
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'
|
||||
@@ -292,6 +293,7 @@ pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" \
|
||||
googletrans==4.0.0-rc1 \
|
||||
httpx==0.13.3 \
|
||||
httpcore==0.9.1 \
|
||||
flask-sock \
|
||||
beautifulsoup4
|
||||
|
||||
cat > "$APP_DIR/usr/lib/python3/dist-packages/cgi.py" << 'PYEOF'
|
||||
|
||||
@@ -1,92 +1,119 @@
|
||||
#!/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
|
||||
|
||||
- Provides REST API endpoints for Proxmox monitoring (system, storage, network, VMs, etc.)
|
||||
- Serves the Next.js dashboard as static files
|
||||
- Integrates a web terminal powered by xterm.js
|
||||
"""
|
||||
|
||||
from flask import Flask, jsonify, request, send_from_directory, send_file
|
||||
from flask_cors import CORS
|
||||
import psutil
|
||||
import subprocess
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import select
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
|
||||
# Cache for Proxmox node name (to avoid repeated API calls)
|
||||
_proxmox_node_cache = {'name': None, 'timestamp': 0}
|
||||
|
||||
def get_proxmox_node_name():
|
||||
"""
|
||||
Get the actual Proxmox node name from the Proxmox API.
|
||||
Uses cache to avoid repeated API calls.
|
||||
Falls back to short hostname if API call fails.
|
||||
"""
|
||||
import time
|
||||
cache_duration = 300 # 5 minutes cache
|
||||
|
||||
current_time = time.time()
|
||||
if _proxmox_node_cache['name'] and (current_time - _proxmox_node_cache['timestamp']) < cache_duration:
|
||||
return _proxmox_node_cache['name']
|
||||
|
||||
try:
|
||||
# Query Proxmox API directly to get the actual node name
|
||||
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:
|
||||
# Get the first node name (in most cases there's only one local node)
|
||||
node_name = nodes[0].get('node', '')
|
||||
if node_name:
|
||||
_proxmox_node_cache['name'] = node_name
|
||||
_proxmox_node_cache['timestamp'] = current_time
|
||||
return node_name
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get Proxmox node name from API: {e}")
|
||||
|
||||
# Fallback to short hostname (without domain) if API call fails
|
||||
hostname = socket.gethostname()
|
||||
short_hostname = hostname.split('.')[0]
|
||||
return short_hostname
|
||||
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime, timedelta
|
||||
import re # Added for regex matching
|
||||
import select # Added for non-blocking read
|
||||
import shutil # Added for shutil.which
|
||||
import xml.etree.ElementTree as ET # Added for XML parsing
|
||||
import math # Imported math for format_bytes function
|
||||
import urllib.parse # Added for URL encoding
|
||||
import platform # Added for platform.release()
|
||||
import hashlib
|
||||
import secrets
|
||||
import jwt
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
|
||||
from flask_health_routes import health_bp
|
||||
import jwt
|
||||
import psutil
|
||||
from flask import Flask, jsonify, request, send_file, send_from_directory
|
||||
from flask_cors import CORS
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
# Ensure local imports work even if working directory changes
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if BASE_DIR not in sys.path:
|
||||
sys.path.insert(0, BASE_DIR)
|
||||
|
||||
from flask_auth_routes import auth_bp
|
||||
from flask_proxmenux_routes import proxmenux_bp
|
||||
from jwt_middleware import require_auth
|
||||
from flask_terminal_routes import terminal_bp, init_terminal_routes # noqa: E402
|
||||
from flask_health_routes import health_bp # noqa: E402
|
||||
from flask_auth_routes import auth_bp # noqa: E402
|
||||
from flask_proxmenux_routes import proxmenux_bp # noqa: E402
|
||||
from jwt_middleware import require_auth # noqa: E402
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Logging
|
||||
# -------------------------------------------------------------------
|
||||
logger = logging.getLogger("proxmenux.flask")
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Proxmox node name cache
|
||||
# -------------------------------------------------------------------
|
||||
_PROXMOX_NODE_CACHE = {"name": None, "timestamp": 0.0}
|
||||
_PROXMOX_NODE_CACHE_TTL = 300 # seconds (5 minutes)
|
||||
|
||||
|
||||
def get_proxmox_node_name() -> str:
|
||||
"""
|
||||
Retrieve the real Proxmox node name.
|
||||
|
||||
- First tries reading from: `pvesh get /nodes`
|
||||
- Uses an in-memory cache to avoid repeated API calls
|
||||
- Falls back to the short hostname if the API call fails
|
||||
"""
|
||||
now = time.time()
|
||||
cached_name = _PROXMOX_NODE_CACHE.get("name")
|
||||
cached_ts = _PROXMOX_NODE_CACHE.get("timestamp", 0.0)
|
||||
|
||||
# Cache hit
|
||||
if cached_name and (now - float(cached_ts)) < _PROXMOX_NODE_CACHE_TTL:
|
||||
return str(cached_name)
|
||||
|
||||
# Try Proxmox API
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pvesh", "get", "/nodes", "--output-format", "json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
check=False,
|
||||
)
|
||||
|
||||
if result.returncode == 0 and result.stdout:
|
||||
nodes = json.loads(result.stdout)
|
||||
if isinstance(nodes, list) and nodes:
|
||||
node_name = nodes[0].get("node")
|
||||
if node_name:
|
||||
_PROXMOX_NODE_CACHE["name"] = node_name
|
||||
_PROXMOX_NODE_CACHE["timestamp"] = now
|
||||
return node_name
|
||||
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to get Proxmox node name from API: %s", exc)
|
||||
|
||||
# Fallback: short hostname (without domain)
|
||||
hostname = socket.gethostname()
|
||||
short_hostname = hostname.split(".", 1)[0]
|
||||
return short_hostname
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Flask application and Blueprints
|
||||
# -------------------------------------------------------------------
|
||||
app = Flask(__name__)
|
||||
CORS(app) # Enable CORS for Next.js frontend
|
||||
|
||||
# Register Blueprints
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(health_bp)
|
||||
app.register_blueprint(proxmenux_bp)
|
||||
|
||||
# Initialize terminal / WebSocket routes
|
||||
init_terminal_routes(app)
|
||||
|
||||
|
||||
def identify_gpu_type(name, vendor=None, bus=None, driver=None):
|
||||
|
||||
129
AppImage/scripts/flask_terminal_routes.py
Normal file
129
AppImage/scripts/flask_terminal_routes.py
Normal file
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ProxMenux Terminal WebSocket Routes
|
||||
Provides a WebSocket endpoint for interactive terminal sessions
|
||||
"""
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_sock import Sock
|
||||
import subprocess
|
||||
import os
|
||||
import pty
|
||||
import select
|
||||
import struct
|
||||
import fcntl
|
||||
import termios
|
||||
import signal
|
||||
|
||||
terminal_bp = Blueprint('terminal', __name__)
|
||||
sock = Sock()
|
||||
|
||||
# Active terminal sessions
|
||||
active_sessions = {}
|
||||
|
||||
@terminal_bp.route('/api/terminal/health', methods=['GET'])
|
||||
def terminal_health():
|
||||
"""Health check for terminal service"""
|
||||
return {'success': True, 'active_sessions': len(active_sessions)}
|
||||
|
||||
def set_winsize(fd, rows, cols):
|
||||
"""Set terminal window size"""
|
||||
try:
|
||||
winsize = struct.pack('HHHH', rows, cols, 0, 0)
|
||||
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
|
||||
except Exception as e:
|
||||
print(f"Error setting window size: {e}")
|
||||
|
||||
@sock.route('/ws/terminal')
|
||||
def terminal_websocket(ws):
|
||||
"""WebSocket endpoint for terminal sessions"""
|
||||
|
||||
# Create pseudo-terminal
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
|
||||
# Start bash process
|
||||
shell_process = subprocess.Popen(
|
||||
['/bin/bash', '-i'],
|
||||
stdin=slave_fd,
|
||||
stdout=slave_fd,
|
||||
stderr=slave_fd,
|
||||
preexec_fn=os.setsid,
|
||||
env=dict(os.environ, TERM='xterm-256color', PS1='\\u@\\h:\\w\\$ ')
|
||||
)
|
||||
|
||||
session_id = id(ws)
|
||||
active_sessions[session_id] = {
|
||||
'process': shell_process,
|
||||
'master_fd': master_fd
|
||||
}
|
||||
|
||||
# Set non-blocking mode for master_fd
|
||||
flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||
|
||||
# Set initial terminal size
|
||||
set_winsize(master_fd, 24, 80)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Use select to wait for data from either WebSocket or PTY
|
||||
readable, _, _ = select.select([ws.sock, master_fd], [], [], 0.1)
|
||||
|
||||
# Read from WebSocket (user input)
|
||||
if ws.sock in readable:
|
||||
try:
|
||||
data = ws.receive(timeout=0)
|
||||
if data is None:
|
||||
break
|
||||
|
||||
# Handle special commands (optional)
|
||||
if data.startswith('\x1b[8;'): # Terminal resize
|
||||
# Parse resize: ESC[8;{rows};{cols}t
|
||||
try:
|
||||
parts = data[4:-1].split(';')
|
||||
rows, cols = int(parts[0]), int(parts[1])
|
||||
set_winsize(master_fd, rows, cols)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
# Send input to bash
|
||||
os.write(master_fd, data.encode('utf-8'))
|
||||
except:
|
||||
break
|
||||
|
||||
# Read from PTY (bash output)
|
||||
if master_fd in readable:
|
||||
try:
|
||||
output = os.read(master_fd, 4096)
|
||||
if output:
|
||||
ws.send(output.decode('utf-8', errors='ignore'))
|
||||
except OSError:
|
||||
# PTY closed
|
||||
break
|
||||
|
||||
# Check if process is still alive
|
||||
if shell_process.poll() is not None:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"Terminal session error: {e}")
|
||||
finally:
|
||||
# Cleanup
|
||||
try:
|
||||
shell_process.terminate()
|
||||
shell_process.wait(timeout=1)
|
||||
except:
|
||||
shell_process.kill()
|
||||
|
||||
os.close(master_fd)
|
||||
os.close(slave_fd)
|
||||
|
||||
if session_id in active_sessions:
|
||||
del active_sessions[session_id]
|
||||
|
||||
ws.close()
|
||||
|
||||
def init_terminal_routes(app):
|
||||
"""Initialize terminal routes with Flask app"""
|
||||
sock.init_app(app)
|
||||
app.register_blueprint(terminal_bp)
|
||||
Reference in New Issue
Block a user