Add scripts and configuration files for mergerfs setup and testing

- Created backup files for wipefs on specific disk partitions.
- Added db-test script to verify metadata.db file integrity.
- Documented successful mergerfs branch and pool configurations in mfs-good.txt.
- Implemented mfs write test in mfs-test.txt.
- Included mergerfs version and device information in mfs.txt.
- Developed rebuild scripts for mergerfs stages, including verification and filesystem creation.
- Generated stage logs for each rebuild step to track disk and filesystem status.
- Added checks for systemd mount requirements and filesystem verification in stage4.
This commit is contained in:
keith committed 2026-10-05 22:57:01 -05:00
1 parent b234845165
commit ff5a793c75
57 files changed
+2795

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
exec python3 "$SCRIPT_DIR/UNKNOWN.PY"
@@ -0,0 +1,42 @@
PLEX BOOKS UNKNOWN SECONDARY CHECK
==================================
This tool reads:
/mnt/recovery/plex-books-manifest.jsonl
and inspects only entries previously classified UNKNOWN.
It NEVER writes to the damaged ext4 filesystem.
It uses debugfs catastrophic/read-only mode and classifies UNKNOWN files as:
ETB_LOST
One or more extent-tree metadata blocks live inside the known-dead PV.
ETB_SURVIVES
Extent-tree metadata blocks are outside the dead PV; investigate further.
EXTENTS_RECOVERABLE
The file now exposes parsable data extents and none overlap the dead PV.
DATA_OVERLAP
Parsable data extents overlap the dead PV.
OTHER_UNKNOWN
Metadata still cannot be interpreted.
Known-dead filesystem blocks:
3418636288 - 4395389951
Run:
mkdir -p /mnt/plexunk
mount -o ro LABEL=PLEXUNK /mnt/plexunk
bash /mnt/plexunk/CHECKUNK.SH
Results are written to:
/mnt/recovery/plex-books-unknown-check.tsv
/mnt/recovery/plex-books-unknown-summary.txt
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
import json
import os
import re
import subprocess
import sys
from pathlib import Path
LV = "/dev/mapper/plex--new-plex--new"
MANIFEST = Path("/mnt/recovery/plex-books-manifest.jsonl")
REPORT = Path("/mnt/recovery/plex-books-unknown-check.tsv")
SUMMARY = Path("/mnt/recovery/plex-books-unknown-summary.txt")
DEAD_START = 3418636288
DEAD_END = 4395389951
inode_re = re.compile(r"Inode:\s*(\d+)\s+Type:\s*([^\s]+)")
size_re = re.compile(r"\bSize:\s*(\d+)")
etb_re = re.compile(r"\(ETB(\d+)\):(\d+)")
extent_re = re.compile(r"\((\d+)(?:-(\d+))?\):(\d+)(?:-(\d+))?")
def die(msg):
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(1)
def human(n):
units = ["B", "KiB", "MiB", "GiB", "TiB"]
x = float(n)
for unit in units:
if x < 1024 or unit == units[-1]:
return f"{x:.2f} {unit}"
x /= 1024
def run(cmd):
return subprocess.run(
cmd,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
def debugfs(command):
env = os.environ.copy()
env["DEBUGFS_PAGER"] = "cat"
return subprocess.run(
["debugfs", "-c", "-R", command, LV],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
)
def preflight():
if os.geteuid() != 0:
die("Run as root.")
if not Path(LV).exists():
die(f"Missing LV: {LV}")
ro = run(["blockdev", "--getro", LV])
if ro.returncode != 0 or ro.stdout.strip() != "1":
die(f"{LV} is not kernel read-only.")
if not MANIFEST.exists():
die(f"Manifest not found: {MANIFEST}")
def classify(record):
inode = int(record["inode"])
path = record["path"]
size = int(record.get("size", 0))
stat_result = debugfs(f"stat <{inode}>")
stat_text = stat_result.stdout
etbs = [
{"level": int(level), "block": int(block)}
for level, block in etb_re.findall(stat_text)
]
real_extents = []
if "EXTENTS:" in stat_text:
extent_text = stat_text.split("EXTENTS:", 1)[1]
for m in extent_re.finditer(extent_text):
# Skip ETB pseudo-lines; extent_re only matches numeric logical starts.
logical_start = int(m.group(1))
logical_end = int(m.group(2) or m.group(1))
physical_start = int(m.group(3))
physical_end = int(m.group(4) or m.group(3))
real_extents.append(
{
"logical_start": logical_start,
"logical_end": logical_end,
"physical_start": physical_start,
"physical_end": physical_end,
}
)
blocks_result = debugfs(f"blocks <{inode}>")
block_nums = []
for token in re.findall(r"\b\d+\b", blocks_result.stdout):
try:
block_nums.append(int(token))
except ValueError:
pass
dead_etbs = [e for e in etbs if DEAD_START <= e["block"] <= DEAD_END]
live_etbs = [e for e in etbs if not (DEAD_START <= e["block"] <= DEAD_END)]
overlapping_extents = [
ex for ex in real_extents
if ex["physical_start"] <= DEAD_END and ex["physical_end"] >= DEAD_START
]
if real_extents:
if overlapping_extents:
cls = "DATA_OVERLAP"
reason = "parsable data extent overlaps dead PV"
else:
cls = "EXTENTS_RECOVERABLE"
reason = "parsable data extents exist and avoid dead PV"
elif etbs:
if dead_etbs:
cls = "ETB_LOST"
reason = "extent-tree metadata block is on dead PV"
elif live_etbs:
cls = "ETB_SURVIVES"
reason = "extent-tree metadata block is outside dead PV; investigate"
else:
cls = "OTHER_UNKNOWN"
reason = "ETB metadata present but could not classify"
else:
cls = "OTHER_UNKNOWN"
reason = "no parsable data extents or ETB references"
return {
"class": cls,
"reason": reason,
"inode": inode,
"path": path,
"size": size,
"etbs": etbs,
"dead_etbs": dead_etbs,
"live_etbs": live_etbs,
"real_extents": real_extents,
"blocks_output": " ".join(str(b) for b in block_nums),
"stat_stderr": stat_result.stderr.strip(),
"blocks_stderr": blocks_result.stderr.strip(),
}
def main():
preflight()
unknown = []
with MANIFEST.open("r", encoding="utf-8") as f:
for line in f:
if not line.strip():
continue
rec = json.loads(line)
if rec.get("status") == "UNKNOWN":
unknown.append(rec)
if not unknown:
print("No UNKNOWN entries found in manifest.")
return
counts = {}
sizes = {}
with REPORT.open("w", encoding="utf-8") as out:
out.write("class\tbytes\tinode\tpath\treason\tetb_blocks\tblocks_output\n")
for idx, rec in enumerate(unknown, 1):
result = classify(rec)
cls = result["class"]
counts[cls] = counts.get(cls, 0) + 1
sizes[cls] = sizes.get(cls, 0) + result["size"]
etb_blocks = ",".join(
f"ETB{e['level']}:{e['block']}" for e in result["etbs"]
)
clean_reason = result["reason"].replace("\t", " ").replace("\n", " ")
clean_path = result["path"].replace("\t", " ")
clean_blocks = result["blocks_output"].replace("\t", " ")
out.write(
f"{cls}\t{result['size']}\t{result['inode']}\t"
f"{clean_path}\t{clean_reason}\t{etb_blocks}\t{clean_blocks}\n"
)
out.flush()
print(
f"[{idx}/{len(unknown)}] {cls:<20} "
f"{human(result['size']):>10} {result['path']}"
)
if result["etbs"]:
print(
" "
+ ", ".join(
f"ETB{e['level']}={e['block']}"
+ (" [DEAD]" if DEAD_START <= e["block"] <= DEAD_END else " [LIVE]")
for e in result["etbs"]
)
)
lines = [
"PLEX BOOKS UNKNOWN SECONDARY CHECK",
"==================================",
f"Unknown entries checked: {len(unknown)}",
f"Dead block range: {DEAD_START}-{DEAD_END}",
"",
]
for cls in sorted(counts):
lines.append(f"{cls:20} {counts[cls]:4d} files {human(sizes[cls])}")
lines += [
"",
f"Report: {REPORT}",
]
text = "\n".join(lines) + "\n"
SUMMARY.write_text(text, encoding="utf-8")
print()
print(text)
if __name__ == "__main__":
main()