Resolve multipath dm devices to underlying sd devices for ZFS pool map
This commit is contained in:
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -70,16 +71,71 @@ async def get_zfs_pool_map() -> dict[str, dict]:
|
|||||||
parts = stripped.split()
|
parts = stripped.split()
|
||||||
dev_path = parts[0]
|
dev_path = parts[0]
|
||||||
try:
|
try:
|
||||||
real = os.path.realpath(dev_path)
|
info = {
|
||||||
dev_name = os.path.basename(real)
|
|
||||||
# Strip partition numbers (sda1 -> sda)
|
|
||||||
dev_name = re.sub(r'\d+$', '', dev_name)
|
|
||||||
pool_map[dev_name] = {
|
|
||||||
"pool": current_pool,
|
"pool": current_pool,
|
||||||
"vdev": current_vdev or 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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.debug("zpool not available")
|
logger.debug("zpool not available")
|
||||||
return pool_map
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user