| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- #!/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 server:
- - 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 <ip> Server host, default: 169.254.100.2
- --port <port> Server port, default: 48888
- --password <value> Admin password, default: Dt123$
- --interface <name> Target Linux interface, e.g. ens33
- --ip <ipv4> IPv4 address for validate/apply
- --prefix <cidr> Prefix length for validate/apply
- --gateway <ipv4> Optional gateway
- --dns <csv> 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 server 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 '[networktool-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 "server_version=$(json_get "$body" "data.server_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
|