Files
Vetting/agent/tests/network.go
T
josh e73e31af92
CI / Lint + build + test (push) Successful in 1m32s
Release / release (push) Successful in 6m28s
live-image: install stage tools and fail loudly if any are missing
The live image was still carrying the Phase 2 package list, so SMART,
CPUStress, and Network each hit a LookPath miss and returned
pass-with-skip. A run that skipped every real check still ended in
"completed" — nothing on the report said the image was broken.

Add smartmontools, stress-ng, fio, iperf3, lshw, lm-sensors,
e2fsprogs, and util-linux to mkosi.conf. Flip the three stages from
skip-pass to fail when their binary is missing so any future
packaging regression blocks the run instead of whispering past it.
Legitimate "no hardware" skips (no GPU, no hwmon, no disks,
non-destructive) are untouched.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 16:39:28 -04:00

147 lines
3.9 KiB
Go

package tests
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os/exec"
"strconv"
"strings"
"time"
)
// NetworkConfig is what the agent passes to Network: the orchestrator's
// iperf3 server address and port. We derive host from OrchestratorURL.
type NetworkConfig struct {
OrchestratorURL string
IperfPort int // 0 = 5201
Duration time.Duration
}
// Network runs iperf3 against the orchestrator's bundled server. Records
// bandwidth as a measurement; fails if iperf3 is missing, the server
// isn't reachable, or throughput is zero.
func Network(ctx context.Context, d Deps, cfg NetworkConfig) Outcome {
if _, err := exec.LookPath("iperf3"); err != nil {
// Live image ships iperf3; absence means packaging regression.
d.Error("Network: iperf3 not found — live image is missing required tool")
return Outcome{
Passed: false,
Message: "iperf3 binary missing from live image",
Summary: "failed (iperf3 missing)",
Extras: map[string]any{"reason": "iperf3_missing"},
}
}
host, err := deriveHost(cfg.OrchestratorURL)
if err != nil || host == "" {
d.Warn("Network: can't derive orchestrator host from URL — skipping stage")
return Outcome{
Passed: true,
Summary: "skipped (no orchestrator host)",
Extras: map[string]any{"skipped": true, "reason": "no_host"},
}
}
port := cfg.IperfPort
if port == 0 {
port = 5201
}
duration := cfg.Duration
if duration <= 0 {
duration = 10 * time.Second
}
args := []string{
"-c", host,
"-p", strconv.Itoa(port),
"-t", strconv.Itoa(int(duration.Seconds())),
"-J", // JSON output
}
d.Info(fmt.Sprintf("Network: iperf3 -c %s -p %d -t %s", host, port, duration))
runCtx, cancel := context.WithTimeout(ctx, duration+30*time.Second)
defer cancel()
cmd := exec.CommandContext(runCtx, "iperf3", args...)
out, err := cmd.Output()
if err != nil {
d.Error("Network: iperf3 client failed: " + err.Error())
return Outcome{
Passed: false,
Message: "iperf3 client error: " + err.Error(),
Summary: "iperf3 failed",
Extras: map[string]any{"stderr_tail": tailLines(string(out), 20)},
}
}
mbps, parsed, err := parseIperfJSON(out)
if err != nil {
d.Error("Network: parse iperf3 output: " + err.Error())
return Outcome{
Passed: false,
Message: "parse iperf3 json: " + err.Error(),
Summary: "parse error",
Extras: map[string]any{"raw": string(out)},
}
}
if d.Sensor != nil {
_ = d.Sensor(ctx, []Sample{{Kind: "iperf", Key: "throughput_mbps", Value: mbps, Unit: "Mbps"}})
}
extras := map[string]any{
"throughput_mbps": mbps,
"iperf_end": parsed,
}
if mbps <= 0 {
return Outcome{
Passed: false,
Message: "iperf3 reported zero throughput",
Summary: "zero throughput",
Extras: extras,
}
}
d.Info(fmt.Sprintf("Network: iperf3 PASSED: %.1f Mbps", mbps))
return Outcome{
Passed: true,
Summary: fmt.Sprintf("%.1f Mbps to %s", mbps, host),
Extras: extras,
}
}
// deriveHost pulls the hostname out of an https://host:port base URL.
func deriveHost(raw string) (string, error) {
if raw == "" {
return "", fmt.Errorf("empty url")
}
u, err := url.Parse(raw)
if err != nil {
return "", err
}
h := u.Hostname()
return strings.TrimSpace(h), nil
}
// parseIperfJSON pulls end.sum_sent.bits_per_second out of iperf3 -J.
// Returns (Mbps, full-json-map, err).
func parseIperfJSON(b []byte) (float64, map[string]any, error) {
var top map[string]any
if err := json.Unmarshal(b, &top); err != nil {
return 0, nil, err
}
end, ok := top["end"].(map[string]any)
if !ok {
return 0, top, fmt.Errorf("missing end")
}
// iperf3 reports either sum_sent (when -R not set) or sum_received.
for _, key := range []string{"sum_sent", "sum_received", "sum"} {
sum, ok := end[key].(map[string]any)
if !ok {
continue
}
bps, ok := sum["bits_per_second"].(float64)
if !ok {
continue
}
return bps / 1_000_000, end, nil
}
return 0, end, fmt.Errorf("no bits_per_second in end.sum_*")
}