Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ded8cff3d | ||
|
|
da1be97377 |
@@ -1,5 +1,51 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.2.1 — 2026-07-09
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Deferred restart raced the rest of boot, leaving all apps and dashboard
|
||||||
|
stats down.** The `truecloud-mw-restart` unit introduced in v0.0.4 relied
|
||||||
|
on systemd ordering (`After=multi-user.target`, `After=ix-postinit.service`),
|
||||||
|
which cannot see middlewared's *internal* boot work. Observed on 25.10.4:
|
||||||
|
the restart fired two seconds into `ix-reporting.service`'s
|
||||||
|
`midclt call reporting.start_service` and before the docker/apps startup
|
||||||
|
task (created on middlewared's system-ready event) had run. Both were
|
||||||
|
killed, and nothing retries them until the next boot — every app stayed
|
||||||
|
down (`docker.status` FAILED, the apps dataset never mounted), netdata
|
||||||
|
never started (no dashboard hardware stats), and the SMB middleware
|
||||||
|
backend was left uninitialized.
|
||||||
|
|
||||||
|
The transient unit now runs `patch/wait_restart.sh` instead of restarting
|
||||||
|
directly: it waits for the systemd boot job queue to drain
|
||||||
|
(`systemctl is-system-running --wait`, covering in-flight `ix-*` oneshots
|
||||||
|
such as ix-reporting), then polls `midclt call docker.status` until the
|
||||||
|
docker state machine leaves its transitional states, then allows a short
|
||||||
|
grace period for middleware-internal tasks with no queryable state before
|
||||||
|
issuing `systemctl try-restart middlewared`. The unit no longer sets
|
||||||
|
`Type=oneshot` — a oneshot's start job stays in the very queue the script
|
||||||
|
waits on and would deadlock on itself. All waits are bounded and fail
|
||||||
|
open: worst case the restart still happens, just later.
|
||||||
|
|
||||||
|
Recovery on a boot that already hit this (without rebooting):
|
||||||
|
`midclt call reporting.start_service` and
|
||||||
|
`midclt call docker.state.start_service true`.
|
||||||
|
|
||||||
|
## 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
|
## v0.1.0 — 2026-07-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -66,9 +66,11 @@ directly.
|
|||||||
|
|
||||||
PREINIT scripts are executed *by* middlewared, which by then has already
|
PREINIT scripts are executed *by* middlewared, which by then has already
|
||||||
imported the stock modules — so after patching, `apply.sh` schedules a single
|
imported the stock modules — so after patching, `apply.sh` schedules a single
|
||||||
detached middlewared restart (transient systemd unit `truecloud-mw-restart`,
|
detached middlewared restart (transient systemd unit `truecloud-mw-restart`
|
||||||
ordered after `multi-user.target`) that loads the patched modules once boot
|
running `patch/wait_restart.sh`) that loads the patched modules once boot has
|
||||||
completes. Expect one middlewared restart shortly after every boot; the UI
|
*actually* settled: the script waits for the systemd boot job queue to drain
|
||||||
|
and for the docker/apps state machine to reach a terminal state before
|
||||||
|
restarting. Expect one middlewared restart shortly after every boot; the UI
|
||||||
and API are briefly unavailable while it happens, and running services are
|
and API are briefly unavailable while it happens, and running services are
|
||||||
not affected.
|
not affected.
|
||||||
|
|
||||||
@@ -113,10 +115,15 @@ Two different things must survive two different events:
|
|||||||
imported the stock modules in step 1 and never re-imports, so the on-disk
|
imported the stock modules in step 1 and never re-imports, so the on-disk
|
||||||
patch alone is not enough. `apply.sh` detects it was invoked by middlewared
|
patch alone is not enough. `apply.sh` detects it was invoked by middlewared
|
||||||
and creates a transient systemd unit (`truecloud-mw-restart`, via
|
and creates a transient systemd unit (`truecloud-mw-restart`, via
|
||||||
`systemd-run --no-block`, ordered after `multi-user.target`) — detached and
|
`systemd-run --no-block`) running `patch/wait_restart.sh` — detached so it
|
||||||
deferred so it cannot disrupt the remainder of the boot sequence.
|
cannot disrupt the remainder of the boot sequence.
|
||||||
5. **Once boot completes, middlewared restarts once** and imports the patched
|
5. **Once boot has settled, middlewared restarts once** and imports the
|
||||||
modules from the overlay. S3/B2 backup support is now active until the next
|
patched modules from the overlay. `wait_restart.sh` holds the restart until
|
||||||
|
the systemd boot job queue has drained (so in-flight `ix-*` units like
|
||||||
|
`ix-reporting` finish first) *and* middlewared's docker/apps startup has
|
||||||
|
reached a terminal state — plain unit ordering cannot see either, and
|
||||||
|
restarting middlewared while they run kills apps and dashboard reporting
|
||||||
|
for the whole boot. S3/B2 backup support is then active until the next
|
||||||
reboot, when the cycle repeats.
|
reboot, when the cycle repeats.
|
||||||
|
|
||||||
What you will observe: one middlewared restart shortly after every boot (a
|
What you will observe: one middlewared restart shortly after every boot (a
|
||||||
@@ -187,18 +194,18 @@ Check [CHANGELOG.md](CHANGELOG.md) to see what changed between versions.
|
|||||||
## Creating a task via CLI
|
## Creating a task via CLI
|
||||||
|
|
||||||
If the UI still shows only Storj after refreshing (e.g. the JS bundle pattern
|
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
|
```bash
|
||||||
# Replace /mnt/tank/truenas-truecloud-patch with your clone path
|
# Replace /mnt/tank/truenas-truecloud-patch with your clone path
|
||||||
|
|
||||||
# List your cloud credentials to find the right ID
|
# List your cloud credentials to find the right ID
|
||||||
python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py \
|
python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py list-credentials
|
||||||
--host 192.168.1.1 --api-key <key> list-credentials
|
|
||||||
|
|
||||||
# Create a task with a B2 credential (id=3)
|
# Create a task with a B2 credential (id=3)
|
||||||
python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py \
|
python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py create \
|
||||||
--host 192.168.1.1 --api-key <key> create \
|
|
||||||
--name "tank-to-b2" \
|
--name "tank-to-b2" \
|
||||||
--path /mnt/tank/data \
|
--path /mnt/tank/data \
|
||||||
--credential 3 \
|
--credential 3 \
|
||||||
@@ -213,7 +220,8 @@ python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py \
|
|||||||
> which re-fetches all repo metadata from the provider every run — glacially slow
|
> 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.
|
> 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 +441,7 @@ grep -c 'STORJ_IX.*S3.*B2' \
|
|||||||
| grep -v ':0'
|
| grep -v ':0'
|
||||||
```
|
```
|
||||||
|
|
||||||
**`create_task.py` SSL error connecting to TrueNAS**
|
**`create_task.py` — "midclt not found" or permission errors**
|
||||||
`create_task.py` talks to the **TrueNAS API**, not your S3 endpoint, and
|
`create_task.py` now talks to the local middleware via `midclt`, so run it **on
|
||||||
verifies its TLS certificate. If your NAS uses a self-signed certificate,
|
the TrueNAS host** (not remotely) as a user with middleware access (root). There
|
||||||
pass `--insecure` — but be aware this disables certificate verification for
|
is no HTTPS/API-key call anymore, so there is no TLS certificate to configure.
|
||||||
the API call that transmits your TrueNAS API key. Adding your NAS certificate
|
|
||||||
to your system's trust store is safer.
|
|
||||||
|
|||||||
Binary file not shown.
+12
-6
@@ -24,7 +24,7 @@
|
|||||||
# Derive PATCH_DIR from this script's location (parent of the patch/ directory).
|
# Derive PATCH_DIR from this script's location (parent of the patch/ directory).
|
||||||
PATCH_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
PATCH_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
LOG="$PATCH_DIR/apply.log"
|
LOG="$PATCH_DIR/apply.log"
|
||||||
VERSION="0.0.4"
|
VERSION="0.2.1"
|
||||||
|
|
||||||
# Rotate log at 512 KB to avoid unbounded growth on a system volume.
|
# Rotate log at 512 KB to avoid unbounded growth on a system volume.
|
||||||
# Keep two prior generations (.1 and .2) so the last three boots are always available.
|
# Keep two prior generations (.1 and .2) so the last three boots are always available.
|
||||||
@@ -329,6 +329,14 @@ fi
|
|||||||
# ix-* boot units still need midclt to answer.
|
# ix-* boot units still need midclt to answer.
|
||||||
# Boot context is detected by the parent process being middlewared; manual
|
# Boot context is detected by the parent process being middlewared; manual
|
||||||
# runs (install.sh, recovery) never trigger a restart.
|
# runs (install.sh, recovery) never trigger a restart.
|
||||||
|
#
|
||||||
|
# The unit runs wait_restart.sh, which blocks until boot has actually
|
||||||
|
# settled (systemd job queue drained, docker/apps state terminal) before
|
||||||
|
# restarting. systemd ordering alone (After=multi-user.target, ≤ v0.0.4)
|
||||||
|
# fired while ix-reporting and the docker/apps startup were still in flight
|
||||||
|
# and killed both — apps and dashboard stats stayed down until the next
|
||||||
|
# boot. No Type=oneshot: a oneshot's start job would hold the boot queue
|
||||||
|
# open against the `is-system-running --wait` inside the script.
|
||||||
|
|
||||||
echo "--- deferred restart ---"
|
echo "--- deferred restart ---"
|
||||||
|
|
||||||
@@ -340,12 +348,10 @@ else
|
|||||||
# A failed unit from an earlier attempt this boot would block systemd-run.
|
# A failed unit from an earlier attempt this boot would block systemd-run.
|
||||||
systemctl reset-failed truecloud-mw-restart.service 2>/dev/null
|
systemctl reset-failed truecloud-mw-restart.service 2>/dev/null
|
||||||
if systemd-run --no-block --collect --unit=truecloud-mw-restart \
|
if systemd-run --no-block --collect --unit=truecloud-mw-restart \
|
||||||
--property=Type=oneshot \
|
/bin/bash "$PATCH_DIR/patch/wait_restart.sh"; then
|
||||||
--property=After=multi-user.target \
|
|
||||||
--property=After=ix-postinit.service \
|
|
||||||
systemctl try-restart middlewared; then
|
|
||||||
echo "OK: Scheduled deferred middlewared restart (unit: truecloud-mw-restart)."
|
echo "OK: Scheduled deferred middlewared restart (unit: truecloud-mw-restart)."
|
||||||
echo " Backend patch becomes active once boot completes."
|
echo " It waits for boot to fully settle (apps started, reporting up),"
|
||||||
|
echo " then restarts middlewared so the backend patch actually loads."
|
||||||
else
|
else
|
||||||
echo "WARNING: Could not schedule deferred restart — backend patch is on disk but NOT loaded."
|
echo "WARNING: Could not schedule deferred restart — backend patch is on disk but NOT loaded."
|
||||||
echo " Activate manually: systemctl restart middlewared"
|
echo " Activate manually: systemctl restart middlewared"
|
||||||
|
|||||||
+46
-55
@@ -3,22 +3,24 @@
|
|||||||
create_task.py — create TrueNAS TrueCloud Backup tasks with S3 or B2 credentials.
|
create_task.py — create TrueNAS TrueCloud Backup tasks with S3 or B2 credentials.
|
||||||
|
|
||||||
The TrueNAS UI normally restricts the credential dropdown to Storj only.
|
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):
|
Compatible providers (after the truecloud-patch backend patch is applied):
|
||||||
S3 — any S3-compatible endpoint (AWS, Wasabi, Cloudflare R2, MinIO, …)
|
S3 — any S3-compatible endpoint (AWS, Wasabi, Cloudflare R2, MinIO, …)
|
||||||
B2 — Backblaze B2 native API
|
B2 — Backblaze B2 native API
|
||||||
STORJ_IX — Storj (unchanged, always worked)
|
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
|
Examples
|
||||||
--------
|
--------
|
||||||
List available cloud credentials:
|
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):
|
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" \\
|
--name "tank-to-b2" \\
|
||||||
--path /mnt/tank/data \\
|
--path /mnt/tank/data \\
|
||||||
--credential 3 \\
|
--credential 3 \\
|
||||||
@@ -28,7 +30,7 @@ Create a task backed by a B2 credential (id=3):
|
|||||||
--keep-last 14
|
--keep-last 14
|
||||||
|
|
||||||
Create a task using an S3-compatible credential (Wasabi, R2, etc.):
|
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" \\
|
--name "tank-to-wasabi" \\
|
||||||
--path /mnt/tank/data \\
|
--path /mnt/tank/data \\
|
||||||
--credential 5 \\
|
--credential 5 \\
|
||||||
@@ -37,54 +39,44 @@ Create a task using an S3-compatible credential (Wasabi, R2, etc.):
|
|||||||
--password "restic-repo-password"
|
--password "restic-repo-password"
|
||||||
|
|
||||||
List existing TrueCloud Backup tasks:
|
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 argparse
|
||||||
import calendar
|
import calendar
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import ssl
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
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__)))
|
_PATCH_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
_STATUS_FILE = os.path.join(_PATCH_DIR, "hook_status.json")
|
_STATUS_FILE = os.path.join(_PATCH_DIR, "hook_status.json")
|
||||||
|
|
||||||
|
|
||||||
def make_client(host, api_key, insecure=False):
|
def midclt_call(method, *args):
|
||||||
"""Return a callable that makes authenticated REST API calls."""
|
"""Call a middleware method locally via `midclt`, the supported JSON-RPC transport
|
||||||
base = f"https://{host}/api/v2.0"
|
that replaces the deprecated /api/v2.0 REST API (removed in TrueNAS 26.04). Must run
|
||||||
headers = {
|
on the TrueNAS host. Each arg is JSON-encoded (a dict for create; none for queries).
|
||||||
"Authorization": f"Bearer {api_key}",
|
Exits with a clear message on failure."""
|
||||||
"Content-Type": "application/json",
|
cmd = ["midclt", "call", method] + [json.dumps(a) for a in args]
|
||||||
}
|
|
||||||
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:
|
try:
|
||||||
with urllib.request.urlopen(req, context=ctx) as resp:
|
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||||
return json.loads(resp.read())
|
except FileNotFoundError:
|
||||||
except urllib.error.HTTPError as exc:
|
print("ERROR: `midclt` not found — run this script ON the TrueNAS host.",
|
||||||
detail = exc.read().decode(errors="replace")
|
file=sys.stderr)
|
||||||
print(f"HTTP {exc.code} {exc.reason}: {detail}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except urllib.error.URLError as exc:
|
except subprocess.SubprocessError as exc:
|
||||||
print(f"Connection error: {exc.reason}", file=sys.stderr)
|
print(f"ERROR: midclt call failed: {exc}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
if proc.returncode != 0:
|
||||||
return call
|
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 ──────────────────────────────────────────────────────────────
|
# ── Sub-commands ──────────────────────────────────────────────────────────────
|
||||||
@@ -185,8 +177,8 @@ def _provider_type(cred):
|
|||||||
return p or "?"
|
return p or "?"
|
||||||
|
|
||||||
|
|
||||||
def cmd_list_credentials(client, _args):
|
def cmd_list_credentials(_args):
|
||||||
creds = client("GET", "/cloudsync/credentials")
|
creds = midclt_call("cloudsync.credentials.query")
|
||||||
if not creds:
|
if not creds:
|
||||||
print("No cloud credentials configured.")
|
print("No cloud credentials configured.")
|
||||||
return
|
return
|
||||||
@@ -196,8 +188,8 @@ def cmd_list_credentials(client, _args):
|
|||||||
print(f"{c['id']:>4} {_provider_type(c):<14} {c['name']}")
|
print(f"{c['id']:>4} {_provider_type(c):<14} {c['name']}")
|
||||||
|
|
||||||
|
|
||||||
def cmd_list_tasks(client, _args):
|
def cmd_list_tasks(_args):
|
||||||
tasks = client("GET", "/cloud_backup")
|
tasks = midclt_call("cloud_backup.query")
|
||||||
if not tasks:
|
if not tasks:
|
||||||
print("No TrueCloud Backup tasks configured.")
|
print("No TrueCloud Backup tasks configured.")
|
||||||
return
|
return
|
||||||
@@ -209,7 +201,7 @@ def cmd_list_tasks(client, _args):
|
|||||||
print(f"{t['id']:>4} {enabled:<8} {ptype:<14} {t.get('description', '')}")
|
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()
|
parts = args.schedule.split()
|
||||||
if len(parts) != 5:
|
if len(parts) != 5:
|
||||||
print(
|
print(
|
||||||
@@ -253,7 +245,7 @@ def cmd_create(client, args):
|
|||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = client("POST", "/cloud_backup", body)
|
result = midclt_call("cloud_backup.create", body)
|
||||||
try:
|
try:
|
||||||
print(f"Created task id={result['id']} name={result['description']!r}")
|
print(f"Created task id={result['id']} name={result['description']!r}")
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
@@ -269,14 +261,12 @@ def main():
|
|||||||
epilog=__doc__.split("Examples")[1] if __doc__ and "Examples" in __doc__ else "",
|
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("--version", "-V", action="version", version=f"truecloud-patch {__version__}")
|
||||||
p.add_argument("--host", default=None, metavar="HOST",
|
# Deprecated & ignored: the tool now uses the local middleware via `midclt` (the
|
||||||
help="TrueNAS hostname or IP address (required except for verify)")
|
# /api/v2.0 REST API is removed in TrueNAS 26.04), so it must run ON the TrueNAS
|
||||||
p.add_argument("--api-key", default=None, metavar="KEY",
|
# host and needs no host/API key. Kept accepted-but-ignored for compatibility.
|
||||||
help="TrueNAS API key — System → API Keys (required except for verify)")
|
p.add_argument("--host", default=None, help=argparse.SUPPRESS)
|
||||||
p.add_argument("--insecure", action="store_true",
|
p.add_argument("--api-key", default=None, help=argparse.SUPPRESS)
|
||||||
help="Skip TLS certificate verification (self-signed certs). "
|
p.add_argument("--insecure", action="store_true", help=argparse.SUPPRESS)
|
||||||
"WARNING: exposes your API key to network interception. "
|
|
||||||
"Prefer adding your cert to the trust store instead.")
|
|
||||||
|
|
||||||
sub = p.add_subparsers(dest="cmd", required=True)
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
@@ -323,16 +313,17 @@ def main():
|
|||||||
cmd_verify()
|
cmd_verify()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not args.host or not args.api_key:
|
if args.host or args.api_key or args.insecure:
|
||||||
p.error("--host and --api-key are required for this command")
|
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":
|
if args.cmd == "list-credentials":
|
||||||
cmd_list_credentials(client, args)
|
cmd_list_credentials(args)
|
||||||
elif args.cmd == "list-tasks":
|
elif args.cmd == "list-tasks":
|
||||||
cmd_list_tasks(client, args)
|
cmd_list_tasks(args)
|
||||||
elif args.cmd == "create":
|
elif args.cmd == "create":
|
||||||
cmd_create(client, args)
|
cmd_create(args)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Executable
+55
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# patch/wait_restart.sh — payload of the transient `truecloud-mw-restart`
|
||||||
|
# unit that apply.sh schedules in boot context (Step 3).
|
||||||
|
#
|
||||||
|
# Why not restart middlewared directly from the unit: systemd ordering
|
||||||
|
# (`After=multi-user.target`, used up to v0.0.4) cannot see middlewared's
|
||||||
|
# *internal* boot work. When the boot targets are reached, two things are
|
||||||
|
# typically still in flight inside middlewared:
|
||||||
|
#
|
||||||
|
# - ix-reporting.service's `midclt call reporting.start_service` (netdata,
|
||||||
|
# which feeds the dashboard hardware stats), and
|
||||||
|
# - the docker/apps startup task middlewared creates on its own
|
||||||
|
# system-ready event (`docker.state.start_service`).
|
||||||
|
#
|
||||||
|
# Restarting middlewared while those run kills them, and nothing retries
|
||||||
|
# them until the next boot: every app stays down (`docker.status` FAILED),
|
||||||
|
# the dashboard shows no stats, and middleware-internal service state (e.g.
|
||||||
|
# the SMB backend) is left uninitialized. Observed on 25.10.4 with v0.0.4.
|
||||||
|
#
|
||||||
|
# So this script waits for both layers to settle before restarting. Every
|
||||||
|
# wait is bounded and fails open: worst case the restart still happens, just
|
||||||
|
# later — a restart on a settled system is harmless (docker, apps and
|
||||||
|
# netdata are independent processes; only the middleware API blips).
|
||||||
|
#
|
||||||
|
# NOTE: the unit must NOT be Type=oneshot. A oneshot's start job stays in
|
||||||
|
# the systemd job queue until the process exits, and `is-system-running
|
||||||
|
# --wait` below waits for that same queue to drain — the unit would deadlock
|
||||||
|
# on itself until the timeout. apply.sh schedules this with the default
|
||||||
|
# service type, whose start job completes at fork.
|
||||||
|
|
||||||
|
# 1. systemd layer: wait for the boot job queue to drain. This covers every
|
||||||
|
# ix-* oneshot still activating, including ix-reporting's in-flight midclt
|
||||||
|
# call. The exit code is irrelevant — a "degraded" boot (any unrelated
|
||||||
|
# failed unit) is still a finished boot. The timeout only guards against
|
||||||
|
# a boot that never settles (e.g. a unit stuck on a network wait).
|
||||||
|
timeout 900 systemctl is-system-running --wait > /dev/null 2>&1
|
||||||
|
|
||||||
|
# 2. middlewared layer: poll the docker state machine until it leaves the
|
||||||
|
# transitional states (PENDING/INITIALIZING/STOPPING/MIGRATING — see
|
||||||
|
# middlewared/plugins/docker/state_utils.py). An empty answer means
|
||||||
|
# midclt could not respond at all; keep waiting. Cap at 10 minutes.
|
||||||
|
for _ in $(seq 1 120); do
|
||||||
|
_status=$(midclt call docker.status 2>/dev/null \
|
||||||
|
| grep -oE '"status": "[A-Z_]+"' | cut -d'"' -f4)
|
||||||
|
case "$_status" in
|
||||||
|
RUNNING|STOPPED|UNCONFIGURED|FAILED|MIGRATION_FAILED) break ;;
|
||||||
|
esac
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
# 3. Grace period for middleware-internal ready-event tasks that expose no
|
||||||
|
# queryable state (smb.configure and friends). Bounded insurance.
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
exec systemctl try-restart middlewared
|
||||||
Reference in New Issue
Block a user