Skip to content

jonth93/acsmon-bash

Repository files navigation

ACS Monitor — bash + curl Examples

Open-source shell scripts and copy-pasteable curl recipes for the ACS Monitor REST API — the network and infrastructure monitoring platform from Anglia Computer Solutions.

Pure shell. Useful for one-shot automation, cron jobs, ad-hoc poking around with curl, runbook snippets, ChatOps, and copy-pasting into Slack threads.

Licensing: these shell examples are open source under the MIT licence. The ACS Monitor application they talk to is licensed commercial software with a free 100-device tier — see acsmon.com for tiers and pricing.

Contents

Requirements

Requirement Notes
bash Any modern Linux/macOS — the scripts use set -euo pipefail
curl 7.68+ recommended (for --fail-with-body)
jq 1.6+ — required for parsing JSON responses
Network Outbound to ACSMON_BASE_URL

Windows users: run under WSL or Git Bash. Native cmd/PowerShell isn't covered here — use the Node.js or Python clients instead.

Quick start

cd api-clients/bash-curl
chmod +x *.sh

export ACSMON_BASE_URL=https://monitoring.example.com
export [email protected]
export ACSMON_PASSWORD='your-strong-password'

# Get a token and capture it for the rest of the session
export ACSMON_TOKEN="$(./login.sh)"

./list-devices.sh
./poll-device.sh 42
./acknowledge-alert.sh 1234 "Investigating in INC-9421"

Environment variables

Every script reads its config from these env vars:

Variable Required for Purpose
ACSMON_BASE_URL all Root URL of your ACS Monitor install (no /api/v1 suffix)
ACSMON_EMAIL login.sh only Email of an API-capable user
ACSMON_PASSWORD login.sh only Password of that user
ACSMON_TOKEN every other script Bearer token (output of login.sh)

Put these in your shell profile, in direnv's .envrc, or in a private ~/.config/acsmon/env you source before invoking the scripts. Never commit them.

Provided scripts

Script What it does
login.sh Exchange ACSMON_EMAIL + ACSMON_PASSWORD for a token (echoed to stdout).
list-devices.sh Print every device in a tidy table, walking pagination automatically.
poll-device.sh Trigger a one-off SNMP poll for a device by ID.
acknowledge-alert.sh Acknowledge an open alert event with an optional note.

Every script:

  • Uses set -euo pipefail so a failed curl aborts the script.
  • Validates required env vars with : "${VAR:?message}".
  • Calls curl -fsS so HTTP 4xx/5xx errors propagate out.
  • Pipes JSON through jq for human-readable output.

Authentication recipes

Trade email + password for a token

TOKEN=$(curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/auth/login" \
    -H 'Content-Type: application/json' \
    -d "$(jq -n --arg e "$ACSMON_EMAIL" --arg p "$ACSMON_PASSWORD" \
              '{email:$e, password:$p}')" \
    | jq -er .token)

export ACSMON_TOKEN="$TOKEN"

jq -er exits non-zero if .token is missing, so a bad-credentials response will abort the script.

Use an existing long-lived token

For cron jobs and CI, prefer a long-lived token issued to a dedicated API user (Settings → Users in the UI). Just export ACSMON_TOKEN=... and skip login.sh entirely.

Revoke a token

curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/auth/logout" \
    -H "Authorization: Bearer $ACSMON_TOKEN"
unset ACSMON_TOKEN

Devices (SNMP) recipes

List the first 10 devices

curl -fsS "$ACSMON_BASE_URL/api/v1/devices?per_page=10" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    -H 'Accept: application/json' | jq .

Find every DOWN device

curl -fsS "$ACSMON_BASE_URL/api/v1/devices?filter[status]=down&per_page=100" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    -H 'Accept: application/json' \
    | jq -r '.data[] | "\(.id)\t\(.name)\t\(.ip_address)"'

Get full details for one device

DEVICE_ID=42
curl -fsS "$ACSMON_BASE_URL/api/v1/devices/$DEVICE_ID" \
    -H "Authorization: Bearer $ACSMON_TOKEN" | jq .

Trigger a one-off SNMP poll

DEVICE_ID=42
curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/devices/$DEVICE_ID/poll" \
    -H "Authorization: Bearer $ACSMON_TOKEN" | jq .

Get the latest metrics for a device

DEVICE_ID=42
curl -fsS "$ACSMON_BASE_URL/api/v1/devices/$DEVICE_ID/metrics" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    | jq '.data[] | {oid: .oid_name, value, unit, polled_at}'

Service-monitor recipes

List every monitor and its current status

curl -fsS "$ACSMON_BASE_URL/api/v1/monitors?per_page=100" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    | jq -r '.data[] | "\(.status)\t\(.type)\t\(.name)\t\(.host):\(.port // "-")"'

Create a new HTTPS monitor

curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/monitors" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    -H 'Content-Type: application/json' \
    -d '{
        "name": "Public website",
        "type": "https",
        "host": "example.com",
        "port": 443,
        "check_interval_seconds": 60,
        "timeout_ms": 5000,
        "config": {
            "method": "GET",
            "path": "/",
            "expected_status": 200,
            "ssl_verify": true,
            "alert_ssl_expiry_days": 14
        }
    }' | jq .

Force an immediate re-check

MONITOR_ID=117
curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/monitors/$MONITOR_ID/check" \
    -H "Authorization: Bearer $ACSMON_TOKEN" | jq .

Last 20 results for a monitor

MONITOR_ID=117
curl -fsS "$ACSMON_BASE_URL/api/v1/monitors/$MONITOR_ID/results?per_page=20" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    | jq -r '.data[] | "\(.checked_at)\t\(.status)\t\(.response_time_ms)ms"'

30-day uptime for SLA reporting

MONITOR_ID=117
curl -fsS "$ACSMON_BASE_URL/api/v1/monitors/$MONITOR_ID/uptime?window=30d" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    | jq '{uptime_percent, total_checks, failed_checks}'

List HTTPS monitors with certs expiring within 30 days

curl -fsS "$ACSMON_BASE_URL/api/v1/monitors?filter[type]=https&per_page=100" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    | jq -r '.data[]
        | select(.ssl_expiry_at != null)
        | select(((.ssl_expiry_at|fromdateiso8601) - now) < 30*86400)
        | "\(.name)\t\(.host)\t\(.ssl_expiry_at)"'

Alert-event recipes

Open critical alerts only

curl -fsS "$ACSMON_BASE_URL/api/v1/alert-events?filter[status]=open&filter[severity]=critical&sort=-triggered_at&per_page=50" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    | jq '.data[] | {id, severity, triggered_at, target: (.monitor.name // .device.name)}'

Acknowledge an event

EVENT_ID=9821
NOTE='Investigating in INC-9421'
curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/alert-events/$EVENT_ID/acknowledge" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    -H 'Content-Type: application/json' \
    -d "$(jq -n --arg n "$NOTE" '{note: $n}')" | jq .

Resolve an event

EVENT_ID=9821
curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/alert-events/$EVENT_ID/resolve" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    -H 'Content-Type: application/json' \
    -d '{"note":"Closed by automation"}' | jq .

Bulk-ack every open warning older than 24 hours

cutoff=$(date -u -d '24 hours ago' +%s)

curl -fsS "$ACSMON_BASE_URL/api/v1/alert-events?filter[status]=open&filter[severity]=warning&per_page=100" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    | jq -r --argjson cutoff "$cutoff" \
        '.data[] | select((.triggered_at|fromdateiso8601) < $cutoff) | .id' \
    | while read -r id; do
        curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/alert-events/$id/acknowledge" \
            -H "Authorization: Bearer $ACSMON_TOKEN" \
            -H 'Content-Type: application/json' \
            -d '{"note":"Auto-ack >24h old"}' >/dev/null
        echo "Acked $id"
    done

System health recipes

Liveness check (use this for an external uptime probe of the app itself)

curl -fsS "$ACSMON_BASE_URL/api/v1/system/health" \
    -H "Authorization: Bearer $ACSMON_TOKEN" | jq .

Exit non-zero if any subsystem is unhealthy

status=$(curl -fsS "$ACSMON_BASE_URL/api/v1/system/health" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    | jq -r '.status')

[ "$status" = "ok" ] || { echo "ACS Monitor unhealthy: $status" >&2; exit 1; }

Pagination patterns

Every list endpoint returns:

{
  "data": [ /* rows */ ],
  "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
  "meta":  { "current_page": 1, "from": 1, "to": 50, "per_page": 50, "total": 593 },
  "next_page_url": "https://.../api/v1/devices?page=2"
}

The simplest "walk every page" loop:

page=1
while :; do
    body=$(curl -fsS "$ACSMON_BASE_URL/api/v1/devices?per_page=100&page=$page" \
        -H "Authorization: Bearer $ACSMON_TOKEN")
    echo "$body" | jq -r '.data[] | "\(.id)\t\(.name)"'
    next=$(echo "$body" | jq -r '.next_page_url // empty')
    [ -n "$next" ] || break
    page=$((page + 1))
done

For a quick total without iterating, use meta.total:

curl -fsS "$ACSMON_BASE_URL/api/v1/devices?per_page=1" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    | jq '.meta.total'

Filtering, sorting, searching

Query param Example Effect
search ?search=core Free-text search across name/host fields
filter[<field>] ?filter[status]=down Exact-match filter (chainable with &)
sort ?sort=-last_polled_at Sort column. Prefix - for descending.
per_page ?per_page=100 Page size (cap depends on endpoint)
page ?page=3 Start page

Combine freely (remember to URL-encode [ as %5B and ] as %5D if your shell mangles them; curl accepts them literally in most shells):

curl -fsS "$ACSMON_BASE_URL/api/v1/alert-events?filter[severity]=critical&filter[status]=open&sort=-triggered_at&per_page=50" \
    -H "Authorization: Bearer $ACSMON_TOKEN" | jq .

Error handling in shell

Use set -euo pipefail and curl -fsS (or --fail-with-body on curl 7.76+) so HTTP errors fail the script:

set -euo pipefail

# Fail loudly, show the error body
if ! body=$(curl --fail-with-body -sS "$ACSMON_BASE_URL/api/v1/devices/99999" \
    -H "Authorization: Bearer $ACSMON_TOKEN" 2>&1); then
    echo "Lookup failed: $body" >&2
    exit 1
fi
echo "$body" | jq .

To inspect status codes manually (e.g. to differentiate 404 from 500):

status=$(curl -sS -o /tmp/resp.json -w '%{http_code}' \
    "$ACSMON_BASE_URL/api/v1/devices/$DEVICE_ID" \
    -H "Authorization: Bearer $ACSMON_TOKEN")

case "$status" in
    200) jq . /tmp/resp.json ;;
    404) echo "Device $DEVICE_ID not found" >&2; exit 0 ;;
    401) echo "Token expired — re-run login.sh" >&2; exit 2 ;;
    *)   echo "HTTP $status:" >&2; cat /tmp/resp.json >&2; exit 1 ;;
esac
Status Meaning
401 Token missing/expired — re-run login.sh
403 Authenticated but lacks the permission slug for that endpoint
404 Resource doesn't exist
422 Validation error — body has { errors: { field: ["..."] } }
429 Rate limited — sleep before retrying (Retry-After header)
5xx Server bug or upstream outage — retry with backoff

Common jq filters

# Just the IDs
jq -r '.data[].id'

# CSV: name, status, last polled
jq -r '.data[] | [.name, .status, .last_polled_at] | @csv'

# Count by status
jq -r '.data | group_by(.status) | map({status: .[0].status, count: length})'

# Max response time across monitors
jq '[.data[].response_time_ms] | max'

# Pull a value out of nested config JSON
jq -r '.data[] | select(.type=="https") | .config.expected_status'

# Pretty-print just the first row
jq '.data[0]'

# Walk the next_page_url field if present
jq -r '.next_page_url // empty'

Cron / scheduled patterns

Hourly: alert-event count to a metrics endpoint

# /etc/cron.d/acsmon-alert-count
0 * * * * acsmon /usr/local/bin/acsmon-alert-count.sh >>/var/log/acsmon.log 2>&1
#!/usr/bin/env bash
# /usr/local/bin/acsmon-alert-count.sh
set -euo pipefail
source /etc/acsmon.env       # exports ACSMON_BASE_URL + ACSMON_TOKEN

count=$(curl -fsS "$ACSMON_BASE_URL/api/v1/alert-events?filter[status]=open&filter[severity]=critical&per_page=1" \
    -H "Authorization: Bearer $ACSMON_TOKEN" \
    | jq '.meta.total')

curl -fsS -X POST https://metrics.example.com/ingest \
    -H 'Content-Type: application/json' \
    -d "$(jq -n --arg c "$count" '{metric:"acsmon.alerts.open_critical", value:($c|tonumber)}')"

Nightly: refresh a long-running token (rotate weekly)

# pulls a fresh token on Sunday and writes it to /etc/acsmon.env
if [ "$(date +%u)" = "7" ]; then
    NEW=$(./login.sh)
    sed -i "s|^export ACSMON_TOKEN=.*|export ACSMON_TOKEN=$NEW|" /etc/acsmon.env
fi

On-demand: poll every device that hasn't been polled in 1 hour

cutoff=$(date -u -d '1 hour ago' +%s)

page=1
while :; do
    body=$(curl -fsS "$ACSMON_BASE_URL/api/v1/devices?per_page=100&page=$page" \
        -H "Authorization: Bearer $ACSMON_TOKEN")

    echo "$body" | jq -r --argjson cutoff "$cutoff" \
        '.data[] | select((.last_polled_at // "1970-01-01T00:00:00Z" | fromdateiso8601) < $cutoff) | .id' \
    | while read -r id; do
        curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/devices/$id/poll" \
            -H "Authorization: Bearer $ACSMON_TOKEN" >/dev/null
        echo "Re-polled $id"
    done

    next=$(echo "$body" | jq -r '.next_page_url // empty')
    [ -n "$next" ] || break
    page=$((page + 1))
done

Token storage best practice

  • Servers / cron: keep tokens in /etc/acsmon.env (mode 0600, owned by the service user) and source it from each script.
  • CI: inject via the platform's secret store (GitHub Actions secrets, GitLab CI variables, Drone, Jenkins credentials).
  • Personal shell: put export ACSMON_TOKEN=... in ~/.acsmonrc with chmod 600 and source ~/.acsmonrc from your .bashrc.
  • Never put tokens in repo files, terminal multiplexer scrollback shared with others, or ~/.bash_history (use HISTCONTROL=ignorespace and prefix sensitive commands with a space).

Troubleshooting

curl: (22) The requested URL returned error: 401 — Token missing/expired. Regenerate with ./login.sh or re-issue the long-lived token in the UI.

curl: (22) ... 403 — The user/role lacks the permission slug for that endpoint. Grant the role via Settings → Users.

curl: (60) SSL certificate problem — Self-signed or untrusted cert on the server. Either install a real cert, add the CA to the system trust store, or pass -k (lab only — never in production).

jq: error: Cannot iterate over null (null) — The response had no data field, often because of an HTTP error. Add -fsS (or --fail-with-body) to your curl to surface the failure.

jq -er .token exits non-zerologin.sh got a 200 but no token field. Almost always a wrong email/password or a deactivated user.

Output is gibberish JSON — You forgot to pipe through jq (or the response is genuinely binary, which the API never returns — re-check the URL).

Pagination loop never ends — You're following next_page_url without re-issuing it; copy the snippet under Pagination patterns verbatim.

Bracket characters in filter URLs are eaten — Some shells parse ?filter[status]=down. Quote the whole URL: curl "...?filter[status]=down".


© Anglia Computer Solutions Ltd. — ACS Monitor is a product of Anglia Computer Solutions. Visit acsmon.com for documentation, demos and licensing.

About

bash + curl snippets and worked examples for the ACS Monitor REST API.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages