Add iSCSI migration support (SCALE archive source)

archive.py:
- Add iscsi_config.json to _CANDIDATES and _KEYWORDS
- parse_archive() now extracts portals, initiators, targets, extents,
  targetextents, and global_config into archive["iscsi"]

migrate.py:
- Add payload builders for all five iSCSI object types
  (extents, initiators, portals, targets, target-extents)
- Add migrate_iscsi() which creates objects in dependency order
  (extents+initiators first, then portals, then targets, then
  target-extent associations) and tracks old→new ID mappings at
  each step so downstream references are correctly remapped
- Conflict detection: extents/targets by name, portals by IP set,
  initiators by comment, target-extents by target+LUN combination
- Skipped objects still populate the ID map so dependent objects
  can remap their references correctly

summary.py:
- Add per-sub-type found/created/skipped/failed counters for iSCSI
- iSCSI rows appear in the report only when iSCSI data was processed

cli.py:
- Add _prompt_iscsi_portals() — shows source IPs per portal and
  prompts for destination IPs in-place; supports MPIO (space-separated)
- Wizard scope menu gains option 3 (iSCSI); portal prompt fires
  automatically after archive parse when iSCSI portals are present
- run() wires in migrate_iscsi()
- argparse --migrate now accepts "iscsi" as a valid choice

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 15:04:08 -05:00
parent 40daf20809
commit e81e3f7fbb
4 changed files with 511 additions and 13 deletions

View File

@@ -42,18 +42,24 @@ _CANDIDATES: dict[str, list[str]] = {
"ixdiagnose/plugins/Sharing/sharing.nfs.query.json",
"ixdiagnose/NFS/sharing.nfs.query.json",
],
"iscsi": [
"ixdiagnose/plugins/iscsi/iscsi_config.json",
"ixdiagnose/plugins/ISCSI/iscsi_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",
# "iscsi" intentionally omitted — iscsi_config.json is used as-is
}
# Keyword fragments for heuristic fallback scan (SCALE archives only)
_KEYWORDS: dict[str, list[str]] = {
"smb_shares": ["sharing.smb", "smb_share", "sharing/smb", "smb_info"],
"nfs_shares": ["sharing.nfs", "nfs_share", "sharing/nfs", "nfs_config"],
"iscsi": ["iscsi_config", "iscsi/iscsi"],
}
# Presence of this path prefix identifies a TrueNAS CORE archive.
@@ -251,13 +257,14 @@ def _open_source_tar(tar_path: str):
def parse_archive(tar_path: str) -> dict[str, Any]:
"""
Extract SMB shares and NFS shares from the debug archive.
Returns: {"smb_shares": list, "nfs_shares": list}
Extract SMB shares, NFS shares, and iSCSI configuration from the debug archive.
Returns: {"smb_shares": list, "nfs_shares": list, "iscsi": dict}
"""
log.info("Opening archive: %s", tar_path)
result: dict[str, Any] = {
"smb_shares": [],
"nfs_shares": [],
"iscsi": {},
}
try:
@@ -288,14 +295,33 @@ def parse_archive(tar_path: str) -> dict[str, Any]:
result[key] = v
break
# iSCSI — combined dict file, not a bare list
iscsi_raw = _find_data(tf, members, "iscsi")
if iscsi_raw and isinstance(iscsi_raw, dict):
result["iscsi"] = {
"global_config": iscsi_raw.get("global_config", {}),
"portals": iscsi_raw.get("portals", []),
"initiators": iscsi_raw.get("initiators", []),
"targets": iscsi_raw.get("targets", []),
"extents": iscsi_raw.get("extents", []),
"targetextents": iscsi_raw.get("targetextents", []),
}
elif iscsi_raw is not None:
log.warning(" iscsi → unexpected format (expected dict)")
except (tarfile.TarError, OSError) as exc:
log.error("Failed to open archive: %s", exc)
sys.exit(1)
iscsi = result["iscsi"]
log.info(
"Parsed: %d SMB share(s), %d NFS share(s)",
"Parsed: %d SMB share(s), %d NFS share(s), "
"iSCSI: %d target(s) / %d extent(s) / %d portal(s)",
len(result["smb_shares"]),
len(result["nfs_shares"]),
len(iscsi.get("targets", [])),
len(iscsi.get("extents", [])),
len(iscsi.get("portals", [])),
)
return result