- docs/MAINTAINABILITY.md: limits and architecture rules - .golangci.yml (pinned v2.14.0) + file-size check; make lint runs first in every gate - web/go.mod stops the Go tool from walking web/node_modules - layering: Target -> internal/snmptarget, TableKey -> profile.Metric.TableKey; poller no longer imports discovery Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
142 lines
5.7 KiB
Bash
Executable File
142 lines
5.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Gate M1: `nmon serve` polls the counters fixture and /api/v1/query returns
|
|
# rising ifHCInOctets. Procedure: docs/milestones/M1-skeleton-and-fixtures.md.
|
|
# Needs: go, make, curl, file, ss, pgrep, python3 (JSON checks).
|
|
set -uo pipefail
|
|
|
|
# Maintainability is a hard rule (docs/MAINTAINABILITY.md): no gate passes with lint findings.
|
|
( cd "$(git rev-parse --show-toplevel)" && make lint ) || { echo "FAIL: make lint"; exit 1; }
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
TMP="$(mktemp -d)"
|
|
FAILS=0
|
|
NMON_PID="" AGENT_PID=""
|
|
|
|
pass() { echo "PASS: $*"; }
|
|
fail() { echo "FAIL: $*"; FAILS=$((FAILS + 1)); }
|
|
die() { fail "$*"; exit 1; }
|
|
|
|
cleanup() {
|
|
local pid
|
|
for pid in $NMON_PID $AGENT_PID; do
|
|
kill "$pid" 2>/dev/null || true
|
|
done
|
|
for pid in $NMON_PID $AGENT_PID; do
|
|
for _ in $(seq 1 100); do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; done
|
|
kill -9 "$pid" 2>/dev/null || true
|
|
done
|
|
if [ "$FAILS" -ne 0 ] && [ -f "$TMP/nmon.log" ]; then
|
|
echo "--- tail of nmon.log"; tail -n 40 "$TMP/nmon.log"
|
|
fi
|
|
rm -rf "$TMP"
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 130' INT TERM
|
|
|
|
pick_port() {
|
|
local p
|
|
while :; do
|
|
p=$((20000 + RANDOM % 30000))
|
|
ss -H -lntu 2>/dev/null | awk '{print $5}' | grep -q "[:.]$p\$" || { echo "$p"; return; }
|
|
done
|
|
}
|
|
|
|
command -v python3 >/dev/null || die "python3 is required for the JSON checks"
|
|
|
|
# 1. Build with the embedded stores; the binary must be static.
|
|
echo "== build"
|
|
make build >"$TMP/build.log" 2>&1 || { tail -n 20 "$TMP/build.log"; die "make build"; }
|
|
if file nmon | grep -q 'statically linked'; then pass "nmon is statically linked"; else die "nmon is not statically linked: $(file nmon)"; fi
|
|
|
|
# 2. Fixture agent on [::1]:<port>.
|
|
echo "== simagent"
|
|
SNMP_PORT="$(pick_port)"
|
|
go build -o "$TMP/simagent" ./internal/simagent/cmd/simagent || die "build simagent"
|
|
"$TMP/simagent" -addr "[::1]:$SNMP_PORT" -rec testdata/snmprec/counters.snmprec >"$TMP/simagent.log" 2>&1 &
|
|
AGENT_PID=$!
|
|
for _ in $(seq 1 50); do grep -q listening "$TMP/simagent.log" 2>/dev/null && break; sleep 0.1; done
|
|
grep -q listening "$TMP/simagent.log" || { cat "$TMP/simagent.log"; die "simagent did not start"; }
|
|
pass "simagent serving counters.snmprec on [::1]:$SNMP_PORT"
|
|
|
|
# 3. nmon serve, data dir and key file in separate places.
|
|
echo "== serve"
|
|
HTTP_PORT="$(pick_port)"
|
|
BASE="http://[::1]:$HTTP_PORT"
|
|
./nmon serve --data-dir "$TMP/data" --secret-key-file "$TMP/keys/secret.key" --insecure-http \
|
|
--listen "[::1]:$HTTP_PORT" --poll-interval 10s --snmp-port "$SNMP_PORT" >"$TMP/nmon.log" 2>&1 &
|
|
NMON_PID=$!
|
|
for _ in $(seq 1 300); do
|
|
curl -fsS "$BASE/openapi.json" >/dev/null 2>&1 && break
|
|
kill -0 "$NMON_PID" 2>/dev/null || break
|
|
sleep 0.2
|
|
done
|
|
curl -fsS "$BASE/openapi.json" >/dev/null 2>&1 || die "nmon serve did not come up"
|
|
pass "nmon serve is up on $BASE"
|
|
|
|
# 4. Add the fixture device.
|
|
echo "== add device"
|
|
CODE="$(curl -sS -o "$TMP/device.json" -w '%{http_code}' -X POST "$BASE/api/v1/devices" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"name":"counters","addresses":["::1"],"snmp":{"version":"v2c","community":"public"}}')"
|
|
[ "$CODE" = 201 ] || { cat "$TMP/device.json"; die "POST /api/v1/devices returned $CODE"; }
|
|
SLUG="$(python3 -I -c 'import json,sys; print(json.load(open(sys.argv[1]))["slug"])' "$TMP/device.json")"
|
|
NIF="$(python3 -I -c 'import json,sys; print(len(json.load(open(sys.argv[1]))["interfaces"]))' "$TMP/device.json")"
|
|
[ "$NIF" -ge 1 ] || die "device $SLUG has no interfaces"
|
|
pass "device $SLUG added with $NIF interfaces"
|
|
|
|
# 5. Three cycles, then query twice one cycle apart.
|
|
echo "== query (waiting for 3 poll cycles)"
|
|
sleep 30
|
|
query() { curl -fsS --get "$BASE/api/v1/query" --data-urlencode 'q=ifHCInOctets' -o "$1"; }
|
|
for _ in $(seq 1 15); do
|
|
query "$TMP/q1.json" && python3 -I -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))["data"]["result"] else 1)' "$TMP/q1.json" && break
|
|
sleep 2
|
|
done
|
|
sleep 10
|
|
|
|
CHECK='
|
|
import json, sys
|
|
a = json.load(open(sys.argv[1]))["data"]["result"]
|
|
b = {s["metric"].get("iface"): float(s["value"][1]) for s in json.load(open(sys.argv[2]))["data"]["result"]}
|
|
if not a:
|
|
print("no ifHCInOctets series"); sys.exit(1)
|
|
for s in a:
|
|
keys = sorted(s["metric"])
|
|
if keys != ["__name__", "device", "iface"]:
|
|
print("labels are", keys); sys.exit(1)
|
|
i = s["metric"]["iface"]
|
|
if i not in b or not b[i] > float(s["value"][1]):
|
|
print("iface", i, "did not rise:", s["value"][1], "->", b.get(i)); sys.exit(1)
|
|
print(len(a), "series, labels exactly {__name__, device, iface}, all values strictly rising")
|
|
'
|
|
OK=0
|
|
for _ in $(seq 1 6); do
|
|
query "$TMP/q2.json" && OUT="$(python3 -I -c "$CHECK" "$TMP/q1.json" "$TMP/q2.json" 2>&1)" && { OK=1; break; }
|
|
sleep 2
|
|
done
|
|
if [ "$OK" = 1 ]; then pass "ifHCInOctets: $OUT"; else fail "ifHCInOctets check: ${OUT:-query failed}"; fi
|
|
|
|
# 6. The metric stores listen on loopback only.
|
|
echo "== listeners"
|
|
PIDS="$(pgrep -P "$NMON_PID" | tr '\n' ' ')"
|
|
N=0 BAD=0
|
|
while read -r addr pid; do
|
|
N=$((N + 1))
|
|
case "$addr" in 127.0.0.1:*) ;; *) BAD=$((BAD + 1)); echo " store pid $pid listens on $addr" ;; esac
|
|
done < <(ss -H -ltnp | awk '{
|
|
n = split($0, parts, "pid="); for (i = 2; i <= n; i++) { split(parts[i], p, ","); print $4, p[1] } }' |
|
|
while read -r addr pid; do for c in $PIDS; do [ "$c" = "$pid" ] && echo "$addr $pid"; done; done)
|
|
if [ "$N" -ge 2 ] && [ "$BAD" -eq 0 ]; then pass "$N store listeners, all on 127.0.0.1"
|
|
elif [ "$N" -lt 2 ]; then fail "found $N store listeners (children: ${PIDS:-none}), expected hot and long"
|
|
else fail "$BAD store listeners are not on 127.0.0.1"; fi
|
|
|
|
# 7. Graceful shutdown.
|
|
echo "== shutdown"
|
|
kill -TERM "$NMON_PID"
|
|
if wait "$NMON_PID"; then pass "nmon exited cleanly on SIGTERM"; else fail "nmon exited with status $?"; fi
|
|
NMON_PID=""
|
|
|
|
if [ "$FAILS" -eq 0 ]; then echo "GATE M1: PASS"; else echo "GATE M1: FAIL ($FAILS)"; exit 1; fi
|