Hi all.
I appreciate any help or advice. I am running a nightly sync with Backblaze, but the sync fails if apps are running. I want to use the cloud sync pre-script and post-script options to stop the apps before the sync runs, then start them again after the sync completes.
The below script will run first and works by making an API call to get the running apps and saves the running apps list to a file (which will be read by the post-script to start the apps again). The script iterates through the app list and stops each app.
The response received from the /app/stop call is a job number. If I check this job, I receive the error below. The job fails because it expects a string for the app_name parameter, however, I believe I am passing a valid string with a valid app_name. Any idea what I am doing wrong? ChatGPT helped me write this, but is now blaming this error on the back-end validation of the API lol
Job Error:
'Input should be a valid string'
Relevant API call (seems to be working, returns a JOB ID):
JOB_ID=$(curl -s -X POST \
-H "Authorization: Bearer $apiKey" \
-H "Content-Type: application/json" \
-d "{\"app_name\": \"$APP\"}" \
"$serverURL/api/v2.0/app/stop")
Complete pre-script.sh:
#!/bin/bash
STATE_FILE="/mnt/pool/Apps/cron-jobs/cloud-sync/state/stopped_apps.txt"
LOG_FILE="/mnt/pool/Apps/cron-jobs/cloud-sync/logs/pre-script.log"
# Server IP or URL
serverURL="http://192.168.50.50"
# TrueNAS API key
apiKey="x-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Ensure directories exist
mkdir -p "$(dirname "$STATE_FILE")"
mkdir -p "$(dirname "$LOG_FILE")"
# Clear previous state
> "$STATE_FILE"
echo "[$(date)] Starting pre-script..." >> "$LOG_FILE"
# Get all apps and filter for running ones
RUNNING_APPS=$(curl -s -X GET \
-H "Authorization: Bearer $apiKey" \
"$serverURL/api/v2.0/app" | jq -r '.[] | select(.state == "RUNNING") | .name')
# Stop each app loop
for APP in $RUNNING_APPS; do
echo "$APP" >> "$STATE_FILE"
echo "[$(date)] Stopping app: $APP" >> "$LOG_FILE"
# Send stop request
JOB_ID=$(curl -s -X POST \
-H "Authorization: Bearer $apiKey" \
-H "Content-Type: application/json" \
-d "{\"app_name\": \"$APP\"}" \
"$serverURL/api/v2.0/app/stop")
echo "[$(date)] JOB ID for $APP: $JOB_ID" >> "$LOG_FILE"
done
# DEBUG List a list of all the jobs
STATUS=$(curl -s -G \
-H "Authorization: Bearer $apiKey" \
-H "Content-Type: application/json" \
"$serverURL/api/v2.0/core/get_jobs")
echo "[$(date)] Status for $JOB_ID: $STATUS" >> "$LOG_FILE"
echo "[$(date)] Pre-script completed. Apps stopped: $(wc -l < "$STATE_FILE")" >> "$LOG_FILE"
Response from the job status request /core/get_jobs:
"id": 10190,
"method": "app.stop",
"arguments": [
{
"app_name": "watchtower"
}
],
"transient": false,
"description": null,
"abortable": false,
"logs_path": null,
"logs_excerpt": null,
"progress": {
"percent": 0,
"description": "",
"extra": null
},
"result": null,
"result_encoding_error": null,
"error": "[EINVAL] app_name: Input should be a valid string\n",
"exception": "Traceback (most recent call last):\n File \"/usr/lib/python3/dist-packages/middlewared/job.py\", line 515, in run\n await self.future\n File \"/usr/lib/python3/dist-packages/middlewared/job.py\", line 562, in __run_body\n rv = await self.middleware.run_in_thread(self.method, *args)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/lib/python3/dist-packages/middlewared/main.py\", line 627, in run_in_thread\n return await self.run_in_executor(io_thread_pool_executor, method, *args, **kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/lib/python3/dist-packages/middlewared/main.py\", line 624, in run_in_executor\n return await loop.run_in_executor(pool, functools.partial(method, *args, **kwargs))\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/lib/python3.11/concurrent/futures/thread.py\", line 58, in run\n result = self.fn(*self.args, **self.kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/lib/python3/dist-packages/middlewared/api/base/decorator.py\", line 99, in wrapped\n args = list(args[:args_index]) + accept_params(accepts, args[args_index:])\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/lib/python3/dist-packages/middlewared/api/base/handler/accept.py\", line 25, in accept_params\n dump = validate_model(model, args_as_dict, exclude_unset=exclude_unset, expose_secrets=expose_secrets)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/usr/lib/python3/dist-packages/middlewared/api/base/handler/accept.py\", line 84, in validate_model\n raise verrors from None\nmiddlewared.service_exception.ValidationErrors: [EINVAL] app_name: Input should be a valid string\n\n",
"exc_info": {
"repr": "ValidationErrors([ValidationError('app_name', 'Input should be a valid string', 22)])",
"type": "VALIDATION",
"errno": null,
"extra": [
[
"app_name",
"Input should be a valid string",
22
]
]
},
"state": "FAILED",
"time_started": {
"$date": 1757897169000
},
"time_finished": {
"$date": 1757897169000
},
"credentials": {
"type": "API_KEY",
"data": {
"username": "admin",
"login_at": {
"$date": 1757897169000
},
"api_key": {
"id": 4,
"name": "cron-jobs"
}
}
}
},

