diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index a074ff067..90804f06e 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -1658,7 +1658,11 @@ the intentionally sparse public response. baseline for Linux md arrays. `mdadm --detail` may enrich level, state, member, UUID, and rebuild fields when available, but missing or failing mdadm detail probes must not hide a kernel-reported md array from the - unified agent report. + unified agent report. Vendor fixed-width role maps must not be treated as + missing members: a QNAP system RAID1 array that reports `[24/2]` while both + active members occupy roles 24 and 25 has two configured members, not 22 + failed disks; ordinary in-range degraded maps such as `[4/3] [UUU_]` retain + their missing-member semantics (#1688). Server-side lifecycle admission must preserve that same continuity across Pulse restart and upgrade. When a standalone host comes back with the same durable machine/report identity and token continuity, the shared admission diff --git a/internal/hostagent/agent_metrics_test.go b/internal/hostagent/agent_metrics_test.go index 3f7fee5bc..a8df934e6 100644 --- a/internal/hostagent/agent_metrics_test.go +++ b/internal/hostagent/agent_metrics_test.go @@ -251,6 +251,30 @@ func TestBuildReport(t *testing.T) { mc.raidArraysFn = nil }) + t.Run("RAID collection preserves QNAP sparse role map health", func(t *testing.T) { + mc.raidArraysFn = func(ctx context.Context) ([]agentshost.RAIDArray, error) { + return parseMDStatArrays(`md13 : active raid1 sdb4[25] sda4[24] + 458880 blocks super 1.0 [24/2] [UU______________________] + bitmap: 1/1 pages [4KB], 65536KB chunk`), nil + } + + report, err := agent.buildReport(context.Background()) + if err != nil { + t.Fatalf("buildReport failed: %v", err) + } + if len(report.RAID) != 1 { + t.Fatalf("expected 1 RAID array, got %d", len(report.RAID)) + } + array := report.RAID[0] + if array.Device != "/dev/md13" || array.State != "active" { + t.Fatalf("unexpected QNAP RAID array summary: %+v", array) + } + if array.TotalDevices != 2 || array.ActiveDevices != 2 || array.WorkingDevices != 2 || array.FailedDevices != 0 { + t.Fatalf("unexpected QNAP RAID array counts: %+v", array) + } + mc.raidArraysFn = nil + }) + // Test case 4: Ceph collection t.Run("Ceph collection", func(t *testing.T) { mc.cephStatusFn = func(ctx context.Context) (*CephClusterStatus, error) { diff --git a/internal/hostagent/mdadm.go b/internal/hostagent/mdadm.go index 3bc5c07e8..67a399384 100644 --- a/internal/hostagent/mdadm.go +++ b/internal/hostagent/mdadm.go @@ -214,9 +214,6 @@ func parseMDStatArraySection(deviceName string, section []string) agentshost.RAI active, _ := strconv.Atoi(matches[2]) array.TotalDevices = total array.ActiveDevices = active - if total > active && !strings.Contains(array.State, "degraded") { - array.State = appendMDStatState(array.State, "degraded") - } } if array.Operation == "" { @@ -255,6 +252,15 @@ func parseMDStatArraySection(deviceName string, section []string) agentshost.RAI if array.TotalDevices == 0 { array.TotalDevices = array.ActiveDevices } + if mdStatUsesSparseRoleBitmap(array, failedDevices) { + // QNAP's system RAID1 arrays expose a fixed-width bay/role bitmap such + // as [24/2] [UU______________________], while their active members use + // roles 24 and 25. Those unused bitmap positions are not failed disks. + array.TotalDevices = array.ActiveDevices + } + if array.TotalDevices > array.ActiveDevices && !strings.Contains(array.State, "degraded") { + array.State = appendMDStatState(array.State, "degraded") + } if failedDevices == 0 && array.TotalDevices > array.ActiveDevices { failedDevices = array.TotalDevices - array.ActiveDevices } @@ -265,6 +271,30 @@ func parseMDStatArraySection(deviceName string, section []string) agentshost.RAI return array } +func mdStatUsesSparseRoleBitmap(array agentshost.RAIDArray, failedDevices int) bool { + if !strings.EqualFold(array.State, "active") || + !strings.EqualFold(array.Level, "raid1") || + failedDevices != 0 || + array.TotalDevices <= array.ActiveDevices || + array.ActiveDevices == 0 { + return false + } + + activeMembers := 0 + for _, device := range array.Devices { + state := strings.ToLower(device.State) + if strings.Contains(state, "faulty") || strings.Contains(state, "spare") { + continue + } + activeMembers++ + if device.Slot < array.TotalDevices { + return false + } + } + + return activeMembers == array.ActiveDevices +} + func parseMDStatDeviceToken(token string, arrayState string) (agentshost.RAIDDevice, bool) { token = strings.Trim(token, ",") matches := mdTokenRe.FindStringSubmatch(token) diff --git a/internal/hostagent/mdadm_test.go b/internal/hostagent/mdadm_test.go index a3506224c..7652f2c2b 100644 --- a/internal/hostagent/mdadm_test.go +++ b/internal/hostagent/mdadm_test.go @@ -699,6 +699,38 @@ unused devices: ` } } +func TestParseMDStatArraysQNAPSystemRAIDRoleBitmap(t *testing.T) { + mdstat := `Personalities : [linear] [raid0] [raid1] [raid10] [raid6] [raid5] [raid4] [multipath] +md1 : active raid1 sdb3[3] sda3[2] + 3897062912 blocks super 1.0 [2/2] [UU] + +md321 : active raid1 sda5[2] sdb5[0] + 6702656 blocks super 1.0 [2/2] [UU] + bitmap: 0/1 pages [0KB], 65536KB chunk + +md13 : active raid1 sdb4[25] sda4[24] + 458880 blocks super 1.0 [24/2] [UU______________________] + bitmap: 1/1 pages [4KB], 65536KB chunk + +md9 : active raid1 sdb1[25] sda1[24] + 530048 blocks super 1.0 [24/2] [UU______________________] + bitmap: 1/1 pages [4KB], 65536KB chunk` + + arrays := parseMDStatArrays(mdstat) + if len(arrays) != 4 { + t.Fatalf("array count = %d, want 4", len(arrays)) + } + for _, index := range []int{2, 3} { + array := arrays[index] + if array.State != "active" { + t.Fatalf("%s state = %q, want active", array.Device, array.State) + } + if array.TotalDevices != 2 || array.ActiveDevices != 2 || array.WorkingDevices != 2 || array.FailedDevices != 0 { + t.Fatalf("unexpected %s device counts: %+v", array.Device, array) + } + } +} + func TestListArrayDevicesError(t *testing.T) { withReadProcMDStat(t, func() ([]byte, error) { return nil, errors.New("read failed")