mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 19:02:31 +00:00
fix(monitor): show quiet rotational disks by their identity after a restart
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user