From 7ce73e34a49cebbbc95c8a570859e9f0e05574db Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Thu, 30 Apr 2026 16:27:25 +0300 Subject: [PATCH] Add NVMe block format tool --- audit/internal/webui/api.go | 2 + audit/internal/webui/api_test.go | 21 ++ audit/internal/webui/nvme_format.go | 368 ++++++++++++++++++++++ audit/internal/webui/page_export_tools.go | 1 + audit/internal/webui/server.go | 2 + audit/internal/webui/server_test.go | 6 + audit/internal/webui/task_runner.go | 6 + audit/internal/webui/tasks.go | 2 + 8 files changed, 408 insertions(+) create mode 100644 audit/internal/webui/nvme_format.go diff --git a/audit/internal/webui/api.go b/audit/internal/webui/api.go index 192cf30..23dbe8d 100644 --- a/audit/internal/webui/api.go +++ b/audit/internal/webui/api.go @@ -125,6 +125,8 @@ func defaultTaskPriority(target string, params taskParams) int { return taskPriorityInstall case "install-to-ram": return taskPriorityInstallToRAM + case "nvme-format": + return taskPriorityInstall case "audit": return taskPriorityAudit case "nvidia-bench-perf", "nvidia-bench-power", "nvidia-bench-autotune": diff --git a/audit/internal/webui/api_test.go b/audit/internal/webui/api_test.go index f43cea4..82f6c06 100644 --- a/audit/internal/webui/api_test.go +++ b/audit/internal/webui/api_test.go @@ -85,6 +85,27 @@ func TestHandleAPIBlackboxStatusReturnsPersistedState(t *testing.T) { } } +func TestParseNVMeFormatModes(t *testing.T) { + raw := ` +lbaf 0 : ms:0 lbads:9 rp:0x2 (in use) +lbaf 1 : ms:8 lbads:9 rp:0x1 +lbaf 2 : ms:0 lbads:12 rp:0 +` + modes := parseNVMeFormatModes(raw) + if len(modes) != 3 { + t.Fatalf("modes=%#v want 3 modes", modes) + } + if modes[0].Mode != 0 || modes[0].DataBytes != 512 || modes[0].MetadataBytes != 0 || !modes[0].InUse { + t.Fatalf("mode 0=%#v", modes[0]) + } + if modes[1].Label != "MODE 1 (512+8)" { + t.Fatalf("mode 1 label=%q", modes[1].Label) + } + if modes[2].DataBytes != 4096 || modes[2].MetadataBytes != 0 { + t.Fatalf("mode 2=%#v", modes[2]) + } +} + func TestHandleAPIBenchmarkNvidiaRunQueuesSelectedGPUs(t *testing.T) { globalQueue.mu.Lock() originalTasks := globalQueue.tasks diff --git a/audit/internal/webui/nvme_format.go b/audit/internal/webui/nvme_format.go new file mode 100644 index 0000000..8142e56 --- /dev/null +++ b/audit/internal/webui/nvme_format.go @@ -0,0 +1,368 @@ +package webui + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "time" +) + +type nvmeFormatMode struct { + Mode int `json:"mode"` + DataBytes int64 `json:"data_bytes"` + MetadataBytes int64 `json:"metadata_bytes"` + InUse bool `json:"in_use"` + Label string `json:"label"` +} + +type nvmeFormatDisk struct { + Device string `json:"device"` + Model string `json:"model,omitempty"` + Serial string `json:"serial,omitempty"` + Size string `json:"size,omitempty"` + CurrentMode int `json:"current_mode"` + CurrentFormat string `json:"current_format"` + Modes []nvmeFormatMode `json:"modes"` + Error string `json:"error,omitempty"` +} + +type nvmeListJSON struct { + Devices []struct { + DevicePath string `json:"DevicePath"` + ModelNumber string `json:"ModelNumber"` + SerialNumber string `json:"SerialNumber"` + PhysicalSize int64 `json:"PhysicalSize"` + } `json:"Devices"` +} + +var ( + nvmeFormatDeviceRE = regexp.MustCompile(`^/dev/nvme[0-9]+n[0-9]+$`) + nvmeLBAFCompactLineRE = regexp.MustCompile(`(?im)^\s*lbaf\s+(\d+)\s*:\s*ms:(\d+)\s+lbads:(\d+).*$`) + nvmeLBAFVerboseLineRE = regexp.MustCompile(`(?im)^\s*LBA Format\s+(\d+)\s*:\s*Metadata Size:\s*(\d+)\s+bytes\s*-\s*Data Size:\s*(\d+)\s+bytes.*$`) + nvmeCommandContext = exec.CommandContext + nvmeListFormatsTimeout = 20 * time.Second +) + +func listNVMeFormatDisks(ctx context.Context) ([]nvmeFormatDisk, error) { + ctx, cancel := context.WithTimeout(ctx, nvmeListFormatsTimeout) + defer cancel() + out, err := nvmeCommandContext(ctx, "nvme", "list", "-o", "json").Output() + if err != nil { + return nil, err + } + var root nvmeListJSON + if err := json.Unmarshal(out, &root); err != nil { + return nil, err + } + disks := make([]nvmeFormatDisk, 0, len(root.Devices)) + seen := map[string]struct{}{} + for _, dev := range root.Devices { + path := strings.TrimSpace(dev.DevicePath) + if !nvmeFormatDeviceRE.MatchString(path) { + continue + } + if _, ok := seen[path]; ok { + continue + } + seen[path] = struct{}{} + disk := nvmeFormatDisk{ + Device: path, + Model: strings.TrimSpace(dev.ModelNumber), + Serial: strings.TrimSpace(dev.SerialNumber), + Size: formatNVMeBytes(dev.PhysicalSize), + CurrentMode: -1, + } + modes, parseErr := readNVMeFormatModes(ctx, path) + if parseErr != nil { + disk.Error = parseErr.Error() + } + disk.Modes = modes + for _, mode := range modes { + if mode.InUse { + disk.CurrentMode = mode.Mode + disk.CurrentFormat = formatNVMeBlock(mode.DataBytes, mode.MetadataBytes) + break + } + } + disks = append(disks, disk) + } + sort.Slice(disks, func(i, j int) bool { return disks[i].Device < disks[j].Device }) + return disks, nil +} + +func readNVMeFormatModes(ctx context.Context, device string) ([]nvmeFormatMode, error) { + if !nvmeFormatDeviceRE.MatchString(device) { + return nil, fmt.Errorf("invalid NVMe device") + } + out, err := nvmeCommandContext(ctx, "nvme", "id-ns", device, "-H").CombinedOutput() + if err != nil { + msg := strings.TrimSpace(string(out)) + if msg == "" { + msg = err.Error() + } + return nil, fmt.Errorf("%s", msg) + } + modes := parseNVMeFormatModes(string(out)) + if len(modes) == 0 { + return nil, fmt.Errorf("no LBA format modes found") + } + return modes, nil +} + +func parseNVMeFormatModes(raw string) []nvmeFormatMode { + byMode := map[int]nvmeFormatMode{} + for _, m := range nvmeLBAFCompactLineRE.FindAllStringSubmatch(raw, -1) { + mode, errMode := strconv.Atoi(m[1]) + metadata, errMS := strconv.ParseInt(m[2], 10, 64) + lbads, errLBADS := strconv.Atoi(m[3]) + if errMode != nil || errMS != nil || errLBADS != nil || lbads < 0 || lbads >= 63 { + continue + } + data := int64(1) << lbads + line := m[0] + byMode[mode] = nvmeFormatMode{ + Mode: mode, + DataBytes: data, + MetadataBytes: metadata, + InUse: strings.Contains(strings.ToLower(line), "in use"), + Label: fmt.Sprintf("MODE %d (%s)", mode, formatNVMeBlock(data, metadata)), + } + } + for _, m := range nvmeLBAFVerboseLineRE.FindAllStringSubmatch(raw, -1) { + mode, errMode := strconv.Atoi(m[1]) + metadata, errMS := strconv.ParseInt(m[2], 10, 64) + data, errData := strconv.ParseInt(m[3], 10, 64) + if errMode != nil || errMS != nil || errData != nil || data <= 0 { + continue + } + line := m[0] + byMode[mode] = nvmeFormatMode{ + Mode: mode, + DataBytes: data, + MetadataBytes: metadata, + InUse: strings.Contains(strings.ToLower(line), "in use"), + Label: fmt.Sprintf("MODE %d (%s)", mode, formatNVMeBlock(data, metadata)), + } + } + modes := make([]nvmeFormatMode, 0, len(byMode)) + for _, mode := range byMode { + modes = append(modes, mode) + } + sort.Slice(modes, func(i, j int) bool { return modes[i].Mode < modes[j].Mode }) + return modes +} + +func runNVMeFormatTask(ctx context.Context, j *jobState, device string, lbaf int) error { + if !nvmeFormatDeviceRE.MatchString(device) { + return fmt.Errorf("invalid NVMe device") + } + modes, err := readNVMeFormatModes(ctx, device) + if err != nil { + return err + } + var selected nvmeFormatMode + found := false + for _, mode := range modes { + if mode.Mode == lbaf { + selected = mode + found = true + break + } + } + if !found { + return fmt.Errorf("MODE %d is not available on %s", lbaf, device) + } + ms := 0 + if selected.MetadataBytes > 0 { + ms = 1 + } + j.append(fmt.Sprintf("Formatting %s to %s with --lbaf=%d --ms=%d --force", device, formatNVMeBlock(selected.DataBytes, selected.MetadataBytes), selected.Mode, ms)) + cmd := nvmeCommandContext(ctx, "nvme", "format", device, fmt.Sprintf("--lbaf=%d", selected.Mode), fmt.Sprintf("--ms=%d", ms), "--force") + return streamCmdJob(j, cmd) +} + +func (h *handler) handleAPINVMeFormats(w http.ResponseWriter, r *http.Request) { + disks, err := listNVMeFormatDisks(r.Context()) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, disks) +} + +func (h *handler) handleAPINVMeFormatRun(w http.ResponseWriter, r *http.Request) { + var req struct { + Device string `json:"device"` + LBAF int `json:"lbaf"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + if !nvmeFormatDeviceRE.MatchString(req.Device) { + writeError(w, http.StatusBadRequest, "invalid NVMe device") + return + } + disks, err := listNVMeFormatDisks(r.Context()) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + var label string + allowed := false + for _, disk := range disks { + if disk.Device != req.Device { + continue + } + for _, mode := range disk.Modes { + if mode.Mode == req.LBAF { + allowed = true + label = mode.Label + break + } + } + } + if !allowed { + writeError(w, http.StatusBadRequest, "LBA format mode is not available for this device") + return + } + name := fmt.Sprintf("NVMe Format %s to %s", filepath.Base(req.Device), label) + t := &Task{ + ID: newJobID("nvme-format"), + Name: name, + Target: "nvme-format", + Priority: defaultTaskPriority("nvme-format", taskParams{}), + Status: TaskPending, + CreatedAt: time.Now(), + params: taskParams{ + Device: req.Device, + LBAF: req.LBAF, + }, + } + globalQueue.enqueue(t) + writeJSON(w, map[string]string{"task_id": t.ID, "job_id": t.ID}) +} + +func formatNVMeBlock(dataBytes, metadataBytes int64) string { + return strconv.FormatInt(dataBytes, 10) + "+" + strconv.FormatInt(metadataBytes, 10) +} + +func formatNVMeBytes(n int64) string { + if n <= 0 { + return "" + } + units := []string{"B", "KB", "MB", "GB", "TB", "PB"} + v := float64(n) + unit := 0 + for v >= 1000 && unit < len(units)-1 { + v /= 1000 + unit++ + } + if unit == 0 { + return fmt.Sprintf("%d B", n) + } + return fmt.Sprintf("%.1f %s", v, units[unit]) +} + +func renderNVMeFormatInline() string { + return `
Loading NVMe disks...
+

Loading...

+` +} + +func renderNVMeFormatCard() string { + return `
NVMe Block Format
` + + `

Lists NVMe namespaces and changes their LBA format through a queued task.

` + + renderNVMeFormatInline() + `
` +} diff --git a/audit/internal/webui/page_export_tools.go b/audit/internal/webui/page_export_tools.go index 910ead6..6cb91d5 100644 --- a/audit/internal/webui/page_export_tools.go +++ b/audit/internal/webui/page_export_tools.go @@ -475,6 +475,7 @@ function installToRAM() {
Services
` + renderServicesInline() + `
+` + renderNVMeFormatCard() + `