diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index d44aac5..380e623 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -1,50 +1,60 @@ "use client" -import { useState, useEffect } from "react" +import { useState } from "react" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Badge } from "./ui/badge" import { Progress } from "./ui/progress" -import { Server, Play, Square, Monitor, Cpu, MemoryStick, AlertCircle, HardDrive, Network } from "lucide-react" +import { Button } from "./ui/button" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog" +import { + Server, + Play, + Square, + Monitor, + Cpu, + MemoryStick, + AlertCircle, + HardDrive, + Network, + Power, + RotateCcw, + Download, + StopCircle, +} from "lucide-react" +import useSWR from "swr" interface VMData { vmid: number name: string status: string - type: string // Added type field to distinguish VM from LXC + type: string cpu: number mem: number maxmem: number disk: number maxdisk: number uptime: number - netin?: number // Added network in - netout?: number // Added network out - diskread?: number // Added disk read - diskwrite?: number // Added disk write + netin?: number + netout?: number + diskread?: number + diskwrite?: number } -const fetchVMData = async (): Promise => { - try { - console.log("[v0] Fetching VM data from Flask server...") - const response = await fetch("/api/vms", { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - signal: AbortSignal.timeout(5000), - }) +const fetcher = async (url: string) => { + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + signal: AbortSignal.timeout(5000), + }) - if (!response.ok) { - throw new Error(`Flask server responded with status: ${response.status}`) - } - - const data = await response.json() - console.log("[v0] Successfully fetched VM data from Flask:", data) - return Array.isArray(data) ? data : [] - } catch (error) { - console.error("[v0] Failed to fetch VM data from Flask server:", error) - throw error + if (!response.ok) { + throw new Error(`Flask server responded with status: ${response.status}`) } + + const data = await response.json() + return Array.isArray(data) ? data : [] } const formatBytes = (bytes: number | undefined): string => { @@ -56,30 +66,64 @@ const formatBytes = (bytes: number | undefined): string => { } export function VirtualMachines() { - const [vmData, setVmData] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) + const { + data: vmData, + error, + isLoading, + mutate, + } = useSWR("/api/vms", fetcher, { + refreshInterval: 30000, // Refresh every 30 seconds + revalidateOnFocus: false, + revalidateOnReconnect: true, + }) - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true) - setError(null) - const result = await fetchVMData() - setVmData(result) - } catch (err) { - setError("Flask server not available. Please ensure the server is running.") - } finally { - setLoading(false) + const [selectedVM, setSelectedVM] = useState(null) + const [controlLoading, setControlLoading] = useState(false) + + const handleVMControl = async (vmid: number, action: string) => { + setControlLoading(true) + try { + const response = await fetch(`/api/vms/${vmid}/control`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ action }), + }) + + if (response.ok) { + // Refresh VM data after action + mutate() + setSelectedVM(null) + } else { + console.error("Failed to control VM") } + } catch (error) { + console.error("Error controlling VM:", error) + } finally { + setControlLoading(false) } + } - fetchData() - const interval = setInterval(fetchData, 30000) - return () => clearInterval(interval) - }, []) + const handleDownloadLogs = async (vmid: number) => { + try { + const response = await fetch(`/api/vms/${vmid}/logs`) + if (response.ok) { + const data = await response.json() + const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }) + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = `vm-${vmid}-logs.json` + a.click() + URL.revokeObjectURL(url) + } + } catch (error) { + console.error("Error downloading logs:", error) + } + } - if (loading) { + if (isLoading) { return (
@@ -89,7 +133,7 @@ export function VirtualMachines() { ) } - if (error) { + if (error || !vmData) { return (
@@ -99,7 +143,8 @@ export function VirtualMachines() {
Flask Server Not Available
- {error || "Unable to connect to the Flask server. Please ensure the server is running and try again."} + {error?.message || + "Unable to connect to the Flask server. Please ensure the server is running and try again."}
@@ -156,7 +201,7 @@ export function VirtualMachines() {
- Total VMs + Total VMs & LXCs @@ -179,7 +224,7 @@ export function VirtualMachines() { -
{(totalCPU * 100).toFixed(0)}%
+
{(totalCPU * 100).toFixed(0)}%

Allocated CPU usage

@@ -190,7 +235,7 @@ export function VirtualMachines() { -
{(totalMemory / 1024 ** 3).toFixed(1)} GB
+
{(totalMemory / 1024 ** 3).toFixed(1)} GB

Allocated RAM

@@ -201,7 +246,7 @@ export function VirtualMachines() { -
+
{runningVMs > 0 ? ((totalCPU / runningVMs) * 100).toFixed(0) : 0}%

Average resource utilization

@@ -230,7 +275,11 @@ export function VirtualMachines() { const typeBadge = getTypeBadge(vm.type) return ( -
+
setSelectedVM(vm)} + >
@@ -256,13 +305,13 @@ export function VirtualMachines() {
CPU Usage
-
{cpuPercent}%
- +
{cpuPercent}%
+
Memory Usage
-
+
{memGB} / {maxMemGB} GB
@@ -308,6 +357,124 @@ export function VirtualMachines() { )} + + {/* VM Details Modal */} + setSelectedVM(null)}> + + + + + {selectedVM?.name} - Details + + + + {selectedVM && ( +
+ {/* Basic Information */} +
+

Basic Information

+
+
+
Name
+
{selectedVM.name}
+
+
+
Type
+ + {getTypeBadge(selectedVM.type).label} + +
+
+
VMID
+
{selectedVM.vmid}
+
+
+
Status
+ + {selectedVM.status.toUpperCase()} + +
+
+
CPU Usage
+
{(selectedVM.cpu * 100).toFixed(1)}%
+
+
+
Memory
+
+ {(selectedVM.mem / 1024 ** 3).toFixed(1)} / {(selectedVM.maxmem / 1024 ** 3).toFixed(1)} GB +
+
+
+
Disk
+
+ {(selectedVM.disk / 1024 ** 3).toFixed(1)} / {(selectedVM.maxdisk / 1024 ** 3).toFixed(1)} GB +
+
+
+
Uptime
+
{formatUptime(selectedVM.uptime)}
+
+
+
+ + {/* Control Actions */} +
+

Control Actions

+
+ + + + +
+
+ + {/* Download Logs */} +
+ +
+
+ )} +
+
) } diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 780ead4..2291054 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -998,7 +998,25 @@ def get_bridge_info(bridge_name): bridge_info['members'] = members for member in members: - if member.startswith(('enp', 'eth', 'eno', 'ens', 'wlan', 'wlp')): + # Check if member is a bond first + if member.startswith('bond'): + bridge_info['physical_interface'] = member + print(f"[v0] Bridge {bridge_name} connected to bond: {member}") + + # Get duplex from bond's active slave + bond_info = get_bond_info(member) + if bond_info['active_slave']: + try: + net_if_stats = psutil.net_if_stats() + if bond_info['active_slave'] in net_if_stats: + stats = net_if_stats[bond_info['active_slave']] + bridge_info['physical_duplex'] = 'full' if stats.duplex == 2 else 'half' if stats.duplex == 1 else 'unknown' + print(f"[v0] Bond {member} active slave {bond_info['active_slave']} duplex: {bridge_info['physical_duplex']}") + except Exception as e: + print(f"[v0] Error getting duplex for bond slave {bond_info['active_slave']}: {e}") + break + # Check if member is a physical interface + elif member.startswith(('enp', 'eth', 'eno', 'ens', 'wlan', 'wlp')): bridge_info['physical_interface'] = member print(f"[v0] Bridge {bridge_name} physical interface: {member}") @@ -1409,6 +1427,149 @@ def api_info(): ] }) +@app.route('/api/vms/', methods=['GET']) +def api_vm_details(vmid): + """Get detailed information for a specific VM/LXC""" + try: + result = subprocess.run(['pvesh', 'get', f'/cluster/resources', '--type', 'vm', '--output-format', 'json'], + capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + resources = json.loads(result.stdout) + for resource in resources: + if resource.get('vmid') == vmid: + vm_type = 'lxc' if resource.get('type') == 'lxc' else 'qemu' + node = resource.get('node', 'pve') + + # Get detailed config + config_result = subprocess.run( + ['pvesh', 'get', f'/nodes/{node}/{vm_type}/{vmid}/config', '--output-format', 'json'], + capture_output=True, text=True, timeout=10 + ) + + config = {} + if config_result.returncode == 0: + config = json.loads(config_result.stdout) + + return jsonify({ + **resource, + 'config': config, + 'node': node, + 'vm_type': vm_type + }) + + return jsonify({'error': f'VM/LXC {vmid} not found'}), 404 + else: + return jsonify({'error': 'Failed to get VM details'}), 500 + except Exception as e: + print(f"Error getting VM details: {e}") + return jsonify({'error': str(e)}), 500 + +@app.route('/api/vms//logs', methods=['GET']) +def api_vm_logs(vmid): + """Download logs for a specific VM/LXC""" + try: + # Get VM type and node + result = subprocess.run(['pvesh', 'get', f'/cluster/resources', '--type', 'vm', '--output-format', 'json'], + capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + resources = json.loads(result.stdout) + vm_info = None + for resource in resources: + if resource.get('vmid') == vmid: + vm_info = resource + break + + if not vm_info: + return jsonify({'error': f'VM/LXC {vmid} not found'}), 404 + + vm_type = 'lxc' if vm_info.get('type') == 'lxc' else 'qemu' + node = vm_info.get('node', 'pve') + + # Get task log + log_result = subprocess.run( + ['pvesh', 'get', f'/nodes/{node}/tasks', '--vmid', str(vmid), '--output-format', 'json'], + capture_output=True, text=True, timeout=10 + ) + + logs = [] + if log_result.returncode == 0: + tasks = json.loads(log_result.stdout) + for task in tasks[:50]: # Last 50 tasks + logs.append({ + 'upid': task.get('upid'), + 'type': task.get('type'), + 'status': task.get('status'), + 'starttime': task.get('starttime'), + 'endtime': task.get('endtime'), + 'user': task.get('user') + }) + + return jsonify({ + 'vmid': vmid, + 'name': vm_info.get('name'), + 'type': vm_type, + 'logs': logs + }) + else: + return jsonify({'error': 'Failed to get VM logs'}), 500 + except Exception as e: + print(f"Error getting VM logs: {e}") + return jsonify({'error': str(e)}), 500 + +@app.route('/api/vms//control', methods=['POST']) +def api_vm_control(vmid): + """Control VM/LXC (start, stop, shutdown, reboot)""" + try: + data = request.get_json() + action = data.get('action') # start, stop, shutdown, reboot + + if action not in ['start', 'stop', 'shutdown', 'reboot']: + return jsonify({'error': 'Invalid action'}), 400 + + # Get VM type and node + result = subprocess.run(['pvesh', 'get', f'/cluster/resources', '--type', 'vm', '--output-format', 'json'], + capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + resources = json.loads(result.stdout) + vm_info = None + for resource in resources: + if resource.get('vmid') == vmid: + vm_info = resource + break + + if not vm_info: + return jsonify({'error': f'VM/LXC {vmid} not found'}), 404 + + vm_type = 'lxc' if vm_info.get('type') == 'lxc' else 'qemu' + node = vm_info.get('node', 'pve') + + # Execute action + control_result = subprocess.run( + ['pvesh', 'create', f'/nodes/{node}/{vm_type}/{vmid}/status/{action}'], + capture_output=True, text=True, timeout=30 + ) + + if control_result.returncode == 0: + return jsonify({ + 'success': True, + 'vmid': vmid, + 'action': action, + 'message': f'Successfully executed {action} on {vm_info.get("name")}' + }) + else: + return jsonify({ + 'success': False, + 'error': control_result.stderr + }), 500 + else: + return jsonify({'error': 'Failed to control VM'}), 500 + except Exception as e: + print(f"Error controlling VM: {e}") + return jsonify({'error': str(e)}), 500 + if __name__ == '__main__': print("Starting ProxMenux Flask Server on port 8008...") print("Server will be accessible on all network interfaces (0.0.0.0:8008)")