#!/usr/bin/env bash set -euo pipefail HOST="169.254.100.2" PORT="48888" PASSWORD="Dt123$" INTERFACE="" IP="" PREFIX="" GATEWAY="" DNS_CSV="" DO_APPLY=0 usage() { cat <<'EOF' Usage: linux-smoke-test.sh [options] Default behavior: Runs read-only smoke checks against the agent: - GET /api/health - GET /api/device/info - GET /api/network/interfaces - GET /api/network/config?interface=... Validate only: linux-smoke-test.sh --interface ens33 --ip 192.168.10.20 --prefix 24 --gateway 192.168.10.1 --dns 8.8.8.8,1.1.1.1 Apply and poll task: linux-smoke-test.sh --interface ens33 --ip 192.168.10.20 --prefix 24 --gateway 192.168.10.1 --dns 8.8.8.8,1.1.1.1 --apply Options: --host Agent host, default: 169.254.100.2 --port Agent port, default: 48888 --password Admin password, default: Dt123$ --interface Target Linux interface, e.g. ens33 --ip IPv4 address for validate/apply --prefix Prefix length for validate/apply --gateway Optional gateway --dns Optional DNS CSV, e.g. 8.8.8.8,1.1.1.1 --apply Actually call /api/network/apply after validate --help Show this help Notes: - Without --apply, the script never changes netplan. - With --apply, the agent will rewrite netplan YAML and run netplan apply. EOF } while [[ $# -gt 0 ]]; do case "$1" in --host) HOST="$2" shift 2 ;; --port) PORT="$2" shift 2 ;; --password) PASSWORD="$2" shift 2 ;; --interface) INTERFACE="$2" shift 2 ;; --ip) IP="$2" shift 2 ;; --prefix) PREFIX="$2" shift 2 ;; --gateway) GATEWAY="$2" shift 2 ;; --dns) DNS_CSV="$2" shift 2 ;; --apply) DO_APPLY=1 shift ;; --help|-h) usage exit 0 ;; *) echo "Unknown argument: $1" >&2 usage >&2 exit 2 ;; esac done for cmd in curl python3; do if ! command -v "$cmd" >/dev/null 2>&1; then echo "Missing required command: $cmd" >&2 exit 2 fi done BASE_URL="http://${HOST}:${PORT}" TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT log() { printf '[quickip-smoke] %s\n' "$*" } request() { local method="$1" local path="$2" local payload="${3:-}" local body_file="$TMP_DIR/body.json" local http_code : > "$body_file" if [[ -n "$payload" ]]; then http_code="$(curl -sS -o "$body_file" -w "%{http_code}" -X "$method" \ -H "X-Admin-Password: ${PASSWORD}" \ -H "Content-Type: application/json" \ --data "$payload" \ --connect-timeout 3 \ --max-time 15 \ "${BASE_URL}${path}" || true)" else http_code="$(curl -sS -o "$body_file" -w "%{http_code}" -X "$method" \ -H "X-Admin-Password: ${PASSWORD}" \ --connect-timeout 3 \ --max-time 15 \ "${BASE_URL}${path}" || true)" fi printf '%s\n' "$http_code" cat "$body_file" } read_http_code() { sed -n '1p' } read_body() { sed '1d' } assert_api_ok() { local op="$1" local http_code="$2" local body="$3" BODY_JSON="$body" python3 - "$op" "$http_code" <<'PY' import json import os import sys op = sys.argv[1] http_code = int(sys.argv[2]) body = os.environ["BODY_JSON"] if not (200 <= http_code < 300): print(f"{op} failed: http={http_code} body={body}", file=sys.stderr) sys.exit(1) try: data = json.loads(body) except json.JSONDecodeError as exc: print(f"{op} failed: invalid json: {exc}: {body}", file=sys.stderr) sys.exit(1) if data.get("code") != 0: print(f"{op} failed: code={data.get('code')} message={data.get('message')} body={body}", file=sys.stderr) sys.exit(1) PY } json_get() { local body="$1" local expr="$2" BODY_JSON="$body" python3 - "$expr" <<'PY' import json import os import sys expr = sys.argv[1] data = json.loads(os.environ["BODY_JSON"]) value = data for part in expr.split('.'): if part: value = value[part] if isinstance(value, list): import json as _json print(_json.dumps(value, ensure_ascii=False)) else: print(value) PY } build_payload() { python3 - "$INTERFACE" "$IP" "$PREFIX" "$GATEWAY" "$DNS_CSV" <<'PY' import json import sys interface, ip, prefix, gateway, dns_csv = sys.argv[1:6] dns = [item.strip() for item in dns_csv.split(',') if item.strip()] payload = { "interface": interface, "ip": ip, "prefix": int(prefix), "gateway": gateway, "dns": dns, } print(json.dumps(payload, ensure_ascii=False)) PY } poll_task() { local task_id="$1" local max_attempts=20 local attempt=1 local connection_failures=0 while (( attempt <= max_attempts )); do local result http_code body status step detail rollback result="$(request GET "/api/tasks/${task_id}")" http_code="$(printf '%s\n' "$result" | read_http_code)" body="$(printf '%s\n' "$result" | read_body)" if [[ "$http_code" == "000" || "$http_code" == "0" || -z "$http_code" ]]; then connection_failures=$((connection_failures + 1)) log "task=${task_id} poll connection failed (${connection_failures}), retrying" sleep 1 attempt=$((attempt + 1)) continue fi connection_failures=0 assert_api_ok "get task ${task_id}" "$http_code" "$body" status="$(json_get "$body" "data.status")" step="$(json_get "$body" "data.step")" detail="$(json_get "$body" "data.detail")" rollback="$(json_get "$body" "data.rollback")" log "task=${task_id} status=${status} step=${step} rollback=${rollback} detail=${detail}" case "$status" in success) return 0 ;; failed|rolled_back) return 1 ;; esac sleep 1 attempt=$((attempt + 1)) done echo "Timed out waiting for task: ${task_id}" >&2 return 1 } log "health" result="$(request GET "/api/health")" http_code="$(printf '%s\n' "$result" | read_http_code)" body="$(printf '%s\n' "$result" | read_body)" assert_api_ok "health" "$http_code" "$body" log "agent_version=$(json_get "$body" "data.agent_version")" log "device info" result="$(request GET "/api/device/info")" http_code="$(printf '%s\n' "$result" | read_http_code)" body="$(printf '%s\n' "$result" | read_body)" assert_api_ok "device info" "$http_code" "$body" log "device_id=$(json_get "$body" "data.device_id") hostname=$(json_get "$body" "data.hostname")" log "interfaces" result="$(request GET "/api/network/interfaces")" http_code="$(printf '%s\n' "$result" | read_http_code)" body="$(printf '%s\n' "$result" | read_body)" assert_api_ok "interfaces" "$http_code" "$body" if [[ -z "$INTERFACE" ]]; then INTERFACE="$(json_get "$body" "data.suggested_target_interface")" fi if [[ -z "$INTERFACE" || "$INTERFACE" == "None" ]]; then echo "Could not determine target interface. Pass --interface explicitly." >&2 exit 1 fi log "target interface=${INTERFACE} management=$(json_get "$body" "data.management_interface")" log "read current config" result="$(request GET "/api/network/config?interface=${INTERFACE}")" http_code="$(printf '%s\n' "$result" | read_http_code)" body="$(printf '%s\n' "$result" | read_body)" assert_api_ok "config ${INTERFACE}" "$http_code" "$body" log "current ip=$(json_get "$body" "data.ip") prefix=$(json_get "$body" "data.prefix") gateway=$(json_get "$body" "data.gateway")" if [[ -n "$IP" || -n "$PREFIX" || -n "$GATEWAY" || -n "$DNS_CSV" || "$DO_APPLY" -eq 1 ]]; then if [[ -z "$IP" || -z "$PREFIX" ]]; then echo "--ip and --prefix are required for validate/apply" >&2 exit 2 fi payload="$(build_payload)" log "validate payload=${payload}" result="$(request POST "/api/network/validate" "$payload")" http_code="$(printf '%s\n' "$result" | read_http_code)" body="$(printf '%s\n' "$result" | read_body)" assert_api_ok "validate" "$http_code" "$body" log "validate passed" if (( DO_APPLY == 1 )); then log "apply" result="$(request POST "/api/network/apply" "$payload")" http_code="$(printf '%s\n' "$result" | read_http_code)" body="$(printf '%s\n' "$result" | read_body)" assert_api_ok "apply" "$http_code" "$body" task_id="$(json_get "$body" "data.task_id")" log "apply submitted task_id=${task_id}" poll_task "$task_id" log "apply task completed successfully" fi else log "read-only smoke checks completed" fi