fix: add undo disk selection capability #2
@@ -0,0 +1,127 @@
|
|||||||
|
"""Drive detection module for disk-checker."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from typing import Dict, List
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DriveInfo:
|
||||||
|
"""Information about a block device."""
|
||||||
|
name: str # e.g., "sda", "sdb"
|
||||||
|
path: str # e.g., "/dev/sda"
|
||||||
|
size: str # e.g., "232.9G"
|
||||||
|
model: str # e.g., "CT250MX500SSD1"
|
||||||
|
type: str # e.g., "disk", "part"
|
||||||
|
mounted: bool # True if currently mounted
|
||||||
|
mount_point: str = "" # e.g., "/", "/home"
|
||||||
|
serial: str = "" # e.g., "2131G5F25137"
|
||||||
|
|
||||||
|
|
||||||
|
def get_mounted_devices() -> set:
|
||||||
|
"""
|
||||||
|
Get set of mounted device paths.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Set of device paths that are currently mounted
|
||||||
|
"""
|
||||||
|
mounted = set()
|
||||||
|
|
||||||
|
# Method 1: Parse lsblk with mount points
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["lsblk", "-J", "-o", "NAME,MOUNTPOINT"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
for device in data.get("blockdevices", []):
|
||||||
|
mountpoint = device.get("mountpoint")
|
||||||
|
if mountpoint:
|
||||||
|
# Add both full path and device name
|
||||||
|
mounted.add(f"/dev/{device['name']}")
|
||||||
|
mounted.add(device["name"])
|
||||||
|
except (subprocess.CalledProcessError, json.JSONDecodeError):
|
||||||
|
print("lsblk failed to retrieve mounted devices information")
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Method 2: Parse /proc/mounts
|
||||||
|
try:
|
||||||
|
with open("/proc/mounts", "r") as f:
|
||||||
|
for line in f:
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2 and parts[0].startswith("/dev/"):
|
||||||
|
mounted.add(parts[0])
|
||||||
|
# Also add device name without /dev/
|
||||||
|
mounted.add(parts[0].replace("/dev/", ""))
|
||||||
|
except (FileNotFoundError, IOError):
|
||||||
|
print("/proc/mounts failed to retrieve mounted devices information")
|
||||||
|
pass
|
||||||
|
|
||||||
|
return mounted
|
||||||
|
|
||||||
|
|
||||||
|
def scan_drives() -> List[DriveInfo]:
|
||||||
|
"""
|
||||||
|
Scan host system for available block devices.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of DriveInfo objects for physical disks
|
||||||
|
"""
|
||||||
|
mounted_devices = get_mounted_devices()
|
||||||
|
drives = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get block devices with lsblk
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"lsblk",
|
||||||
|
"-J", # JSON output
|
||||||
|
"-d", # Only disks (not partitions)
|
||||||
|
"-b", # Show size in bytes
|
||||||
|
"-o", "NAME,SIZE,TYPE,MODEL,Serial,MOUNTPOINT"
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
|
||||||
|
for device in data.get("blockdevices", []):
|
||||||
|
# Only include physical disks
|
||||||
|
if device.get("type") != "disk":
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = device.get("name", "")
|
||||||
|
path = f"/dev/{name}"
|
||||||
|
|
||||||
|
# Check if mounted
|
||||||
|
is_mounted = path in mounted_devices or name in mounted_devices
|
||||||
|
|
||||||
|
drive = DriveInfo(
|
||||||
|
name=name,
|
||||||
|
path=path,
|
||||||
|
size=device.get("size", "Unknown"),
|
||||||
|
model=device.get("model") or "",
|
||||||
|
type="disk",
|
||||||
|
mounted=is_mounted,
|
||||||
|
mount_point=device.get("mountpoint", ""),
|
||||||
|
serial=device.get("serial") or ""
|
||||||
|
)
|
||||||
|
drives.append(drive)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
# If lsblk fails, return empty list
|
||||||
|
print("lsblk failed to parse drives data")
|
||||||
|
print(e)
|
||||||
|
pass
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print("json decoder error")
|
||||||
|
print(e)
|
||||||
|
pass
|
||||||
|
|
||||||
|
return drives
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
from fastapi import APIRouter, Request
|
from fastapi import APIRouter, Request
|
||||||
|
|
||||||
from disk_checker.core.tool_scanner import ToolInfo
|
from disk_checker.core.tool_scanner import ToolInfo
|
||||||
|
from disk_checker.core.drive_scanner import DriveInfo, scan_drives
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
||||||
@@ -20,3 +21,25 @@ async def get_available_tools(request: Request):
|
|||||||
}
|
}
|
||||||
for tool_name, tool in tools.items()
|
for tool_name, tool in tools.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@api_router.get("/drives")
|
||||||
|
async def get_available_drives(request: Request):
|
||||||
|
"""
|
||||||
|
Get list of available block devices
|
||||||
|
Mounted drives are marked as unavailable
|
||||||
|
"""
|
||||||
|
drives = scan_drives()
|
||||||
|
return {
|
||||||
|
"drives": [
|
||||||
|
{
|
||||||
|
"name": drive.name,
|
||||||
|
"path": drive.path,
|
||||||
|
"size": drive.size,
|
||||||
|
"model": drive.model,
|
||||||
|
"mounted": drive.mounted,
|
||||||
|
"mount_point": drive.mount_point,
|
||||||
|
"serial": drive.serial
|
||||||
|
}
|
||||||
|
for drive in drives
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -15,3 +15,31 @@
|
|||||||
.badge {
|
.badge {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Drive cards styling */
|
||||||
|
.drive-card {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-card.available {
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-card.disabled {
|
||||||
|
border-left: 4px solid #6c757d;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-card:hover:not(.disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make disabled cards look unavailable */
|
||||||
|
.drive-card.disabled .card-title {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-card.disabled .card-text {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
+1
-1
@@ -16,9 +16,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!--Available tools block, tools are loaded automatically-->
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|||||||
+60
-1
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
|
||||||
|
<!--Available tools section-->
|
||||||
<h2>Available Benchmarking Tools</h2>
|
<h2>Available Benchmarking Tools</h2>
|
||||||
|
|
||||||
<!-- Server-side rendered tools -->
|
<!-- Server-side rendered tools -->
|
||||||
@@ -29,6 +31,63 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div id="tools-content"></div>
|
<!--Available drives section-->
|
||||||
|
<h2 class="mt-5">Available Drives</h2>
|
||||||
|
<p class="text-muted">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
Mounted drives are grayed out and cannot be benchmarked.
|
||||||
|
</p>
|
||||||
|
<div id="drives-list" hx-get="/api/v1/drives" hx-trigger="load" hx-target="#drives-content"></div>
|
||||||
|
<div id="drives-content" class="row"></div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// HTMX event to render drives after loading
|
||||||
|
document.addEventListener('htmx:afterRequest', function (evt) {
|
||||||
|
if (evt.detail.target.id === 'drives-content') {
|
||||||
|
renderDrives(JSON.parse(evt.detail.xhr.response));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderDrives(data) {
|
||||||
|
const container = document.getElementById('drives-content');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
if (!data.drives || data.drives.length === 0) {
|
||||||
|
container.innerHTML = '<div class="col-12"><p class="text-muted">No drives found.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.drives.forEach(drive => {
|
||||||
|
const col = document.createElement('div');
|
||||||
|
col.className = 'col-md-4 mb-3';
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = `drive-card ${drive.mounted ? 'disabled' : 'available'}`;
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">${drive.name}</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>Size:</strong> ${drive.size}<br>
|
||||||
|
<strong>Model:</strong> ${drive.model || 'Unknown'}<br>
|
||||||
|
${drive.serial ? `<strong>Serial:</strong> ${drive.serial}<br>` : ''}
|
||||||
|
${drive.mount_point ? `<strong>Mounted at:</strong> ${drive.mount_point}<br>` : ''}
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
${drive.mounted
|
||||||
|
? '<span class="badge bg-secondary">Mounted - Unavailable</span>'
|
||||||
|
: '<span class="badge bg-success">Available for Benchmark</span>'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
col.appendChild(card);
|
||||||
|
container.appendChild(col);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user