create_task.py: migrate REST /api/v2.0 -> midclt (removed in TrueNAS 26.04)
The tool created cloud_backup tasks via POST /api/v2.0/cloud_backup, which is deprecated and removed in TrueNAS 26.04. It now calls the middleware directly with midclt (cloudsync.credentials.query / cloud_backup.query / cloud_backup.create), so it runs on the TrueNAS host with no host address or API key. --host/--api-key/--insecure are kept accepted-but-ignored for compatibility. Dropped the ssl/urllib HTTP client. v0.2.0.
This commit is contained in:
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## v0.2.0 — 2026-07-08
|
||||
|
||||
### Changed
|
||||
|
||||
- **`create_task.py` now uses the TrueNAS middleware via `midclt` instead of the
|
||||
deprecated `/api/v2.0` REST API**, which is removed in TrueNAS 26.04. Practical
|
||||
effects:
|
||||
- Run the script **on the TrueNAS host** — it uses the local middleware socket, so
|
||||
it no longer needs a host address or API key.
|
||||
- `--host`, `--api-key`, and `--insecure` are accepted but **ignored** (a deprecation
|
||||
note is printed); they will be removed in a future release.
|
||||
- `list-credentials` → `cloudsync.credentials.query`, `list-tasks` →
|
||||
`cloud_backup.query`, `create` → `cloud_backup.create`.
|
||||
- Dropped the `ssl`/`urllib` HTTP client; no TLS certificate handling is needed anymore.
|
||||
|
||||
## v0.1.0 — 2026-07-08
|
||||
|
||||
### Added
|
||||
|
||||
@@ -187,18 +187,18 @@ Check [CHANGELOG.md](CHANGELOG.md) to see what changed between versions.
|
||||
## Creating a task via CLI
|
||||
|
||||
If the UI still shows only Storj after refreshing (e.g. the JS bundle pattern
|
||||
changed in a new TrueNAS version), create tasks directly via the REST API:
|
||||
changed in a new TrueNAS version), create tasks directly. Run this **on the
|
||||
TrueNAS host** — it talks to the local middleware via `midclt`, so it needs no
|
||||
host address or API key:
|
||||
|
||||
```bash
|
||||
# Replace /mnt/tank/truenas-truecloud-patch with your clone path
|
||||
|
||||
# List your cloud credentials to find the right ID
|
||||
python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py \
|
||||
--host 192.168.1.1 --api-key <key> list-credentials
|
||||
python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py list-credentials
|
||||
|
||||
# Create a task with a B2 credential (id=3)
|
||||
python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py \
|
||||
--host 192.168.1.1 --api-key <key> create \
|
||||
python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py create \
|
||||
--name "tank-to-b2" \
|
||||
--path /mnt/tank/data \
|
||||
--credential 3 \
|
||||
@@ -213,7 +213,8 @@ python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py \
|
||||
> which re-fetches all repo metadata from the provider every run — glacially slow
|
||||
> on large repos. Point it at a writable dir on a pool with free space.
|
||||
|
||||
Get an API key from **System → API Keys → Add**.
|
||||
> Versions ≤ 0.1.0 used the `/api/v2.0` REST API with `--host`/`--api-key`; those
|
||||
> flags are now accepted-but-ignored (REST is removed in TrueNAS 26.04).
|
||||
|
||||
---
|
||||
|
||||
@@ -433,9 +434,7 @@ grep -c 'STORJ_IX.*S3.*B2' \
|
||||
| grep -v ':0'
|
||||
```
|
||||
|
||||
**`create_task.py` SSL error connecting to TrueNAS**
|
||||
`create_task.py` talks to the **TrueNAS API**, not your S3 endpoint, and
|
||||
verifies its TLS certificate. If your NAS uses a self-signed certificate,
|
||||
pass `--insecure` — but be aware this disables certificate verification for
|
||||
the API call that transmits your TrueNAS API key. Adding your NAS certificate
|
||||
to your system's trust store is safer.
|
||||
**`create_task.py` — "midclt not found" or permission errors**
|
||||
`create_task.py` now talks to the local middleware via `midclt`, so run it **on
|
||||
the TrueNAS host** (not remotely) as a user with middleware access (root). There
|
||||
is no HTTPS/API-key call anymore, so there is no TLS certificate to configure.
|
||||
|
||||
Binary file not shown.
+49
-58
@@ -3,22 +3,24 @@
|
||||
create_task.py — create TrueNAS TrueCloud Backup tasks with S3 or B2 credentials.
|
||||
|
||||
The TrueNAS UI normally restricts the credential dropdown to Storj only.
|
||||
This script bypasses that restriction by calling the REST API directly.
|
||||
This script bypasses that restriction by talking to the TrueNAS middleware
|
||||
directly via `midclt` (the /api/v2.0 REST API is removed in TrueNAS 26.04).
|
||||
|
||||
Compatible providers (after the truecloud-patch backend patch is applied):
|
||||
S3 — any S3-compatible endpoint (AWS, Wasabi, Cloudflare R2, MinIO, …)
|
||||
B2 — Backblaze B2 native API
|
||||
STORJ_IX — Storj (unchanged, always worked)
|
||||
|
||||
Requires a TrueNAS API key: UI → System → API Keys → Add.
|
||||
Run this ON the TrueNAS host — it uses the local middleware socket via `midclt`,
|
||||
so no host address or API key is needed.
|
||||
|
||||
Examples
|
||||
--------
|
||||
List available cloud credentials:
|
||||
python3 create_task.py --host 192.168.1.1 --api-key <key> list-credentials
|
||||
python3 create_task.py list-credentials
|
||||
|
||||
Create a task backed by a B2 credential (id=3):
|
||||
python3 create_task.py --host 192.168.1.1 --api-key <key> create \\
|
||||
python3 create_task.py create \\
|
||||
--name "tank-to-b2" \\
|
||||
--path /mnt/tank/data \\
|
||||
--credential 3 \\
|
||||
@@ -28,7 +30,7 @@ Create a task backed by a B2 credential (id=3):
|
||||
--keep-last 14
|
||||
|
||||
Create a task using an S3-compatible credential (Wasabi, R2, etc.):
|
||||
python3 create_task.py --host 192.168.1.1 --api-key <key> create \\
|
||||
python3 create_task.py create \\
|
||||
--name "tank-to-wasabi" \\
|
||||
--path /mnt/tank/data \\
|
||||
--credential 5 \\
|
||||
@@ -37,54 +39,44 @@ Create a task using an S3-compatible credential (Wasabi, R2, etc.):
|
||||
--password "restic-repo-password"
|
||||
|
||||
List existing TrueCloud Backup tasks:
|
||||
python3 create_task.py --host 192.168.1.1 --api-key <key> list-tasks
|
||||
python3 create_task.py list-tasks
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import calendar
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "0.2.0"
|
||||
|
||||
_PATCH_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
_STATUS_FILE = os.path.join(_PATCH_DIR, "hook_status.json")
|
||||
|
||||
|
||||
def make_client(host, api_key, insecure=False):
|
||||
"""Return a callable that makes authenticated REST API calls."""
|
||||
base = f"https://{host}/api/v2.0"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
ctx = ssl.create_default_context()
|
||||
if insecure:
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def call(method, path, body=None):
|
||||
url = base + path
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=ctx) as resp:
|
||||
return json.loads(resp.read())
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode(errors="replace")
|
||||
print(f"HTTP {exc.code} {exc.reason}: {detail}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except urllib.error.URLError as exc:
|
||||
print(f"Connection error: {exc.reason}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return call
|
||||
def midclt_call(method, *args):
|
||||
"""Call a middleware method locally via `midclt`, the supported JSON-RPC transport
|
||||
that replaces the deprecated /api/v2.0 REST API (removed in TrueNAS 26.04). Must run
|
||||
on the TrueNAS host. Each arg is JSON-encoded (a dict for create; none for queries).
|
||||
Exits with a clear message on failure."""
|
||||
cmd = ["midclt", "call", method] + [json.dumps(a) for a in args]
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
except FileNotFoundError:
|
||||
print("ERROR: `midclt` not found — run this script ON the TrueNAS host.",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except subprocess.SubprocessError as exc:
|
||||
print(f"ERROR: midclt call failed: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if proc.returncode != 0:
|
||||
print(f"ERROR: midclt {method}: {(proc.stderr or proc.stdout).strip()}",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
out = proc.stdout.strip()
|
||||
return json.loads(out) if out else None
|
||||
|
||||
|
||||
# ── Sub-commands ──────────────────────────────────────────────────────────────
|
||||
@@ -185,8 +177,8 @@ def _provider_type(cred):
|
||||
return p or "?"
|
||||
|
||||
|
||||
def cmd_list_credentials(client, _args):
|
||||
creds = client("GET", "/cloudsync/credentials")
|
||||
def cmd_list_credentials(_args):
|
||||
creds = midclt_call("cloudsync.credentials.query")
|
||||
if not creds:
|
||||
print("No cloud credentials configured.")
|
||||
return
|
||||
@@ -196,8 +188,8 @@ def cmd_list_credentials(client, _args):
|
||||
print(f"{c['id']:>4} {_provider_type(c):<14} {c['name']}")
|
||||
|
||||
|
||||
def cmd_list_tasks(client, _args):
|
||||
tasks = client("GET", "/cloud_backup")
|
||||
def cmd_list_tasks(_args):
|
||||
tasks = midclt_call("cloud_backup.query")
|
||||
if not tasks:
|
||||
print("No TrueCloud Backup tasks configured.")
|
||||
return
|
||||
@@ -209,7 +201,7 @@ def cmd_list_tasks(client, _args):
|
||||
print(f"{t['id']:>4} {enabled:<8} {ptype:<14} {t.get('description', '')}")
|
||||
|
||||
|
||||
def cmd_create(client, args):
|
||||
def cmd_create(args):
|
||||
parts = args.schedule.split()
|
||||
if len(parts) != 5:
|
||||
print(
|
||||
@@ -253,7 +245,7 @@ def cmd_create(client, args):
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
result = client("POST", "/cloud_backup", body)
|
||||
result = midclt_call("cloud_backup.create", body)
|
||||
try:
|
||||
print(f"Created task id={result['id']} name={result['description']!r}")
|
||||
except (KeyError, TypeError):
|
||||
@@ -269,14 +261,12 @@ def main():
|
||||
epilog=__doc__.split("Examples")[1] if __doc__ and "Examples" in __doc__ else "",
|
||||
)
|
||||
p.add_argument("--version", "-V", action="version", version=f"truecloud-patch {__version__}")
|
||||
p.add_argument("--host", default=None, metavar="HOST",
|
||||
help="TrueNAS hostname or IP address (required except for verify)")
|
||||
p.add_argument("--api-key", default=None, metavar="KEY",
|
||||
help="TrueNAS API key — System → API Keys (required except for verify)")
|
||||
p.add_argument("--insecure", action="store_true",
|
||||
help="Skip TLS certificate verification (self-signed certs). "
|
||||
"WARNING: exposes your API key to network interception. "
|
||||
"Prefer adding your cert to the trust store instead.")
|
||||
# Deprecated & ignored: the tool now uses the local middleware via `midclt` (the
|
||||
# /api/v2.0 REST API is removed in TrueNAS 26.04), so it must run ON the TrueNAS
|
||||
# host and needs no host/API key. Kept accepted-but-ignored for compatibility.
|
||||
p.add_argument("--host", default=None, help=argparse.SUPPRESS)
|
||||
p.add_argument("--api-key", default=None, help=argparse.SUPPRESS)
|
||||
p.add_argument("--insecure", action="store_true", help=argparse.SUPPRESS)
|
||||
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
@@ -323,16 +313,17 @@ def main():
|
||||
cmd_verify()
|
||||
return
|
||||
|
||||
if not args.host or not args.api_key:
|
||||
p.error("--host and --api-key are required for this command")
|
||||
if args.host or args.api_key or args.insecure:
|
||||
print("NOTE: --host/--api-key/--insecure are deprecated and ignored; this tool "
|
||||
"now uses the local middleware (midclt) and must run on the TrueNAS host.",
|
||||
file=sys.stderr)
|
||||
|
||||
client = make_client(args.host, args.api_key, args.insecure)
|
||||
if args.cmd == "list-credentials":
|
||||
cmd_list_credentials(client, args)
|
||||
cmd_list_credentials(args)
|
||||
elif args.cmd == "list-tasks":
|
||||
cmd_list_tasks(client, args)
|
||||
cmd_list_tasks(args)
|
||||
elif args.cmd == "create":
|
||||
cmd_create(client, args)
|
||||
cmd_create(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user