Skip to content

Commit a8a28fe

Browse files
sradcoAI Assistant
andcommitted
blockdevice: add DMMultipathDevices for DM-multipath sysfs parsing
Add DMMultipathDevices() method to blockdevice.FS that discovers Device Mapper multipath devices by scanning /sys/block/dm-* and filtering on dm/uuid prefix "mpath-". For each multipath device it reads the device name, UUID, suspended state, size, and enumerates underlying path devices with their raw state from /sys/block/<path>/device/state. This provides the sysfs parsing layer that node_exporter's dmmultipath collector will import, following the established pattern where collectors delegate filesystem parsing to procfs. Signed-off-by: Shirly Radco <sradco@redhat.com> Co-authored-by: AI Assistant <noreply@cursor.com>
1 parent 465fd94 commit a8a28fe

4 files changed

Lines changed: 414 additions & 43 deletions

File tree

blockdevice/dm_multipath.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package blockdevice
15+
16+
import (
17+
"fmt"
18+
"os"
19+
"strings"
20+
21+
"github.com/prometheus/procfs"
22+
"github.com/prometheus/procfs/internal/util"
23+
)
24+
25+
// DMMultipathDevice contains information about a single DM-multipath device
26+
// discovered by scanning /sys/block/dm-* entries whose dm/uuid starts with
27+
// "mpath-".
28+
type DMMultipathDevice struct {
29+
// Name is the device-mapper name (from dm/name), e.g. "mpathA".
30+
Name string
31+
// SysfsName is the kernel block device name, e.g. "dm-5".
32+
SysfsName string
33+
// UUID is the full DM UUID string, e.g. "mpath-360000000000001".
34+
UUID string
35+
// Suspended is true when dm/suspended reads "1".
36+
Suspended bool
37+
// SizeBytes is the device size in bytes (sectors × 512).
38+
SizeBytes uint64
39+
// Paths lists the underlying block devices from the slaves/ directory.
40+
Paths []DMMultipathPath
41+
}
42+
43+
// DMMultipathPath represents one underlying path device for a DM-multipath map.
44+
type DMMultipathPath struct {
45+
// Device is the block device name, e.g. "sdi".
46+
Device string
47+
// State is the raw device state read from
48+
// /sys/block/<device>/device/state, e.g. "running", "offline", "live".
49+
State string
50+
}
51+
52+
// DMMultipathDevices discovers DM-multipath devices by scanning
53+
// /sys/block/dm-* and filtering on dm/uuid prefix "mpath-".
54+
//
55+
// It returns a slice of DMMultipathDevice structs. If no multipath devices
56+
// are found, it returns an empty (non-nil) slice and no error.
57+
func (fs FS) DMMultipathDevices() ([]DMMultipathDevice, error) {
58+
blockDir := fs.sys.Path(sysBlockPath)
59+
60+
entries, err := os.ReadDir(blockDir)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
devices := make([]DMMultipathDevice, 0)
66+
for _, entry := range entries {
67+
if !strings.HasPrefix(entry.Name(), "dm-") {
68+
continue
69+
}
70+
71+
uuid, err := util.SysReadFile(fs.sys.Path(sysBlockPath, entry.Name(), sysBlockDM, "uuid"))
72+
if err != nil {
73+
// dm/uuid missing means this is not a device-mapper device; skip it.
74+
if os.IsNotExist(err) {
75+
continue
76+
}
77+
return nil, fmt.Errorf("failed to read dm/uuid for %s: %w", entry.Name(), err)
78+
}
79+
if !strings.HasPrefix(uuid, "mpath-") {
80+
continue
81+
}
82+
83+
name, err := util.SysReadFile(fs.sys.Path(sysBlockPath, entry.Name(), sysBlockDM, "name"))
84+
if err != nil {
85+
return nil, fmt.Errorf("failed to read dm/name for %s: %w", entry.Name(), err)
86+
}
87+
88+
suspendedVal, err := util.ReadUintFromFile(fs.sys.Path(sysBlockPath, entry.Name(), sysBlockDM, "suspended"))
89+
if err != nil {
90+
return nil, fmt.Errorf("failed to read dm/suspended for %s: %w", entry.Name(), err)
91+
}
92+
93+
sectors, err := util.ReadUintFromFile(fs.sys.Path(sysBlockPath, entry.Name(), sysBlockSize))
94+
if err != nil {
95+
return nil, fmt.Errorf("failed to read size for %s: %w", entry.Name(), err)
96+
}
97+
98+
paths, err := fs.dmMultipathPaths(entry.Name())
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
devices = append(devices, DMMultipathDevice{
104+
Name: name,
105+
SysfsName: entry.Name(),
106+
UUID: uuid,
107+
Suspended: suspendedVal == 1,
108+
SizeBytes: sectors * procfs.SectorSize,
109+
Paths: paths,
110+
})
111+
}
112+
113+
return devices, nil
114+
}
115+
116+
// dmMultipathPaths reads the slaves/ directory of a dm device and returns
117+
// the path devices with their states.
118+
func (fs FS) dmMultipathPaths(dmDevice string) ([]DMMultipathPath, error) {
119+
slavesDir := fs.sys.Path(sysBlockPath, dmDevice, sysUnderlyingDev)
120+
121+
entries, err := os.ReadDir(slavesDir)
122+
if err != nil {
123+
if os.IsNotExist(err) {
124+
return nil, nil
125+
}
126+
return nil, err
127+
}
128+
129+
paths := make([]DMMultipathPath, 0, len(entries))
130+
for _, entry := range entries {
131+
state, err := util.SysReadFile(fs.sys.Path(sysBlockPath, entry.Name(), sysDevicePath, "state"))
132+
if err != nil {
133+
return nil, fmt.Errorf("failed to read device/state for %s: %w", entry.Name(), err)
134+
}
135+
paths = append(paths, DMMultipathPath{
136+
Device: entry.Name(),
137+
State: state,
138+
})
139+
}
140+
141+
return paths, nil
142+
}

blockdevice/dm_multipath_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package blockdevice
15+
16+
import (
17+
"testing"
18+
19+
"github.com/google/go-cmp/cmp"
20+
)
21+
22+
func TestDMMultipathDevices(t *testing.T) {
23+
blockdevice, err := NewFS(procfsFixtures, sysfsFixtures)
24+
if err != nil {
25+
t.Fatalf("failed to access blockdevice fs: %v", err)
26+
}
27+
28+
devices, err := blockdevice.DMMultipathDevices()
29+
if err != nil {
30+
t.Fatal(err)
31+
}
32+
33+
expected := []DMMultipathDevice{
34+
{
35+
Name: "mpathA",
36+
SysfsName: "dm-1",
37+
UUID: "mpath-360000000000001",
38+
Suspended: false,
39+
SizeBytes: 104857600 * 512,
40+
Paths: []DMMultipathPath{
41+
{Device: "sdb", State: "running"},
42+
{Device: "sdc", State: "offline"},
43+
},
44+
},
45+
{
46+
Name: "mpathB",
47+
SysfsName: "dm-2",
48+
UUID: "mpath-360000000000002",
49+
Suspended: true,
50+
SizeBytes: 209715200 * 512,
51+
Paths: []DMMultipathPath{
52+
{Device: "sdd", State: "running"},
53+
{Device: "sde", State: "running"},
54+
},
55+
},
56+
}
57+
58+
if diff := cmp.Diff(expected, devices); diff != "" {
59+
t.Fatalf("unexpected DMMultipathDevices (-want +got):\n%s", diff)
60+
}
61+
}
62+
63+
func TestDMMultipathDevicesFiltersNonMultipath(t *testing.T) {
64+
blockdevice, err := NewFS(procfsFixtures, sysfsFixtures)
65+
if err != nil {
66+
t.Fatalf("failed to access blockdevice fs: %v", err)
67+
}
68+
69+
devices, err := blockdevice.DMMultipathDevices()
70+
if err != nil {
71+
t.Fatal(err)
72+
}
73+
74+
for _, dev := range devices {
75+
if dev.SysfsName == "dm-0" {
76+
t.Error("dm-0 (LVM device) should have been filtered out")
77+
}
78+
}
79+
}

blockdevice/stats_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func TestBlockDevice(t *testing.T) {
7575
if err != nil {
7676
t.Fatal(err)
7777
}
78-
expectedNumOfDevices := 8
78+
expectedNumOfDevices := 14
7979
if len(devices) != expectedNumOfDevices {
8080
t.Fatalf(failMsgFormat, "Incorrect number of devices", expectedNumOfDevices, len(devices))
8181
}
@@ -95,18 +95,18 @@ func TestBlockDevice(t *testing.T) {
9595
if device0stats.WeightedIOTicks != 6088971 {
9696
t.Errorf(failMsgFormat, "Incorrect time in queue", 6088971, device0stats.WeightedIOTicks)
9797
}
98-
device7stats, count, err := blockdevice.SysBlockDeviceStat(devices[7])
98+
device9stats, count, err := blockdevice.SysBlockDeviceStat(devices[9])
9999
if count != 15 {
100100
t.Errorf(failMsgFormat, "Incorrect number of stats read", 15, count)
101101
}
102102
if err != nil {
103103
t.Fatal(err)
104104
}
105-
if device7stats.WriteSectors != 286915323 {
106-
t.Errorf(failMsgFormat, "Incorrect write merges", 286915323, device7stats.WriteSectors)
105+
if device9stats.WriteSectors != 286915323 {
106+
t.Errorf(failMsgFormat, "Incorrect write merges", 286915323, device9stats.WriteSectors)
107107
}
108-
if device7stats.DiscardTicks != 12 {
109-
t.Errorf(failMsgFormat, "Incorrect discard ticks", 12, device7stats.DiscardTicks)
108+
if device9stats.DiscardTicks != 12 {
109+
t.Errorf(failMsgFormat, "Incorrect discard ticks", 12, device9stats.DiscardTicks)
110110
}
111111
blockQueueStatExpected := BlockQueueStats{
112112
AddRandom: 1,
@@ -147,7 +147,7 @@ func TestBlockDevice(t *testing.T) {
147147
WriteZeroesMaxBytes: 0,
148148
}
149149

150-
blockQueueStat, err := blockdevice.SysBlockDeviceQueueStats(devices[7])
150+
blockQueueStat, err := blockdevice.SysBlockDeviceQueueStats(devices[9])
151151
if err != nil {
152152
t.Fatal(err)
153153
}
@@ -181,7 +181,7 @@ func TestBlockDmInfo(t *testing.T) {
181181
t.Fatalf("unexpected BlockQueueStat (-want +got):\n%s", diff)
182182
}
183183

184-
dm1Info, err := blockdevice.SysBlockDeviceMapperInfo(devices[1])
184+
dm1Info, err := blockdevice.SysBlockDeviceMapperInfo(devices[9])
185185
if err != nil {
186186
var pErr *os.PathError
187187
if errors.As(err, &pErr) {
@@ -232,12 +232,12 @@ func TestSysBlockDeviceSize(t *testing.T) {
232232
if err != nil {
233233
t.Fatal(err)
234234
}
235-
size7, err := blockdevice.SysBlockDeviceSize(devices[7])
235+
size9, err := blockdevice.SysBlockDeviceSize(devices[9])
236236
if err != nil {
237237
t.Fatal(err)
238238
}
239-
size7Expected := uint64(1920383410176)
240-
if size7 != size7Expected {
241-
t.Errorf("Incorrect BlockDeviceSize, expected: \n%+v, got: \n%+v", size7Expected, size7)
239+
size9Expected := uint64(1920383410176)
240+
if size9 != size9Expected {
241+
t.Errorf("Incorrect BlockDeviceSize, expected: \n%+v, got: \n%+v", size9Expected, size9)
242242
}
243243
}

0 commit comments

Comments
 (0)