fix(installer): disable os-prober to prevent update-grub hang on multi-disk hardware

On N100 mini-PCs with eMMC + NVMe, update-grub's os-prober probes all
block devices via the bind-mounted /dev, hanging indefinitely on the eMMC.
GRUB_DISABLE_OS_PROBER=true skips os-prober in the chroot.

Also apply modernize lint fixes to vm test files (atomic.Int32).
This commit is contained in:
joshsiv-mulga
2026-05-14 08:10:01 +10:00
parent 38373b2fc7
commit f8d025d07e
3 changed files with 22 additions and 21 deletions
+1
View File
@@ -574,6 +574,7 @@ GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR=Spinifex
GRUB_CMDLINE_LINUX_DEFAULT=""
GRUB_CMDLINE_LINUX="console=tty0 console=ttyS0,115200 systemd.show_status=1"
GRUB_DISABLE_OS_PROBER=true
`
if err := os.WriteFile(filepath.Join(mountRoot, "etc/default/grub"), []byte(grubDefault), 0o644); err != nil {
+9 -9
View File
@@ -218,9 +218,9 @@ func TestQueryGuestDeviceMapWait(t *testing.T) {
t.Run("device present on first attempt makes no extra calls", func(t *testing.T) {
sleeps := stubDeviceMapRetrySleep(t)
var calls int32
var calls atomic.Int32
qmpClient, cancel := newMockQMPClient(t, func(cmd qmp.QMPCommand) map[string]any {
atomic.AddInt32(&calls, 1)
calls.Add(1)
require.Equal(t, "query-block", cmd.Execute)
return map[string]any{
"return": []qmp.BlockDevice{
@@ -235,15 +235,15 @@ func TestQueryGuestDeviceMapWait(t *testing.T) {
require.NoError(t, err)
assert.Contains(t, deviceMap, "vdisk-vol-new")
assert.Equal(t, int32(1), atomic.LoadInt32(&calls), "exactly one query-block call expected")
assert.Equal(t, int32(1), calls.Load(), "exactly one query-block call expected")
assert.Empty(t, *sleeps, "no sleep should occur on first-attempt success")
})
t.Run("device missing all 5 attempts invokes final probe and returns last result", func(t *testing.T) {
sleeps := stubDeviceMapRetrySleep(t)
var calls int32
var calls atomic.Int32
qmpClient, cancel := newMockQMPClient(t, func(cmd qmp.QMPCommand) map[string]any {
atomic.AddInt32(&calls, 1)
calls.Add(1)
require.Equal(t, "query-block", cmd.Execute)
return map[string]any{
"return": []qmp.BlockDevice{
@@ -256,7 +256,7 @@ func TestQueryGuestDeviceMapWait(t *testing.T) {
deviceMap, err := queryGuestDeviceMapWait(qmpClient, "i-1", "vdisk-vol-missing")
require.NoError(t, err)
assert.Equal(t, int32(6), atomic.LoadInt32(&calls),
assert.Equal(t, int32(6), calls.Load(),
"5 retry attempts plus the final fall-through probe must all run")
assert.Equal(t, []time.Duration{retryDelay, retryDelay, retryDelay, retryDelay}, *sleeps,
"4 sleeps must occur between the 5 in-loop attempts (none after the final fall-through)")
@@ -267,9 +267,9 @@ func TestQueryGuestDeviceMapWait(t *testing.T) {
t.Run("device appears on third attempt skips remaining retries", func(t *testing.T) {
sleeps := stubDeviceMapRetrySleep(t)
var calls int32
var calls atomic.Int32
qmpClient, cancel := newMockQMPClient(t, func(cmd qmp.QMPCommand) map[string]any {
n := atomic.AddInt32(&calls, 1)
n := calls.Add(1)
require.Equal(t, "query-block", cmd.Execute)
devices := []qmp.BlockDevice{
bootDevice("os", "/machine/peripheral-anon/device[0]/virtio-backend"),
@@ -285,7 +285,7 @@ func TestQueryGuestDeviceMapWait(t *testing.T) {
require.NoError(t, err)
assert.Contains(t, deviceMap, "vdisk-vol-late")
assert.Equal(t, int32(3), atomic.LoadInt32(&calls),
assert.Equal(t, int32(3), calls.Load(),
"third-attempt success must not trigger a 4th or 5th probe")
assert.Equal(t, []time.Duration{retryDelay, retryDelay}, *sleeps,
"exactly 2 sleeps must occur (after attempts 1 and 2); none after the 3rd success")
+12 -12
View File
@@ -663,9 +663,9 @@ func TestTryBlockdevDel(t *testing.T) {
t.Run("success on first attempt logs no retry message", func(t *testing.T) {
buf := captureSlog(t)
var attempts int32
var attempts atomic.Int32
qmpClient, cancel := newMockQMPClient(t, func(cmd qmp.QMPCommand) map[string]any {
atomic.AddInt32(&attempts, 1)
attempts.Add(1)
return nil
})
defer cancel()
@@ -673,7 +673,7 @@ func TestTryBlockdevDel(t *testing.T) {
m := NewManagerWithDeps(Deps{})
err := m.tryBlockdevDel(&VM{ID: "i-1", QMPClient: qmpClient}, "nbd-vol-1")
require.NoError(t, err)
assert.Equal(t, int32(1), atomic.LoadInt32(&attempts))
assert.Equal(t, int32(1), attempts.Load())
assert.NotContains(t, buf.String(), "succeeded after retry",
"first-attempt success must not log the retry-success line")
})
@@ -681,9 +681,9 @@ func TestTryBlockdevDel(t *testing.T) {
t.Run("success on third attempt logs succeeded after retry", func(t *testing.T) {
buf := captureSlog(t)
var attempts int32
var attempts atomic.Int32
qmpClient, cancel := newMockQMPClient(t, func(cmd qmp.QMPCommand) map[string]any {
n := atomic.AddInt32(&attempts, 1)
n := attempts.Add(1)
if n < 3 {
return map[string]any{"error": map[string]any{"class": "GenericError", "desc": inUseDesc}}
}
@@ -694,16 +694,16 @@ func TestTryBlockdevDel(t *testing.T) {
m := NewManagerWithDeps(Deps{})
err := m.tryBlockdevDel(&VM{ID: "i-1", QMPClient: qmpClient}, "nbd-vol-1")
require.NoError(t, err)
assert.Equal(t, int32(3), atomic.LoadInt32(&attempts))
assert.Equal(t, int32(3), attempts.Load())
logs := buf.String()
assert.Contains(t, logs, "succeeded after retry")
assert.Contains(t, logs, "attempts=3")
})
t.Run("non-in-use error on second attempt returns immediately", func(t *testing.T) {
var attempts int32
var attempts atomic.Int32
qmpClient, cancel := newMockQMPClient(t, func(cmd qmp.QMPCommand) map[string]any {
n := atomic.AddInt32(&attempts, 1)
n := attempts.Add(1)
switch n {
case 1:
return map[string]any{"error": map[string]any{"class": "GenericError", "desc": inUseDesc}}
@@ -717,14 +717,14 @@ func TestTryBlockdevDel(t *testing.T) {
err := m.tryBlockdevDel(&VM{ID: "i-1", QMPClient: qmpClient}, "nbd-vol-1")
require.Error(t, err)
assert.Contains(t, err.Error(), "permission denied")
assert.Equal(t, int32(2), atomic.LoadInt32(&attempts),
assert.Equal(t, int32(2), attempts.Load(),
"non-retryable error must stop the loop on the attempt that produced it")
})
t.Run("twenty in-use errors return the last error", func(t *testing.T) {
var attempts int32
var attempts atomic.Int32
qmpClient, cancel := newMockQMPClient(t, func(cmd qmp.QMPCommand) map[string]any {
atomic.AddInt32(&attempts, 1)
attempts.Add(1)
return map[string]any{"error": map[string]any{"class": "GenericError", "desc": inUseDesc}}
})
defer cancel()
@@ -734,7 +734,7 @@ func TestTryBlockdevDel(t *testing.T) {
require.Error(t, err)
assert.True(t, isQMPNodeInUse(err),
"exhaustion must return the last 'in use' error, not a wrapped or sentinel value")
assert.Equal(t, int32(blockdevDelMaxAttempts), atomic.LoadInt32(&attempts),
assert.Equal(t, int32(blockdevDelMaxAttempts), attempts.Load(),
"retry must cap at blockdevDelMaxAttempts attempts")
})
}