Apply disk exclusions and idle policy to automatic LVM scans

This commit is contained in:
Jonatan
2026-10-16 20:45:17 +02:00
parent 3000c1cd66
commit 4ffc883c32
6 changed files with 250 additions and 69 deletions
+1
View File
@@ -141,6 +141,7 @@ cp "$SCRIPT_DIR/oci_console_logs.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "
cp "$SCRIPT_DIR/oci_instance_info.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ oci_instance_info.py not found"
cp "$SCRIPT_DIR/oci_operations.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ oci_operations.py not found"
cp "$SCRIPT_DIR/disk_temperature_history.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ disk_temperature_history.py not found"
cp "$SCRIPT_DIR/lvm_device_scope.py" "$APP_DIR/usr/bin/"
cp "$SCRIPT_DIR/smartctl_resolver.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ smartctl_resolver.py not found"
cp "$SCRIPT_DIR/disk_identity.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ disk_identity.py not found"
cp "$SCRIPT_DIR/health_thresholds.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ health_thresholds.py not found"
+8 -39
View File
@@ -4188,11 +4188,12 @@ def get_system_disks():
try:
# 4. Check LVM physical volumes. `--devices` (added when a disk
# is parked) keeps pvs from scanning — and waking — standby HDDs.
dev_args = _lvm_device_args()
result = subprocess.run(
['pvs', *_lvm_device_args(), '--noheadings', '-o', 'pv_name,vg_name'],
['pvs', *dev_args, '--noheadings', '-o', 'pv_name,vg_name'],
capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
) if dev_args is not None else None
if result is not None and result.returncode == 0:
for line in result.stdout.strip().split('\n'):
if line.strip():
parts = line.split()
@@ -5002,42 +5003,10 @@ def _hdd_in_standby(disk_name: str) -> bool:
return parked
def _lvm_device_args() -> list:
"""Build the `--devices` argument that keeps an LVM tool (pvs/lvs/
vgs) off parked disks.
LVM tools scan every block device by default, which spins up a
standby HDD (issue #232). When any rotational disk is asleep, hand
LVM an explicit list of every block node EXCEPT those belonging to a
parked disk, so it never opens the sleeping drives. Each partition
must be listed too — `--devices /dev/sda` alone does not cover a PV
on /dev/sda3. Returns [] when nothing is parked, so hosts without
standby disks keep the plain pvs/lvs/vgs call unchanged.
"""
try:
r = subprocess.run(
['lsblk', '-rno', 'NAME,TYPE,PKNAME'],
capture_output=True, text=True, timeout=5)
if r.returncode != 0:
return []
nodes = []
for line in r.stdout.strip().split('\n'):
parts = line.split()
if len(parts) < 2 or parts[1] not in ('disk', 'part'):
continue
name = parts[0]
base = name if parts[1] == 'disk' else (parts[2] if len(parts) > 2 else name)
nodes.append((name, base))
standby_bases = {
name for name, base in nodes
if name == base and _hdd_in_standby(name)
}
if not standby_bases:
return []
devices = [f'/dev/{name}' for name, base in nodes if base not in standby_bases]
return ['--devices', ','.join(devices)] if devices else []
except (subprocess.SubprocessError, OSError):
return []
def _lvm_device_args():
"""None means no automatic scan; [] permits LVM's normal scan."""
from lvm_device_scope import lvm_device_args
return lvm_device_args(_hdd_in_standby)
def get_smart_data(disk_name):
+7 -30
View File
@@ -153,36 +153,10 @@ def _hdd_in_standby(disk_name: str) -> bool:
return parked
def _lvm_device_args() -> list:
"""`--devices` list scoping pvs/lvs/vgs off parked disks so they
aren't spun up by the LVM scan (issue #232). Empty when nothing is
parked, so the plain command runs unchanged. Each partition is
listed too — a whole-disk path alone doesn't cover a PV on a
partition."""
try:
r = subprocess.run(
['lsblk', '-rno', 'NAME,TYPE,PKNAME'],
capture_output=True, text=True, timeout=5)
if r.returncode != 0:
return []
nodes = []
for line in r.stdout.strip().split('\n'):
parts = line.split()
if len(parts) < 2 or parts[1] not in ('disk', 'part'):
continue
name = parts[0]
base = name if parts[1] == 'disk' else (parts[2] if len(parts) > 2 else name)
nodes.append((name, base))
standby_bases = {
name for name, base in nodes
if name == base and _hdd_in_standby(name)
}
if not standby_bases:
return []
devices = [f'/dev/{name}' for name, base in nodes if base not in standby_bases]
return ['--devices', ','.join(devices)] if devices else []
except Exception:
return []
def _lvm_device_args():
"""None means no automatic scan; [] permits LVM's normal scan."""
from lvm_device_scope import lvm_device_args
return lvm_device_args(_hdd_in_standby)
def _is_disk_removable(disk_name: str) -> bool:
@@ -2303,6 +2277,9 @@ class HealthMonitor:
# `--devices` keeps lvs from scanning parked HDDs (issue #232).
dev_args = _lvm_device_args()
if dev_args is None:
return {'status': 'INFO', 'skipped': True,
'reason': 'Automatic LVM scan skipped by disk read policy or unavailable inventory'}
result = subprocess.run(
['lvs', *dev_args, '--noheadings', '--options', 'lv_name,vg_name,lv_attr'],
capture_output=True,
+63
View File
@@ -0,0 +1,63 @@
# ==========================================================
# ProxMenux Monitor - LVM disk read scope
# Author : Jonatan
# GitHub : github.com/jcastro
# Maintainer : MacRimi
# Copyright : (c) 2026 MacRimi & contributors
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# 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;
# their partitions are excluded too. No eligible devices means no scan.
# ==========================================================
import re
import subprocess
def lvm_device_args(standby_check):
"""Return [] for an unrestricted scan, --devices args, or None to skip.
A failed inventory must never fall back to an unrestricted scan. None
is distinct from []: if every disk is excluded/idle/asleep, callers
must not start pvs/lvs/vgs at all.
"""
try:
from disk_temperature_history import disk_read_policy, excluded_disk_names
result = subprocess.run(["lsblk", "-rno", "NAME,TYPE,PKNAME"],
capture_output=True, text=True, timeout=5)
if result.returncode:
return None
nodes = []
for line in result.stdout.splitlines():
parts = line.split()
if len(parts) < 2 or parts[1] not in ("disk", "part"):
continue
name = parts[0]
base = name if parts[1] == "disk" else parts[2] if len(parts) > 2 else ""
if not re.fullmatch(r"[A-Za-z0-9._+-]+", name) or not re.fullmatch(r"[A-Za-z0-9._+-]+", base):
return None
nodes.append((name, base))
disks = {name for name, base in nodes if name == base}
if not disks or any(base not in disks for _, base in nodes):
return None
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.
if disk_read_policy(disk, excluded) != "read" or standby_check(disk):
blocked.add(disk)
if not blocked:
return []
devices = [f"/dev/{name}" for name, base in nodes if base not in blocked]
if not devices:
return None
return ["--devices", ",".join(dict.fromkeys(devices))]
except Exception:
# Unavailable exclusions, topology or policy are not permission to
# open devices that may have been deliberately excluded.
return None