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:
@@ -57,7 +57,7 @@ from .archive import parse_archive, list_archive_and_exit
|
||||
from .client import TrueNASClient, check_dataset_paths, create_missing_datasets
|
||||
from .colors import log, _bold, _bold_cyan, _bold_red, _bold_yellow, _cyan, _dim, _green, _yellow
|
||||
from .csv_source import parse_csv_sources
|
||||
from .migrate import migrate_smb_shares, migrate_nfs_shares
|
||||
from .migrate import migrate_smb_shares, migrate_nfs_shares, migrate_iscsi
|
||||
from .summary import Summary
|
||||
|
||||
|
||||
@@ -103,6 +103,10 @@ async def run(
|
||||
await migrate_nfs_shares(
|
||||
client, archive["nfs_shares"], args.dry_run, summary)
|
||||
|
||||
if "iscsi" in migrate_set:
|
||||
await migrate_iscsi(
|
||||
client, archive.get("iscsi", {}), args.dry_run, summary)
|
||||
|
||||
if args.dry_run and summary.paths_to_create:
|
||||
summary.missing_datasets = await check_dataset_paths(
|
||||
client, summary.paths_to_create,
|
||||
@@ -156,6 +160,40 @@ def _prompt_csv_path(share_type: str) -> Optional[str]:
|
||||
print(f" {_bold_red('File not found:')} {raw}")
|
||||
|
||||
|
||||
|
||||
def _prompt_iscsi_portals(iscsi: dict) -> None:
|
||||
"""Walk each portal and prompt for destination IPs in-place."""
|
||||
portals = iscsi.get("portals", [])
|
||||
if not portals:
|
||||
return
|
||||
|
||||
print(f"\n {_bold('iSCSI Portal Configuration')}")
|
||||
print(f" {_dim('Portal IP addresses are unique per system and must be updated.')}")
|
||||
print(f" {_dim('For MPIO, enter multiple IPs separated by spaces.')}")
|
||||
|
||||
for portal in portals:
|
||||
comment = portal.get("comment", "")
|
||||
listen = portal.get("listen", [])
|
||||
src_ips = " ".join(f"{l['ip']}:{l['port']}" for l in listen)
|
||||
default_port = listen[0]["port"] if listen else 3260
|
||||
|
||||
label = f"Portal {portal['id']}" + (f" ({comment!r})" if comment else "")
|
||||
print(f"\n {_bold(label)}")
|
||||
print(f" {_dim('Source IP(s):')} {src_ips}")
|
||||
|
||||
raw = _prompt(" Destination IP(s)").strip()
|
||||
if not raw:
|
||||
print(f" {_yellow('⚠')} No IPs entered — keeping source IPs.")
|
||||
continue
|
||||
|
||||
port_raw = _prompt(" Port", default=str(default_port))
|
||||
port = int(port_raw) if port_raw.isdigit() else default_port
|
||||
dest_ips = raw.split()
|
||||
portal["listen"] = [{"ip": ip, "port": port} for ip in dest_ips]
|
||||
print(f" {_green('✓')} Portal: {', '.join(f'{ip}:{port}' for ip in dest_ips)}")
|
||||
print()
|
||||
|
||||
|
||||
def _select_shares(shares: list[dict], share_type: str) -> list[dict]:
|
||||
"""
|
||||
Display a numbered list of *shares* and return only those the user selects.
|
||||
@@ -298,22 +336,27 @@ def interactive_mode() -> None:
|
||||
print(f"\n {_bold('What to migrate?')}")
|
||||
print(f" {_cyan('1.')} SMB shares")
|
||||
print(f" {_cyan('2.')} NFS shares")
|
||||
print(f" {_cyan('3.')} iSCSI (targets, extents, portals, initiator groups)")
|
||||
sel_raw = _prompt(
|
||||
"Selection (space-separated numbers, Enter for all)", default="1 2"
|
||||
"Selection (space-separated numbers, Enter for all)", default="1 2 3"
|
||||
)
|
||||
_sel_map = {"1": "smb", "2": "nfs"}
|
||||
_sel_map = {"1": "smb", "2": "nfs", "3": "iscsi"}
|
||||
migrate = []
|
||||
for tok in sel_raw.split():
|
||||
if tok in _sel_map and _sel_map[tok] not in migrate:
|
||||
migrate.append(_sel_map[tok])
|
||||
if not migrate:
|
||||
migrate = ["smb", "nfs"]
|
||||
migrate = ["smb", "nfs", "iscsi"]
|
||||
|
||||
# ── Parse archive ───────────────────────────────────────────────────────
|
||||
print()
|
||||
archive_data = parse_archive(str(chosen))
|
||||
extra_ns = {"debug_tar": str(chosen)}
|
||||
|
||||
# ── iSCSI portal IP remapping ────────────────────────────────────────
|
||||
if "iscsi" in migrate and archive_data.get("iscsi", {}).get("portals"):
|
||||
_prompt_iscsi_portals(archive_data["iscsi"])
|
||||
|
||||
# ── Select individual shares (common) ──────────────────────────────────────
|
||||
if "smb" in migrate and archive_data["smb_shares"]:
|
||||
archive_data["smb_shares"] = _select_shares(archive_data["smb_shares"], "SMB")
|
||||
@@ -447,11 +490,11 @@ def main() -> None:
|
||||
p.add_argument(
|
||||
"--migrate",
|
||||
nargs="+",
|
||||
choices=["smb", "nfs"],
|
||||
default=["smb", "nfs"],
|
||||
choices=["smb", "nfs", "iscsi"],
|
||||
default=["smb", "nfs", "iscsi"],
|
||||
metavar="TYPE",
|
||||
help=(
|
||||
"What to migrate. Choices: smb nfs "
|
||||
"What to migrate. Choices: smb nfs iscsi "
|
||||
"(default: both). Example: --migrate smb"
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user