fix: add undo disk selection capability #2

Closed
bot wants to merge 26 commits from fio_workflow into main
5 changed files with 246 additions and 9 deletions
Showing only changes of commit d2216f9eec - Show all commits
+127
View File
@@ -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
+23
View File
@@ -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
]
}
+28
View File
@@ -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
View File
@@ -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
View File
@@ -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 %}