Can't Stop Apps using API

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"
    }
   }
  }
 },

To me, seems that your request is Is not well formatted

Instead of use an array named param you use a JSON containing app_name.

(Just my opinion, don’t use chat gpt without revision at all what he produces )

1 Like

Thanks for the quick reply. I ended up going a different route by using midclt. Here is the solution I ended up using:

pre-script.sh (stops running apps)

#!/bin/bash

# -------------------------------
# Pre-script: Stop running TrueNAS apps
# -------------------------------

STATE_FILE="/mnt/pool/Apps/cron-jobs/cloud-sync/state/stopped_apps.txt"
LOG_FILE="/mnt/pool/Apps/cron-jobs/cloud-sync/logs/cloud-sync.log"

# 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 list of running apps
RUNNING_APPS=$(midclt call app.query | jq -r '.[] | select(.state=="RUNNING") | .name')

if [ -z "$RUNNING_APPS" ]; then
    echo "[$(date)] No running apps found." >> "$LOG_FILE"
else
    echo "$RUNNING_APPS" | while IFS= read -r APP; do
        echo "$APP" >> "$STATE_FILE"
        echo "[$(date)] Stopping app: $APP" >> "$LOG_FILE"

        # Stop the app
        midclt call app.stop "$APP"

    done
fi

echo "[$(date)] Pre-script completed. Apps stopped: $(wc -l < "$STATE_FILE")" >> "$LOG_FILE"

post-script.sh (starts apps which were stopped previously)

#!/bin/bash

# -------------------------------
# Post-script: Restart TrueNAS apps stopped by pre-script
# -------------------------------

STATE_FILE="/mnt/pool/Apps/cron-jobs/cloud-sync/state/stopped_apps.txt"
LOG_FILE="/mnt/pool/Apps/cron-jobs/cloud-sync/logs/cloud-sync.log"

# Ensure log directory exists
mkdir -p "$(dirname "$LOG_FILE")"
touch "$LOG_FILE"

echo "[$(date)] Starting post-script..." >> "$LOG_FILE"

# Check if state file exists
if [[ ! -f "$STATE_FILE" ]]; then
    echo "[$(date)] No state file found. Nothing to restart." >> "$LOG_FILE"
    exit 0
fi

# Restart each app listed in the state file
while IFS= read -r APP; do
    echo "[$(date)] Starting app: $APP" >> "$LOG_FILE"

    # Start the app
    midclt call app.start "$APP"

done < "$STATE_FILE"

echo "[$(date)] Post-script completed. Apps restarted." >> "$LOG_FILE"
1 Like