Skip to content

Commit 7e7c41d

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 7e7c41d

4 files changed

Lines changed: 406 additions & 43 deletions

File tree

blockdevice/dm_multipath.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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+
"os"
18+
"strings"
19+
20+
"github.com/prometheus/procfs"
21+
"github.com/prometheus/procfs/internal/util"
22+
)
23+
24+
// DMMultipathDevice contains information about a single DM-multipath device
25+
// discovered by scanning /sys/block/dm-* entries whose dm/uuid starts with
26+
// "mpath-".
27+
type DMMultipathDevice struct {
28+
// Name is the device-mapper name (from dm/name), e.g. "mpathA".
29+
Name string
30+
// SysfsName is the kernel block device name, e.g. "dm-5".
31+
SysfsName string
32+
// UUID is the full DM UUID string, e.g. "mpath-360000000000001".
33+
UUID string
34+
// Suspended is true when dm/suspended reads "1".
35+
Suspended bool
36+
// SizeBytes is the device size in bytes (sectors × 512).
37+
SizeBytes uint64
38+
// Paths lists the underlying block devices from the slaves/ directory.
39+
Paths []DMMultipathPath
40+
}
41+
42+
// DMMultipathPath represents one underlying path device for a DM-multipath map.
43+
type DMMultipathPath struct {
44+
// Device is the block device name, e.g. "sdi".
45+
Device string
46+
// State is the raw device state read from
47+
// /sys/block/<device>/device/state, e.g. "running", "offline", "live".
48+
State string
49+
}
50+
51+
// DMMultipathDevices discovers DM-multipath devices by scanning
52+
// /sys/block/dm-* and filtering on dm/uuid prefix "mpath-".
53+
//
54+
// It returns a slice of DMMultipathDevice structs. If no multipath devices
55+
// are found, it returns an empty (non-nil) slice and no error.
56+
func (fs FS) DMMultipathDevices() ([]DMMultipathDevice, error) {
57+
blockDir := fs.sys.Path(sysBlockPath)
58+
59+
entries, err := os.ReadDir(blockDir)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
devices := make([]DMMultipathDevice, 0)
65+
for _, entry := range entries {
66+
if !strings.HasPrefix(entry.Name(), "dm-") {
67+
continue
68+
}
69+
70+
uuid, err := util.SysReadFile(fs.sys.Path(sysBlockPath, entry.Name(), sysBlockDM, "uuid"))
71+
if err != nil || !strings.HasPrefix(uuid, "mpath-") {
72+
continue
73+
}
74+
75+
name, err := util.SysReadFile(fs.sys.Path(sysBlockPath, entry.Name(), sysBlockDM, "name"))
76+
if err != nil {
77+
name = entry.Name()
78+
}
79+
80+
suspended := false
81+
if val, err := util.ReadUintFromFile(fs.sys.Path(sysBlockPath, entry.Name(), sysBlockDM, "suspended")); err == nil {
82+
suspended = val == 1
83+
}
84+
85+
var sizeBytes uint64
86+
if sectors, err := util.ReadUintFromFile(fs.sys.Path(sysBlockPath, entry.Name(), sysBlockSize)); err == nil {
87+
sizeBytes = sectors * procfs.SectorSize
88+
}
89+
90+
paths, err := fs.dmMultipathPaths(entry.Name())
91+
if err != nil {
92+
return nil, err
93+
}
94+
95+
devices = append(devices, DMMultipathDevice{
96+
Name: name,
97+
SysfsName: entry.Name(),
98+
UUID: uuid,
99+
Suspended: suspended,
100+
SizeBytes: sizeBytes,
101+
Paths: paths,
102+
})
103+
}
104+
105+
return devices, nil
106+
}
107+
108+
// dmMultipathPaths reads the slaves/ directory of a dm device and returns
109+
// the path devices with their states.
110+
func (fs FS) dmMultipathPaths(dmDevice string) ([]DMMultipathPath, error) {
111+
slavesDir := fs.sys.Path(sysBlockPath, dmDevice, sysUnderlyingDev)
112+
113+
entries, err := os.ReadDir(slavesDir)
114+
if err != nil {
115+
if os.IsNotExist(err) {
116+
return nil, nil
117+
}
118+
return nil, err
119+
}
120+
121+
paths := make([]DMMultipathPath, 0, len(entries))
122+
for _, entry := range entries {
123+
state := "unknown"
124+
if val, err := util.SysReadFile(fs.sys.Path(sysBlockPath, entry.Name(), sysDevicePath, "state")); err == nil {
125+
state = val
126+
}
127+
paths = append(paths, DMMultipathPath{
128+
Device: entry.Name(),
129+
State: state,
130+
})
131+
}
132+
133+
return paths, nil
134+
}

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)