linux-smoke-test.sh 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. HOST="169.254.100.2"
  4. PORT="48888"
  5. PASSWORD="Dt123$"
  6. INTERFACE=""
  7. IP=""
  8. PREFIX=""
  9. GATEWAY=""
  10. DNS_CSV=""
  11. DO_APPLY=0
  12. usage() {
  13. cat <<'EOF'
  14. Usage:
  15. linux-smoke-test.sh [options]
  16. Default behavior:
  17. Runs read-only smoke checks against the server:
  18. - GET /api/health
  19. - GET /api/device/info
  20. - GET /api/network/interfaces
  21. - GET /api/network/config?interface=...
  22. Validate only:
  23. 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
  24. Apply and poll task:
  25. 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
  26. Options:
  27. --host <ip> Server host, default: 169.254.100.2
  28. --port <port> Server port, default: 48888
  29. --password <value> Admin password, default: Dt123$
  30. --interface <name> Target Linux interface, e.g. ens33
  31. --ip <ipv4> IPv4 address for validate/apply
  32. --prefix <cidr> Prefix length for validate/apply
  33. --gateway <ipv4> Optional gateway
  34. --dns <csv> Optional DNS CSV, e.g. 8.8.8.8,1.1.1.1
  35. --apply Actually call /api/network/apply after validate
  36. --help Show this help
  37. Notes:
  38. - Without --apply, the script never changes netplan.
  39. - With --apply, the server will rewrite netplan YAML and run netplan apply.
  40. EOF
  41. }
  42. while [[ $# -gt 0 ]]; do
  43. case "$1" in
  44. --host)
  45. HOST="$2"
  46. shift 2
  47. ;;
  48. --port)
  49. PORT="$2"
  50. shift 2
  51. ;;
  52. --password)
  53. PASSWORD="$2"
  54. shift 2
  55. ;;
  56. --interface)
  57. INTERFACE="$2"
  58. shift 2
  59. ;;
  60. --ip)
  61. IP="$2"
  62. shift 2
  63. ;;
  64. --prefix)
  65. PREFIX="$2"
  66. shift 2
  67. ;;
  68. --gateway)
  69. GATEWAY="$2"
  70. shift 2
  71. ;;
  72. --dns)
  73. DNS_CSV="$2"
  74. shift 2
  75. ;;
  76. --apply)
  77. DO_APPLY=1
  78. shift
  79. ;;
  80. --help|-h)
  81. usage
  82. exit 0
  83. ;;
  84. *)
  85. echo "Unknown argument: $1" >&2
  86. usage >&2
  87. exit 2
  88. ;;
  89. esac
  90. done
  91. for cmd in curl python3; do
  92. if ! command -v "$cmd" >/dev/null 2>&1; then
  93. echo "Missing required command: $cmd" >&2
  94. exit 2
  95. fi
  96. done
  97. BASE_URL="http://${HOST}:${PORT}"
  98. TMP_DIR="$(mktemp -d)"
  99. trap 'rm -rf "$TMP_DIR"' EXIT
  100. log() {
  101. printf '[networktool-smoke] %s\n' "$*"
  102. }
  103. request() {
  104. local method="$1"
  105. local path="$2"
  106. local payload="${3:-}"
  107. local body_file="$TMP_DIR/body.json"
  108. local http_code
  109. : > "$body_file"
  110. if [[ -n "$payload" ]]; then
  111. http_code="$(curl -sS -o "$body_file" -w "%{http_code}" -X "$method" \
  112. -H "X-Admin-Password: ${PASSWORD}" \
  113. -H "Content-Type: application/json" \
  114. --data "$payload" \
  115. --connect-timeout 3 \
  116. --max-time 15 \
  117. "${BASE_URL}${path}" || true)"
  118. else
  119. http_code="$(curl -sS -o "$body_file" -w "%{http_code}" -X "$method" \
  120. -H "X-Admin-Password: ${PASSWORD}" \
  121. --connect-timeout 3 \
  122. --max-time 15 \
  123. "${BASE_URL}${path}" || true)"
  124. fi
  125. printf '%s\n' "$http_code"
  126. cat "$body_file"
  127. }
  128. read_http_code() {
  129. sed -n '1p'
  130. }
  131. read_body() {
  132. sed '1d'
  133. }
  134. assert_api_ok() {
  135. local op="$1"
  136. local http_code="$2"
  137. local body="$3"
  138. BODY_JSON="$body" python3 - "$op" "$http_code" <<'PY'
  139. import json
  140. import os
  141. import sys
  142. op = sys.argv[1]
  143. http_code = int(sys.argv[2])
  144. body = os.environ["BODY_JSON"]
  145. if not (200 <= http_code < 300):
  146. print(f"{op} failed: http={http_code} body={body}", file=sys.stderr)
  147. sys.exit(1)
  148. try:
  149. data = json.loads(body)
  150. except json.JSONDecodeError as exc:
  151. print(f"{op} failed: invalid json: {exc}: {body}", file=sys.stderr)
  152. sys.exit(1)
  153. if data.get("code") != 0:
  154. print(f"{op} failed: code={data.get('code')} message={data.get('message')} body={body}", file=sys.stderr)
  155. sys.exit(1)
  156. PY
  157. }
  158. json_get() {
  159. local body="$1"
  160. local expr="$2"
  161. BODY_JSON="$body" python3 - "$expr" <<'PY'
  162. import json
  163. import os
  164. import sys
  165. expr = sys.argv[1]
  166. data = json.loads(os.environ["BODY_JSON"])
  167. value = data
  168. for part in expr.split('.'):
  169. if part:
  170. value = value[part]
  171. if isinstance(value, list):
  172. import json as _json
  173. print(_json.dumps(value, ensure_ascii=False))
  174. else:
  175. print(value)
  176. PY
  177. }
  178. build_payload() {
  179. python3 - "$INTERFACE" "$IP" "$PREFIX" "$GATEWAY" "$DNS_CSV" <<'PY'
  180. import json
  181. import sys
  182. interface, ip, prefix, gateway, dns_csv = sys.argv[1:6]
  183. dns = [item.strip() for item in dns_csv.split(',') if item.strip()]
  184. payload = {
  185. "interface": interface,
  186. "ip": ip,
  187. "prefix": int(prefix),
  188. "gateway": gateway,
  189. "dns": dns,
  190. }
  191. print(json.dumps(payload, ensure_ascii=False))
  192. PY
  193. }
  194. poll_task() {
  195. local task_id="$1"
  196. local max_attempts=20
  197. local attempt=1
  198. local connection_failures=0
  199. while (( attempt <= max_attempts )); do
  200. local result http_code body status step detail rollback
  201. result="$(request GET "/api/tasks/${task_id}")"
  202. http_code="$(printf '%s\n' "$result" | read_http_code)"
  203. body="$(printf '%s\n' "$result" | read_body)"
  204. if [[ "$http_code" == "000" || "$http_code" == "0" || -z "$http_code" ]]; then
  205. connection_failures=$((connection_failures + 1))
  206. log "task=${task_id} poll connection failed (${connection_failures}), retrying"
  207. sleep 1
  208. attempt=$((attempt + 1))
  209. continue
  210. fi
  211. connection_failures=0
  212. assert_api_ok "get task ${task_id}" "$http_code" "$body"
  213. status="$(json_get "$body" "data.status")"
  214. step="$(json_get "$body" "data.step")"
  215. detail="$(json_get "$body" "data.detail")"
  216. rollback="$(json_get "$body" "data.rollback")"
  217. log "task=${task_id} status=${status} step=${step} rollback=${rollback} detail=${detail}"
  218. case "$status" in
  219. success)
  220. return 0
  221. ;;
  222. failed|rolled_back)
  223. return 1
  224. ;;
  225. esac
  226. sleep 1
  227. attempt=$((attempt + 1))
  228. done
  229. echo "Timed out waiting for task: ${task_id}" >&2
  230. return 1
  231. }
  232. log "health"
  233. result="$(request GET "/api/health")"
  234. http_code="$(printf '%s\n' "$result" | read_http_code)"
  235. body="$(printf '%s\n' "$result" | read_body)"
  236. assert_api_ok "health" "$http_code" "$body"
  237. log "server_version=$(json_get "$body" "data.server_version")"
  238. log "device info"
  239. result="$(request GET "/api/device/info")"
  240. http_code="$(printf '%s\n' "$result" | read_http_code)"
  241. body="$(printf '%s\n' "$result" | read_body)"
  242. assert_api_ok "device info" "$http_code" "$body"
  243. log "device_id=$(json_get "$body" "data.device_id") hostname=$(json_get "$body" "data.hostname")"
  244. log "interfaces"
  245. result="$(request GET "/api/network/interfaces")"
  246. http_code="$(printf '%s\n' "$result" | read_http_code)"
  247. body="$(printf '%s\n' "$result" | read_body)"
  248. assert_api_ok "interfaces" "$http_code" "$body"
  249. if [[ -z "$INTERFACE" ]]; then
  250. INTERFACE="$(json_get "$body" "data.suggested_target_interface")"
  251. fi
  252. if [[ -z "$INTERFACE" || "$INTERFACE" == "None" ]]; then
  253. echo "Could not determine target interface. Pass --interface explicitly." >&2
  254. exit 1
  255. fi
  256. log "target interface=${INTERFACE} management=$(json_get "$body" "data.management_interface")"
  257. log "read current config"
  258. result="$(request GET "/api/network/config?interface=${INTERFACE}")"
  259. http_code="$(printf '%s\n' "$result" | read_http_code)"
  260. body="$(printf '%s\n' "$result" | read_body)"
  261. assert_api_ok "config ${INTERFACE}" "$http_code" "$body"
  262. log "current ip=$(json_get "$body" "data.ip") prefix=$(json_get "$body" "data.prefix") gateway=$(json_get "$body" "data.gateway")"
  263. if [[ -n "$IP" || -n "$PREFIX" || -n "$GATEWAY" || -n "$DNS_CSV" || "$DO_APPLY" -eq 1 ]]; then
  264. if [[ -z "$IP" || -z "$PREFIX" ]]; then
  265. echo "--ip and --prefix are required for validate/apply" >&2
  266. exit 2
  267. fi
  268. payload="$(build_payload)"
  269. log "validate payload=${payload}"
  270. result="$(request POST "/api/network/validate" "$payload")"
  271. http_code="$(printf '%s\n' "$result" | read_http_code)"
  272. body="$(printf '%s\n' "$result" | read_body)"
  273. assert_api_ok "validate" "$http_code" "$body"
  274. log "validate passed"
  275. if (( DO_APPLY == 1 )); then
  276. log "apply"
  277. result="$(request POST "/api/network/apply" "$payload")"
  278. http_code="$(printf '%s\n' "$result" | read_http_code)"
  279. body="$(printf '%s\n' "$result" | read_body)"
  280. assert_api_ok "apply" "$http_code" "$body"
  281. task_id="$(json_get "$body" "data.task_id")"
  282. log "apply submitted task_id=${task_id}"
  283. poll_task "$task_id"
  284. log "apply task completed successfully"
  285. fi
  286. else
  287. log "read-only smoke checks completed"
  288. fi