mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-12-14 16:16:21 +00:00
Update AppImage
This commit is contained in:
@@ -88,6 +88,7 @@ cp "$SCRIPT_DIR/flask_proxmenux_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || ec
|
|||||||
cp "$SCRIPT_DIR/flask_terminal_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_terminal_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"
|
||||||
cp "$SCRIPT_DIR/hardware_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ hardware_monitor.py not found"
|
cp "$SCRIPT_DIR/hardware_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ hardware_monitor.py not found"
|
||||||
cp "$SCRIPT_DIR/proxmox_storage_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ proxmox_storage_monitor.py not found"
|
cp "$SCRIPT_DIR/proxmox_storage_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ proxmox_storage_monitor.py not found"
|
||||||
|
cp "$SCRIPT_DIR/flask_script_runner.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_script_runner.py not found"
|
||||||
|
|
||||||
echo "📋 Adding translation support..."
|
echo "📋 Adding translation support..."
|
||||||
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'
|
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'
|
||||||
|
|||||||
211
AppImage/scripts/flask_script_runner.py
Normal file
211
AppImage/scripts/flask_script_runner.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script Runner System for ProxMenux
|
||||||
|
Executes bash scripts and provides real-time log streaming with interactive menu support
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
class ScriptRunner:
|
||||||
|
"""Manages script execution with real-time log streaming and menu interactions"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.active_sessions = {}
|
||||||
|
self.log_dir = Path("/var/log/proxmenux/scripts")
|
||||||
|
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.interaction_handlers = {}
|
||||||
|
|
||||||
|
def create_session(self, script_name):
|
||||||
|
"""Create a new script execution session"""
|
||||||
|
session_id = str(uuid.uuid4())[:8]
|
||||||
|
log_file = self.log_dir / f"{script_name}_{session_id}_{int(time.time())}.log"
|
||||||
|
|
||||||
|
self.active_sessions[session_id] = {
|
||||||
|
'script_name': script_name,
|
||||||
|
'log_file': str(log_file),
|
||||||
|
'start_time': datetime.now().isoformat(),
|
||||||
|
'status': 'initializing',
|
||||||
|
'process': None,
|
||||||
|
'exit_code': None,
|
||||||
|
'pending_interaction': None
|
||||||
|
}
|
||||||
|
|
||||||
|
return session_id
|
||||||
|
|
||||||
|
def execute_script(self, script_path, session_id, env_vars=None):
|
||||||
|
"""Execute a script in web mode with logging"""
|
||||||
|
if session_id not in self.active_sessions:
|
||||||
|
return {'success': False, 'error': 'Invalid session ID'}
|
||||||
|
|
||||||
|
session = self.active_sessions[session_id]
|
||||||
|
log_file = session['log_file']
|
||||||
|
|
||||||
|
# Prepare environment
|
||||||
|
env = os.environ.copy()
|
||||||
|
env['EXECUTION_MODE'] = 'web'
|
||||||
|
env['LOG_FILE'] = log_file
|
||||||
|
|
||||||
|
if env_vars:
|
||||||
|
env.update(env_vars)
|
||||||
|
|
||||||
|
# Initialize log file
|
||||||
|
with open(log_file, 'w') as f:
|
||||||
|
f.write(json.dumps({
|
||||||
|
'type': 'init',
|
||||||
|
'session_id': session_id,
|
||||||
|
'script': script_path,
|
||||||
|
'timestamp': int(time.time())
|
||||||
|
}) + '\n')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Execute script
|
||||||
|
session['status'] = 'running'
|
||||||
|
process = subprocess.Popen(
|
||||||
|
['/bin/bash', script_path],
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
universal_newlines=True
|
||||||
|
)
|
||||||
|
|
||||||
|
session['process'] = process
|
||||||
|
|
||||||
|
# Monitor output for interactions
|
||||||
|
def monitor_output():
|
||||||
|
for line in process.stdout:
|
||||||
|
with open(log_file, 'a') as f:
|
||||||
|
f.write(line)
|
||||||
|
|
||||||
|
# Check for interaction requests
|
||||||
|
try:
|
||||||
|
if line.strip().startswith('{'):
|
||||||
|
data = json.loads(line.strip())
|
||||||
|
if data.get('type') == 'interaction_request':
|
||||||
|
session['pending_interaction'] = data
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
monitor_thread = threading.Thread(target=monitor_output, daemon=True)
|
||||||
|
monitor_thread.start()
|
||||||
|
|
||||||
|
# Wait for completion
|
||||||
|
process.wait()
|
||||||
|
monitor_thread.join(timeout=5)
|
||||||
|
|
||||||
|
session['exit_code'] = process.returncode
|
||||||
|
session['status'] = 'completed' if process.returncode == 0 else 'failed'
|
||||||
|
session['end_time'] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'session_id': session_id,
|
||||||
|
'exit_code': process.returncode,
|
||||||
|
'log_file': log_file
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session['status'] = 'error'
|
||||||
|
session['error'] = str(e)
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_session_status(self, session_id):
|
||||||
|
"""Get current status of a script execution session"""
|
||||||
|
if session_id not in self.active_sessions:
|
||||||
|
return {'success': False, 'error': 'Session not found'}
|
||||||
|
|
||||||
|
session = self.active_sessions[session_id]
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'session_id': session_id,
|
||||||
|
'status': session['status'],
|
||||||
|
'start_time': session['start_time'],
|
||||||
|
'script_name': session['script_name'],
|
||||||
|
'exit_code': session['exit_code'],
|
||||||
|
'pending_interaction': session.get('pending_interaction')
|
||||||
|
}
|
||||||
|
|
||||||
|
def respond_to_interaction(self, session_id, interaction_id, value):
|
||||||
|
"""Respond to a script interaction request"""
|
||||||
|
if session_id not in self.active_sessions:
|
||||||
|
return {'success': False, 'error': 'Session not found'}
|
||||||
|
|
||||||
|
session = self.active_sessions[session_id]
|
||||||
|
|
||||||
|
# Write response to file that script is waiting for
|
||||||
|
response_file = f"/tmp/nvidia_response_{interaction_id}.json"
|
||||||
|
with open(response_file, 'w') as f:
|
||||||
|
json.dump({
|
||||||
|
'interaction_id': interaction_id,
|
||||||
|
'value': value,
|
||||||
|
'timestamp': int(time.time())
|
||||||
|
}, f)
|
||||||
|
|
||||||
|
# Clear pending interaction
|
||||||
|
session['pending_interaction'] = None
|
||||||
|
|
||||||
|
return {'success': True}
|
||||||
|
|
||||||
|
def stream_logs(self, session_id):
|
||||||
|
"""Generator that yields log entries as they are written"""
|
||||||
|
if session_id not in self.active_sessions:
|
||||||
|
yield json.dumps({'type': 'error', 'message': 'Invalid session ID'})
|
||||||
|
return
|
||||||
|
|
||||||
|
session = self.active_sessions[session_id]
|
||||||
|
log_file = session['log_file']
|
||||||
|
|
||||||
|
# Wait for log file to be created
|
||||||
|
timeout = 10
|
||||||
|
start = time.time()
|
||||||
|
while not os.path.exists(log_file) and (time.time() - start) < timeout:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
if not os.path.exists(log_file):
|
||||||
|
yield json.dumps({'type': 'error', 'message': 'Log file not created'})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Stream log file
|
||||||
|
with open(log_file, 'r') as f:
|
||||||
|
# Start from beginning
|
||||||
|
f.seek(0)
|
||||||
|
|
||||||
|
while session['status'] in ['initializing', 'running']:
|
||||||
|
line = f.readline()
|
||||||
|
if line:
|
||||||
|
# Try to parse as JSON, yield as-is if not JSON
|
||||||
|
try:
|
||||||
|
log_entry = json.loads(line.strip())
|
||||||
|
yield json.dumps(log_entry)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
yield json.dumps({'type': 'raw', 'message': line.strip()})
|
||||||
|
else:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Read any remaining lines after completion
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
log_entry = json.loads(line.strip())
|
||||||
|
yield json.dumps(log_entry)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
yield json.dumps({'type': 'raw', 'message': line.strip()})
|
||||||
|
|
||||||
|
def cleanup_session(self, session_id):
|
||||||
|
"""Clean up a completed session"""
|
||||||
|
if session_id in self.active_sessions:
|
||||||
|
del self.active_sessions[session_id]
|
||||||
|
return {'success': True}
|
||||||
|
return {'success': False, 'error': 'Session not found'}
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
script_runner = ScriptRunner()
|
||||||
@@ -36,6 +36,8 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||||||
if BASE_DIR not in sys.path:
|
if BASE_DIR not in sys.path:
|
||||||
sys.path.insert(0, BASE_DIR)
|
sys.path.insert(0, BASE_DIR)
|
||||||
|
|
||||||
|
from flask_script_runner import script_runner
|
||||||
|
import threading
|
||||||
from proxmox_storage_monitor import proxmox_storage_monitor
|
from proxmox_storage_monitor import proxmox_storage_monitor
|
||||||
from flask_terminal_routes import terminal_bp, init_terminal_routes # noqa: E402
|
from flask_terminal_routes import terminal_bp, init_terminal_routes # noqa: E402
|
||||||
from flask_health_routes import health_bp # noqa: E402
|
from flask_health_routes import health_bp # noqa: E402
|
||||||
@@ -6342,6 +6344,81 @@ def api_vm_config_update(vmid):
|
|||||||
pass
|
pass
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/scripts/execute', methods=['POST'])
|
||||||
|
def execute_script():
|
||||||
|
"""Execute a script with real-time logging"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
script_name = data.get('script_name')
|
||||||
|
script_params = data.get('params', {})
|
||||||
|
|
||||||
|
# Map script names to file paths
|
||||||
|
script_map = {
|
||||||
|
'nvidia_installer': '/usr/local/share/proxmenux/scripts/gpu_tpu/nvidia_installer.sh',
|
||||||
|
}
|
||||||
|
|
||||||
|
if script_name not in script_map:
|
||||||
|
return jsonify({'success': False, 'error': 'Unknown script'}), 400
|
||||||
|
|
||||||
|
script_path = script_map[script_name]
|
||||||
|
|
||||||
|
if not os.path.exists(script_path):
|
||||||
|
return jsonify({'success': False, 'error': 'Script file not found'}), 404
|
||||||
|
|
||||||
|
# Create session and start execution in background thread
|
||||||
|
session_id = script_runner.create_session(script_name)
|
||||||
|
|
||||||
|
def run_script():
|
||||||
|
script_runner.execute_script(script_path, session_id, script_params)
|
||||||
|
|
||||||
|
thread = threading.Thread(target=run_script, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'session_id': session_id
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/scripts/status/<session_id>', methods=['GET'])
|
||||||
|
def get_script_status(session_id):
|
||||||
|
"""Get status of a running script"""
|
||||||
|
try:
|
||||||
|
status = script_runner.get_session_status(session_id)
|
||||||
|
return jsonify(status)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/scripts/respond', methods=['POST'])
|
||||||
|
def respond_to_script():
|
||||||
|
"""Respond to script interaction"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
session_id = data.get('session_id')
|
||||||
|
interaction_id = data.get('interaction_id')
|
||||||
|
value = data.get('value')
|
||||||
|
|
||||||
|
result = script_runner.respond_to_interaction(session_id, interaction_id, value)
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/scripts/logs/<session_id>', methods=['GET'])
|
||||||
|
def stream_script_logs(session_id):
|
||||||
|
"""Stream logs from a running script"""
|
||||||
|
try:
|
||||||
|
def generate():
|
||||||
|
for log_entry in script_runner.stream_logs(session_id):
|
||||||
|
yield f"data: {log_entry}\n\n"
|
||||||
|
|
||||||
|
return Response(generate(), mimetype='text/event-stream')
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# API endpoints available at: /api/system, /api/system-info, /api/storage, /api/proxmox-storage, /api/network, /api/vms, /api/logs, /api/health, /api/hardware, /api/prometheus, /api/node/metrics
|
# API endpoints available at: /api/system, /api/system-info, /api/storage, /api/proxmox-storage, /api/network, /api/vms, /api/logs, /api/health, /api/hardware, /api/prometheus, /api/node/metrics
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user