From 3ebe005ba46ee1af1182f7e85ea6de0db386ba84 Mon Sep 17 00:00:00 2001 From: Shirly Radco Date: Mon, 16 Mar 2026 12:50:14 +0200 Subject: [PATCH] sysfs: add NVMeSubsystemClass for NVMe-oF subsystem parsing Add NVMeSubsystemClass() method to sysfs.FS that reads /sys/class/nvme-subsystem/ to discover NVMe over Fabrics subsystems. For each subsystem it reads: - NQN, model, serial, I/O policy - Controller paths with state, transport, and address - Namespace block devices (e.g. nvme0n1) for subsystem-to-device mapping The namespace discovery enables precise correlation between NVMe block devices and their parent subsystems, which is needed for workload-aware storage path health alerting. Signed-off-by: Shirly Radco Co-authored-by: Cursor --- sysfs/class_nvme_subsystem.go | 158 +++++++++++++++++++++++++++++ sysfs/class_nvme_subsystem_test.go | 103 +++++++++++++++++++ testdata/fixtures.ttar | 148 +++++++++++++++++++++++++++ 3 files changed, 409 insertions(+) create mode 100644 sysfs/class_nvme_subsystem.go create mode 100644 sysfs/class_nvme_subsystem_test.go diff --git a/sysfs/class_nvme_subsystem.go b/sysfs/class_nvme_subsystem.go new file mode 100644 index 00000000..c222609b --- /dev/null +++ b/sysfs/class_nvme_subsystem.go @@ -0,0 +1,158 @@ +// 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. + +//go:build linux + +package sysfs + +import ( + "fmt" + "os" + "regexp" + "strings" + + "github.com/prometheus/procfs/internal/util" +) + +const nvmeSubsystemClassPath = "class/nvme-subsystem" + +var ( + nvmeSubsystemControllerRE = regexp.MustCompile(`^nvme\d+$`) + nvmeNamespaceRE = regexp.MustCompile(`^nvme\d+n\d+$`) +) + +// NVMeSubsystem contains info from /sys/class/nvme-subsystem//. +type NVMeSubsystem struct { + // Name is the subsystem directory name, e.g. "nvme-subsys0". + Name string + // NQN is the NVMe Qualified Name from subsysnqn. + NQN string + // Model is the subsystem model string. + Model string + // Serial is the subsystem serial number. + Serial string + // IOPolicy is the multipath I/O policy, e.g. "numa", "round-robin". + IOPolicy string + // Controllers lists the NVMe controllers under this subsystem. + Controllers []NVMeSubsystemController + // Namespaces lists the NVMe namespace block devices (e.g. "nvme0n1") + // that belong to this subsystem. + Namespaces []string +} + +// NVMeSubsystemController contains info about a single NVMe controller +// within an NVMe subsystem. +type NVMeSubsystemController struct { + // Name is the controller directory name, e.g. "nvme0". + Name string + // State is the controller state, e.g. "live", "connecting", "dead". + State string + // Transport is the transport type, e.g. "tcp", "fc", "rdma". + Transport string + // Address is the controller address string. + Address string +} + +// NVMeSubsystemClass is a collection of NVMe subsystems from +// /sys/class/nvme-subsystem. +type NVMeSubsystemClass []NVMeSubsystem + +// NVMeSubsystemClass returns info for all NVMe subsystems read from +// /sys/class/nvme-subsystem. +func (fs FS) NVMeSubsystemClass() (NVMeSubsystemClass, error) { + path := fs.sys.Path(nvmeSubsystemClassPath) + + entries, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + var subsystems NVMeSubsystemClass + for _, entry := range entries { + if !strings.HasPrefix(entry.Name(), "nvme-subsys") { + continue + } + subsys, err := fs.parseNVMeSubsystem(entry.Name()) + if err != nil { + return nil, err + } + subsystems = append(subsystems, *subsys) + } + + return subsystems, nil +} + +func (fs FS) parseNVMeSubsystem(name string) (*NVMeSubsystem, error) { + path := fs.sys.Path(nvmeSubsystemClassPath, name) + subsys := &NVMeSubsystem{Name: name} + + for _, attr := range [...]struct { + file string + dest *string + }{ + {"subsysnqn", &subsys.NQN}, + {"model", &subsys.Model}, + {"serial", &subsys.Serial}, + {"iopolicy", &subsys.IOPolicy}, + } { + val, err := util.SysReadFile(fs.sys.Path(nvmeSubsystemClassPath, name, attr.file)) + if err != nil { + return nil, fmt.Errorf("failed to read %s for %s: %w", attr.file, name, err) + } + *attr.dest = val + } + + entries, err := os.ReadDir(path) + if err != nil { + return nil, fmt.Errorf("failed to list controllers for %s: %w", name, err) + } + + seen := make(map[string]bool) + for _, entry := range entries { + eName := entry.Name() + if nvmeSubsystemControllerRE.MatchString(eName) { + ctrl, err := fs.parseNVMeSubsystemController(name, eName) + if err != nil { + return nil, err + } + subsys.Controllers = append(subsys.Controllers, *ctrl) + } + if nvmeNamespaceRE.MatchString(eName) && !seen[eName] { + seen[eName] = true + subsys.Namespaces = append(subsys.Namespaces, eName) + } + } + + return subsys, nil +} + +func (fs FS) parseNVMeSubsystemController(subsysName, ctrlName string) (*NVMeSubsystemController, error) { + ctrl := &NVMeSubsystemController{Name: ctrlName} + + for _, attr := range [...]struct { + file string + dest *string + }{ + {"state", &ctrl.State}, + {"transport", &ctrl.Transport}, + {"address", &ctrl.Address}, + } { + val, err := util.SysReadFile(fs.sys.Path(nvmeSubsystemClassPath, subsysName, ctrlName, attr.file)) + if err != nil { + return nil, fmt.Errorf("failed to read %s for %s/%s: %w", attr.file, subsysName, ctrlName, err) + } + *attr.dest = val + } + + return ctrl, nil +} diff --git a/sysfs/class_nvme_subsystem_test.go b/sysfs/class_nvme_subsystem_test.go new file mode 100644 index 00000000..79957121 --- /dev/null +++ b/sysfs/class_nvme_subsystem_test.go @@ -0,0 +1,103 @@ +// 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. + +//go:build linux + +package sysfs + +import ( + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestNVMeSubsystemClass(t *testing.T) { + fs, err := NewFS(sysTestFixtures) + if err != nil { + t.Fatal(err) + } + + got, err := fs.NVMeSubsystemClass() + if err != nil { + t.Fatal(err) + } + + want := NVMeSubsystemClass{ + { + Name: "nvme-subsys0", + NQN: "nqn.2014-08.org.nvmexpress:uuid:a34c4f3a-0d6f-5cec-dead-beefcafebabe", + Model: "Dell PowerStore", + Serial: "SN12345678", + IOPolicy: "round-robin", + Controllers: []NVMeSubsystemController{ + {Name: "nvme0", State: "live", Transport: "fc", Address: "nn-0x200400a0986b4321:pn-0x210400a0986b4321"}, + {Name: "nvme1", State: "live", Transport: "fc", Address: "nn-0x200400a0986b4322:pn-0x210400a0986b4322"}, + {Name: "nvme2", State: "dead", Transport: "fc", Address: "nn-0x200400a0986b4323:pn-0x210400a0986b4323"}, + }, + Namespaces: []string{"nvme0n1"}, + }, + { + Name: "nvme-subsys1", + NQN: "nqn.2014-08.org.nvmexpress:uuid:b45d5e4b-1e7f-6ded-beef-deadcafe1234", + Model: "NetApp ONTAP", + Serial: "NTAP98765", + IOPolicy: "numa", + Controllers: []NVMeSubsystemController{ + {Name: "nvme3", State: "live", Transport: "tcp", Address: "traddr=10.0.0.1,trsvcid=4420"}, + {Name: "nvme4", State: "connecting", Transport: "rdma", Address: "traddr=10.0.0.2,trsvcid=4420"}, + }, + Namespaces: []string{"nvme3n1", "nvme4n1"}, + }, + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("unexpected NVMeSubsystemClass (-want +got):\n%s", diff) + } +} + +func TestNVMeSubsystemClassNotPresent(t *testing.T) { + root := t.TempDir() + + fs, err := NewFS(root) + if err != nil { + t.Fatal(err) + } + + _, err = fs.NVMeSubsystemClass() + if err == nil { + t.Fatal("expected error when nvme-subsystem directory does not exist") + } +} + +func TestNVMeSubsystemClassEmpty(t *testing.T) { + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "class", "nvme-subsystem"), 0o755); err != nil { + t.Fatal(err) + } + + fs, err := NewFS(root) + if err != nil { + t.Fatal(err) + } + + got, err := fs.NVMeSubsystemClass() + if err != nil { + t.Fatal(err) + } + + if len(got) != 0 { + t.Fatalf("expected 0 subsystems, got %d", len(got)) + } +} diff --git a/testdata/fixtures.ttar b/testdata/fixtures.ttar index c3753713..3b9b0ccf 100644 --- a/testdata/fixtures.ttar +++ b/testdata/fixtures.ttar @@ -6995,6 +6995,154 @@ Lines: 1 live Mode: 644 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/class/nvme-subsystem +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/class/nvme-subsystem/nvme-subsys0 +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/iopolicy +Lines: 1 +round-robin +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/model +Lines: 1 +Dell PowerStore +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme0 +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme0/address +Lines: 1 +nn-0x200400a0986b4321:pn-0x210400a0986b4321 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme0/state +Lines: 1 +live +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme0/transport +Lines: 1 +fc +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme0n1 +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme1 +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme1/address +Lines: 1 +nn-0x200400a0986b4322:pn-0x210400a0986b4322 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme1/state +Lines: 1 +live +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme1/transport +Lines: 1 +fc +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme2 +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme2/address +Lines: 1 +nn-0x200400a0986b4323:pn-0x210400a0986b4323 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme2/state +Lines: 1 +dead +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/nvme2/transport +Lines: 1 +fc +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/serial +Lines: 1 +SN12345678 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys0/subsysnqn +Lines: 1 +nqn.2014-08.org.nvmexpress:uuid:a34c4f3a-0d6f-5cec-dead-beefcafebabe +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/class/nvme-subsystem/nvme-subsys1 +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys1/iopolicy +Lines: 1 +numa +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys1/model +Lines: 1 +NetApp ONTAP +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/class/nvme-subsystem/nvme-subsys1/nvme3 +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys1/nvme3/address +Lines: 1 +traddr=10.0.0.1,trsvcid=4420 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys1/nvme3/state +Lines: 1 +live +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys1/nvme3/transport +Lines: 1 +tcp +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/class/nvme-subsystem/nvme-subsys1/nvme3n1 +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/class/nvme-subsystem/nvme-subsys1/nvme4 +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys1/nvme4/address +Lines: 1 +traddr=10.0.0.2,trsvcid=4420 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys1/nvme4/state +Lines: 1 +connecting +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys1/nvme4/transport +Lines: 1 +rdma +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/sys/class/nvme-subsystem/nvme-subsys1/nvme4n1 +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys1/serial +Lines: 1 +NTAP98765 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/class/nvme-subsystem/nvme-subsys1/subsysnqn +Lines: 1 +nqn.2014-08.org.nvmexpress:uuid:b45d5e4b-1e7f-6ded-beef-deadcafe1234 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Directory: fixtures/sys/class/power_supply Mode: 755 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -