From e094c05cae02f6096abde29793f34a8431c2c97e Mon Sep 17 00:00:00 2001 From: scott Date: Wed, 4 Mar 2026 10:03:41 -0500 Subject: [PATCH] =?UTF-8?q?Fix=20CORE=E2=86=92SCALE=20compatibility=20for?= =?UTF-8?q?=20SMB=20and=20NFS=20share=20payloads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strip CORE-only SMB share field (vuid) and NFS share fields (paths, alldirs, quiet) that are rejected by the SCALE API. Convert CORE's NFS paths list to the single path string SCALE expects. Also include NFS paths in dry-run dataset existence checks. Co-Authored-By: Claude Sonnet 4.6 --- truenas_migrate.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/truenas_migrate.py b/truenas_migrate.py index 292bddf..d7fcada 100644 --- a/truenas_migrate.py +++ b/truenas_migrate.py @@ -539,16 +539,33 @@ def list_archive_and_exit(tar_path: str) -> None: # Read-only / server-generated fields that must NOT be sent on create/update _SMB_SHARE_READONLY = frozenset({"id", "locked"}) -_NFS_SHARE_READONLY = frozenset({"id", "locked"}) _SMB_CONFIG_READONLY = frozenset({"id", "server_sid"}) +# CORE SMB share fields that do not exist in the SCALE API +_SMB_SHARE_CORE_EXTRAS = frozenset({ + "vuid", # server-generated Time Machine UUID; SCALE sets this automatically +}) + +# CORE NFS share fields that do not exist in the SCALE API +_NFS_SHARE_CORE_EXTRAS = frozenset({ + "paths", # CORE uses a list; SCALE uses a single "path" string (converted below) + "alldirs", # removed in SCALE + "quiet", # removed in SCALE +}) + def _smb_share_payload(share: dict) -> dict: - return {k: v for k, v in share.items() if k not in _SMB_SHARE_READONLY} + exclude = _SMB_SHARE_READONLY | _SMB_SHARE_CORE_EXTRAS + return {k: v for k, v in share.items() if k not in exclude} def _nfs_share_payload(share: dict) -> dict: - return {k: v for k, v in share.items() if k not in _NFS_SHARE_READONLY} + payload = {k: v for k, v in share.items() + if k not in {"id", "locked"} | _NFS_SHARE_CORE_EXTRAS} + # CORE stores export paths as a list under "paths"; SCALE expects a single "path" string. + if "path" not in payload and share.get("paths"): + payload["path"] = share["paths"][0] + return payload def _smb_config_payload(config: dict) -> dict: @@ -952,7 +969,10 @@ async def migrate_nfs_shares( log.info(" Destination has %d existing NFS share(s).", len(existing_paths)) for share in shares: - path = share.get("path", "").rstrip("/") + # CORE archives store paths as a list; SCALE uses a single string. + core_paths = share.get("paths") or [] + path = (share.get("path") or (core_paths[0] if core_paths else "")).rstrip("/") + all_paths = [p.rstrip("/") for p in (core_paths if core_paths else ([path] if path else []))] log.info("── NFS export %r", path) if path in existing_paths: @@ -966,8 +986,7 @@ async def migrate_nfs_shares( if dry_run: log.info(" [DRY RUN] would create NFS export for %r", path) summary.nfs_created += 1 - if path: - summary.paths_to_create.append(path) + summary.paths_to_create.extend(all_paths) continue try: