| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- #!/usr/bin/env bash
- set -euo pipefail
- HOST="169.254.100.2"
- PORT="48888"
- PASSWORD="Dieteng2026"
- INTERFACE=""
- IP=""
- PREFIX=""
- GATEWAY=""
- DNS_CSV=""
- DO_APPLY=0
- DHCP4=0
- APPLY_DECISION="confirm"
- 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
- linux-smoke-test.sh --interface ens33 --dhcp4
- 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
- linux-smoke-test.sh --interface ens33 --dhcp4 --apply
- Options:
- --host <ip> Server host, default: 169.254.100.2
- --port <port> Server port, default: 48888
- --password <value> Admin password, default: Dieteng2026
- --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
- --dhcp4 Validate/apply DHCP IPv4 mode instead of static IPv4
- --apply Actually call /api/network/apply after validate
- --cancel-apply With --apply, cancel at confirmation step and expect rollback
- --help Show this help
- Notes:
- - Without --apply, the script never changes netplan.
- - With --apply, the server rewrites netplan YAML, runs netplan apply, then waits 20 seconds for confirmation.
- - By default the script confirms apply tasks automatically when they enter confirming.
- 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
- ;;
- --dhcp4)
- DHCP4=1
- shift
- ;;
- --apply)
- DO_APPLY=1
- shift
- ;;
- --cancel-apply)
- APPLY_DECISION="cancel"
- 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" "$DHCP4" <<'PY'
- import json
- import sys
- interface, ip, prefix, gateway, dns_csv, dhcp4 = sys.argv[1:7]
- is_dhcp4 = dhcp4 == "1"
- dns = [item.strip() for item in dns_csv.split(',') if item.strip()]
- payload = {
- "interface": interface,
- "dhcp4": is_dhcp4,
- "ip": "" if is_dhcp4 else ip,
- "prefix": 0 if is_dhcp4 else int(prefix),
- "gateway": "" if is_dhcp4 else gateway,
- "dns": [] if is_dhcp4 else dns,
- }
- print(json.dumps(payload, ensure_ascii=False))
- PY
- }
- build_task_payload() {
- python3 - "$1" <<'PY'
- import json
- import sys
- print(json.dumps({"task_id": sys.argv[1]}, ensure_ascii=False))
- PY
- }
- poll_task() {
- local task_id="$1"
- local max_attempts=20
- local attempt=1
- local connection_failures=0
- local confirmation_sent=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}"
- if [[ "$status" == "running" && "$step" == "confirming" && "$confirmation_sent" -eq 0 ]]; then
- local decision_path decision_op decision_result decision_code decision_body
- if [[ "$APPLY_DECISION" == "cancel" ]]; then
- decision_path="/api/network/apply/cancel"
- decision_op="cancel apply ${task_id}"
- else
- decision_path="/api/network/apply/confirm"
- decision_op="confirm apply ${task_id}"
- fi
- log "task=${task_id} ${APPLY_DECISION} at confirmation step"
- decision_result="$(request POST "$decision_path" "$(build_task_payload "$task_id")")"
- decision_code="$(printf '%s\n' "$decision_result" | read_http_code)"
- decision_body="$(printf '%s\n' "$decision_result" | read_body)"
- assert_api_ok "$decision_op" "$decision_code" "$decision_body"
- confirmation_sent=1
- fi
- case "$status" in
- success)
- if [[ "$APPLY_DECISION" == "cancel" ]]; then
- echo "Expected rollback after cancel, but task succeeded: ${task_id}" >&2
- return 1
- fi
- return 0
- ;;
- rolled_back)
- if [[ "$APPLY_DECISION" == "cancel" ]]; then
- return 0
- fi
- return 1
- ;;
- failed)
- 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 dhcp4=$(json_get "$body" "data.dhcp4") 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 || "$DHCP4" -eq 1 ]]; then
- if [[ "$DHCP4" -eq 0 && ( -z "$IP" || -z "$PREFIX" ) ]]; then
- echo "--ip and --prefix are required for static validate/apply; use --dhcp4 for DHCP mode" >&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"
- if [[ "$APPLY_DECISION" == "cancel" ]]; then
- log "apply task rolled back after cancellation as expected"
- else
- log "apply task completed successfully"
- fi
- fi
- else
- log "read-only smoke checks completed"
- fi
|