mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-10-03 16:46:18 +00:00
Update AppImage
This commit is contained in:
@@ -5,7 +5,7 @@ import { Badge } from "./ui/badge"
|
|||||||
import { Button } from "./ui/button"
|
import { Button } from "./ui/button"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
|
||||||
import { SystemOverview } from "./system-overview"
|
import { SystemOverview } from "./system-overview"
|
||||||
import { StorageMetrics } from "./storage-metrics"
|
import { StorageOverview } from "./storage-overview"
|
||||||
import { NetworkMetrics } from "./network-metrics"
|
import { NetworkMetrics } from "./network-metrics"
|
||||||
import { VirtualMachines } from "./virtual-machines"
|
import { VirtualMachines } from "./virtual-machines"
|
||||||
import { SystemLogs } from "./system-logs"
|
import { SystemLogs } from "./system-logs"
|
||||||
@@ -47,7 +47,8 @@ export function ProxmoxDashboard() {
|
|||||||
console.log("[v0] Fetching system data from Flask server...")
|
console.log("[v0] Fetching system data from Flask server...")
|
||||||
console.log("[v0] Current window location:", window.location.href)
|
console.log("[v0] Current window location:", window.location.href)
|
||||||
|
|
||||||
const apiUrl = "/api/system"
|
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||||
|
const apiUrl = `${baseUrl}/api/system`
|
||||||
|
|
||||||
console.log("[v0] API URL:", apiUrl)
|
console.log("[v0] API URL:", apiUrl)
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ export function ProxmoxDashboard() {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
cache: "no-store",
|
||||||
})
|
})
|
||||||
console.log("[v0] Response status:", response.status)
|
console.log("[v0] Response status:", response.status)
|
||||||
|
|
||||||
@@ -104,7 +106,7 @@ export function ProxmoxDashboard() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSystemData()
|
fetchSystemData()
|
||||||
const interval = setInterval(fetchSystemData, 10000) // Updated interval to 10 seconds
|
const interval = setInterval(fetchSystemData, 10000)
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [fetchSystemData])
|
}, [fetchSystemData])
|
||||||
|
|
||||||
@@ -268,7 +270,7 @@ export function ProxmoxDashboard() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="storage" className="space-y-6">
|
<TabsContent value="storage" className="space-y-6">
|
||||||
<StorageMetrics key={`storage-${componentKey}`} />
|
<StorageOverview key={`storage-${componentKey}`} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="network" className="space-y-6">
|
<TabsContent value="network" className="space-y-6">
|
||||||
|
295
AppImage/components/storage-overview.tsx
Normal file
295
AppImage/components/storage-overview.tsx
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Thermometer } from "lucide-react"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Progress } from "@/components/ui/progress"
|
||||||
|
|
||||||
|
interface DiskInfo {
|
||||||
|
name: string
|
||||||
|
size?: string
|
||||||
|
temperature: number
|
||||||
|
health: string
|
||||||
|
power_on_hours?: number
|
||||||
|
smart_status?: string
|
||||||
|
model?: string
|
||||||
|
serial?: string
|
||||||
|
mountpoint?: string
|
||||||
|
fstype?: string
|
||||||
|
total?: number
|
||||||
|
used?: number
|
||||||
|
available?: number
|
||||||
|
usage_percent?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ZFSPool {
|
||||||
|
name: string
|
||||||
|
size: string
|
||||||
|
allocated: string
|
||||||
|
free: string
|
||||||
|
health: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StorageData {
|
||||||
|
total: number
|
||||||
|
used: number
|
||||||
|
available: number
|
||||||
|
disks: DiskInfo[]
|
||||||
|
zfs_pools: ZFSPool[]
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StorageOverview() {
|
||||||
|
const [storageData, setStorageData] = useState<StorageData | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
const fetchStorageData = async () => {
|
||||||
|
try {
|
||||||
|
const baseUrl =
|
||||||
|
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
|
||||||
|
const response = await fetch(`${baseUrl}/api/storage`)
|
||||||
|
const data = await response.json()
|
||||||
|
setStorageData(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching storage data:", error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchStorageData()
|
||||||
|
const interval = setInterval(fetchStorageData, 15000) // Update every 15 seconds
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getHealthIcon = (health: string) => {
|
||||||
|
switch (health.toLowerCase()) {
|
||||||
|
case "healthy":
|
||||||
|
case "passed":
|
||||||
|
case "online":
|
||||||
|
return <CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||||
|
case "warning":
|
||||||
|
return <AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||||||
|
case "critical":
|
||||||
|
case "failed":
|
||||||
|
case "degraded":
|
||||||
|
return <XCircle className="h-5 w-5 text-red-500" />
|
||||||
|
default:
|
||||||
|
return <AlertTriangle className="h-5 w-5 text-gray-500" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHealthBadge = (health: string) => {
|
||||||
|
switch (health.toLowerCase()) {
|
||||||
|
case "healthy":
|
||||||
|
case "passed":
|
||||||
|
case "online":
|
||||||
|
return <Badge className="bg-green-500/10 text-green-500 border-green-500/20">Healthy</Badge>
|
||||||
|
case "warning":
|
||||||
|
return <Badge className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">Warning</Badge>
|
||||||
|
case "critical":
|
||||||
|
case "failed":
|
||||||
|
case "degraded":
|
||||||
|
return <Badge className="bg-red-500/10 text-red-500 border-red-500/20">Critical</Badge>
|
||||||
|
default:
|
||||||
|
return <Badge className="bg-gray-500/10 text-gray-500 border-gray-500/20">Unknown</Badge>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTempColor = (temp: number) => {
|
||||||
|
if (temp === 0) return "text-gray-500"
|
||||||
|
if (temp < 45) return "text-green-500"
|
||||||
|
if (temp < 60) return "text-yellow-500"
|
||||||
|
return "text-red-500"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-muted-foreground">Loading storage information...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!storageData || storageData.error) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-red-500">Error loading storage data: {storageData?.error || "Unknown error"}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Storage Summary */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total Storage</CardTitle>
|
||||||
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{storageData.total} GB</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Used Storage</CardTitle>
|
||||||
|
<Database className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{storageData.used} GB</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{storageData.total > 0 ? Math.round((storageData.used / storageData.total) * 100) : 0}% used
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Available Storage</CardTitle>
|
||||||
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{storageData.available} GB</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ZFS Pools */}
|
||||||
|
{storageData.zfs_pools && storageData.zfs_pools.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5" />
|
||||||
|
ZFS Pools
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{storageData.zfs_pools.map((pool) => (
|
||||||
|
<div key={pool.name} className="border rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<h3 className="font-semibold text-lg">{pool.name}</h3>
|
||||||
|
{getHealthBadge(pool.health)}
|
||||||
|
</div>
|
||||||
|
{getHealthIcon(pool.health)}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Size</p>
|
||||||
|
<p className="font-medium">{pool.size}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Allocated</p>
|
||||||
|
<p className="font-medium">{pool.allocated}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Free</p>
|
||||||
|
<p className="font-medium">{pool.free}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Physical Disks */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<HardDrive className="h-5 w-5" />
|
||||||
|
Physical Disks & SMART Status
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{storageData.disks.map((disk) => (
|
||||||
|
<div key={disk.name} className="border rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<HardDrive className="h-5 w-5 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">/dev/{disk.name}</h3>
|
||||||
|
{disk.model && disk.model !== "Unknown" && (
|
||||||
|
<p className="text-sm text-muted-foreground">{disk.model}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{disk.temperature > 0 && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Thermometer className={`h-4 w-4 ${getTempColor(disk.temperature)}`} />
|
||||||
|
<span className={`text-sm font-medium ${getTempColor(disk.temperature)}`}>
|
||||||
|
{disk.temperature}°C
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{getHealthBadge(disk.health)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
|
{disk.size && (
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Size</p>
|
||||||
|
<p className="font-medium">{disk.size}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{disk.smart_status && disk.smart_status !== "unknown" && (
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">SMART Status</p>
|
||||||
|
<p className="font-medium capitalize">{disk.smart_status}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{disk.power_on_hours && disk.power_on_hours > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Power On Hours</p>
|
||||||
|
<p className="font-medium">{disk.power_on_hours.toLocaleString()}h</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{disk.serial && disk.serial !== "Unknown" && (
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Serial</p>
|
||||||
|
<p className="font-medium text-xs">{disk.serial}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{disk.mountpoint && (
|
||||||
|
<div className="mt-3 pt-3 border-t">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">Mounted at: </span>
|
||||||
|
<span className="font-medium">{disk.mountpoint}</span>
|
||||||
|
{disk.fstype && <span className="text-muted-foreground ml-2">({disk.fstype})</span>}
|
||||||
|
</div>
|
||||||
|
{disk.usage_percent !== undefined && (
|
||||||
|
<span className="text-sm font-medium">{disk.usage_percent}%</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{disk.usage_percent !== undefined && <Progress value={disk.usage_percent} className="h-2" />}
|
||||||
|
{disk.total && disk.used && disk.available && (
|
||||||
|
<div className="flex justify-between text-xs text-muted-foreground mt-1">
|
||||||
|
<span>{disk.used} GB used</span>
|
||||||
|
<span>
|
||||||
|
{disk.available} GB free of {disk.total} GB
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@@ -342,7 +342,8 @@ def get_storage_info():
|
|||||||
'total': 0,
|
'total': 0,
|
||||||
'used': 0,
|
'used': 0,
|
||||||
'available': 0,
|
'available': 0,
|
||||||
'disks': []
|
'disks': [],
|
||||||
|
'zfs_pools': []
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get disk usage for root partition
|
# Get disk usage for root partition
|
||||||
@@ -351,27 +352,67 @@ def get_storage_info():
|
|||||||
storage_data['used'] = round(disk_usage.used / (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
|
storage_data['available'] = round(disk_usage.free / (1024**3), 1) # GB
|
||||||
|
|
||||||
# Get individual disk information
|
try:
|
||||||
|
# List all block devices
|
||||||
|
result = subprocess.run(['lsblk', '-d', '-n', '-o', 'NAME,SIZE,TYPE'],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 3 and parts[2] == 'disk':
|
||||||
|
disk_name = parts[0]
|
||||||
|
disk_size = parts[1]
|
||||||
|
|
||||||
|
# Get SMART data for this disk
|
||||||
|
smart_data = get_smart_data(disk_name)
|
||||||
|
|
||||||
|
disk_info = {
|
||||||
|
'name': disk_name,
|
||||||
|
'size': disk_size,
|
||||||
|
'temperature': smart_data.get('temperature', 0),
|
||||||
|
'health': smart_data.get('health', 'unknown'),
|
||||||
|
'power_on_hours': smart_data.get('power_on_hours', 0),
|
||||||
|
'smart_status': smart_data.get('smart_status', 'unknown'),
|
||||||
|
'model': smart_data.get('model', 'Unknown'),
|
||||||
|
'serial': smart_data.get('serial', 'Unknown')
|
||||||
|
}
|
||||||
|
storage_data['disks'].append(disk_info)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting disk list: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['zpool', 'list', '-H', '-o', 'name,size,alloc,free,health'],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = line.split('\t')
|
||||||
|
if len(parts) >= 5:
|
||||||
|
pool_info = {
|
||||||
|
'name': parts[0],
|
||||||
|
'size': parts[1],
|
||||||
|
'allocated': parts[2],
|
||||||
|
'free': parts[3],
|
||||||
|
'health': parts[4]
|
||||||
|
}
|
||||||
|
storage_data['zfs_pools'].append(pool_info)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Note: ZFS not available or no pools: {e}")
|
||||||
|
|
||||||
|
# Get individual disk partitions
|
||||||
disk_partitions = psutil.disk_partitions()
|
disk_partitions = psutil.disk_partitions()
|
||||||
for partition in disk_partitions:
|
for partition in disk_partitions:
|
||||||
try:
|
try:
|
||||||
partition_usage = psutil.disk_usage(partition.mountpoint)
|
partition_usage = psutil.disk_usage(partition.mountpoint)
|
||||||
|
|
||||||
|
# Find corresponding disk info
|
||||||
disk_temp = 0
|
disk_temp = 0
|
||||||
try:
|
for disk in storage_data['disks']:
|
||||||
# Try to get disk temperature from sensors
|
if disk['name'] in partition.device:
|
||||||
if hasattr(psutil, "sensors_temperatures"):
|
disk_temp = disk['temperature']
|
||||||
temps = psutil.sensors_temperatures()
|
|
||||||
if temps:
|
|
||||||
for name, entries in temps.items():
|
|
||||||
if 'disk' in name.lower() or 'hdd' in name.lower() or 'sda' in name.lower():
|
|
||||||
if entries:
|
|
||||||
disk_temp = entries[0].current
|
|
||||||
break
|
break
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
disk_info = {
|
partition_info = {
|
||||||
'name': partition.device,
|
'name': partition.device,
|
||||||
'mountpoint': partition.mountpoint,
|
'mountpoint': partition.mountpoint,
|
||||||
'fstype': partition.fstype,
|
'fstype': partition.fstype,
|
||||||
@@ -379,10 +420,13 @@ def get_storage_info():
|
|||||||
'used': round(partition_usage.used / (1024**3), 1),
|
'used': round(partition_usage.used / (1024**3), 1),
|
||||||
'available': round(partition_usage.free / (1024**3), 1),
|
'available': round(partition_usage.free / (1024**3), 1),
|
||||||
'usage_percent': round((partition_usage.used / partition_usage.total) * 100, 1),
|
'usage_percent': round((partition_usage.used / partition_usage.total) * 100, 1),
|
||||||
'health': 'unknown', # Would need SMART data for real health
|
|
||||||
'temperature': disk_temp
|
'temperature': disk_temp
|
||||||
}
|
}
|
||||||
storage_data['disks'].append(disk_info)
|
|
||||||
|
# Add to disks list if not already there
|
||||||
|
if not any(d['name'] == partition.device for d in storage_data['disks']):
|
||||||
|
storage_data['disks'].append(partition_info)
|
||||||
|
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
print(f"Permission denied accessing {partition.mountpoint}")
|
print(f"Permission denied accessing {partition.mountpoint}")
|
||||||
continue
|
continue
|
||||||
@@ -390,15 +434,6 @@ def get_storage_info():
|
|||||||
print(f"Error accessing partition {partition.device}: {e}")
|
print(f"Error accessing partition {partition.device}: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not storage_data['disks'] and storage_data['total'] == 0:
|
|
||||||
return {
|
|
||||||
'error': 'No storage data available - unable to access disk information',
|
|
||||||
'total': 0,
|
|
||||||
'used': 0,
|
|
||||||
'available': 0,
|
|
||||||
'disks': []
|
|
||||||
}
|
|
||||||
|
|
||||||
return storage_data
|
return storage_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -408,9 +443,78 @@ def get_storage_info():
|
|||||||
'total': 0,
|
'total': 0,
|
||||||
'used': 0,
|
'used': 0,
|
||||||
'available': 0,
|
'available': 0,
|
||||||
'disks': []
|
'disks': [],
|
||||||
|
'zfs_pools': []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_smart_data(disk_name):
|
||||||
|
"""Get SMART data for a specific disk"""
|
||||||
|
smart_data = {
|
||||||
|
'temperature': 0,
|
||||||
|
'health': 'unknown',
|
||||||
|
'power_on_hours': 0,
|
||||||
|
'smart_status': 'unknown',
|
||||||
|
'model': 'Unknown',
|
||||||
|
'serial': 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to get SMART data using smartctl
|
||||||
|
result = subprocess.run(['smartctl', '-a', f'/dev/{disk_name}'],
|
||||||
|
capture_output=True, text=True, timeout=10)
|
||||||
|
|
||||||
|
if result.returncode in [0, 4]: # 0 = success, 4 = some SMART values exceeded threshold
|
||||||
|
output = result.stdout
|
||||||
|
|
||||||
|
# Parse SMART status
|
||||||
|
if 'SMART overall-health self-assessment test result: PASSED' in output:
|
||||||
|
smart_data['smart_status'] = 'passed'
|
||||||
|
smart_data['health'] = 'healthy'
|
||||||
|
elif 'SMART overall-health self-assessment test result: FAILED' in output:
|
||||||
|
smart_data['smart_status'] = 'failed'
|
||||||
|
smart_data['health'] = 'critical'
|
||||||
|
|
||||||
|
# Parse temperature
|
||||||
|
for line in output.split('\n'):
|
||||||
|
if 'Temperature_Celsius' in line or 'Temperature' in line:
|
||||||
|
parts = line.split()
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
if part.isdigit() and int(part) > 0 and int(part) < 100:
|
||||||
|
smart_data['temperature'] = int(part)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Parse power on hours
|
||||||
|
if 'Power_On_Hours' in line:
|
||||||
|
parts = line.split()
|
||||||
|
for part in parts:
|
||||||
|
if part.isdigit() and int(part) > 0:
|
||||||
|
smart_data['power_on_hours'] = int(part)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Parse model
|
||||||
|
if 'Device Model:' in line or 'Model Number:' in line:
|
||||||
|
smart_data['model'] = line.split(':', 1)[1].strip()
|
||||||
|
|
||||||
|
# Parse serial
|
||||||
|
if 'Serial Number:' in line or 'Serial number:' in line:
|
||||||
|
smart_data['serial'] = line.split(':', 1)[1].strip()
|
||||||
|
|
||||||
|
# Determine health based on temperature and SMART status
|
||||||
|
if smart_data['temperature'] > 0:
|
||||||
|
if smart_data['temperature'] > 60:
|
||||||
|
smart_data['health'] = 'warning'
|
||||||
|
elif smart_data['temperature'] > 70:
|
||||||
|
smart_data['health'] = 'critical'
|
||||||
|
elif smart_data['smart_status'] == 'passed':
|
||||||
|
smart_data['health'] = 'healthy'
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"smartctl not found - install smartmontools package")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting SMART data for {disk_name}: {e}")
|
||||||
|
|
||||||
|
return smart_data
|
||||||
|
|
||||||
def get_network_info():
|
def get_network_info():
|
||||||
"""Get network interface information"""
|
"""Get network interface information"""
|
||||||
try:
|
try:
|
||||||
|
Reference in New Issue
Block a user