Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eed157c2db | |||
| a2c8aea0df | |||
| b21f03cd26 | |||
| cac5b9c86e | |||
| b5d04ef045 | |||
| fcd64438ea | |||
| 0e39e7d960 | |||
|
|
58d6da0e4f | ||
|
|
7ce73e34a4 | ||
|
|
8a21809ade |
@@ -64,6 +64,8 @@ func run(args []string, stdout, stderr io.Writer) (exitCode int) {
|
|||||||
return runExport(args[1:], stdout, stderr)
|
return runExport(args[1:], stdout, stderr)
|
||||||
case "preflight":
|
case "preflight":
|
||||||
return runPreflight(args[1:], stdout, stderr)
|
return runPreflight(args[1:], stdout, stderr)
|
||||||
|
case "install-to-ram":
|
||||||
|
return runInstallToRAM(args[1:], stdout, stderr)
|
||||||
case "support-bundle":
|
case "support-bundle":
|
||||||
return runSupportBundle(args[1:], stdout, stderr)
|
return runSupportBundle(args[1:], stdout, stderr)
|
||||||
case "web":
|
case "web":
|
||||||
@@ -90,6 +92,7 @@ func printRootUsage(w io.Writer) {
|
|||||||
fmt.Fprintln(w, `bee commands:
|
fmt.Fprintln(w, `bee commands:
|
||||||
bee audit --runtime auto|local|livecd --output stdout|file:<path>
|
bee audit --runtime auto|local|livecd --output stdout|file:<path>
|
||||||
bee preflight --output stdout|file:<path>
|
bee preflight --output stdout|file:<path>
|
||||||
|
bee install-to-ram
|
||||||
bee export --target <device>
|
bee export --target <device>
|
||||||
bee support-bundle --output stdout|file:<path>
|
bee support-bundle --output stdout|file:<path>
|
||||||
bee web --listen :80 [--audit-path `+app.DefaultAuditJSONPath+`]
|
bee web --listen :80 [--audit-path `+app.DefaultAuditJSONPath+`]
|
||||||
@@ -109,6 +112,8 @@ func runHelp(args []string, stdout, stderr io.Writer) int {
|
|||||||
return runExport([]string{"--help"}, stdout, stdout)
|
return runExport([]string{"--help"}, stdout, stdout)
|
||||||
case "preflight":
|
case "preflight":
|
||||||
return runPreflight([]string{"--help"}, stdout, stdout)
|
return runPreflight([]string{"--help"}, stdout, stdout)
|
||||||
|
case "install-to-ram":
|
||||||
|
return runInstallToRAM([]string{"--help"}, stdout, stdout)
|
||||||
case "support-bundle":
|
case "support-bundle":
|
||||||
return runSupportBundle([]string{"--help"}, stdout, stdout)
|
return runSupportBundle([]string{"--help"}, stdout, stdout)
|
||||||
case "web":
|
case "web":
|
||||||
@@ -252,6 +257,32 @@ func runPreflight(args []string, stdout, stderr io.Writer) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runInstallToRAM(args []string, stdout, stderr io.Writer) int {
|
||||||
|
fs := flag.NewFlagSet("install-to-ram", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(stderr)
|
||||||
|
fs.Usage = func() {
|
||||||
|
fmt.Fprintln(stderr, "usage: bee install-to-ram")
|
||||||
|
}
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
if err == flag.ErrHelp {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if fs.NArg() != 0 {
|
||||||
|
fs.Usage()
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
application := app.New(platform.New())
|
||||||
|
logLine := func(s string) { fmt.Fprintln(stdout, s) }
|
||||||
|
if err := application.RunInstallToRAM(context.Background(), logLine); err != nil {
|
||||||
|
slog.Error("run install-to-ram", "err", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func runSupportBundle(args []string, stdout, stderr io.Writer) int {
|
func runSupportBundle(args []string, stdout, stderr io.Writer) int {
|
||||||
fs := flag.NewFlagSet("support-bundle", flag.ContinueOnError)
|
fs := flag.NewFlagSet("support-bundle", flag.ContinueOnError)
|
||||||
fs.SetOutput(stderr)
|
fs.SetOutput(stderr)
|
||||||
|
|||||||
@@ -14,6 +14,22 @@ import (
|
|||||||
const installToRAMDir = "/dev/shm/bee-live"
|
const installToRAMDir = "/dev/shm/bee-live"
|
||||||
const copyProgressLogStep int64 = 100 * 1024 * 1024
|
const copyProgressLogStep int64 = 100 * 1024 * 1024
|
||||||
|
|
||||||
|
var liveMediumSquashfsGlob = func() ([]string, error) {
|
||||||
|
return filepath.Glob("/run/live/medium/live/*.squashfs")
|
||||||
|
}
|
||||||
|
|
||||||
|
var runRemountMedium = func() ([]byte, error) {
|
||||||
|
return exec.Command("bee-remount-medium").CombinedOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
var umountLiveMedium = func() error {
|
||||||
|
return exec.Command("umount", "/run/live/medium").Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
var ejectDevice = func(device string) error {
|
||||||
|
return exec.Command("eject", device).Run()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *System) IsLiveMediaInRAM() bool {
|
func (s *System) IsLiveMediaInRAM() bool {
|
||||||
return s.LiveMediaRAMState().InRAM
|
return s.LiveMediaRAMState().InRAM
|
||||||
}
|
}
|
||||||
@@ -140,8 +156,7 @@ func (s *System) RunInstallToRAM(ctx context.Context, logFunc func(string)) (ret
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
squashfsFiles, err := filepath.Glob("/run/live/medium/live/*.squashfs")
|
squashfsFiles, sourceAvailable := ensureLiveMediumAvailable(log)
|
||||||
sourceAvailable := err == nil && len(squashfsFiles) > 0
|
|
||||||
|
|
||||||
dstDir := installToRAMDir
|
dstDir := installToRAMDir
|
||||||
|
|
||||||
@@ -171,7 +186,7 @@ func (s *System) RunInstallToRAM(ctx context.Context, logFunc func(string)) (ret
|
|||||||
}
|
}
|
||||||
goto bindMedium
|
goto bindMedium
|
||||||
}
|
}
|
||||||
return fmt.Errorf("no squashfs files found in /run/live/medium/live/ and no prior RAM copy in %s — reconnect the installation medium and retry", dstDir)
|
return fmt.Errorf("no squashfs files found in /run/live/medium/live/ and no prior RAM copy in %s — reconnect the installation medium and retry (or run bee-remount-medium as root)", dstDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -254,10 +269,83 @@ bindMedium:
|
|||||||
if status.InRAM {
|
if status.InRAM {
|
||||||
log(fmt.Sprintf("Verification passed: live medium now served from %s.", describeLiveBootSource(status)))
|
log(fmt.Sprintf("Verification passed: live medium now served from %s.", describeLiveBootSource(status)))
|
||||||
}
|
}
|
||||||
log("Done. Squashfs files are in RAM. Installation media can be safely disconnected.")
|
detachInstallMedium(status, log)
|
||||||
|
log("Done. Squashfs files are in RAM. Installation media has been detached when possible.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tryRemountLiveMedium(log func(string)) error {
|
||||||
|
output, err := runRemountMedium()
|
||||||
|
trimmed := strings.TrimSpace(string(output))
|
||||||
|
if err != nil {
|
||||||
|
if trimmed != "" && log != nil {
|
||||||
|
for _, line := range strings.Split(trimmed, "\n") {
|
||||||
|
log("bee-remount-medium: " + line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if trimmed != "" && log != nil {
|
||||||
|
for _, line := range strings.Split(trimmed, "\n") {
|
||||||
|
log("bee-remount-medium: " + line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureLiveMediumAvailable(log func(string)) ([]string, bool) {
|
||||||
|
squashfsFiles, err := liveMediumSquashfsGlob()
|
||||||
|
sourceAvailable := err == nil && len(squashfsFiles) > 0
|
||||||
|
if sourceAvailable {
|
||||||
|
return squashfsFiles, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if log != nil {
|
||||||
|
log("Live medium not mounted at /run/live/medium — attempting automatic remount scan...")
|
||||||
|
}
|
||||||
|
if remountErr := tryRemountLiveMedium(log); remountErr != nil {
|
||||||
|
if log != nil {
|
||||||
|
log(fmt.Sprintf("Automatic remount did not restore the live medium: %v", remountErr))
|
||||||
|
}
|
||||||
|
return squashfsFiles, false
|
||||||
|
}
|
||||||
|
|
||||||
|
squashfsFiles, err = liveMediumSquashfsGlob()
|
||||||
|
sourceAvailable = err == nil && len(squashfsFiles) > 0
|
||||||
|
if sourceAvailable && log != nil {
|
||||||
|
log("Live medium restored after remount scan.")
|
||||||
|
}
|
||||||
|
return squashfsFiles, sourceAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
func detachInstallMedium(status LiveBootSource, log func(string)) {
|
||||||
|
if log == nil {
|
||||||
|
log = func(string) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Detaching original installation medium...")
|
||||||
|
if err := umountLiveMedium(); err != nil {
|
||||||
|
log(fmt.Sprintf("Warning: could not unmount /run/live/medium: %v", err))
|
||||||
|
} else {
|
||||||
|
log("Unmounted /run/live/medium.")
|
||||||
|
}
|
||||||
|
|
||||||
|
device := strings.TrimSpace(status.Device)
|
||||||
|
if device == "" {
|
||||||
|
device = strings.TrimSpace(status.Source)
|
||||||
|
}
|
||||||
|
if device == "" || !strings.HasPrefix(device, "/dev/") {
|
||||||
|
log("No block device identified for eject; skipping media eject.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ejectDevice(device); err != nil {
|
||||||
|
log(fmt.Sprintf("Warning: could not eject %s: %v", device, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log(fmt.Sprintf("Ejected %s.", device))
|
||||||
|
}
|
||||||
|
|
||||||
func verifyInstallToRAMStatus(status LiveBootSource, dstDir string, mediumRebound bool, log func(string)) error {
|
func verifyInstallToRAMStatus(status LiveBootSource, dstDir string, mediumRebound bool, log func(string)) error {
|
||||||
if status.InRAM {
|
if status.InRAM {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package platform
|
package platform
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestInferLiveBootKind(t *testing.T) {
|
func TestInferLiveBootKind(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -124,3 +127,156 @@ func TestShouldLogCopyProgress(t *testing.T) {
|
|||||||
t.Fatal("expected final completion log")
|
t.Fatal("expected final completion log")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTryRemountLiveMedium(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
orig := runRemountMedium
|
||||||
|
t.Cleanup(func() {
|
||||||
|
runRemountMedium = orig
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
runRemountMedium = func() ([]byte, error) {
|
||||||
|
return []byte("[10:57:31] Mounted /dev/sr1 on /run/live/medium\n"), nil
|
||||||
|
}
|
||||||
|
var logs []string
|
||||||
|
if err := tryRemountLiveMedium(func(msg string) { logs = append(logs, msg) }); err != nil {
|
||||||
|
t.Fatalf("tryRemountLiveMedium() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(logs) != 1 || logs[0] != "bee-remount-medium: [10:57:31] Mounted /dev/sr1 on /run/live/medium" {
|
||||||
|
t.Fatalf("logs=%v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failure", func(t *testing.T) {
|
||||||
|
runRemountMedium = func() ([]byte, error) {
|
||||||
|
return []byte("must be run as root\n"), fmt.Errorf("exit status 1")
|
||||||
|
}
|
||||||
|
var logs []string
|
||||||
|
err := tryRemountLiveMedium(func(msg string) { logs = append(logs, msg) })
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
if len(logs) != 1 || logs[0] != "bee-remount-medium: must be run as root" {
|
||||||
|
t.Fatalf("logs=%v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureLiveMediumAvailableRemountsSource(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
origGlob := liveMediumSquashfsGlob
|
||||||
|
origRemount := runRemountMedium
|
||||||
|
t.Cleanup(func() {
|
||||||
|
liveMediumSquashfsGlob = origGlob
|
||||||
|
runRemountMedium = origRemount
|
||||||
|
})
|
||||||
|
|
||||||
|
callCount := 0
|
||||||
|
liveMediumSquashfsGlob = func() ([]string, error) {
|
||||||
|
callCount++
|
||||||
|
if callCount == 1 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return []string{"/run/live/medium/live/filesystem.squashfs"}, nil
|
||||||
|
}
|
||||||
|
runRemountMedium = func() ([]byte, error) {
|
||||||
|
return []byte("Mounted /dev/sr1 on /run/live/medium\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var logs []string
|
||||||
|
files, ok := ensureLiveMediumAvailable(func(msg string) { logs = append(logs, msg) })
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected live medium to become available after remount")
|
||||||
|
}
|
||||||
|
if callCount < 2 {
|
||||||
|
t.Fatalf("liveMediumSquashfsGlob called %d times, want at least 2", callCount)
|
||||||
|
}
|
||||||
|
if len(files) != 1 || files[0] != "/run/live/medium/live/filesystem.squashfs" {
|
||||||
|
t.Fatalf("files=%v", files)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, msg := range logs {
|
||||||
|
if msg == "Live medium restored after remount scan." {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("expected remount success log, logs=%v", logs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetachInstallMedium(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
origUmount := umountLiveMedium
|
||||||
|
origEject := ejectDevice
|
||||||
|
t.Cleanup(func() {
|
||||||
|
umountLiveMedium = origUmount
|
||||||
|
ejectDevice = origEject
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
var umountCalled bool
|
||||||
|
var ejected string
|
||||||
|
umountLiveMedium = func() error {
|
||||||
|
umountCalled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ejectDevice = func(device string) error {
|
||||||
|
ejected = device
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var logs []string
|
||||||
|
detachInstallMedium(LiveBootSource{Kind: "cdrom", Device: "/dev/sr1"}, func(msg string) { logs = append(logs, msg) })
|
||||||
|
if !umountCalled {
|
||||||
|
t.Fatal("expected umountLiveMedium to be called")
|
||||||
|
}
|
||||||
|
if ejected != "/dev/sr1" {
|
||||||
|
t.Fatalf("ejected=%q want /dev/sr1", ejected)
|
||||||
|
}
|
||||||
|
if len(logs) < 3 {
|
||||||
|
t.Fatalf("logs=%v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no device", func(t *testing.T) {
|
||||||
|
umountLiveMedium = func() error { return nil }
|
||||||
|
ejectDevice = func(device string) error {
|
||||||
|
t.Fatalf("unexpected eject for %q", device)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var logs []string
|
||||||
|
detachInstallMedium(LiveBootSource{Kind: "ram", Source: "tmpfs"}, func(msg string) { logs = append(logs, msg) })
|
||||||
|
found := false
|
||||||
|
for _, msg := range logs {
|
||||||
|
if msg == "No block device identified for eject; skipping media eject." {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("logs=%v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("eject failure is warning only", func(t *testing.T) {
|
||||||
|
umountLiveMedium = func() error { return nil }
|
||||||
|
ejectDevice = func(device string) error { return fmt.Errorf("exit status 1") }
|
||||||
|
var logs []string
|
||||||
|
detachInstallMedium(LiveBootSource{Kind: "usb", Device: "/dev/sdb1"}, func(msg string) { logs = append(logs, msg) })
|
||||||
|
found := false
|
||||||
|
for _, msg := range logs {
|
||||||
|
if msg == "Warning: could not eject /dev/sdb1: exit status 1" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("logs=%v", logs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ func defaultTaskPriority(target string, params taskParams) int {
|
|||||||
return taskPriorityInstall
|
return taskPriorityInstall
|
||||||
case "install-to-ram":
|
case "install-to-ram":
|
||||||
return taskPriorityInstallToRAM
|
return taskPriorityInstallToRAM
|
||||||
|
case "nvme-format":
|
||||||
|
return taskPriorityInstall
|
||||||
case "audit":
|
case "audit":
|
||||||
return taskPriorityAudit
|
return taskPriorityAudit
|
||||||
case "nvidia-bench-perf", "nvidia-bench-power", "nvidia-bench-autotune":
|
case "nvidia-bench-perf", "nvidia-bench-power", "nvidia-bench-autotune":
|
||||||
|
|||||||
@@ -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) {
|
func TestHandleAPIBenchmarkNvidiaRunQueuesSelectedGPUs(t *testing.T) {
|
||||||
globalQueue.mu.Lock()
|
globalQueue.mu.Lock()
|
||||||
originalTasks := globalQueue.tasks
|
originalTasks := globalQueue.tasks
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ func (j *jobState) writeLogLineLocked(line string) {
|
|||||||
j.logBuf = bufio.NewWriterSize(f, 64*1024)
|
j.logBuf = bufio.NewWriterSize(f, 64*1024)
|
||||||
}
|
}
|
||||||
_, _ = j.logBuf.WriteString(line + "\n")
|
_, _ = j.logBuf.WriteString(line + "\n")
|
||||||
|
_ = j.logBuf.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// closeLog flushes and closes the log file. Called after all task output is done.
|
// closeLog flushes and closes the log file. Called after all task output is done.
|
||||||
|
|||||||
368
audit/internal/webui/nvme_format.go
Normal file
368
audit/internal/webui/nvme_format.go
Normal file
@@ -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 `<div id="nvme-format-status" style="font-size:13px;color:var(--muted);margin-bottom:12px">Loading NVMe disks...</div>
|
||||||
|
<div id="nvme-format-table"><p style="color:var(--muted);font-size:13px">Loading...</p></div>
|
||||||
|
<script>
|
||||||
|
function nvmeFormatEsc(s) {
|
||||||
|
return String(s == null ? '' : s).replace(/[&<>"']/g, function(c) {
|
||||||
|
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function loadNVMeFormats() {
|
||||||
|
var status = document.getElementById('nvme-format-status');
|
||||||
|
var table = document.getElementById('nvme-format-table');
|
||||||
|
status.textContent = 'Loading NVMe disks...';
|
||||||
|
status.style.color = 'var(--muted)';
|
||||||
|
table.innerHTML = '<p style="color:var(--muted);font-size:13px">Loading...</p>';
|
||||||
|
fetch('/api/tools/nvme-formats').then(function(r) { return r.json().then(function(d) { if (!r.ok) throw new Error(d.error || ('HTTP ' + r.status)); return d; }); }).then(function(disks) {
|
||||||
|
window._nvmeFormatDisks = Array.isArray(disks) ? disks : [];
|
||||||
|
if (!window._nvmeFormatDisks.length) {
|
||||||
|
status.textContent = 'No NVMe disks found.';
|
||||||
|
table.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
status.textContent = window._nvmeFormatDisks.length + ' NVMe disk(s) found.';
|
||||||
|
var rows = window._nvmeFormatDisks.map(function(d, idx) {
|
||||||
|
var current = d.current_format ? (d.current_format + ' / MODE ' + d.current_mode) : 'unknown';
|
||||||
|
var detail = [d.model || '', d.serial || '', d.size || ''].filter(Boolean).join(' | ');
|
||||||
|
var options = (d.modes || []).map(function(m) {
|
||||||
|
return '<option value="' + m.mode + '"' + (m.in_use ? ' selected' : '') + '>' + nvmeFormatEsc(m.label) + '</option>';
|
||||||
|
}).join('');
|
||||||
|
var disabled = options ? '' : ' disabled';
|
||||||
|
var err = d.error ? '<div style="font-size:12px;color:var(--crit-fg,#9f3a38);margin-top:4px">' + nvmeFormatEsc(d.error) + '</div>' : '';
|
||||||
|
return '<tr>'
|
||||||
|
+ '<td style="font-family:monospace;white-space:nowrap">' + nvmeFormatEsc(d.device) + (detail ? '<div style="font-family:inherit;font-size:12px;color:var(--muted)">' + nvmeFormatEsc(detail) + '</div>' : '') + '</td>'
|
||||||
|
+ '<td style="white-space:nowrap">' + nvmeFormatEsc(current) + err + '</td>'
|
||||||
|
+ '<td style="white-space:nowrap"><select id="nvme-format-select-' + idx + '"' + disabled + '>' + options + '</select></td>'
|
||||||
|
+ '<td style="white-space:nowrap"><button class="btn btn-sm btn-primary" onclick="nvmeFormatRun(' + idx + ', this)"' + disabled + '>Apply</button><div class="nvme-format-row-msg" style="margin-top:6px;font-size:12px;color:var(--muted)"></div></td>'
|
||||||
|
+ '</tr>';
|
||||||
|
}).join('');
|
||||||
|
table.innerHTML = '<table><tr><th>Disk</th><th>Current block / mode</th><th>New mode</th><th>Action</th></tr>' + rows + '</table>';
|
||||||
|
}).catch(function(e) {
|
||||||
|
status.textContent = 'Error loading NVMe disks: ' + e.message;
|
||||||
|
status.style.color = 'var(--crit-fg,#9f3a38)';
|
||||||
|
table.innerHTML = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function nvmeWaitTaskDone(taskID, rowMsg) {
|
||||||
|
var timer = setInterval(function() {
|
||||||
|
fetch('/api/tasks').then(function(r) { return r.json(); }).then(function(tasks) {
|
||||||
|
var task = (tasks || []).find(function(t) { return t.id === taskID; });
|
||||||
|
if (!task) return;
|
||||||
|
if (task.status === 'done' || task.status === 'failed' || task.status === 'cancelled') {
|
||||||
|
clearInterval(timer);
|
||||||
|
rowMsg.textContent = 'Task ' + taskID + ': ' + task.status + (task.error ? ' - ' + task.error : '');
|
||||||
|
rowMsg.style.color = task.status === 'done' ? 'var(--ok,green)' : 'var(--crit-fg,#9f3a38)';
|
||||||
|
loadNVMeFormats();
|
||||||
|
}
|
||||||
|
}).catch(function(){});
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
function nvmeFormatRun(idx, btn) {
|
||||||
|
var disk = (window._nvmeFormatDisks || [])[idx];
|
||||||
|
var select = document.getElementById('nvme-format-select-' + idx);
|
||||||
|
var row = btn.closest('td');
|
||||||
|
var rowMsg = row.querySelector('.nvme-format-row-msg');
|
||||||
|
if (!disk || !select) return;
|
||||||
|
var lbaf = parseInt(select.value, 10);
|
||||||
|
var mode = (disk.modes || []).find(function(m) { return m.mode === lbaf; });
|
||||||
|
if (!mode) return;
|
||||||
|
if (!window.confirm('Format ' + disk.device + ' to ' + mode.label + '? This erases data on the namespace.')) return;
|
||||||
|
btn.disabled = true;
|
||||||
|
rowMsg.style.color = 'var(--muted)';
|
||||||
|
rowMsg.textContent = 'Queued...';
|
||||||
|
fetch('/api/tools/nvme-format/run', {
|
||||||
|
method:'POST',
|
||||||
|
headers:{'Content-Type':'application/json'},
|
||||||
|
body:JSON.stringify({device: disk.device, lbaf: lbaf})
|
||||||
|
}).then(function(r) { return r.json().then(function(d) { if (!r.ok) throw new Error(d.error || ('HTTP ' + r.status)); return d; }); }).then(function(d) {
|
||||||
|
rowMsg.textContent = 'Task ' + d.task_id + ' queued.';
|
||||||
|
nvmeWaitTaskDone(d.task_id, rowMsg);
|
||||||
|
}).catch(function(e) {
|
||||||
|
rowMsg.style.color = 'var(--crit-fg,#9f3a38)';
|
||||||
|
rowMsg.textContent = 'Error: ' + e.message;
|
||||||
|
}).finally(function() {
|
||||||
|
btn.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loadNVMeFormats();
|
||||||
|
</script>`
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderNVMeFormatCard() string {
|
||||||
|
return `<div class="card"><div class="card-head">NVMe Block Format <button class="btn btn-sm btn-secondary" onclick="loadNVMeFormats()" style="margin-left:auto">↻ Refresh</button></div><div class="card-body">` +
|
||||||
|
`<p style="font-size:13px;color:var(--muted);margin-bottom:12px">Lists NVMe namespaces and changes their LBA format through a queued task.</p>` +
|
||||||
|
renderNVMeFormatInline() + `</div></div>`
|
||||||
|
}
|
||||||
@@ -431,7 +431,7 @@ fetch('/api/system/ram-status').then(r=>r.json()).then(d=>{
|
|||||||
else if (kind === 'disk') label = 'disk (' + source + ')';
|
else if (kind === 'disk') label = 'disk (' + source + ')';
|
||||||
else label = source;
|
else label = source;
|
||||||
boot.textContent = 'Current boot source: ' + label + '.';
|
boot.textContent = 'Current boot source: ' + label + '.';
|
||||||
txt.textContent = d.message || 'Checking...';
|
txt.textContent = d.blocked_reason || d.message || 'Checking...';
|
||||||
if (d.status === 'ok' || d.in_ram) {
|
if (d.status === 'ok' || d.in_ram) {
|
||||||
txt.style.color = 'var(--ok, green)';
|
txt.style.color = 'var(--ok, green)';
|
||||||
} else if (d.status === 'failed') {
|
} else if (d.status === 'failed') {
|
||||||
@@ -475,6 +475,7 @@ function installToRAM() {
|
|||||||
<div class="card"><div class="card-head">Services</div><div class="card-body">` +
|
<div class="card"><div class="card-head">Services</div><div class="card-body">` +
|
||||||
renderServicesInline() + `</div></div>
|
renderServicesInline() + `</div></div>
|
||||||
|
|
||||||
|
` + renderNVMeFormatCard() + `
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function checkTools() {
|
function checkTools() {
|
||||||
|
|||||||
@@ -307,6 +307,8 @@ func NewHandler(opts HandlerOptions) http.Handler {
|
|||||||
|
|
||||||
// Tools
|
// Tools
|
||||||
mux.HandleFunc("GET /api/tools/check", h.handleAPIToolsCheck)
|
mux.HandleFunc("GET /api/tools/check", h.handleAPIToolsCheck)
|
||||||
|
mux.HandleFunc("GET /api/tools/nvme-formats", h.handleAPINVMeFormats)
|
||||||
|
mux.HandleFunc("POST /api/tools/nvme-format/run", h.handleAPINVMeFormatRun)
|
||||||
|
|
||||||
// GPU presence / tools
|
// GPU presence / tools
|
||||||
mux.HandleFunc("GET /api/gpu/presence", h.handleAPIGPUPresence)
|
mux.HandleFunc("GET /api/gpu/presence", h.handleAPIGPUPresence)
|
||||||
|
|||||||
@@ -677,6 +677,12 @@ func TestToolsPageRendersNvidiaSelfHealSection(t *testing.T) {
|
|||||||
if !strings.Contains(body, `/api/blackbox/status`) {
|
if !strings.Contains(body, `/api/blackbox/status`) {
|
||||||
t.Fatalf("tools page missing black-box status api usage: %s", body)
|
t.Fatalf("tools page missing black-box status api usage: %s", body)
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(body, `NVMe Block Format`) {
|
||||||
|
t.Fatalf("tools page missing nvme block format section: %s", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, `/api/tools/nvme-formats`) || !strings.Contains(body, `/api/tools/nvme-format/run`) {
|
||||||
|
t.Fatalf("tools page missing nvme format api usage: %s", body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBenchmarkPageRendersGPUSelectionControls(t *testing.T) {
|
func TestBenchmarkPageRendersGPUSelectionControls(t *testing.T) {
|
||||||
|
|||||||
@@ -376,6 +376,12 @@ func executeTaskWithOptions(opts *HandlerOptions, t *Task, j *jobState, ctx cont
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
err = a.RunInstallToRAM(ctx, j.append)
|
err = a.RunInstallToRAM(ctx, j.append)
|
||||||
|
case "nvme-format":
|
||||||
|
if strings.TrimSpace(t.params.Device) == "" {
|
||||||
|
err = fmt.Errorf("device is required")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
err = runNVMeFormatTask(ctx, j, t.params.Device, t.params.LBAF)
|
||||||
default:
|
default:
|
||||||
j.append("ERROR: unknown target: " + t.Target)
|
j.append("ERROR: unknown target: " + t.Target)
|
||||||
j.finish("unknown target")
|
j.finish("unknown target")
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ var taskNames = map[string]string{
|
|||||||
"support-bundle": "Support Bundle",
|
"support-bundle": "Support Bundle",
|
||||||
"install": "Install to Disk",
|
"install": "Install to Disk",
|
||||||
"install-to-ram": "Install to RAM",
|
"install-to-ram": "Install to RAM",
|
||||||
|
"nvme-format": "NVMe Block Format Change",
|
||||||
}
|
}
|
||||||
|
|
||||||
// burnNames maps target → human-readable name when a burn profile is set.
|
// burnNames maps target → human-readable name when a burn profile is set.
|
||||||
@@ -137,6 +138,7 @@ type taskParams struct {
|
|||||||
RampRunID string `json:"ramp_run_id,omitempty"`
|
RampRunID string `json:"ramp_run_id,omitempty"`
|
||||||
DisplayName string `json:"display_name,omitempty"`
|
DisplayName string `json:"display_name,omitempty"`
|
||||||
Device string `json:"device,omitempty"` // for install
|
Device string `json:"device,omitempty"` // for install
|
||||||
|
LBAF int `json:"lbaf,omitempty"`
|
||||||
PlatformComponents []string `json:"platform_components,omitempty"`
|
PlatformComponents []string `json:"platform_components,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,6 +600,17 @@ func (q *taskQueue) startRecoveredTaskMonitorLocked(t *Task, j *jobState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *taskQueue) runTaskExternal(t *Task, j *jobState) {
|
func (q *taskQueue) runTaskExternal(t *Task, j *jobState) {
|
||||||
|
startedKmsgWatch := false
|
||||||
|
if q.kmsgWatcher != nil && isSATTarget(t.Target) {
|
||||||
|
q.kmsgWatcher.NotifyTaskStarted(t.ID, t.Target)
|
||||||
|
startedKmsgWatch = true
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if startedKmsgWatch && q.kmsgWatcher != nil {
|
||||||
|
q.kmsgWatcher.NotifyTaskFinished(t.ID)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
stopTail := make(chan struct{})
|
stopTail := make(chan struct{})
|
||||||
doneTail := make(chan struct{})
|
doneTail := make(chan struct{})
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
@@ -126,6 +126,23 @@ func TestNewTaskJobStateLoadsExistingLog(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJobAppendFlushesTaskLogImmediately(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "task.log")
|
||||||
|
j := newTaskJobState(path)
|
||||||
|
|
||||||
|
j.append("live-line")
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(data) != "live-line\n" {
|
||||||
|
t.Fatalf("log=%q want live-line newline", string(data))
|
||||||
|
}
|
||||||
|
j.closeLog()
|
||||||
|
}
|
||||||
|
|
||||||
func TestTaskQueueSnapshotSortsNewestFirst(t *testing.T) {
|
func TestTaskQueueSnapshotSortsNewestFirst(t *testing.T) {
|
||||||
now := time.Date(2026, 4, 2, 12, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 4, 2, 12, 0, 0, 0, time.UTC)
|
||||||
q := &taskQueue{
|
q := &taskQueue{
|
||||||
@@ -849,3 +866,82 @@ func TestExecuteTaskMarksPanicsAsFailedAndClosesKmsgWindow(t *testing.T) {
|
|||||||
t.Fatalf("expected kmsg window to be cleared, got %+v", window)
|
t.Fatalf("expected kmsg window to be cleared, got %+v", window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunTaskExternalOpensAndClosesKmsgWindow(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
releasePath := filepath.Join(dir, "release")
|
||||||
|
readyPath := filepath.Join(dir, "ready")
|
||||||
|
q := &taskQueue{
|
||||||
|
opts: &HandlerOptions{ExportDir: dir},
|
||||||
|
logsDir: filepath.Join(dir, "tasks"),
|
||||||
|
kmsgWatcher: newKmsgWatcher(nil),
|
||||||
|
trigger: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(q.logsDir, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tk := &Task{
|
||||||
|
ID: "cpu-external-1",
|
||||||
|
Name: "CPU SAT",
|
||||||
|
Target: "cpu",
|
||||||
|
Status: TaskRunning,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
q.assignTaskLogPathLocked(tk)
|
||||||
|
j := newTaskJobState(tk.LogPath)
|
||||||
|
|
||||||
|
orig := externalTaskRunnerCommand
|
||||||
|
externalTaskRunnerCommand = func(exportDir, taskID string) (*exec.Cmd, error) {
|
||||||
|
script := "printf ready > \"$1\"; while [ ! -f \"$2\" ]; do sleep 0.05; done"
|
||||||
|
return exec.Command("sh", "-c", script, "sh", readyPath, releasePath), nil
|
||||||
|
}
|
||||||
|
defer func() { externalTaskRunnerCommand = orig }()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
q.runTaskExternal(tk, j)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if _, err := os.Stat(readyPath); err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(readyPath); err != nil {
|
||||||
|
t.Fatalf("external runner did not start: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q.kmsgWatcher.mu.Lock()
|
||||||
|
activeCount := q.kmsgWatcher.activeCount
|
||||||
|
window := q.kmsgWatcher.window
|
||||||
|
q.kmsgWatcher.mu.Unlock()
|
||||||
|
if activeCount != 1 {
|
||||||
|
t.Fatalf("activeCount while running=%d want 1", activeCount)
|
||||||
|
}
|
||||||
|
if window == nil || len(window.targets) != 1 || window.targets[0] != "cpu" {
|
||||||
|
t.Fatalf("window while running=%+v", window)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(releasePath, []byte("1\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("runTaskExternal did not return")
|
||||||
|
}
|
||||||
|
|
||||||
|
q.kmsgWatcher.mu.Lock()
|
||||||
|
activeCount = q.kmsgWatcher.activeCount
|
||||||
|
window = q.kmsgWatcher.window
|
||||||
|
q.kmsgWatcher.mu.Unlock()
|
||||||
|
if activeCount != 0 {
|
||||||
|
t.Fatalf("activeCount after finish=%d want 0", activeCount)
|
||||||
|
}
|
||||||
|
if window != nil {
|
||||||
|
t.Fatalf("expected kmsg window to be cleared, got %+v", window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
2
bible
2
bible
Submodule bible updated: d2600f1279...1d89a4918e
@@ -16,6 +16,12 @@ else
|
|||||||
LB_LINUX_PACKAGES="linux-image"
|
LB_LINUX_PACKAGES="linux-image"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -n "${BEE_ISO_VOLUME:-}" ]; then
|
||||||
|
LB_ISO_VOLUME="${BEE_ISO_VOLUME}"
|
||||||
|
else
|
||||||
|
LB_ISO_VOLUME="EASY_BEE_${BEE_GPU_VENDOR_UPPER:-NVIDIA}"
|
||||||
|
fi
|
||||||
|
|
||||||
lb config noauto \
|
lb config noauto \
|
||||||
--distribution bookworm \
|
--distribution bookworm \
|
||||||
--architectures amd64 \
|
--architectures amd64 \
|
||||||
@@ -30,9 +36,9 @@ lb config noauto \
|
|||||||
--linux-flavours "amd64" \
|
--linux-flavours "amd64" \
|
||||||
--linux-packages "${LB_LINUX_PACKAGES}" \
|
--linux-packages "${LB_LINUX_PACKAGES}" \
|
||||||
--memtest memtest86+ \
|
--memtest memtest86+ \
|
||||||
--iso-volume "EASY_BEE_${BEE_GPU_VENDOR_UPPER:-NVIDIA}" \
|
--iso-volume "${LB_ISO_VOLUME}" \
|
||||||
--iso-application "EASY-BEE-${BEE_GPU_VENDOR_UPPER:-NVIDIA}" \
|
--iso-application "EASY-BEE-${BEE_GPU_VENDOR_UPPER:-NVIDIA}" \
|
||||||
--bootappend-live "boot=live components video=1920x1080 console=ttyS0,115200n8 console=tty0 loglevel=3 systemd.show_status=1 username=bee user-fullname=Bee modprobe.blacklist=nouveau,snd_hda_intel,snd_hda_codec_realtek,snd_hda_codec_generic,soundcore" \
|
--bootappend-live "boot=live live-media-label=${LB_ISO_VOLUME} components video=1920x1080 console=ttyS0,115200n8 console=tty0 loglevel=3 systemd.show_status=1 username=bee user-fullname=Bee modprobe.blacklist=nouveau,snd_hda_intel,snd_hda_codec_realtek,snd_hda_codec_generic,soundcore" \
|
||||||
--debootstrap-options "--include=ca-certificates" \
|
--debootstrap-options "--include=ca-certificates" \
|
||||||
--apt-recommends false \
|
--apt-recommends false \
|
||||||
--chroot-squashfs-compression-type zstd \
|
--chroot-squashfs-compression-type zstd \
|
||||||
|
|||||||
@@ -69,12 +69,27 @@ mkdir -p "${CACHE_ROOT}"
|
|||||||
: "${GOMODCACHE:=${CACHE_ROOT}/go-mod}"
|
: "${GOMODCACHE:=${CACHE_ROOT}/go-mod}"
|
||||||
export GOCACHE GOMODCACHE
|
export GOCACHE GOMODCACHE
|
||||||
|
|
||||||
resolve_audit_version() {
|
resolve_project_version() {
|
||||||
|
if [ -n "${BEE_VERSION:-}" ]; then
|
||||||
|
echo "${BEE_VERSION}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${BEE_AUDIT_VERSION:-}" ] && [ -n "${BEE_ISO_VERSION:-}" ] && [ "${BEE_AUDIT_VERSION}" != "${BEE_ISO_VERSION}" ]; then
|
||||||
|
echo "ERROR: BEE_AUDIT_VERSION (${BEE_AUDIT_VERSION}) and BEE_ISO_VERSION (${BEE_ISO_VERSION}) differ; versioning must stay synchronized" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "${BEE_AUDIT_VERSION:-}" ]; then
|
if [ -n "${BEE_AUDIT_VERSION:-}" ]; then
|
||||||
echo "${BEE_AUDIT_VERSION}"
|
echo "${BEE_AUDIT_VERSION}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -n "${BEE_ISO_VERSION:-}" ]; then
|
||||||
|
echo "${BEE_ISO_VERSION}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
tag="$(git -C "${REPO_ROOT}" describe --tags --match 'v[0-9]*' --abbrev=7 --dirty 2>/dev/null || true)"
|
tag="$(git -C "${REPO_ROOT}" describe --tags --match 'v[0-9]*' --abbrev=7 --dirty 2>/dev/null || true)"
|
||||||
case "${tag}" in
|
case "${tag}" in
|
||||||
v*)
|
v*)
|
||||||
@@ -97,35 +112,6 @@ resolve_audit_version() {
|
|||||||
date +%Y%m%d
|
date +%Y%m%d
|
||||||
}
|
}
|
||||||
|
|
||||||
# ISO image versioned separately from the audit binary (iso/v* tags).
|
|
||||||
resolve_iso_version() {
|
|
||||||
if [ -n "${BEE_ISO_VERSION:-}" ]; then
|
|
||||||
echo "${BEE_ISO_VERSION}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Plain v* tags (e.g. v2.7) take priority — this is the current tagging scheme
|
|
||||||
tag="$(git -C "${REPO_ROOT}" describe --tags --match 'v[0-9]*' --abbrev=7 --dirty 2>/dev/null || true)"
|
|
||||||
case "${tag}" in
|
|
||||||
v*)
|
|
||||||
echo "${tag#v}"
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Legacy iso/v* tags fallback
|
|
||||||
tag="$(git -C "${REPO_ROOT}" describe --tags --match 'iso/v*' --abbrev=7 --dirty 2>/dev/null || true)"
|
|
||||||
case "${tag}" in
|
|
||||||
iso/v*)
|
|
||||||
echo "${tag#iso/v}"
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Fall back to audit version so the name is still meaningful
|
|
||||||
resolve_audit_version
|
|
||||||
}
|
|
||||||
|
|
||||||
sync_builder_workdir() {
|
sync_builder_workdir() {
|
||||||
src_dir="$1"
|
src_dir="$1"
|
||||||
dst_dir="$2"
|
dst_dir="$2"
|
||||||
@@ -550,6 +536,11 @@ validate_iso_live_boot_entries() {
|
|||||||
rm -f "$grub_cfg" "$isolinux_cfg"
|
rm -f "$grub_cfg" "$isolinux_cfg"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
grep -q 'linux .*live-media-label=EASY_BEE_' "$grub_cfg" || {
|
||||||
|
echo "ERROR: GRUB live entry is missing live-media-label pinning" >&2
|
||||||
|
rm -f "$grub_cfg" "$isolinux_cfg"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
grep -q 'append .*boot=live ' "$isolinux_cfg" || {
|
grep -q 'append .*boot=live ' "$isolinux_cfg" || {
|
||||||
echo "ERROR: isolinux live entry is missing boot=live" >&2
|
echo "ERROR: isolinux live entry is missing boot=live" >&2
|
||||||
@@ -561,11 +552,52 @@ validate_iso_live_boot_entries() {
|
|||||||
rm -f "$grub_cfg" "$isolinux_cfg"
|
rm -f "$grub_cfg" "$isolinux_cfg"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
grep -q 'append .*live-media-label=EASY_BEE_' "$isolinux_cfg" || {
|
||||||
|
echo "ERROR: isolinux live entry is missing live-media-label pinning" >&2
|
||||||
|
rm -f "$grub_cfg" "$isolinux_cfg"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
rm -f "$grub_cfg" "$isolinux_cfg"
|
rm -f "$grub_cfg" "$isolinux_cfg"
|
||||||
echo "=== live boot validation OK ==="
|
echo "=== live boot validation OK ==="
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_iso_grub_theme_assets() {
|
||||||
|
iso_path="$1"
|
||||||
|
echo "=== validating GRUB theme assets in ISO ==="
|
||||||
|
|
||||||
|
[ -f "$iso_path" ] || {
|
||||||
|
echo "ERROR: ISO not found for GRUB theme validation: $iso_path" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
require_iso_reader "$iso_path" >/dev/null 2>&1 || {
|
||||||
|
echo "ERROR: ISO reader unavailable for GRUB theme validation" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
iso_files="$(mktemp)"
|
||||||
|
iso_list_files "$iso_path" > "$iso_files" || {
|
||||||
|
echo "ERROR: failed to list ISO files for GRUB theme validation" >&2
|
||||||
|
rm -f "$iso_files"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for required in \
|
||||||
|
boot/grub/config.cfg \
|
||||||
|
boot/grub/theme.cfg \
|
||||||
|
boot/grub/live-theme/theme.txt \
|
||||||
|
boot/grub/live-theme/bee-logo.tga; do
|
||||||
|
grep -q "^${required}$" "$iso_files" || {
|
||||||
|
echo "ERROR: missing GRUB theme asset in ISO: ${required}" >&2
|
||||||
|
rm -f "$iso_files"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -f "$iso_files"
|
||||||
|
echo "=== GRUB theme validation OK ==="
|
||||||
|
}
|
||||||
|
|
||||||
validate_iso_nvidia_runtime() {
|
validate_iso_nvidia_runtime() {
|
||||||
iso_path="$1"
|
iso_path="$1"
|
||||||
[ "$BEE_GPU_VENDOR" = "nvidia" ] || return 0
|
[ "$BEE_GPU_VENDOR" = "nvidia" ] || return 0
|
||||||
@@ -698,16 +730,6 @@ write_canonical_grub_cfg() {
|
|||||||
cat > "$cfg" <<EOF
|
cat > "$cfg" <<EOF
|
||||||
source /boot/grub/config.cfg
|
source /boot/grub/config.cfg
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo " ███████╗ █████╗ ███████╗██╗ ██╗ ██████╗ ███████╗███████╗"
|
|
||||||
echo " ██╔════╝██╔══██╗██╔════╝╚██╗ ██╔╝ ██╔══██╗██╔════╝██╔════╝"
|
|
||||||
echo " █████╗ ███████║███████╗ ╚████╔╝ █████╗██████╔╝█████╗ █████╗"
|
|
||||||
echo " ██╔══╝ ██╔══██║╚════██║ ╚██╔╝ ╚════╝██╔══██╗██╔══╝ ██╔══╝"
|
|
||||||
echo " ███████╗██║ ██║███████║ ██║ ██████╔╝███████╗███████╗"
|
|
||||||
echo " ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═════╝ ╚══════╝╚══════╝"
|
|
||||||
echo " Hardware Audit LiveCD"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
menuentry "EASY-BEE" {
|
menuentry "EASY-BEE" {
|
||||||
linux ${kernel} ${append_live} bee.display=kms bee.nvidia.mode=normal pci=realloc net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable pcie_aspm=off intel_idle.max_cstate=1 processor.max_cstate=1 nowatchdog nosoftlockup
|
linux ${kernel} ${append_live} bee.display=kms bee.nvidia.mode=normal pci=realloc net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable pcie_aspm=off intel_idle.max_cstate=1 processor.max_cstate=1 nowatchdog nosoftlockup
|
||||||
initrd ${initrd}
|
initrd ${initrd}
|
||||||
@@ -746,13 +768,13 @@ write_canonical_isolinux_cfg() {
|
|||||||
cat > "$cfg" <<EOF
|
cat > "$cfg" <<EOF
|
||||||
label live-@FLAVOUR@-normal
|
label live-@FLAVOUR@-normal
|
||||||
menu label ^EASY-BEE
|
menu label ^EASY-BEE
|
||||||
menu default
|
|
||||||
linux ${kernel}
|
linux ${kernel}
|
||||||
initrd ${initrd}
|
initrd ${initrd}
|
||||||
append ${append_live} nomodeset bee.nvidia.mode=normal net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable pcie_aspm=off intel_idle.max_cstate=1 processor.max_cstate=1 nowatchdog nosoftlockup
|
append ${append_live} nomodeset bee.nvidia.mode=normal net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable pcie_aspm=off intel_idle.max_cstate=1 processor.max_cstate=1 nowatchdog nosoftlockup
|
||||||
|
|
||||||
label live-@FLAVOUR@-toram
|
label live-@FLAVOUR@-toram
|
||||||
menu label EASY-BEE (^load to RAM)
|
menu label EASY-BEE (^load to RAM)
|
||||||
|
menu default
|
||||||
linux ${kernel}
|
linux ${kernel}
|
||||||
initrd ${initrd}
|
initrd ${initrd}
|
||||||
append ${append_live} toram nomodeset bee.nvidia.mode=normal net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable pcie_aspm=off intel_idle.max_cstate=1 processor.max_cstate=1 nowatchdog nosoftlockup
|
append ${append_live} toram nomodeset bee.nvidia.mode=normal net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable pcie_aspm=off intel_idle.max_cstate=1 processor.max_cstate=1 nowatchdog nosoftlockup
|
||||||
@@ -992,11 +1014,11 @@ recover_iso_memtest() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
AUDIT_VERSION_EFFECTIVE="$(resolve_audit_version)"
|
PROJECT_VERSION_EFFECTIVE="$(resolve_project_version)"
|
||||||
ISO_VERSION_EFFECTIVE="$(resolve_iso_version)"
|
ISO_BASENAME="easy-bee-${BUILD_VARIANT}-v${PROJECT_VERSION_EFFECTIVE}-amd64"
|
||||||
ISO_BASENAME="easy-bee-${BUILD_VARIANT}-v${ISO_VERSION_EFFECTIVE}-amd64"
|
|
||||||
# Versioned output directory: dist/easy-bee-v4.1/ — all final artefacts live here.
|
# Versioned output directory: dist/easy-bee-v4.1/ — all final artefacts live here.
|
||||||
OUT_DIR="${DIST_DIR}/easy-bee-v${ISO_VERSION_EFFECTIVE}"
|
OUT_DIR="${DIST_DIR}/easy-bee-v${PROJECT_VERSION_EFFECTIVE}"
|
||||||
|
ISO_VERSION_LABEL_TOKEN="$(printf '%s' "${PROJECT_VERSION_EFFECTIVE}" | tr '[:lower:].-' '[:upper:]__')"
|
||||||
mkdir -p "${OUT_DIR}"
|
mkdir -p "${OUT_DIR}"
|
||||||
LOG_DIR="${OUT_DIR}/${ISO_BASENAME}.logs"
|
LOG_DIR="${OUT_DIR}/${ISO_BASENAME}.logs"
|
||||||
LOG_ARCHIVE="${OUT_DIR}/${ISO_BASENAME}.logs.tar.gz"
|
LOG_ARCHIVE="${OUT_DIR}/${ISO_BASENAME}.logs.tar.gz"
|
||||||
@@ -1172,7 +1194,7 @@ fi
|
|||||||
|
|
||||||
echo "=== bee ISO build (variant: ${BUILD_VARIANT}) ==="
|
echo "=== bee ISO build (variant: ${BUILD_VARIANT}) ==="
|
||||||
echo "Debian: ${DEBIAN_VERSION}, Kernel ABI: ${DEBIAN_KERNEL_ABI}, Go: ${GO_VERSION}"
|
echo "Debian: ${DEBIAN_VERSION}, Kernel ABI: ${DEBIAN_KERNEL_ABI}, Go: ${GO_VERSION}"
|
||||||
echo "Audit version: ${AUDIT_VERSION_EFFECTIVE}, ISO version: ${ISO_VERSION_EFFECTIVE}"
|
echo "Project version: ${PROJECT_VERSION_EFFECTIVE}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
run_step "sync git submodules" "05-git-submodules" \
|
run_step "sync git submodules" "05-git-submodules" \
|
||||||
@@ -1192,7 +1214,7 @@ if [ "$NEED_BUILD" = "1" ]; then
|
|||||||
"cd '${REPO_ROOT}/audit' && \
|
"cd '${REPO_ROOT}/audit' && \
|
||||||
env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
|
env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
|
||||||
go build \
|
go build \
|
||||||
-ldflags '-s -w -X main.Version=${AUDIT_VERSION_EFFECTIVE}' \
|
-ldflags '-s -w -X main.Version=${PROJECT_VERSION_EFFECTIVE}' \
|
||||||
-o '${BEE_BIN}' \
|
-o '${BEE_BIN}' \
|
||||||
./cmd/bee"
|
./cmd/bee"
|
||||||
echo "binary: $BEE_BIN"
|
echo "binary: $BEE_BIN"
|
||||||
@@ -1467,8 +1489,10 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
cat > "${OVERLAY_STAGE_DIR}/etc/bee-release" <<EOF
|
cat > "${OVERLAY_STAGE_DIR}/etc/bee-release" <<EOF
|
||||||
BEE_ISO_VERSION=${ISO_VERSION_EFFECTIVE}
|
BEE_VERSION=${PROJECT_VERSION_EFFECTIVE}
|
||||||
BEE_AUDIT_VERSION=${AUDIT_VERSION_EFFECTIVE}
|
export BEE_VERSION
|
||||||
|
BEE_ISO_VERSION=${PROJECT_VERSION_EFFECTIVE}
|
||||||
|
BEE_AUDIT_VERSION=${PROJECT_VERSION_EFFECTIVE}
|
||||||
BEE_BUILD_VARIANT=${BUILD_VARIANT}
|
BEE_BUILD_VARIANT=${BUILD_VARIANT}
|
||||||
BEE_GPU_VENDOR=${BEE_GPU_VENDOR}
|
BEE_GPU_VENDOR=${BEE_GPU_VENDOR}
|
||||||
BUILD_DATE=${BUILD_DATE}
|
BUILD_DATE=${BUILD_DATE}
|
||||||
@@ -1561,6 +1585,7 @@ if ! needs_full_build; then
|
|||||||
fast_path_rebuild_iso
|
fast_path_rebuild_iso
|
||||||
ISO_RAW="${LB_DIR}/live-image-amd64.hybrid.iso"
|
ISO_RAW="${LB_DIR}/live-image-amd64.hybrid.iso"
|
||||||
validate_iso_live_boot_entries "$ISO_RAW"
|
validate_iso_live_boot_entries "$ISO_RAW"
|
||||||
|
validate_iso_grub_theme_assets "$ISO_RAW"
|
||||||
validate_iso_nvidia_runtime "$ISO_RAW"
|
validate_iso_nvidia_runtime "$ISO_RAW"
|
||||||
cp "$ISO_RAW" "$ISO_OUT"
|
cp "$ISO_RAW" "$ISO_OUT"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -1575,7 +1600,8 @@ echo "=== building ISO (variant: ${BUILD_VARIANT}) ==="
|
|||||||
|
|
||||||
# Export for auto/config
|
# Export for auto/config
|
||||||
BEE_GPU_VENDOR_UPPER="$(echo "${BUILD_VARIANT}" | tr 'a-z-' 'A-Z_')"
|
BEE_GPU_VENDOR_UPPER="$(echo "${BUILD_VARIANT}" | tr 'a-z-' 'A-Z_')"
|
||||||
export BEE_GPU_VENDOR_UPPER
|
BEE_ISO_VOLUME="EASY_BEE_${BEE_GPU_VENDOR_UPPER}_V${ISO_VERSION_LABEL_TOKEN}"
|
||||||
|
export BEE_GPU_VENDOR_UPPER BEE_ISO_VOLUME
|
||||||
|
|
||||||
cd "${LB_DIR}"
|
cd "${LB_DIR}"
|
||||||
run_step_sh "live-build clean" "80-lb-clean" "lb clean --all 2>&1 | tail -3"
|
run_step_sh "live-build clean" "80-lb-clean" "lb clean --all 2>&1 | tail -3"
|
||||||
@@ -1615,6 +1641,7 @@ if [ -f "$ISO_RAW" ]; then
|
|||||||
fi
|
fi
|
||||||
validate_iso_memtest "$ISO_RAW"
|
validate_iso_memtest "$ISO_RAW"
|
||||||
validate_iso_live_boot_entries "$ISO_RAW"
|
validate_iso_live_boot_entries "$ISO_RAW"
|
||||||
|
validate_iso_grub_theme_assets "$ISO_RAW"
|
||||||
validate_iso_nvidia_runtime "$ISO_RAW"
|
validate_iso_nvidia_runtime "$ISO_RAW"
|
||||||
cp "$ISO_RAW" "$ISO_OUT"
|
cp "$ISO_RAW" "$ISO_OUT"
|
||||||
touch "${FULL_BUILD_MARKER}"
|
touch "${FULL_BUILD_MARKER}"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
set default=0
|
set default=1
|
||||||
set timeout=5
|
set timeout=10
|
||||||
|
|
||||||
if [ x$feature_default_font_path = xy ] ; then
|
if [ x$feature_default_font_path = xy ] ; then
|
||||||
font=unicode
|
font=unicode
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
label live-@FLAVOUR@-normal
|
label live-@FLAVOUR@-normal
|
||||||
menu label ^EASY-BEE
|
menu label ^EASY-BEE
|
||||||
menu default
|
|
||||||
linux @LINUX@
|
linux @LINUX@
|
||||||
initrd @INITRD@
|
initrd @INITRD@
|
||||||
append @APPEND_LIVE@ nomodeset bee.nvidia.mode=normal net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable pcie_aspm=off intel_idle.max_cstate=1 processor.max_cstate=1 nowatchdog nosoftlockup
|
append @APPEND_LIVE@ nomodeset bee.nvidia.mode=normal net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable pcie_aspm=off intel_idle.max_cstate=1 processor.max_cstate=1 nowatchdog nosoftlockup
|
||||||
|
|
||||||
label live-@FLAVOUR@-toram
|
label live-@FLAVOUR@-toram
|
||||||
menu label EASY-BEE (^load to RAM)
|
menu label EASY-BEE (^load to RAM)
|
||||||
|
menu default
|
||||||
linux @LINUX@
|
linux @LINUX@
|
||||||
initrd @INITRD@
|
initrd @INITRD@
|
||||||
append @APPEND_LIVE@ toram nomodeset bee.nvidia.mode=normal net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable pcie_aspm=off intel_idle.max_cstate=1 processor.max_cstate=1 nowatchdog nosoftlockup
|
append @APPEND_LIVE@ toram nomodeset bee.nvidia.mode=normal net.ifnames=0 biosdevname=0 mitigations=off transparent_hugepage=always numa_balancing=disable pcie_aspm=off intel_idle.max_cstate=1 processor.max_cstate=1 nowatchdog nosoftlockup
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Description=Bee: run self-heal checks periodically
|
|||||||
|
|
||||||
[Timer]
|
[Timer]
|
||||||
OnBootSec=45sec
|
OnBootSec=45sec
|
||||||
OnUnitActiveSec=60sec
|
OnUnitActiveSec=3min
|
||||||
AccuracySec=15sec
|
AccuracySec=15sec
|
||||||
Unit=bee-selfheal.service
|
Unit=bee-selfheal.service
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ done
|
|||||||
log() { echo "[$(date +%H:%M:%S)] $*"; }
|
log() { echo "[$(date +%H:%M:%S)] $*"; }
|
||||||
die() { log "ERROR: $*" >&2; exit 1; }
|
die() { log "ERROR: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
die "bee-remount-medium must be run as root (use sudo or a root shell)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Return all candidate block devices (optical + removable USB mass storage)
|
# Return all candidate block devices (optical + removable USB mass storage)
|
||||||
find_candidates() {
|
find_candidates() {
|
||||||
# CD/DVD drives
|
# CD/DVD drives
|
||||||
|
|||||||
@@ -8,11 +8,17 @@ EXPORT_DIR="/appdata/bee/export"
|
|||||||
AUDIT_JSON="${EXPORT_DIR}/bee-audit.json"
|
AUDIT_JSON="${EXPORT_DIR}/bee-audit.json"
|
||||||
RUNTIME_JSON="${EXPORT_DIR}/runtime-health.json"
|
RUNTIME_JSON="${EXPORT_DIR}/runtime-health.json"
|
||||||
LOCK_DIR="/run/bee-selfheal.lock"
|
LOCK_DIR="/run/bee-selfheal.lock"
|
||||||
|
EVENTS=0
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
echo "[${LOG_PREFIX}] $*"
|
echo "[${LOG_PREFIX}] $*"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log_event() {
|
||||||
|
EVENTS=$((EVENTS + 1))
|
||||||
|
log "$*"
|
||||||
|
}
|
||||||
|
|
||||||
have_nvidia_gpu() {
|
have_nvidia_gpu() {
|
||||||
lspci -Dn 2>/dev/null | awk '$2 ~ /^03(00|02):$/ && $3 ~ /^10de:/ { found=1; exit } END { exit(found ? 0 : 1) }'
|
lspci -Dn 2>/dev/null | awk '$2 ~ /^03(00|02):$/ && $3 ~ /^10de:/ { found=1; exit } END { exit(found ? 0 : 1) }'
|
||||||
}
|
}
|
||||||
@@ -56,24 +62,22 @@ web_healthy() {
|
|||||||
mkdir -p "${EXPORT_DIR}" /run
|
mkdir -p "${EXPORT_DIR}" /run
|
||||||
|
|
||||||
if ! mkdir "${LOCK_DIR}" 2>/dev/null; then
|
if ! mkdir "${LOCK_DIR}" 2>/dev/null; then
|
||||||
log "another self-heal run is already active"
|
log_event "another self-heal run is already active"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
trap 'rmdir "${LOCK_DIR}" >/dev/null 2>&1 || true' EXIT
|
trap 'rmdir "${LOCK_DIR}" >/dev/null 2>&1 || true' EXIT
|
||||||
|
|
||||||
log "start"
|
|
||||||
|
|
||||||
if have_nvidia_gpu && [ ! -e /dev/nvidia0 ]; then
|
if have_nvidia_gpu && [ ! -e /dev/nvidia0 ]; then
|
||||||
log "NVIDIA GPU detected but /dev/nvidia0 is missing"
|
log_event "NVIDIA GPU detected but /dev/nvidia0 is missing"
|
||||||
restart_service bee-nvidia.service || true
|
restart_service bee-nvidia.service || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
runtime_state="$(artifact_state "${RUNTIME_JSON}")"
|
runtime_state="$(artifact_state "${RUNTIME_JSON}")"
|
||||||
if [ "${runtime_state}" != "ready" ]; then
|
if [ "${runtime_state}" != "ready" ]; then
|
||||||
if [ "${runtime_state}" = "interrupted" ]; then
|
if [ "${runtime_state}" = "interrupted" ]; then
|
||||||
log "runtime-health.json.tmp exists — interrupted runtime-health write detected"
|
log_event "runtime-health.json.tmp exists — interrupted runtime-health write detected"
|
||||||
else
|
else
|
||||||
log "runtime-health.json missing or empty"
|
log_event "runtime-health.json missing or empty"
|
||||||
fi
|
fi
|
||||||
restart_service bee-preflight.service || true
|
restart_service bee-preflight.service || true
|
||||||
fi
|
fi
|
||||||
@@ -81,19 +85,17 @@ fi
|
|||||||
audit_state="$(artifact_state "${AUDIT_JSON}")"
|
audit_state="$(artifact_state "${AUDIT_JSON}")"
|
||||||
if [ "${audit_state}" != "ready" ]; then
|
if [ "${audit_state}" != "ready" ]; then
|
||||||
if [ "${audit_state}" = "interrupted" ]; then
|
if [ "${audit_state}" = "interrupted" ]; then
|
||||||
log "bee-audit.json.tmp exists — interrupted audit write detected"
|
log_event "bee-audit.json.tmp exists — interrupted audit write detected"
|
||||||
else
|
else
|
||||||
log "bee-audit.json missing or empty"
|
log_event "bee-audit.json missing or empty"
|
||||||
fi
|
fi
|
||||||
restart_service bee-audit.service || true
|
restart_service bee-audit.service || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! service_active bee-web.service; then
|
if ! service_active bee-web.service; then
|
||||||
log "bee-web.service is not active"
|
log_event "bee-web.service is not active"
|
||||||
restart_service bee-web.service || true
|
restart_service bee-web.service || true
|
||||||
elif ! web_healthy; then
|
elif ! web_healthy; then
|
||||||
log "bee-web health check failed"
|
log_event "bee-web health check failed"
|
||||||
restart_service bee-web.service || true
|
restart_service bee-web.service || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "done"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user