{t(`storage.zfs.classes.${vdev.class}`)}
{getZfsVdevLabel(vdev)}
@@ -2219,13 +2266,7 @@ export function StorageOverview({ disksOnly = false }: { disksOnly?: boolean } =
setSmartJsonData(null)
}
}}>
-
+
{selectedDisk?.connection_type === 'usb' ? (
diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json
index 1ec257ca..dbefcb3c 100644
--- a/AppImage/messages/en/common.json
+++ b/AppImage/messages/en/common.json
@@ -322,6 +322,8 @@
"fragmentation": "Fragmentation",
"dedupRatio": "Dedup Ratio",
"layout": "Layout",
+ "totalSize": "Total size",
+ "fragShort": "Frag.",
"singleDisk": "Single disk",
"stripe": "Stripe",
"mirror": "Mirror",
diff --git a/AppImage/messages/es/common.json b/AppImage/messages/es/common.json
index f795fcd5..c515d02e 100644
--- a/AppImage/messages/es/common.json
+++ b/AppImage/messages/es/common.json
@@ -369,8 +369,8 @@
"notApplicable": "n / A",
"error": "Error",
"system": "Sistema",
- "standby": "En reposo",
- "standbyTitle": "La unidad está en espera: se omitió smartctl para mantenerla apagada",
+ "standby": "En espera",
+ "standbyTitle": "El disco está parado: no se le envía nada para no despertarlo",
"filesystemCorruption": "Se detectó corrupción en el sistema de archivos",
"ioErrorOne": "{count} Error de E/S en 5 minutos",
"ioErrorMany": "{count} Errores de E/S en 5 minutos",
@@ -983,53 +983,55 @@
"temperatureUnavailableCount": "No disponible: {count}",
"zfs": {
"classes": {
- "normal": "datos",
+ "normal": "Datos",
"special": "Especial",
- "dedup": "Deduplicación",
- "log": "Iniciar sesión",
- "cache": "caché",
+ "dedup": "Dedup",
+ "log": "Log",
+ "cache": "Caché",
"spare": "Repuesto"
},
- "poolDetails": "diseño de la piscina, estado de limpieza y estado del dispositivo",
- "detailsUnavailable": "El diseño detallado del grupo requiere OpenZFS 2.3 o posterior.",
+ "poolDetails": "Estructura del pool, estado del scrub y salud de los dispositivos",
+ "detailsUnavailable": "La estructura detallada del pool requiere OpenZFS 2.3 o posterior.",
"status": "Estado",
"action": "Acción",
"capacity": "Capacidad",
"fragmentation": "Fragmentación",
- "dedupRatio": "Relación de deduplicación",
- "layout": "Diseño",
- "singleDisk": "disco único",
- "stripe": "Raya",
+ "dedupRatio": "Ratio de deduplicación",
+ "layout": "Estructura",
+ "totalSize": "Tamaño total",
+ "fragShort": "Frag.",
+ "singleDisk": "Disco único",
+ "stripe": "Stripe",
"mirror": "Espejo",
"raidz": "RAIDZ{parity}",
"draid": "dRAID{parity}",
- "diskCountOne": "disco {count}",
- "diskCountMany": "discos {count}",
- "errorsReported": "Errores reportados",
+ "diskCountOne": "{count} disco",
+ "diskCountMany": "{count} discos",
+ "errorsReported": "Errores detectados",
"scanTitle": "Scrub / Resilver",
- "scrub": "fregar",
+ "scrub": "Scrub",
"resilver": "Resilver",
- "neverScrubbed": "Nunca fregado",
- "scanRunning": "{kind} en progreso ({percent}%)",
+ "neverScrubbed": "Sin scrub todavía",
+ "scanRunning": "{kind} en curso ({percent}%)",
"scanCanceled": "{kind} cancelado",
"scanFinished": "{kind} terminado {date}",
- "started": "iniciado",
- "ended": "Terminado",
- "repaired": "reparado",
- "scanErrors": "errores de escaneo",
- "dataErrors": "errores de datos {count}",
+ "started": "Inicio",
+ "ended": "Fin",
+ "repaired": "Reparado",
+ "scanErrors": "Errores del análisis",
+ "dataErrors": "{count} errores de datos",
"noDataErrors": "No hay errores de datos conocidos",
"device": "Dispositivo",
"disk": "Disco",
"state": "Estado",
- "readErrors": "errores de lectura",
- "writeErrors": "errores de escritura",
- "checksumErrors": "errores de suma de comprobación",
+ "readErrors": "Errores de lectura",
+ "writeErrors": "Errores de escritura",
+ "checksumErrors": "Errores de checksum",
"readShort": "R",
"writeShort": "W",
"checksumShort": "CK",
"slowIos": "E/S lentas",
- "via": "vía {name}"
+ "via": "a través de {name}"
},
"storageController": "Controlador de almacenamiento",
"noController": "No se detectó ningún controlador",
diff --git a/AppImage/scripts/disk_temperature_history.py b/AppImage/scripts/disk_temperature_history.py
index 2100c355..0cff3ab2 100644
--- a/AppImage/scripts/disk_temperature_history.py
+++ b/AppImage/scripts/disk_temperature_history.py
@@ -410,60 +410,16 @@ def _extract_temperature(data: dict[str, Any]) -> Optional[float]:
# ---------------------------------------------------------------------------
-# ── Leaving idle and excluded disks alone ──────────────────────────
+# ── Leaving excluded disks alone ─────────────────────────────────
#
-# `-n standby` keeps a periodic reader from waking a disk that is asleep.
-# It does not let an awake one fall asleep: a drive's spin-down timer
-# counts time without commands, and a read every minute resets it, so an
-# unused drive whose timer is longer than that never spins down — and a
-# drive that parks its heads when idle loads them again on every read.
-#
-# So a rotational disk with no I/O since it was last looked at is not read
-# at all, not even asked for its power mode. The counters come from
-# /proc/diskstats, which costs no disk access, and a SMART query does not
-# move them — passthrough commands are not accounted as reads or writes —
-# so any change means something else used the disk. A disk in use is
-# already awake, and reading it then costs nothing. Solid-state disks have
-# no spindle and no heads, and keep their reading.
-#
-# A disk seen for the first time is read once, so a Monitor that has just
-# started still has values to show; after that it is left alone for as
-# long as nothing uses it.
+# A disk the user excludes in Settings receives nothing from a periodic
+# reader, not even the power-mode question, so it can spin down on its own
+# timer. Every other disk is read on schedule: `-n standby` still keeps a
+# reader from waking one that is already asleep.
READ = "read"
-IDLE = "idle"
EXCLUDED = "excluded"
-_last_io: dict[str, tuple[int, int]] = {}
-_idle_state: dict[str, float] = {}
-_IDLE_TTL = 600 # same horizon as the standby badge
-
-
-def _read_diskstats() -> dict[str, tuple[int, int]]:
- """Reads and writes completed per device, from /proc/diskstats."""
- out: dict[str, tuple[int, int]] = {}
- try:
- with open("/proc/diskstats") as f:
- for line in f:
- parts = line.split()
- if len(parts) < 8:
- continue
- try:
- out[parts[2]] = (int(parts[3]), int(parts[7]))
- except ValueError:
- continue
- except OSError:
- pass
- return out
-
-
-def _is_rotational(disk_name: str) -> bool:
- try:
- with open(f"/sys/block/{disk_name}/queue/rotational") as f:
- return f.read().strip() == "1"
- except OSError:
- return False
-
_EXCLUDED_TTL = 15
_excluded_cache: Optional[tuple[float, set]] = None
@@ -503,50 +459,20 @@ _excluded_disk_names = excluded_disk_names
def disk_read_policy(disk_name: str, excluded: Optional[set] = None) -> str:
- """Whether a periodic reader may touch this disk now: READ, IDLE or
- EXCLUDED. Shared by every reader that runs on its own, so the
- temperature poller and the storage view cannot disagree about a disk.
- ``excluded`` lets a caller that checks many disks pass the list once."""
+ """Whether a periodic reader may touch this disk: READ or EXCLUDED.
+ Shared by every reader that runs on its own, so the temperature poller
+ and the storage view cannot disagree about a disk. ``excluded`` lets a
+ caller that checks many disks pass the list once."""
if excluded is None:
excluded = _excluded_disk_names()
- if disk_name in excluded:
- _idle_state.pop(disk_name, None)
- return EXCLUDED
- if not _is_rotational(disk_name):
- return READ
- current = _read_diskstats().get(disk_name)
- with _cache_lock:
- previous = _last_io.get(disk_name)
- if current is not None:
- _last_io[disk_name] = current
- if current is None or previous is None or current != previous:
- _idle_state.pop(disk_name, None)
- return READ
- _idle_state[disk_name] = time.time()
- return IDLE
-
-
-def settle_own_reads(disk_names) -> None:
- """A reader that has just read these disks itself takes their counters as
- the new baseline, so its own I/O never counts as activity."""
- stats = _read_diskstats()
- with _cache_lock:
- for name in disk_names:
- if name in _last_io and name in stats:
- _last_io[name] = stats[name]
-
-
-def is_disk_idle(disk_name: str) -> bool:
- """True while the disk is being left alone for having no I/O."""
- ts = _idle_state.get(disk_name)
- return ts is not None and (time.time() - ts) < _IDLE_TTL
+ return EXCLUDED if disk_name in excluded else READ
def record_all_disk_temperatures() -> int:
"""Sample the disks that may be read now and persist their temperature.
- USB disks are included. A disk the user excluded, or a rotational one
- with no I/O since the last cycle, is skipped — see ``disk_read_policy``.
+ USB disks are included. A disk the user excluded is skipped — see
+ ``disk_read_policy``.
Sampling fans out across a thread pool so a host with N disks pays
roughly the time of the slowest single ``smartctl`` call instead of
diff --git a/AppImage/scripts/flask_health_routes.py b/AppImage/scripts/flask_health_routes.py
index a5287b69..798249a8 100644
--- a/AppImage/scripts/flask_health_routes.py
+++ b/AppImage/scripts/flask_health_routes.py
@@ -807,7 +807,6 @@ def get_disks_for_exclusion():
**disk,
'excluded': entry is not None,
'excluded_at': entry.get('excluded_at') if entry else None,
- 'idle': _dth.is_disk_idle(disk['name']),
'present': True,
})
# An excluded disk that is not connected right now — an unplugged USB
@@ -826,7 +825,6 @@ def get_disks_for_exclusion():
'rotational': False,
'excluded': True,
'excluded_at': entry.get('excluded_at'),
- 'idle': False,
'present': False,
})
result.sort(key=lambda d: (not d['present'], d['name']))
diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py
index 3a9db342..a90a6b47 100644
--- a/AppImage/scripts/flask_server.py
+++ b/AppImage/scripts/flask_server.py
@@ -4418,9 +4418,6 @@ def get_system_disks():
['pvs', *dev_args, '--noheadings', '-o', 'pv_name,vg_name'],
capture_output=True, text=True, timeout=5
) if dev_args is not None else None
- if result is not None:
- from lvm_device_scope import settle_scan_reads
- settle_scan_reads()
if result is not None and result.returncode == 0:
for line in result.stdout.strip().split('\n'):
if line.strip():
@@ -4558,12 +4555,10 @@ def get_storage_info():
# temperature graph isn't a monitor bug — the
# disk is parked. See issue #232.
in_standby = False
- in_idle = False
in_excluded = False
try:
import disk_temperature_history as _dth
in_standby = _dth.is_disk_in_standby(disk_name)
- in_idle = _dth.is_disk_idle(disk_name)
in_excluded = disk_name in _dth.excluded_disk_names()
except Exception:
pass
@@ -4574,7 +4569,6 @@ def get_storage_info():
'size_bytes': disk_size_bytes,
'temperature': smart_data.get('temperature', 0),
'standby': in_standby,
- 'idle': in_idle,
'excluded': in_excluded,
'health': smart_data.get('health', 'unknown'),
'power_on_hours': smart_data.get('power_on_hours', 0),
@@ -5305,28 +5299,19 @@ def get_smart_data(disk_name):
if cached and now - cached[0] < _SMART_RESULT_TTL:
return dict(cached[1])
- # Excluded, or rotational with no I/O since it was last looked at: send
- # it nothing — not even the power-mode question below, which is still a
- # command. Serve what is known, without a temperature that would only be
- # stale. Same rule as the temperature poller, so the two agree.
+ # Excluded in Settings: send it nothing — not even the power-mode
+ # question below, which is still a command. Serve what is known, without
+ # a temperature that would only be stale. Same rule as the temperature
+ # poller, so the two agree.
try:
import disk_temperature_history as _dth
policy = _dth.disk_read_policy(disk_name)
except Exception:
policy = 'read'
if policy != 'read':
- 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['excluded'] = policy == 'excluded'
- base['idle'] = policy == 'idle'
+ base['excluded'] = True
return base
if _hdd_in_standby(disk_name):
diff --git a/AppImage/scripts/health_monitor.py b/AppImage/scripts/health_monitor.py
index cea8de4f..5bbfbc1e 100644
--- a/AppImage/scripts/health_monitor.py
+++ b/AppImage/scripts/health_monitor.py
@@ -2288,9 +2288,6 @@ class HealthMonitor:
text=True,
timeout=3
)
- from lvm_device_scope import settle_scan_reads
- settle_scan_reads()
-
if result.returncode != 0:
return {'status': 'WARNING', 'reason': 'lvs command failed'}
@@ -2314,7 +2311,6 @@ class HealthMonitor:
text=True,
timeout=3
)
- settle_scan_reads()
if vg_result.returncode == 0 and vg_result.stdout.strip():
return {'status': 'WARNING', 'reason': 'No active LVM volumes detected'}
else:
diff --git a/AppImage/scripts/lvm_device_scope.py b/AppImage/scripts/lvm_device_scope.py
index c3c410bd..c4c36d2d 100644
--- a/AppImage/scripts/lvm_device_scope.py
+++ b/AppImage/scripts/lvm_device_scope.py
@@ -9,20 +9,17 @@
# Last Updated: 16/10/2026
# ==========================================================
# Description:
-# Applies the automatic disk-read policy to LVM metadata scans in both
-# Monitor readers. Excluded and idle disks receive no power-mode probe;
-# every device built on them is excluded too, and the scan's own reads
-# are not taken as activity. No eligible devices means no scan.
+# Applies the disk exclusions to LVM metadata scans in both Monitor
+# readers. Excluded disks receive no power-mode probe and a disk in
+# standby is not woken; every device built on them is excluded too. No
+# eligible devices means no scan.
# ==========================================================
import json
import re
import subprocess
-import threading
_NAME = re.compile(r"[A-Za-z0-9._+-]+")
-_scan_lock = threading.Lock()
-_scanned_disks = set()
def _topology():
@@ -116,14 +113,11 @@ def lvm_device_args(standby_check):
excluded = excluded_disk_names()
blocked = set()
for disk in sorted(disks):
- # Test policy first: even CHECK POWER MODE is forbidden for an
- # excluded/idle disk. Some USB bridges do not honour -n standby.
+ # Test the exclusion first: even CHECK POWER MODE is forbidden
+ # for an excluded disk. Some USB bridges do not honour -n standby.
if disk_read_policy(disk, excluded) != "read" or standby_check(disk):
blocked.add(disk)
allowed = sorted(f"/dev/{name}" for name in volumes if not (_disks_under(name, parents) & blocked))
- with _scan_lock:
- _scanned_disks.clear()
- _scanned_disks.update(disks - blocked)
if not blocked:
return []
filters = _lvm_filters()
@@ -140,15 +134,3 @@ def lvm_device_args(standby_check):
# Unavailable exclusions, topology or policy are not permission to
# open devices that may have been deliberately excluded.
return None
-
-
-def settle_scan_reads():
- """The LVM scan reads the disks it is allowed to. Those reads are its
- own, so they must not make an unused disk look busy at the next check."""
- try:
- from disk_temperature_history import settle_own_reads
- with _scan_lock:
- disks = set(_scanned_disks)
- settle_own_reads(disks)
- except Exception:
- pass
diff --git a/AppImage/scripts/oci_manager.py b/AppImage/scripts/oci_manager.py
index 5d8624a1..ab4ff0da 100644
--- a/AppImage/scripts/oci_manager.py
+++ b/AppImage/scripts/oci_manager.py
@@ -48,7 +48,8 @@ DEV_SCRIPTS_CATALOG = os.path.join(os.path.dirname(__file__), "..", "..", "oci",
# Encryption key file
ENCRYPTION_KEY_FILE = os.path.join(OCI_BASE_DIR, ".encryption_key")
-# Default storage for templates
+# Preferred storage for templates; another active one is used when it is
+# disabled, as `local` is on a BTRFS installation.
DEFAULT_STORAGE = "local"
# Default storage for rootfs (will be auto-detected)
@@ -389,7 +390,22 @@ def _host_arch() -> str:
}.get(machine, 'amd64')
-def _download_alpine_template(storage: str = DEFAULT_STORAGE) -> bool:
+def _template_storage() -> str:
+ """An active storage that holds container templates: `local` when it
+ can, otherwise the first other one, such as `local-btrfs`."""
+ rc, out, _ = _run_pve_cmd(["pvesm", "status", "--content", "vztmpl"])
+ active = []
+ if rc == 0:
+ for line in out.strip().split("\n")[1:]:
+ parts = line.split()
+ if len(parts) >= 3 and parts[2] == "active":
+ active.append(parts[0])
+ if not active or DEFAULT_STORAGE in active:
+ return DEFAULT_STORAGE
+ return active[0]
+
+
+def _download_alpine_template(storage: Optional[str] = None) -> bool:
"""Download the latest Alpine LXC template using pveam.
Filters by the host's architecture so an x86_64 host does not end up
@@ -397,6 +413,7 @@ def _download_alpine_template(storage: str = DEFAULT_STORAGE) -> bool:
kept the last match and could pick any arch that `pveam available`
happened to list (issue #324).
"""
+ storage = storage or _template_storage()
print("[*] Downloading Alpine Linux template...")
logger.info("Downloading Alpine template via pveam")
@@ -443,8 +460,9 @@ def _download_alpine_template(storage: str = DEFAULT_STORAGE) -> bool:
return True
-def _find_alpine_template(storage: str = DEFAULT_STORAGE, auto_download: bool = True) -> Optional[str]:
+def _find_alpine_template(storage: Optional[str] = None, auto_download: bool = True) -> Optional[str]:
"""Find an available Alpine LXC template, optionally downloading if not found."""
+ storage = storage or _template_storage()
template_dir = "/var/lib/vz/template/cache"
# Try to get correct path from storage config
@@ -577,7 +595,7 @@ def _configure_tailscale(vmid: int, config: Dict[str, Any]) -> bool:
# =================================================================
# OCI Image Management
# =================================================================
-def pull_oci_image(image: str, tag: str = "latest", storage: str = DEFAULT_STORAGE) -> Dict[str, Any]:
+def pull_oci_image(image: str, tag: str = "latest", storage: Optional[str] = None) -> Dict[str, Any]:
"""
Pull an OCI image from a registry and store as LXC template.
Uses Proxmox's pvesh API to download OCI images (same as GUI).
@@ -590,6 +608,7 @@ def pull_oci_image(image: str, tag: str = "latest", storage: str = DEFAULT_STORA
Returns:
Dict with success status and template path
"""
+ storage = storage or _template_storage()
result = {
"success": False,
"message": "",
diff --git a/AppImage/scripts/tests/test_secure_gateway_template_storage.py b/AppImage/scripts/tests/test_secure_gateway_template_storage.py
new file mode 100644
index 00000000..25160278
--- /dev/null
+++ b/AppImage/scripts/tests/test_secure_gateway_template_storage.py
@@ -0,0 +1,35 @@
+import sys
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+SCRIPTS_DIR = Path(__file__).resolve().parents[1]
+if str(SCRIPTS_DIR) not in sys.path:
+ sys.path.insert(0, str(SCRIPTS_DIR))
+
+import oci_manager # noqa: E402
+
+HEADER = "Name Type Status Total Used Available %\n"
+
+
+class TemplateStorage(unittest.TestCase):
+ """The Alpine template goes to a storage Proxmox can use."""
+
+ def storage(self, listing, rc=0):
+ with patch.object(oci_manager, "_run_pve_cmd", return_value=(rc, HEADER + listing, "")):
+ return oci_manager._template_storage()
+
+ def test_local_is_used_while_it_is_active(self):
+ self.assertEqual(self.storage("local dir active 1 1 1 1%\nnas nfs active 1 1 1 1%\n"), "local")
+
+ def test_another_storage_is_used_when_local_is_disabled(self):
+ # On a BTRFS installation `local` is disabled and not listed.
+ self.assertEqual(self.storage("local-btrfs btrfs active 1 1 1 1%\n"), "local-btrfs")
+ self.assertEqual(self.storage("nas nfs inactive 0 0 0 0%\nlocal-btrfs btrfs active 1 1 1 1%\n"), "local-btrfs")
+
+ def test_an_unreadable_status_keeps_local(self):
+ self.assertEqual(self.storage("", rc=1), "local")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_lvm_disk_read_policy.py b/tests/test_lvm_disk_read_policy.py
index c7502079..f259ccae 100644
--- a/tests/test_lvm_disk_read_policy.py
+++ b/tests/test_lvm_disk_read_policy.py
@@ -125,13 +125,6 @@ class LvmScopeTests(unittest.TestCase):
self.excluded = set()
self.assertEqual(self.args(inventory), [])
- def test_the_scan_settles_its_own_reads_on_the_disks_it_read(self):
- self.excluded = {"sda"}
- settled = Mock()
- self.disk_module.settle_own_reads = settled
- self.args()
- scope.settle_scan_reads()
- settled.assert_called_once_with({"nvme0n1", "sdb"})
class LvmReaderTests(unittest.TestCase):
@@ -174,9 +167,20 @@ class LvmReaderTests(unittest.TestCase):
self.assertIn('cp "$SCRIPT_DIR/lvm_device_scope.py" "$APP_DIR/usr/bin/"', (SCRIPTS / "build_appimage.sh").read_text())
+class DiskReadPolicyTests(unittest.TestCase):
+ """Only the exclusions in Settings keep a disk from being read."""
+
+ def test_a_disk_without_activity_is_still_read_unless_excluded(self):
+ import disk_temperature_history as dth
+ with patch.object(dth, "_excluded_disk_names", return_value={"sdc"}):
+ self.assertEqual(dth.disk_read_policy("sdb"), dth.READ)
+ self.assertEqual(dth.disk_read_policy("sdb"), dth.READ)
+ self.assertEqual(dth.disk_read_policy("sdc"), dth.EXCLUDED)
+
+
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."""
+ """A rotational disk is read on schedule; a parked one is not woken and
+ is still shown by its model, serial and type."""
def setUp(self):
folder = tempfile.TemporaryDirectory()
@@ -187,10 +191,9 @@ class QuietDiskIdentityTests(unittest.TestCase):
("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.policy = "read"
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)
@@ -209,21 +212,17 @@ class QuietDiskIdentityTests(unittest.TestCase):
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):
+ def test_a_spinning_disk_is_read(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.assertEqual((data["model"], data["rotation_rate"], data["health"], data["temperature"]),
+ ("WDC WD40EFRX-68N32N0", 5400, "healthy", 31))
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"]),
+ self.assertEqual((data["model"], data["serial"], data["rotation_rate"], data["standby"]),
("WDC WD40EFRX-68N32N0", "WD-WCC7K4FV37J0", -1, True))
def test_an_excluded_disk_receives_no_command(self):