From 0a42cb572fd52003e8c73b6e091b654e7693ff68 Mon Sep 17 00:00:00 2001 From: scott Date: Tue, 3 Mar 2026 13:57:52 -0500 Subject: [PATCH] Update to debug paths --- truenas_migrate.py | 80 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/truenas_migrate.py b/truenas_migrate.py index d94d385..be19690 100644 --- a/truenas_migrate.py +++ b/truenas_migrate.py @@ -50,6 +50,7 @@ from __future__ import annotations import argparse import asyncio +import contextlib import json import logging import ssl @@ -154,34 +155,53 @@ class Summary: _CANDIDATES: dict[str, list[str]] = { "smb_shares": [ + # SCALE 24.04+ – combined plugin file; shares are under "sharing_smb_query" + "ixdiagnose/plugins/smb/smb_info.json", + # Older SCALE layouts "ixdiagnose/plugins/SMB/sharing.smb.query.json", "ixdiagnose/plugins/Sharing/sharing.smb.query.json", "ixdiagnose/SMB/sharing.smb.query.json", + # CORE / freenas-debug "freenas-debug/sharing/smb.json", "sharing/smb.json", "middleware/sharing.smb.query.json", ], "nfs_shares": [ + # SCALE 24.04+ – combined plugin file; shares are under "sharing_nfs_query" + "ixdiagnose/plugins/nfs/nfs_config.json", + # Older SCALE layouts "ixdiagnose/plugins/NFS/sharing.nfs.query.json", "ixdiagnose/plugins/Sharing/sharing.nfs.query.json", "ixdiagnose/NFS/sharing.nfs.query.json", + # CORE / freenas-debug "freenas-debug/sharing/nfs.json", "sharing/nfs.json", "middleware/sharing.nfs.query.json", ], "smb_config": [ + # SCALE 24.04+ – combined plugin file; config is under "smb_config" + "ixdiagnose/plugins/smb/smb_info.json", + # Older SCALE layouts "ixdiagnose/plugins/SMB/smb.config.json", "ixdiagnose/SMB/smb.config.json", + # CORE / freenas-debug "freenas-debug/SMB/smb_config.json", "middleware/smb.config.json", ], } +# When a candidate file bundles multiple datasets, pull out the right sub-key. +_KEY_WITHIN_FILE: dict[str, str] = { + "smb_shares": "sharing_smb_query", + "nfs_shares": "sharing_nfs_query", + "smb_config": "smb_config", +} + # Keyword fragments for heuristic fallback scan _KEYWORDS: dict[str, list[str]] = { - "smb_shares": ["sharing.smb", "smb_share", "sharing/smb"], - "nfs_shares": ["sharing.nfs", "nfs_share", "sharing/nfs"], - "smb_config": ["smb.config", "smb_config"], + "smb_shares": ["sharing.smb", "smb_share", "sharing/smb", "smb_info"], + "nfs_shares": ["sharing.nfs", "nfs_share", "sharing/nfs", "nfs_config"], + "smb_config": ["smb.config", "smb_config", "smb_info"], } @@ -203,6 +223,20 @@ def _read_json(tf: tarfile.TarFile, info: tarfile.TarInfo) -> Optional[Any]: return None +def _extract_subkey(raw: Any, data_type: str) -> Optional[Any]: + """ + When a JSON file bundles multiple datasets, pull out the sub-key that + corresponds to data_type (e.g. "sharing_smb_query" from smb_info.json). + Falls back to the raw value when no sub-key mapping exists. + """ + if not isinstance(raw, dict): + return raw + key = _KEY_WITHIN_FILE.get(data_type) + if key and key in raw: + return raw[key] + return raw + + def _find_data( tf: tarfile.TarFile, members: dict[str, tarfile.TarInfo], @@ -222,7 +256,8 @@ def _find_data( info = member break if info is not None: - result = _read_json(tf, info) + raw = _read_json(tf, info) + result = _extract_subkey(raw, data_type) if result is not None: log.info(" %-12s → %s", data_type, info.name) return result @@ -234,7 +269,8 @@ def _find_data( if not path.lower().endswith(".json"): continue if any(kw in path.lower() for kw in keywords): - result = _read_json(tf, members[path]) + raw = _read_json(tf, members[path]) + result = _extract_subkey(raw, data_type) if result is not None: log.info(" %-12s → %s (heuristic)", data_type, path) return result @@ -242,6 +278,36 @@ def _find_data( return None +@contextlib.contextmanager +def _open_source_tar(tar_path: str): + """ + Open the archive that actually contains the ixdiagnose data. + + TrueNAS HA debug bundles (25.04+) wrap each node's ixdiagnose snapshot + in a separate .txz inside the outer .tgz. We prefer the member whose + name includes '_active'; if none is labelled that way we fall back to the + first .txz found. Single-node (non-HA) bundles are used directly. + """ + with tarfile.open(tar_path, "r:*") as outer: + txz_members = [ + m for m in outer.getmembers() + if m.name.lower().endswith(".txz") and m.isfile() + ] + if not txz_members: + yield outer + return + + # HA bundle – pick the active node's inner archive + active = next( + (m for m in txz_members if "_active" in m.name.lower()), + txz_members[0], + ) + log.info(" HA bundle detected; reading inner archive: %s", active.name) + fh = outer.extractfile(active) + with tarfile.open(fileobj=fh, mode="r:*") as inner: + yield inner + + def parse_archive(tar_path: str) -> dict[str, Any]: """ Extract SMB shares, NFS shares, and SMB config from the debug archive. @@ -255,7 +321,7 @@ def parse_archive(tar_path: str) -> dict[str, Any]: } try: - with tarfile.open(tar_path, "r:*") as tf: + with _open_source_tar(tar_path) as tf: members = _members_map(tf) log.info(" Archive contains %d total entries.", len(members)) @@ -297,7 +363,7 @@ def list_archive_and_exit(tar_path: str) -> None: """ print(f"\nJSON files in archive: {tar_path}\n") try: - with tarfile.open(tar_path, "r:*") as tf: + with _open_source_tar(tar_path) as tf: json_members = sorted( (m for m in tf.getmembers() if m.name.endswith(".json")), key=lambda m: m.name,