feat: add human-readable RAID recovery formatting with German time units and improved member detection from mdstat
Add format_transfer_rate() to convert mdstat rates (e.g., 133628K/sec) into readable values like "130.5 MB/s" using 1024-based units. Add format_remaining_time() to convert mdstat durations (e.g., 450.1min) into compact German format like "7 Std. 30 Min." with day/hour/minute components. Update raid_status() to parse member devices directly from array_line in /proc/mdstat to avoid cross
This commit is contained in:
@@ -240,6 +240,45 @@ def _flatten_lsblk(nodes):
|
||||
result.extend(_flatten_lsblk(node.get("children")))
|
||||
return result
|
||||
|
||||
def format_transfer_rate(value):
|
||||
"""Convert mdstat rates such as 133628K/sec into a readable value."""
|
||||
if not value or value == "—":
|
||||
return "—"
|
||||
match = re.match(r"([\d.]+)\s*([KMGTP]?)(?:B)?/sec", value, re.IGNORECASE)
|
||||
if not match:
|
||||
return value
|
||||
amount = float(match.group(1))
|
||||
unit = match.group(2).upper()
|
||||
multiplier = {"": 1, "K": 1024, "M": 1024 ** 2, "G": 1024 ** 3, "T": 1024 ** 4, "P": 1024 ** 5}.get(unit, 1)
|
||||
bytes_per_second = amount * multiplier
|
||||
units = ((1024 ** 3, "GB/s"), (1024 ** 2, "MB/s"), (1024, "KB/s"))
|
||||
for threshold, label in units:
|
||||
if bytes_per_second >= threshold:
|
||||
return f"{bytes_per_second / threshold:.1f} {label}"
|
||||
return f"{bytes_per_second:.0f} B/s"
|
||||
|
||||
def format_remaining_time(value):
|
||||
"""Convert mdstat values such as 450.1min into a compact German duration."""
|
||||
if not value or value == "—":
|
||||
return "—"
|
||||
match = re.match(r"([\d.]+)(min|sec|hours?|days?)", value, re.IGNORECASE)
|
||||
if not match:
|
||||
return value
|
||||
amount = float(match.group(1))
|
||||
unit = match.group(2).lower()
|
||||
total_minutes = amount / 60 if unit.startswith("sec") else amount if unit.startswith("min") else amount * 60 if unit.startswith("hour") else amount * 1440
|
||||
total_minutes = max(0, round(total_minutes))
|
||||
days, remainder = divmod(total_minutes, 1440)
|
||||
hours, minutes = divmod(remainder, 60)
|
||||
parts = []
|
||||
if days:
|
||||
parts.append(f"{days} T")
|
||||
if hours or days:
|
||||
parts.append(f"{hours} Std.")
|
||||
if minutes and len(parts) < 2:
|
||||
parts.append(f"{minutes} Min.")
|
||||
return " ".join(parts) or "< 1 Min."
|
||||
|
||||
def raid_status():
|
||||
mdstat, mdstat_error = "", ""
|
||||
try:
|
||||
@@ -249,7 +288,15 @@ def raid_status():
|
||||
array_name = Path(RAID_DEVICE).name
|
||||
array_line = next((line for line in mdstat.splitlines() if line.startswith(f"{array_name} :")), "")
|
||||
profile_match = re.search(r"(raid\d+)", array_line)
|
||||
members_match = re.search(r"\[(\d+)/(\d+)\]\s+\[([^\]]+)\]", mdstat)
|
||||
# Match the member count on this RAID's own line. Searching all of
|
||||
# /proc/mdstat can accidentally pick a different array's [x/y] value.
|
||||
members_match = re.search(r"\[(\d+)/(\d+)\]\s+\[([^\]]+)\]", array_line)
|
||||
mdstat_members = []
|
||||
for member_name, slot in re.findall(r"\b([A-Za-z0-9][A-Za-z0-9._-]*)\[(\d+)\]", array_line):
|
||||
member_suffix = re.search(rf"\b{re.escape(member_name)}\[{re.escape(slot)}\]\(([^)]+)\)", array_line)
|
||||
member_state = member_suffix.group(1).lower() if member_suffix else "active"
|
||||
member_status = "failed" if "f" in member_state else ("spare" if "s" in member_state else "active")
|
||||
mdstat_members.append({"device": f"/dev/{member_name}", "slot": slot, "state": member_state, "status": member_status})
|
||||
progress_match = re.search(r"(?:recovery|resync|reshape|check)\s*=\s*([\d.]+)%", mdstat, re.IGNORECASE)
|
||||
finish_match = re.search(r"finish=([^\s]+)", mdstat)
|
||||
speed_match = re.search(r"speed=([^\s]+)", mdstat)
|
||||
@@ -272,6 +319,10 @@ def raid_status():
|
||||
if match:
|
||||
state_text, device = match.group(2), match.group(3)
|
||||
devices.append({"device": device, "slot": match.group(1), "state": state_text, "status": "active" if "active" in state_text.lower() else ("spare" if "spare" in state_text.lower() else "failed")})
|
||||
known_devices = {Path(disk["device"]).name for disk in devices}
|
||||
for disk in mdstat_members:
|
||||
if Path(disk["device"]).name not in known_devices:
|
||||
devices.append(disk)
|
||||
lsblk_output, _ = _read_command(["lsblk", "-J", "-b", "-o", "NAME,SIZE,MODEL,SERIAL,TYPE,PATH"])
|
||||
try:
|
||||
block_devices = {item.get("path") or f"/dev/{item.get('name')}": item for item in _flatten_lsblk(json.loads(lsblk_output).get("blockdevices", []))}
|
||||
@@ -308,11 +359,16 @@ def raid_status():
|
||||
status = "ok"
|
||||
else:
|
||||
status = "failed"
|
||||
mdstat_active = int(members_match.group(1)) if members_match else 0
|
||||
mdstat_total = int(members_match.group(2)) if members_match else 0
|
||||
active_devices = mdstat_active or detail.get("active_devices", 0)
|
||||
raid_devices = mdstat_total or detail.get("raid_devices", 0) or len(devices)
|
||||
failed_devices = detail.get("failed_devices", 0) or max(0, raid_devices - active_devices)
|
||||
return {"device": RAID_DEVICE, "status": status, "label": {"ok": "OK", "degraded": "DEGRADED", "failed": "FAILED", "rebuilding": "REBUILDING"}.get(status, "UNKNOWN"),
|
||||
"level": detail.get("level", "—"), "raid_devices": detail.get("raid_devices") or (int(members_match.group(2)) if members_match else 0),
|
||||
"active_devices": detail.get("active_devices") or (int(members_match.group(1)) if members_match else 0), "failed_devices": detail.get("failed_devices", 0),
|
||||
"level": detail.get("level", "—"), "raid_devices": raid_devices,
|
||||
"active_devices": active_devices, "failed_devices": failed_devices,
|
||||
"devices": devices, "recovery": {"active": rebuilding, "percent": float(progress_match.group(1)) if progress_match else 0,
|
||||
"finish": finish_match.group(1) if finish_match else "—", "speed": speed_match.group(1) if speed_match else "—"},
|
||||
"finish": format_remaining_time(finish_match.group(1)) if finish_match else "—", "speed": format_transfer_rate(speed_match.group(1)) if speed_match else "—"},
|
||||
"error": None if array_line or mdadm_output else (mdadm_error or mdstat_error or "RAID-Status nicht verfügbar"), "updated_at": time.time()}
|
||||
|
||||
def datastore_snapshot():
|
||||
|
||||
Reference in New Issue
Block a user