Files
ProxMenux/AppImage/scripts/disk_identity.py
T

77 lines
2.6 KiB
Python
Raw Normal View History

"""
Physical disks and a stable identity for each, read without touching them.
Everything here comes from udev's database and /sys through lsblk, which
reads these columns without opening the block device. A drive that is
asleep, or idle and about to be, is not disturbed by being listed.
The identity matters because a kernel name is not one: a USB drive can be
sda on one boot and sdb on the next, so anything the user attaches to a
disk has to follow the disk, not the letter it happened to get.
"""
import re
import subprocess
from typing import Any, Dict, List
_LSBLK_TIMEOUT = 5
_FIELD_RE = re.compile(r'(\w+)="([^"]*)"')
_SKIP_PREFIXES = ("loop", "zd", "nbd", "ram", "sr")
def disk_key(serial: str, wwn: str, name: str) -> str:
"""Stable identity: the serial where udev knows one, then the WWN,
and only as a last resort the kernel name."""
serial = (serial or "").strip()
wwn = (wwn or "").strip()
if serial:
return f"serial:{serial}"
if wwn:
return f"wwn:{wwn}"
return f"name:{name}"
def list_physical_disks() -> List[Dict[str, Any]]:
"""Every physical disk with its identity and the facts the interface
shows about it. Returns an empty list if lsblk cannot be read."""
try:
proc = subprocess.run(
["lsblk", "-d", "-n", "-P", "-b", "-o",
"NAME,TYPE,MODEL,SERIAL,WWN,SIZE,TRAN,ROTA"],
capture_output=True, text=True, timeout=_LSBLK_TIMEOUT,
)
except (subprocess.TimeoutExpired, OSError):
return []
if proc.returncode != 0:
return []
disks: List[Dict[str, Any]] = []
for line in proc.stdout.splitlines():
fields = dict(_FIELD_RE.findall(line))
name = fields.get("NAME", "")
if fields.get("TYPE") != "disk" or not name or name.startswith(_SKIP_PREFIXES):
continue
serial = fields.get("SERIAL", "").strip()
wwn = fields.get("WWN", "").strip()
try:
size = int(fields.get("SIZE") or 0)
except ValueError:
size = 0
disks.append({
"name": name,
"key": disk_key(serial, wwn, name),
"model": fields.get("MODEL", "").strip(),
"serial": serial,
"size_bytes": size,
"transport": fields.get("TRAN", "").strip(),
"rotational": fields.get("ROTA", "").strip() == "1",
})
return disks
def names_for_keys(keys) -> set:
"""Current kernel names of the disks whose identity is in ``keys``."""
if not keys:
return set()
return {d["name"] for d in list_physical_disks() if d["key"] in keys}