While re-enabling SMART self-tests after the 25.10 upgrade migrated them to cron jobs, I read through middlewared/alert/source/smart.py to confirm what still reaches an operator now that the SMART UI is gone. The ATA and SCSI paths look right. The NVMe path reads the wrong end of the self-test log, and I believe it makes SMARTFailedSelfTest unreachable for NVMe devices.
Version: TrueNAS SCALE 25.10.5, smartmontools 7.4
The code
/usr/lib/python3/dist-packages/middlewared/alert/source/smart.py, the ATA parser, correctly takes the newest entry and says why:
# NAS-140419: smartctl writes table[] newest-first (see ataPrintSmartSelfTestlog
# in smartmontools/ataprint.cpp), so [0] is the most recent test.
test_failed = not ata_tests[0]["status"]["passed"]
The NVMe parser, two functions below, indexes from the opposite end:
def parse_nvme_smart_info(self, data: dict) -> SmartInfo:
latest_entry = data.get("nvme_self_test_log", {}).get(
"table", [{"self_test_result": {"value": -1}}]
)[-1]
return SmartInfo(
smart_testfail=latest_entry["self_test_result"]["value"] in (5, 6, 7),
)
[-1] is the last element β the oldest of the 20 retained entries β although the variable is named latest_entry. The result values (5, 6, 7) match the NVMe spec correctly (fatal/unknown error, failed segment unknown, failed segments); as far as I can tell only the index is wrong.
Reproduction
smartctl presents the NVMe self-test log newest-first, same as ATA. On an Intel Optane SSDPEK1A118GA:
$ sudo smartctl --json -l selftest /dev/nvme1 | jq '.nvme_self_test_log.table | {n: length, first: .[0], last: .[-1]}'
{
"n": 20,
"first": {
"self_test_code": { "value": 1, "string": "Short" },
"self_test_result": { "value": 0, "string": "Completed without error" },
"power_on_hours": 20011
},
"last": {
"self_test_code": { "value": 1, "string": "Short" },
"self_test_result": { "value": 0, "string": "Completed without error" },
"power_on_hours": 19555
}
}
Index 0 is POH 20011 (most recent); index 19 is POH 19555 (oldest) β 456 power-on hours and 19 daily self-tests apart. The alert source is reading the 19555 entry.
This is not vendor-specific. Same ordering on every NVMe device in this system, across two vendors and two controller families:
| Device | Model | table[0] POH |
table[-1] POH |
|---|---|---|---|
nvme1 |
Intel Optane SSDPEK1A118GA | 20011 | 19555 |
nvme2 |
Seagate IronWolf ZP2000NM30002 | 25144 | 24750 |
nvme3 |
Seagate IronWolf ZP2000NM30002 | 25135 | 24680 |
All three hold full 20-entry logs, all newest-first. nvme-cliβs nvme self-test-log, independent of smartctl, shows the same ordering β entry [0] carries the highest power-on-hours count.
Impact
Once a driveβs 20-entry log is full β which it is on any drive that has been self-testing for three weeks β a failure recorded at table[0] has to survive 20 further self-tests to reach the slot the parser examines. It ages out of the log before it gets there. So SMARTFailedSelfTest is registered, is driven by a live IntervalSchedule(timedelta(minutes=90)) poll, and still cannot fire for an NVMe device.
This matters more since 25.10 removed the SMART UI: there is no longer a screen where youβd notice the failed test yourself, and no smart/test/results endpoint to query, so the alert is the only path left. Anyone relying on it for NVMe drive health is not covered.
Suggested fix
Change [-1] to [0], mirroring parse_ata_smart_info. A regression test with a β₯2-entry NVMe log where only the newest entry has a failing result would pin the ordering β a single-entry fixture passes either way, which may be how this survived.
Happy to test a fix or provide more data from this system.