diff --git a/CHANGELOG.md b/CHANGELOG.md index 872d71e..469971a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,15 @@ own job runner, and later `ix-*` boot units still need midclt. Manual runs of `apply.sh` never trigger a restart. +- **`TypeError: string indices must be integers` when creating a B2 task on + TrueNAS 24.10 (Electric Eel)** (#1). The credential schema differs between + releases: on 24.10 `credentials["provider"]` is the type string (`"B2"`) + with the account/key in `credentials["attributes"]`, while 25.04+ moved + them into a provider dict. The injected `get_restic_config` only handled + the 25.04+ shape. It now detects the schema and reads the credentials from + the right place on both; `create_task.py list-credentials` and `list-tasks` + got the same treatment. + - **`create_task.py verify` false-positive after reboot.** `verify` trusted `hook_status.json`, which only records that the files were patched on disk — not that the running process loaded them. `verify` now also compares the diff --git a/patch/apply.sh b/patch/apply.sh index 9b0e1e4..4540081 100755 --- a/patch/apply.sh +++ b/patch/apply.sh @@ -186,6 +186,11 @@ B2_BLOCK = """ # TRUECLOUD_PATCH — added by truenas-truecloud-patch/patch/apply.sh def _tc_get_restic_config(task): p = task["credentials"]["provider"] + if not isinstance(p, dict): + # TrueNAS <= 24.10: provider is the type string ("B2") and the + # account/key live in the credential's attributes dict. 25.04+ + # moved them into a provider dict. + p = task["credentials"]["attributes"] return "", {"B2_ACCOUNT_ID": p["account"], "B2_ACCOUNT_KEY": p["key"]} B2RcloneRemote.get_restic_config = staticmethod(_tc_get_restic_config) diff --git a/patch/create_task.py b/patch/create_task.py index d05e135..a9860f7 100755 --- a/patch/create_task.py +++ b/patch/create_task.py @@ -177,6 +177,14 @@ def cmd_verify(): print(f"Check {os.path.join(_PATCH_DIR, 'apply.log')} and journalctl -u middlewared") sys.exit(1) +def _provider_type(cred): + """Provider type string across schemas (<=24.10 plain str, >=25.04 dict).""" + p = (cred or {}).get("provider") + if isinstance(p, dict): + return p.get("type", "?") + return p or "?" + + def cmd_list_credentials(client, _args): creds = client("GET", "/cloudsync/credentials") if not creds: @@ -185,7 +193,7 @@ def cmd_list_credentials(client, _args): print(f"{'ID':>4} {'Provider':<14} Name") print("─" * 55) for c in sorted(creds, key=lambda x: x["id"]): - print(f"{c['id']:>4} {c['provider']['type']:<14} {c['name']}") + print(f"{c['id']:>4} {_provider_type(c):<14} {c['name']}") def cmd_list_tasks(client, _args): @@ -196,8 +204,7 @@ def cmd_list_tasks(client, _args): print(f"{'ID':>4} {'Enabled':<8} {'Provider':<14} Name") print("─" * 60) for t in sorted(tasks, key=lambda x: x["id"]): - creds = t.get("credentials") or {} - ptype = (creds.get("provider") or {}).get("type", "?") + ptype = _provider_type(t.get("credentials")) enabled = "yes" if t.get("enabled") else "no" print(f"{t['id']:>4} {enabled:<8} {ptype:<14} {t.get('description', '')}")