Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions blockdevice/dm_multipath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package blockdevice

import (
"fmt"
"os"
"strings"

"github.com/prometheus/procfs"
"github.com/prometheus/procfs/internal/util"
)

// DMMultipathDevice contains information about a single DM-multipath device
// discovered by scanning /sys/block/dm-* entries whose dm/uuid starts with
// "mpath-".
type DMMultipathDevice struct {
// Name is the device-mapper name (from dm/name), e.g. "mpathA".
Name string
// SysfsName is the kernel block device name, e.g. "dm-5".
SysfsName string
// UUID is the full DM UUID string, e.g. "mpath-360000000000001".
UUID string
// Suspended is true when dm/suspended reads "1".
Suspended bool
// SizeBytes is the device size in bytes (sectors × 512).
SizeBytes uint64
// Paths lists the underlying block devices from the slaves/ directory.
Paths []DMMultipathPath
}

// DMMultipathPath represents one underlying path device for a DM-multipath map.
type DMMultipathPath struct {
// Device is the block device name, e.g. "sdi".
Device string
// State is the raw device state read from
// /sys/block/<device>/device/state, e.g. "running", "offline", "live".
State string
}

// DMMultipathDevices discovers DM-multipath devices by scanning
// /sys/block/dm-* and filtering on dm/uuid prefix "mpath-".
//
// It returns a slice of DMMultipathDevice structs. If no multipath devices
// are found, it returns an empty (non-nil) slice and no error.
func (fs FS) DMMultipathDevices() ([]DMMultipathDevice, error) {
blockDir := fs.sys.Path(sysBlockPath)

entries, err := os.ReadDir(blockDir)
if err != nil {
return nil, err
}

devices := make([]DMMultipathDevice, 0)
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "dm-") {
continue
}

uuid, err := util.SysReadFile(fs.sys.Path(sysBlockPath, entry.Name(), sysBlockDM, "uuid"))
if err != nil {
// dm/uuid missing means this is not a device-mapper device; skip it.
if os.IsNotExist(err) {
continue
}
return nil, fmt.Errorf("failed to read dm/uuid for %s: %w", entry.Name(), err)
}
if !strings.HasPrefix(uuid, "mpath-") {
continue
}

name, err := util.SysReadFile(fs.sys.Path(sysBlockPath, entry.Name(), sysBlockDM, "name"))
if err != nil {
return nil, fmt.Errorf("failed to read dm/name for %s: %w", entry.Name(), err)
}

suspendedVal, err := util.ReadUintFromFile(fs.sys.Path(sysBlockPath, entry.Name(), sysBlockDM, "suspended"))
if err != nil {
return nil, fmt.Errorf("failed to read dm/suspended for %s: %w", entry.Name(), err)
}

sectors, err := util.ReadUintFromFile(fs.sys.Path(sysBlockPath, entry.Name(), sysBlockSize))
if err != nil {
return nil, fmt.Errorf("failed to read size for %s: %w", entry.Name(), err)
}

paths, err := fs.dmMultipathPaths(entry.Name())
if err != nil {
return nil, err
}

devices = append(devices, DMMultipathDevice{
Name: name,
SysfsName: entry.Name(),
UUID: uuid,
Suspended: suspendedVal == 1,
SizeBytes: sectors * procfs.SectorSize,
Paths: paths,
})
}

return devices, nil
}

// dmMultipathPaths reads the slaves/ directory of a dm device and returns
// the path devices with their states.
func (fs FS) dmMultipathPaths(dmDevice string) ([]DMMultipathPath, error) {
slavesDir := fs.sys.Path(sysBlockPath, dmDevice, sysUnderlyingDev)

entries, err := os.ReadDir(slavesDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}

paths := make([]DMMultipathPath, 0, len(entries))
for _, entry := range entries {
state, err := util.SysReadFile(fs.sys.Path(sysBlockPath, entry.Name(), sysDevicePath, "state"))
if err != nil {
return nil, fmt.Errorf("failed to read device/state for %s: %w", entry.Name(), err)
}
paths = append(paths, DMMultipathPath{
Device: entry.Name(),
State: state,
})
}

return paths, nil
}
79 changes: 79 additions & 0 deletions blockdevice/dm_multipath_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package blockdevice

import (
"testing"

"github.com/google/go-cmp/cmp"
)

func TestDMMultipathDevices(t *testing.T) {
blockdevice, err := NewFS(procfsFixtures, sysfsFixtures)
if err != nil {
t.Fatalf("failed to access blockdevice fs: %v", err)
}

devices, err := blockdevice.DMMultipathDevices()
if err != nil {
t.Fatal(err)
}

expected := []DMMultipathDevice{
{
Name: "mpathA",
SysfsName: "dm-1",
UUID: "mpath-360000000000001",
Suspended: false,
SizeBytes: 104857600 * 512,
Paths: []DMMultipathPath{
{Device: "sdb", State: "running"},
{Device: "sdc", State: "offline"},
},
},
{
Name: "mpathB",
SysfsName: "dm-2",
UUID: "mpath-360000000000002",
Suspended: true,
SizeBytes: 209715200 * 512,
Paths: []DMMultipathPath{
{Device: "sdd", State: "running"},
{Device: "sde", State: "running"},
},
},
}

if diff := cmp.Diff(expected, devices); diff != "" {
t.Fatalf("unexpected DMMultipathDevices (-want +got):\n%s", diff)
}
}

func TestDMMultipathDevicesFiltersNonMultipath(t *testing.T) {
blockdevice, err := NewFS(procfsFixtures, sysfsFixtures)
if err != nil {
t.Fatalf("failed to access blockdevice fs: %v", err)
}

devices, err := blockdevice.DMMultipathDevices()
if err != nil {
t.Fatal(err)
}

for _, dev := range devices {
if dev.SysfsName == "dm-0" {
t.Error("dm-0 (LVM device) should have been filtered out")
}
}
}
24 changes: 12 additions & 12 deletions blockdevice/stats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestBlockDevice(t *testing.T) {
if err != nil {
t.Fatal(err)
}
expectedNumOfDevices := 8
expectedNumOfDevices := 14
if len(devices) != expectedNumOfDevices {
t.Fatalf(failMsgFormat, "Incorrect number of devices", expectedNumOfDevices, len(devices))
}
Expand All @@ -95,18 +95,18 @@ func TestBlockDevice(t *testing.T) {
if device0stats.WeightedIOTicks != 6088971 {
t.Errorf(failMsgFormat, "Incorrect time in queue", 6088971, device0stats.WeightedIOTicks)
}
device7stats, count, err := blockdevice.SysBlockDeviceStat(devices[7])
device9stats, count, err := blockdevice.SysBlockDeviceStat(devices[9])
if count != 15 {
t.Errorf(failMsgFormat, "Incorrect number of stats read", 15, count)
}
if err != nil {
t.Fatal(err)
}
if device7stats.WriteSectors != 286915323 {
t.Errorf(failMsgFormat, "Incorrect write merges", 286915323, device7stats.WriteSectors)
if device9stats.WriteSectors != 286915323 {
t.Errorf(failMsgFormat, "Incorrect write merges", 286915323, device9stats.WriteSectors)
}
if device7stats.DiscardTicks != 12 {
t.Errorf(failMsgFormat, "Incorrect discard ticks", 12, device7stats.DiscardTicks)
if device9stats.DiscardTicks != 12 {
t.Errorf(failMsgFormat, "Incorrect discard ticks", 12, device9stats.DiscardTicks)
}
blockQueueStatExpected := BlockQueueStats{
AddRandom: 1,
Expand Down Expand Up @@ -147,7 +147,7 @@ func TestBlockDevice(t *testing.T) {
WriteZeroesMaxBytes: 0,
}

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

dm1Info, err := blockdevice.SysBlockDeviceMapperInfo(devices[1])
dm1Info, err := blockdevice.SysBlockDeviceMapperInfo(devices[9])
if err != nil {
var pErr *os.PathError
if errors.As(err, &pErr) {
Expand Down Expand Up @@ -232,12 +232,12 @@ func TestSysBlockDeviceSize(t *testing.T) {
if err != nil {
t.Fatal(err)
}
size7, err := blockdevice.SysBlockDeviceSize(devices[7])
size9, err := blockdevice.SysBlockDeviceSize(devices[9])
if err != nil {
t.Fatal(err)
}
size7Expected := uint64(1920383410176)
if size7 != size7Expected {
t.Errorf("Incorrect BlockDeviceSize, expected: \n%+v, got: \n%+v", size7Expected, size7)
size9Expected := uint64(1920383410176)
if size9 != size9Expected {
t.Errorf("Incorrect BlockDeviceSize, expected: \n%+v, got: \n%+v", size9Expected, size9)
}
}
Loading