From 0e39e7d96087d22c257231afb8a6d58a32d2c406 Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Sun, 3 May 2026 14:07:47 +0300 Subject: [PATCH] Make toram default and add install-to-ram CLI --- audit/cmd/bee/main.go | 31 +++++++ audit/internal/platform/install_to_ram.go | 57 +++++++++++- .../internal/platform/install_to_ram_test.go | 86 ++++++++++++++++++- audit/internal/webui/page_export_tools.go | 2 +- iso/builder/build.sh | 50 ++++++++--- .../config/bootloaders/grub-efi/config.cfg | 4 +- .../config/bootloaders/isolinux/live.cfg.in | 2 +- iso/overlay/usr/local/bin/bee-remount-medium | 4 + 8 files changed, 217 insertions(+), 19 deletions(-) diff --git a/audit/cmd/bee/main.go b/audit/cmd/bee/main.go index 2e80ab5..86f94af 100644 --- a/audit/cmd/bee/main.go +++ b/audit/cmd/bee/main.go @@ -64,6 +64,8 @@ func run(args []string, stdout, stderr io.Writer) (exitCode int) { return runExport(args[1:], stdout, stderr) case "preflight": return runPreflight(args[1:], stdout, stderr) + case "install-to-ram": + return runInstallToRAM(args[1:], stdout, stderr) case "support-bundle": return runSupportBundle(args[1:], stdout, stderr) case "web": @@ -90,6 +92,7 @@ func printRootUsage(w io.Writer) { fmt.Fprintln(w, `bee commands: bee audit --runtime auto|local|livecd --output stdout|file: bee preflight --output stdout|file: + bee install-to-ram bee export --target bee support-bundle --output stdout|file: 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) case "preflight": return runPreflight([]string{"--help"}, stdout, stdout) + case "install-to-ram": + return runInstallToRAM([]string{"--help"}, stdout, stdout) case "support-bundle": return runSupportBundle([]string{"--help"}, stdout, stdout) case "web": @@ -252,6 +257,32 @@ func runPreflight(args []string, stdout, stderr io.Writer) int { 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 { fs := flag.NewFlagSet("support-bundle", flag.ContinueOnError) fs.SetOutput(stderr) diff --git a/audit/internal/platform/install_to_ram.go b/audit/internal/platform/install_to_ram.go index de7eabe..0960963 100644 --- a/audit/internal/platform/install_to_ram.go +++ b/audit/internal/platform/install_to_ram.go @@ -14,6 +14,14 @@ import ( const installToRAMDir = "/dev/shm/bee-live" 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() +} + func (s *System) IsLiveMediaInRAM() bool { return s.LiveMediaRAMState().InRAM } @@ -140,8 +148,7 @@ func (s *System) RunInstallToRAM(ctx context.Context, logFunc func(string)) (ret return nil } - squashfsFiles, err := filepath.Glob("/run/live/medium/live/*.squashfs") - sourceAvailable := err == nil && len(squashfsFiles) > 0 + squashfsFiles, sourceAvailable := ensureLiveMediumAvailable(log) dstDir := installToRAMDir @@ -171,7 +178,7 @@ func (s *System) RunInstallToRAM(ctx context.Context, logFunc func(string)) (ret } 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) } { @@ -258,6 +265,50 @@ bindMedium: 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 verifyInstallToRAMStatus(status LiveBootSource, dstDir string, mediumRebound bool, log func(string)) error { if status.InRAM { return nil diff --git a/audit/internal/platform/install_to_ram_test.go b/audit/internal/platform/install_to_ram_test.go index de9c8c7..706adc0 100644 --- a/audit/internal/platform/install_to_ram_test.go +++ b/audit/internal/platform/install_to_ram_test.go @@ -1,6 +1,9 @@ package platform -import "testing" +import ( + "fmt" + "testing" +) func TestInferLiveBootKind(t *testing.T) { t.Parallel() @@ -124,3 +127,84 @@ func TestShouldLogCopyProgress(t *testing.T) { 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) + } +} diff --git a/audit/internal/webui/page_export_tools.go b/audit/internal/webui/page_export_tools.go index 6cb91d5..7d31210 100644 --- a/audit/internal/webui/page_export_tools.go +++ b/audit/internal/webui/page_export_tools.go @@ -431,7 +431,7 @@ fetch('/api/system/ram-status').then(r=>r.json()).then(d=>{ else if (kind === 'disk') label = 'disk (' + source + ')'; else label = source; 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) { txt.style.color = 'var(--ok, green)'; } else if (d.status === 'failed') { diff --git a/iso/builder/build.sh b/iso/builder/build.sh index f052e44..5a2276d 100755 --- a/iso/builder/build.sh +++ b/iso/builder/build.sh @@ -566,6 +566,42 @@ validate_iso_live_boot_entries() { 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)" + list_iso_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() { iso_path="$1" [ "$BEE_GPU_VENDOR" = "nvidia" ] || return 0 @@ -698,16 +734,6 @@ write_canonical_grub_cfg() { cat > "$cfg" < "$cfg" <&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) find_candidates() { # CD/DVD drives