fix(monitor): show quiet rotational disks by their identity after a restart

This commit is contained in:
MacRimi
2026-10-27 17:20:44 +01:00
parent 688b7f54bc
commit 085bb084eb
2 changed files with 99 additions and 2 deletions
+38 -2
View File
@@ -5048,6 +5048,34 @@ def _lvm_device_args():
return lvm_device_args(_hdd_in_standby) return lvm_device_args(_hdd_in_standby)
def _smart_identity_payload(disk_name):
"""The empty payload with what the host knows of the disk without sending
it any command: its model, its serial and whether it spins."""
base = _smart_default_payload()
try:
model = Path(f'/sys/block/{disk_name}/device/model').read_text().strip()
if model:
base['model'] = model
except OSError:
pass
try:
device = Path(f'/sys/block/{disk_name}/dev').read_text().strip()
for line in Path(f'/run/udev/data/b{device}').read_text().splitlines():
key, _, value = line.partition('=')
if key == 'E:ID_MODEL' and value.strip():
base['model'] = value.strip().replace('_', ' ')
elif key == 'E:ID_SERIAL_SHORT' and value.strip():
base['serial'] = value.strip()
except OSError:
pass
try:
if Path(f'/sys/block/{disk_name}/queue/rotational').read_text().strip() == '1':
base['rotation_rate'] = -1 # HDD, RPM unknown
except OSError:
pass
return base
def get_smart_data(disk_name): def get_smart_data(disk_name):
"""Cached wrapper around the underlying smartctl probe. """Cached wrapper around the underlying smartctl probe.
@@ -5086,7 +5114,15 @@ def get_smart_data(disk_name):
except Exception: except Exception:
policy = 'read' policy = 'read'
if policy != 'read': if policy != 'read':
base = dict(cached[1]) if cached else _smart_default_payload() if not cached and policy == 'idle' and not _hdd_in_standby(disk_name):
# Nothing is known of it yet, as after a restart: a disk that is
# spinning is read once; a parked one is not woken.
result = _get_smart_data_uncached(disk_name)
_dth.settle_own_reads([disk_name])
if _smart_data_useful(result):
_smart_result_cache[disk_name] = (now, dict(result))
cached = _smart_result_cache[disk_name]
base = dict(cached[1]) if cached else _smart_identity_payload(disk_name)
base['temperature'] = 0 base['temperature'] = 0
base['excluded'] = policy == 'excluded' base['excluded'] = policy == 'excluded'
base['idle'] = policy == 'idle' base['idle'] = policy == 'idle'
@@ -5098,7 +5134,7 @@ def get_smart_data(disk_name):
# the disk sleeps. The Storage view paints its own Standby badge # the disk sleeps. The Storage view paints its own Standby badge
# from the temperature poller's flag. Don't refresh the cache # from the temperature poller's flag. Don't refresh the cache
# timestamp: the moment the disk is active again we re-probe. # timestamp: the moment the disk is active again we re-probe.
base = dict(cached[1]) if cached else _smart_default_payload() base = dict(cached[1]) if cached else _smart_identity_payload(disk_name)
base['standby'] = True base['standby'] = True
base['temperature'] = 0 base['temperature'] = 0
return base return base
+61
View File
@@ -4,6 +4,7 @@ import json
from pathlib import Path from pathlib import Path
import subprocess import subprocess
import sys import sys
import tempfile
import types import types
import unittest import unittest
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
@@ -147,5 +148,65 @@ class LvmReaderTests(unittest.TestCase):
self.assertIn('cp "$SCRIPT_DIR/lvm_device_scope.py" "$APP_DIR/usr/bin/"', (SCRIPTS / "build_appimage.sh").read_text()) self.assertIn('cp "$SCRIPT_DIR/lvm_device_scope.py" "$APP_DIR/usr/bin/"', (SCRIPTS / "build_appimage.sh").read_text())
class QuietDiskIdentityTests(unittest.TestCase):
"""A quiet rotational disk the Monitor knows nothing of yet, as after a
restart, is still shown by its model, serial and type."""
def setUp(self):
folder = tempfile.TemporaryDirectory()
self.addCleanup(folder.cleanup)
root = Path(folder.name)
for name, value in (("sys/block/sdb/device/model", "WDC WD40EFRX-68N\n"), ("sys/block/sdb/dev", "8:16\n"),
("sys/block/sdb/queue/rotational", "1\n"),
("run/udev/data/b8:16", "E:ID_MODEL=WDC_WD40EFRX-68N32N0\nE:ID_SERIAL_SHORT=WD-WCC7K4FV37J0\n")):
(root / name).parent.mkdir(parents=True, exist_ok=True)
(root / name).write_text(value)
self.policy = "idle"
self.disk_module = types.ModuleType("disk_temperature_history")
self.disk_module.disk_read_policy = lambda disk: self.policy
self.disk_module.settle_own_reads = Mock()
modules = patch.dict(sys.modules, disk_temperature_history=self.disk_module)
modules.start()
self.addCleanup(modules.stop)
self.standby = Mock(return_value=False)
self.read = Mock(return_value={"model": "WDC WD40EFRX-68N32N0", "serial": "WD-WCC7K4FV37J0",
"rotation_rate": 5400, "health": "healthy", "temperature": 31})
self.cache = {}
namespace = {"Path": lambda path: root / path.lstrip("/"), "time": __import__("time"),
"_smart_result_cache": self.cache, "_SMART_RESULT_TTL": 30, "_hdd_in_standby": self.standby,
"_get_smart_data_uncached": self.read, "_smart_data_useful": lambda data: data.get("model") != "Unknown",
"_smart_fail_backoff": {}, "_smart_fail_counts": {}, "_smart_probe_cache": {},
"_SMART_FAIL_THRESHOLD": 3, "_SMART_FAIL_BACKOFF_SEC": 3600}
tree = ast.parse((SCRIPTS / "flask_server.py").read_text())
nodes = [n for n in tree.body if isinstance(n, ast.FunctionDef)
and n.name in ("_smart_default_payload", "_smart_identity_payload", "get_smart_data")]
exec(compile(ast.Module(body=nodes, type_ignores=[]), "flask_server.py", "exec"), namespace)
self.get = namespace["get_smart_data"]
def test_a_spinning_disk_is_read_once(self):
data = self.get("sdb")
self.assertEqual((data["model"], data["rotation_rate"], data["health"], data["idle"]),
("WDC WD40EFRX-68N32N0", 5400, "healthy", True))
self.disk_module.settle_own_reads.assert_called_once_with(["sdb"])
self.cache["sdb"] = (0, self.cache["sdb"][1])
self.get("sdb")
self.read.assert_called_once()
self.assertEqual(self.standby.call_count, 1)
def test_a_parked_disk_is_not_woken_and_keeps_its_identity(self):
self.standby.return_value = True
data = self.get("sdb")
self.read.assert_not_called()
self.assertEqual((data["model"], data["serial"], data["rotation_rate"], data["idle"]),
("WDC WD40EFRX-68N32N0", "WD-WCC7K4FV37J0", -1, True))
def test_an_excluded_disk_receives_no_command(self):
self.policy = "excluded"
data = self.get("sdb")
self.standby.assert_not_called()
self.read.assert_not_called()
self.assertEqual((data["model"], data["rotation_rate"], data["excluded"]), ("WDC WD40EFRX-68N32N0", -1, True))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()