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...
Lists NVMe namespaces and changes their LBA format through a queued task.
` + renderNVMeFormatInline() + `