Files
troubleshoot/pkg/analyze/host_block_devices_match_test.go
Evans Mungai 670a510a2d feat(analyze): optional additionalDeviceTypes parameter for blockDevices (#2002)
feat(analyze): optional additionalDeviceTypes for blockDevices; refactor match config and tests

Allow preflights to count extra lsblk TYPE values (e.g. loop, lvm) by listing them in
blockDevices.additionalDeviceTypes on BlockDevicesAnalyze. Types in this list are
eligible whether or not includeUnmountedPartitions is set; disk and optional
partitions behave as before.

Refactor matching to use blockDevicesMatchConfig and document eligibility on that
type. Add host_block_devices_match_test.go for type-rule tables and preflight-style
integration cases; keep classic scenarios in host_block_devices_test.go with a
shared analyzeHostBlockDevicesOutput helper.
Regenerate CRDs and deepcopy for the new API field.

Signed-off-by: Evans Mungai <evans@replicated.com>
2026-03-31 20:29:51 +01:00

103 lines
4.5 KiB
Go

package analyzer
import (
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestHostBlockDevices_deviceTypeEligibility documents type rules in isolation (see blockDevicesMatchConfig in host_block_devices.go).
func TestHostBlockDevices_deviceTypeEligibility(t *testing.T) {
tests := []struct {
name string
cfg blockDevicesMatchConfig
devType string
want bool
}{
{name: "disk always", cfg: blockDevicesMatchConfig{}, devType: "disk", want: true},
{name: "part without flag", cfg: blockDevicesMatchConfig{}, devType: "part", want: false},
{name: "part with flag", cfg: blockDevicesMatchConfig{includeUnmountedPartitions: true}, devType: "part", want: true},
{name: "loop without additional", cfg: blockDevicesMatchConfig{}, devType: "loop", want: false},
{name: "loop with additional", cfg: blockDevicesMatchConfig{additionalDeviceTypes: []string{"loop"}}, devType: "loop", want: true},
{name: "lvm with additional", cfg: blockDevicesMatchConfig{additionalDeviceTypes: []string{"lvm"}}, devType: "lvm", want: true},
{name: "crypt with additional", cfg: blockDevicesMatchConfig{additionalDeviceTypes: []string{"crypt"}}, devType: "crypt", want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isEligibleDeviceType(tt.devType, tt.cfg)
assert.Equal(t, tt.want, got)
})
}
}
// TestHostBlockDevices_additionalDeviceTypes covers end-to-end analyze behavior for AdditionalDeviceTypes and representative preflights.
func TestHostBlockDevices_additionalDeviceTypes(t *testing.T) {
const rawStoragePass = "At least one raw block device is available for storage."
const rawStorageFail = "No raw block devices found. At least one unformatted, unmounted disk is required for storage. Attach a raw disk and ensure it has no filesystem or mount point."
rawStorageOutcomes := []*troubleshootv1beta2.Outcome{
{Fail: &troubleshootv1beta2.SingleOutcome{When: ".* == 0", Message: rawStorageFail}},
{Pass: &troubleshootv1beta2.SingleOutcome{When: ".* >= 1", Message: rawStoragePass}},
}
tests := []struct {
name string
devices []collect.BlockDeviceInfo
hostAnalyzer *troubleshootv1beta2.BlockDevicesAnalyze
want []*AnalyzeResult
}{
{
name: "preflight-style 10GiB loop0 with includeUnmountedPartitions + additionalDeviceTypes loop",
devices: []collect.BlockDeviceInfo{{
Name: "loop0", KernelName: "loop0", Type: "loop", Major: 7, Minor: 0,
Size: 10737418240, ReadOnly: false, Removable: false,
}},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
IncludeUnmountedPartitions: true,
MinimumAcceptableSize: 10737418240,
AdditionalDeviceTypes: []string{"loop"},
Outcomes: rawStorageOutcomes,
},
want: []*AnalyzeResult{{Title: "Block Devices", IsPass: true, Message: rawStoragePass}},
},
{
name: "preflight-style 10GiB LVM with includeUnmountedPartitions + additionalDeviceTypes lvm",
devices: []collect.BlockDeviceInfo{{
Name: "ceph--vg-lv--osd0", KernelName: "dm-0", Type: "lvm", Major: 252, Minor: 0,
Size: 10737418240, ReadOnly: false, Removable: false,
}},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
IncludeUnmountedPartitions: true,
MinimumAcceptableSize: 10737418240,
AdditionalDeviceTypes: []string{"lvm"},
Outcomes: rawStorageOutcomes,
},
want: []*AnalyzeResult{{Title: "Block Devices", IsPass: true, Message: rawStoragePass}},
},
{
name: "loop counts with only additionalDeviceTypes (includeUnmountedPartitions false)",
devices: []collect.BlockDeviceInfo{{
Name: "loop0", KernelName: "loop0", Type: "loop", Major: 7,
}},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
AdditionalDeviceTypes: []string{"loop"},
Outcomes: []*troubleshootv1beta2.Outcome{
{Pass: &troubleshootv1beta2.SingleOutcome{When: ".* > 0", Message: "Block device available"}},
{Fail: &troubleshootv1beta2.SingleOutcome{Message: "No block device available"}},
},
},
want: []*AnalyzeResult{{Title: "Block Devices", IsPass: true, Message: "Block device available"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := analyzeHostBlockDevicesOutput(t, tt.devices, tt.hostAnalyzer)
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}