Skip to content

Commit 8108167

Browse files
authored
Merge pull request #138 from NearNodeFlash/monitor-drives
Add NVMe monitoring with configurable periods and SMART log health ch…
2 parents 5149e7d + 1c095e6 commit 8108167

12 files changed

Lines changed: 289 additions & 43 deletions

File tree

internal/switchtec/cmd/fabric/perf.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020, 2021, 2022 Hewlett Packard Enterprise Development LP
2+
* Copyright 2020-2025 Hewlett Packard Enterprise Development LP
33
* Other additional copyright holders may be indicated within.
44
*
55
* The entirety of this work is licensed under the Apache License,
@@ -51,7 +51,7 @@ func (cmd *BandwidthCmd) Run() error {
5151
return err
5252
}
5353

54-
// Resetting the bandwidht counter takes about 1s
54+
// Resetting the bandwidth counter takes about 1s
5555
time.Sleep(time.Second)
5656

5757
// Record the bandwidth counters

internal/switchtec/pkg/nvme/nvme.go

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020, 2021, 2022 Hewlett Packard Enterprise Development LP
2+
* Copyright 2020-2025 Hewlett Packard Enterprise Development LP
33
* Other additional copyright holders may be indicated within.
44
*
55
* The entirety of this work is licensed under the Apache License,
@@ -22,6 +22,7 @@ package nvme
2222
import (
2323
"encoding/binary"
2424
"fmt"
25+
"math/rand"
2526
"path"
2627
"regexp"
2728
"strconv"
@@ -33,6 +34,8 @@ import (
3334
"github.com/google/uuid"
3435

3536
"github.com/NearNodeFlash/nnf-ec/internal/switchtec/pkg/switchtec"
37+
"github.com/NearNodeFlash/nnf-ec/pkg/ec"
38+
sf "github.com/NearNodeFlash/nnf-ec/pkg/rfsf/pkg/models"
3639
)
3740

3841
// Device describes the NVMe Device and its attributes
@@ -532,7 +535,7 @@ type id_ctrl struct {
532535
NVMSetIdentifierMaximum uint16 // NSETIDMAX
533536
EnduraceGroupIdentifierMaximum uint16 // ENDGIDMAX
534537
ANATranstitionTime uint8 // ANATT
535-
AsymentricNamespaceAccessCapabilities uint8 // ANACAP
538+
AsymmetricNamespaceAccessCapabilities uint8 // ANACAP
536539
ANAGroupIdenfierMaximum uint32 // ANAGRPMAX
537540
NumberOfANAGroupIdentifiers uint32 // NANAGRPID
538541
PersistentEventLogSize uint32 // PELS
@@ -1101,7 +1104,7 @@ type SmartLog struct {
11011104
DataUnitsWrittenLo uint64
11021105
DataUnitsWrittenHi uint64
11031106
HostReadsLo uint64
1104-
HistReadsHi uint64
1107+
HostReadsHi uint64
11051108
HostWritesLo uint64
11061109
HostWritesHi uint64
11071110
ControllerBusyTimeLo uint64
@@ -1126,6 +1129,7 @@ type SmartLog struct {
11261129
Reserved232 [280]uint8
11271130
}
11281131

1132+
// GetSmartLog - retrieve the smartlog information
11291133
func (dev *Device) GetSmartLog() (*SmartLog, error) {
11301134

11311135
log := new(SmartLog)
@@ -1146,6 +1150,53 @@ func (dev *Device) GetSmartLog() (*SmartLog, error) {
11461150
return log, nil
11471151
}
11481152

1153+
// InterpretSmartLog - calculate the drive's swordfish status
1154+
func InterpretSmartLog(log *SmartLog) sf.ResourceState {
1155+
// Check critical warnings first - check individual bitfield members
1156+
if log.CriticalWarning.SpareCapacity != 0 { // Spare capacity
1157+
return sf.DISABLED_RST
1158+
}
1159+
if log.CriticalWarning.ReadOnly != 0 { // Read-only mode
1160+
return sf.DISABLED_RST
1161+
}
1162+
1163+
// Check other critical warnings for degraded state
1164+
if log.CriticalWarning.Temperature != 0 ||
1165+
log.CriticalWarning.Degraded != 0 ||
1166+
log.CriticalWarning.BackupFailed != 0 ||
1167+
log.CriticalWarning.PersistentMemoryRegionReadOnly != 0 {
1168+
return sf.STANDBY_OFFLINE_RST
1169+
}
1170+
1171+
return sf.ENABLED_RST // Healthy
1172+
}
1173+
1174+
func MangleSmartLog(log *SmartLog) {
1175+
1176+
// Occaionally managle the smart log data
1177+
if rand.Intn(100) < 10 { // 10% chance of simulating critical condition
1178+
warningType := rand.Intn(6)
1179+
switch warningType {
1180+
case 0:
1181+
log.CriticalWarning.SpareCapacity = 1
1182+
log.AvailableSpare = 5 // Below threshold
1183+
case 1:
1184+
log.CriticalWarning.Temperature = 1
1185+
log.CompositeTemperature = 358 // ~85°C
1186+
case 2:
1187+
log.CriticalWarning.Degraded = 1
1188+
case 3:
1189+
log.CriticalWarning.ReadOnly = 1
1190+
case 4:
1191+
log.CriticalWarning.BackupFailed = 1
1192+
case 5:
1193+
log.CriticalWarning.PersistentMemoryRegionReadOnly = 1
1194+
}
1195+
}
1196+
1197+
}
1198+
1199+
// Log contstants
11491200
const (
11501201
LogCdw10LogPageIdentiferMask = 0xFF
11511202
LogCdw10LogPageIdentiferShift = 0
@@ -1166,7 +1217,7 @@ const (
11661217
)
11671218

11681219
func (dev *Device) getNsidLog(logPageIdentifier uint8, retainAsynchronousEvent uint8, nsid uint32, buf []byte) error {
1169-
return dev.getLog(logPageIdentifier, 0, 0, nsid, 0, buf)
1220+
return dev.getLog(logPageIdentifier, 0, retainAsynchronousEvent, nsid, 0, buf)
11701221
}
11711222

11721223
func (dev *Device) getLog(logPageIdentifier uint8, logSpecificField uint8, retainAsynchronousEvent uint8, nsid uint32, logPageOffset uint64, buf []byte) error {
@@ -1200,3 +1251,26 @@ func (dev *Device) getLog(logPageIdentifier uint8, logSpecificField uint8, retai
12001251
return dev.ops.submitAdminPassthru(dev, &cmd, buf)
12011252

12021253
}
1254+
1255+
// LogSmartLog outputs the SMART log data in a structured way for logging.
1256+
func LogSmartLog(log ec.Logger, smartLog *SmartLog, context ...interface{}) {
1257+
if smartLog == nil {
1258+
log.Error(nil, "SMART log is nil", context...)
1259+
return
1260+
}
1261+
log.Info("SMART log data",
1262+
append(context,
1263+
"criticalWarning.spareCapacity", smartLog.CriticalWarning.SpareCapacity,
1264+
"criticalWarning.temperature", smartLog.CriticalWarning.Temperature,
1265+
"criticalWarning.degraded", smartLog.CriticalWarning.Degraded,
1266+
"criticalWarning.readOnly", smartLog.CriticalWarning.ReadOnly,
1267+
"criticalWarning.backupFailed", smartLog.CriticalWarning.BackupFailed,
1268+
"criticalWarning.persistentMemoryRegionReadOnly", smartLog.CriticalWarning.PersistentMemoryRegionReadOnly,
1269+
"availableSpare", smartLog.AvailableSpare,
1270+
"availableSpareThreshold", smartLog.AvailableSpareThreshold,
1271+
"percentageUsed", smartLog.PercentageUsed,
1272+
"compositeTemperature", smartLog.CompositeTemperature,
1273+
"mediaErrorsLo", smartLog.MediaErrorsLo,
1274+
"numberErrorLogEntriesLo", smartLog.NumberErrorLogEntriesLo,
1275+
)...)
1276+
}

pkg/manager-fabric/manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1125,7 +1125,7 @@ func Start() error {
11251125
event.EventManager.Publish(msgreg.FabricReadyNnf(m.id))
11261126

11271127
// Run the Fabric Monitor in a background thread.
1128-
go NewFabricMonitor(m).Run()
1128+
StartFabricMonitor(m)
11291129

11301130
return nil
11311131
}

pkg/manager-fabric/monitor.go

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020, 2021, 2022 Hewlett Packard Enterprise Development LP
2+
* Copyright 2020-2025 Hewlett Packard Enterprise Development LP
33
* Other additional copyright holders may be indicated within.
44
*
55
* The entirety of this work is licensed under the Apache License,
@@ -27,23 +27,53 @@ import (
2727
"github.com/NearNodeFlash/nnf-ec/internal/switchtec/pkg/switchtec"
2828
)
2929

30-
// The Fabric Monitor is responsible for ensuring that the fabriic and related sub-resource
30+
// NewFabricMonitor - The Fabric Monitor is responsible for ensuring that the fabriic and related sub-resource
3131
// are updated with the latest information from the switch. This runs as a background
3232
// thread, and periodically queries the fabric.
33-
func NewFabricMonitor(f *Fabric) *monitor {
34-
return &monitor{fabric: f}
33+
func NewMonitor(f *Fabric, i time.Duration) *monitor {
34+
return &monitor{fabric: f, interval: i}
3535
}
3636

3737
type monitor struct {
38-
fabric *Fabric
38+
fabric *Fabric
39+
interval time.Duration
40+
}
41+
42+
// StartFabricMonitor starts the fabric monitor in a background goroutine if the period is non-zero.
43+
func StartFabricMonitor(fabric *Fabric) {
44+
defaultFabricMonitorPeriod := 60 * time.Second
45+
fabricMonitorPeriod := defaultFabricMonitorPeriod
46+
if periodStr := os.Getenv("NNF_FABRIC_MONITOR_PERIOD"); periodStr != "" {
47+
if d, err := time.ParseDuration(periodStr); err == nil {
48+
fabricMonitorPeriod = d
49+
} else {
50+
if fabric != nil && !fabric.log.IsZero() {
51+
fabric.log.Info("Invalid NNF_FABRIC_MONITOR_PERIOD, using default", "value", fabricMonitorPeriod, "error", err)
52+
}
53+
}
54+
}
55+
56+
// A period of 0 means don't start the monitor.
57+
if fabricMonitorPeriod == 0 {
58+
if fabric != nil && !fabric.log.IsZero() {
59+
fabric.log.Info("Not starting fabric monitor", "monitorPeriod", fabricMonitorPeriod)
60+
}
61+
return
62+
}
63+
64+
mon := NewMonitor(fabric, fabricMonitorPeriod)
65+
go mon.Run()
66+
if fabric != nil && !fabric.log.IsZero() {
67+
fabric.log.Info("Started fabric monitor", "monitorPeriod", fabricMonitorPeriod)
68+
}
69+
3970
}
4071

4172
// Run Fabric Monitor forever
4273
func (m *monitor) Run() {
4374

4475
for {
45-
46-
time.Sleep(time.Second * 60)
76+
time.Sleep(m.interval)
4777

4878
for idx := range m.fabric.switches {
4979
s := &m.fabric.switches[idx]
@@ -62,13 +92,13 @@ func (m *monitor) Run() {
6292
}
6393

6494
for _, event := range events {
65-
physPortId, isDown := m.getEventInfo(event)
95+
physPortID, isDown := m.getEventInfo(event)
6696

67-
if physPortId == invalidPhysicalPortId {
97+
if physPortID == invalidPhysicalPortId {
6898
continue
6999
}
70100

71-
if p := s.findPortByPhysicalPortId(physPortId); p != nil {
101+
if p := s.findPortByPhysicalPortId(physPortID); p != nil {
72102
p.notify(isDown)
73103
}
74104
}

pkg/manager-nnf/manager.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,8 @@ func (s *StorageService) EventHandler(e event.Event) error {
625625
}
626626

627627
log.Info("Storage Service Enabled", "health", s.health)
628+
629+
nvme.StartNVMeMonitor(s.log)
628630
}
629631

630632
// Check for storage pool events

pkg/manager-nvme/manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1239,7 +1239,7 @@ func (mgr *Manager) StorageIdControllersControllerIdGet(storageId, controllerId
12391239
return ec.NewErrNotFound().WithError(err).WithCause(fmt.Sprintf("Storage Controller fabric endpoint not found: Storage: %s Controller: %s", storageId, controllerId))
12401240
}
12411241

1242-
percentageUsage, err := s.device.GetWearLevelAsPercentageUsed()
1242+
percentageUsage, err := GetWearLevelAsPercentageUsed(s.device)
12431243
if err != nil {
12441244
return ec.NewErrInternalServerError().WithError(err).WithCause(fmt.Sprintf("Storage Controller failed to retrieve SMART data: Storage: %s", storageId))
12451245
}

pkg/manager-nvme/monitor.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2025 Hewlett Packard Enterprise Development LP
3+
* Other additional copyright holders may be indicated within.
4+
*
5+
* The entirety of this work is licensed under the Apache License,
6+
* Version 2.0 (the "License"); you may not use this file except
7+
* in compliance with the License.
8+
*
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package nvme
21+
22+
import (
23+
"os"
24+
"time"
25+
26+
"github.com/NearNodeFlash/nnf-ec/internal/switchtec/pkg/nvme"
27+
"github.com/NearNodeFlash/nnf-ec/pkg/ec"
28+
sf "github.com/NearNodeFlash/nnf-ec/pkg/rfsf/pkg/models"
29+
)
30+
31+
// StartNVMeMonitor starts a background goroutine that periodically queries all NVMe devices for their SMART log.
32+
func StartNVMeMonitor(log ec.Logger) {
33+
// Read drive monitor period from environment variable
34+
defaultDriveMonitorPeriod := 1 * time.Hour
35+
driveMonitorPeriod := defaultDriveMonitorPeriod
36+
if periodStr := os.Getenv("NNF_DRIVE_MONITOR_PERIOD"); periodStr != "" {
37+
if d, err := time.ParseDuration(periodStr); err == nil {
38+
driveMonitorPeriod = d
39+
} else {
40+
log.Info("Invalid NNF_DRIVE_MONITOR_PERIOD, using default", "value", driveMonitorPeriod, "error", err)
41+
}
42+
}
43+
44+
// A period of 0 means don't start the monitor.
45+
if driveMonitorPeriod == 0 {
46+
log.Info("Not starting NVMe monitor", "monitorPeriod", driveMonitorPeriod)
47+
return
48+
}
49+
50+
monitor := NewMonitor(log, driveMonitorPeriod)
51+
go monitor.Run()
52+
log.Info("Started NVMe monitor", "monitorPeriod", driveMonitorPeriod)
53+
}
54+
55+
// Monitor periodically queries all NVMe devices for their SMART log
56+
// and can be started as a background goroutine.
57+
type Monitor struct {
58+
Log ec.Logger
59+
Interval time.Duration
60+
GetDevices func() []*nvme.Device // Function to return all NVMe devices
61+
}
62+
63+
// NewMonitor creates a new NVMe monitor with the given polling interval and device getter.
64+
func NewMonitor(log ec.Logger, interval time.Duration) *Monitor {
65+
return &Monitor{
66+
Log: log,
67+
Interval: interval,
68+
}
69+
}
70+
71+
// Run starts the monitor loop. It periodically calls GetSmartLog on all devices.
72+
func (m *Monitor) Run() {
73+
for {
74+
time.Sleep(m.Interval)
75+
76+
storages := GetStorage()
77+
for _, storage := range storages {
78+
if !storage.IsEnabled() {
79+
continue
80+
}
81+
82+
m.checkSmartLog(storage)
83+
}
84+
}
85+
}
86+
87+
func (m *Monitor) checkSmartLog(storage *Storage) {
88+
log := m.Log
89+
90+
smartLog, err := storage.device.GetSmartLog()
91+
if err != nil {
92+
log.Error(err, "smartlog request failed", "serial", storage.SerialNumber(), "slot", storage.Slot())
93+
storage.state = sf.DISABLED_RST
94+
}
95+
96+
// nvme.MangleSmartLog(smartLog)
97+
98+
state := nvme.InterpretSmartLog(smartLog)
99+
if state != storage.state {
100+
log.Info("smartlog state change", "old state", storage.state, "new state", state, "serial", storage.SerialNumber(), "slot", storage.Slot())
101+
storage.state = state
102+
103+
nvme.LogSmartLog(log, smartLog)
104+
}
105+
}

0 commit comments

Comments
 (0)