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 disk_checker.core.tool_scanner import ToolInfo
|
||||
from disk_checker.core.drive_scanner import DriveInfo, scan_drives
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@@ -19,4 +20,26 @@ async def get_available_tools(request: Request):
|
||||
"version" : tool.version
|
||||
}
|
||||
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
|
||||
]
|
||||
}
|
||||
@@ -14,4 +14,32 @@
|
||||
|
||||
.badge {
|
||||
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>
|
||||
</nav>
|
||||
|
||||
<!--Available tools block, tools are loaded automatically-->
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
{% block extra_js %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
+67
-8
@@ -2,8 +2,10 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
|
||||
<!--Available tools section-->
|
||||
<h2>Available Benchmarking Tools</h2>
|
||||
|
||||
|
||||
<!-- Server-side rendered tools -->
|
||||
{% if tools %}
|
||||
<div class="row">
|
||||
@@ -14,12 +16,12 @@
|
||||
<h5 class="card-title">{{ tool.name }}</h5>
|
||||
<p class="card-text">
|
||||
{% if tool.available %}
|
||||
<span class="badge bg-success">Available</span>
|
||||
{% if tool.version %}
|
||||
<small class="text-muted">{{ tool.version }}</small>
|
||||
{% endif %}
|
||||
<span class="badge bg-success">Available</span>
|
||||
{% if tool.version %}
|
||||
<small class="text-muted">{{ tool.version }}</small>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Not Installed</span>
|
||||
<span class="badge bg-secondary">Not Installed</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
@@ -28,7 +30,64 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% 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>
|
||||
{% 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