Update AppImage

This commit is contained in:
MacRimi
2025-11-30 16:02:44 +01:00
parent 0a369621a3
commit bb7dacea91
3 changed files with 289 additions and 0 deletions

View File

@@ -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/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/flask_script_runner.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_script_runner.py not found"
echo "📋 Adding translation support..."
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'

View 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()

View File

@@ -36,6 +36,8 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
if BASE_DIR not in sys.path:
sys.path.insert(0, BASE_DIR)
from flask_script_runner import script_runner
import threading
from proxmox_storage_monitor import proxmox_storage_monitor
from flask_terminal_routes import terminal_bp, init_terminal_routes # noqa: E402
from flask_health_routes import health_bp # noqa: E402
@@ -6342,6 +6344,81 @@ def api_vm_config_update(vmid):
pass
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__':
# 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