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:
1 parent
b234845165
commit
ff5a793c75
57 files changed
+2795
No files matched your search
Executable
+4
@@ -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
|
||||
Executable
+225
@@ -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()
|
||||
Reference in new issue
Block a user