diff --git a/services/zfs.py b/services/zfs.py index 8bca249..d758021 100644 --- a/services/zfs.py +++ b/services/zfs.py @@ -2,6 +2,7 @@ import asyncio import os import logging import re +from pathlib import Path logger = logging.getLogger(__name__) @@ -70,16 +71,71 @@ async def get_zfs_pool_map() -> dict[str, dict]: parts = stripped.split() dev_path = parts[0] try: - real = os.path.realpath(dev_path) - dev_name = os.path.basename(real) - # Strip partition numbers (sda1 -> sda) - dev_name = re.sub(r'\d+$', '', dev_name) - pool_map[dev_name] = { + info = { "pool": current_pool, "vdev": current_vdev or current_pool, } + # Resolve symlink and map the device + real = os.path.realpath(dev_path) + dev_name = os.path.basename(real) + + # Map the resolved device (strip partition suffix) + base_dev = _strip_partition(dev_name) + pool_map[base_dev] = info + + # For device-mapper (multipath), also map the + # underlying slave sd devices. + if base_dev.startswith("dm-"): + for slave in _resolve_dm_slaves(base_dev): + pool_map[slave] = info except Exception: pass except FileNotFoundError: logger.debug("zpool not available") return pool_map + + +def _strip_partition(dev_name: str) -> str: + """Strip partition suffix from a device name. + + sda1 -> sda, nvme0n1p1 -> nvme0n1, dm-14 stays dm-14 (it's a + separate dm device for the partition, resolve via slaves). + """ + # dm devices: partition dm devices are separate dm-N entries, + # resolve via /sys/block/dm-N/slaves to find parent dm device + if dev_name.startswith("dm-"): + parent = _get_dm_parent(dev_name) + return parent if parent else dev_name + + # NVMe: nvme0n1p1 -> nvme0n1 + m = re.match(r'^(nvme\d+n\d+)p\d+$', dev_name) + if m: + return m.group(1) + + # Standard: sda1 -> sda + return re.sub(r'\d+$', '', dev_name) + + +def _get_dm_parent(dm_name: str) -> str | None: + """For a dm partition device, find the parent dm device via slaves.""" + slave_dir = Path(f"/sys/block/{dm_name}/slaves") + if slave_dir.is_dir(): + slaves = list(slave_dir.iterdir()) + if len(slaves) == 1 and slaves[0].name.startswith("dm-"): + return slaves[0].name + return None + + +def _resolve_dm_slaves(dm_name: str) -> list[str]: + """Get underlying sd device names for a device-mapper device.""" + slaves = [] + slave_dir = Path(f"/sys/block/{dm_name}/slaves") + if slave_dir.is_dir(): + for s in slave_dir.iterdir(): + name = s.name + if name.startswith("dm-"): + # Nested dm — recurse + slaves.extend(_resolve_dm_slaves(name)) + else: + slaves.append(name) + return slaves