R-1247: the weekly upload line test — the agent's own iperf3 client to ep0's tunnel-only server, Tuesday 10:00-15:59 after 10 quiet minutes, under the heavy-op gate, best of 3; line_test stanza + GET /host/line-test; 15 tests red-proved; unreleased (next v0.164.0)
gates / gates (push) Successful in 1m35s
gates / gates (push) Successful in 1m35s
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LineTestReader is the R-1247 weekly upload line test (internal/linetest.Tester) as the local API reads it.
|
||||
type LineTestReader interface {
|
||||
Value() (mbps float64, at time.Time, ok bool)
|
||||
}
|
||||
|
||||
// LineTestResponse is GET /host/line-test. Present=false: never measured, or the job is not wired (no tunnel, disabled,
|
||||
// or an agent before the route answers 404 — the controller reads all three as „unknown").
|
||||
type LineTestResponse struct {
|
||||
Present bool `json:"present"`
|
||||
Mbps float64 `json:"mbps,omitempty"`
|
||||
MeasuredAt string `json:"measured_at,omitempty"` // RFC3339 UTC
|
||||
Method string `json:"method,omitempty"` // "iperf3-tunnel": a 10-second upload to ep0 inside the tunnel
|
||||
}
|
||||
|
||||
// SetLineTest wires the weekly line test (main.go builds it after the local API server).
|
||||
func (s *Server) SetLineTest(r LineTestReader) { s.lineTest = r }
|
||||
|
||||
func (s *Server) handleLineTest(w http.ResponseWriter, r *http.Request, vmid int) {
|
||||
if s.lineTest == nil {
|
||||
writeOK(w, LineTestResponse{Present: false})
|
||||
return
|
||||
}
|
||||
mbps, at, ok := s.lineTest.Value()
|
||||
if !ok {
|
||||
writeOK(w, LineTestResponse{Present: false})
|
||||
return
|
||||
}
|
||||
s.logger.Debug("local-api: /host/line-test served", "vmid", vmid, "mbps", mbps, "measured_at", at.UTC().Format(time.RFC3339))
|
||||
writeOK(w, LineTestResponse{Present: true, Mbps: mbps, MeasuredAt: at.UTC().Format(time.RFC3339), Method: "iperf3-tunnel"})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fixedLine struct {
|
||||
mbps float64
|
||||
ok bool
|
||||
}
|
||||
|
||||
func (f fixedLine) Value() (float64, time.Time, bool) {
|
||||
return f.mbps, time.Date(2026, 10, 24, 6, 31, 0, 0, time.UTC), f.ok
|
||||
}
|
||||
|
||||
func lineTestBody(t *testing.T, s *Server) LineTestResponse {
|
||||
t.Helper()
|
||||
rec := httptest.NewRecorder()
|
||||
s.handleLineTest(rec, httptest.NewRequest("GET", "/host/line-test", nil), 9201)
|
||||
var env struct {
|
||||
OK bool `json:"ok"`
|
||||
Data LineTestResponse `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &env); err != nil {
|
||||
t.Fatalf("decode %q: %v", rec.Body.String(), err)
|
||||
}
|
||||
return env.Data
|
||||
}
|
||||
|
||||
// R-1247: the controller sets qBittorrent's limit only from a PRESENT value; never measured and not wired must both
|
||||
// read present:false, never a zero line.
|
||||
func TestLineTestRoute(t *testing.T) {
|
||||
s := &Server{logger: slog.Default()}
|
||||
if got := lineTestBody(t, s); got.Present {
|
||||
t.Fatalf("not wired: %+v, want present:false", got)
|
||||
}
|
||||
s.SetLineTest(fixedLine{ok: false})
|
||||
if got := lineTestBody(t, s); got.Present || got.Mbps != 0 {
|
||||
t.Fatalf("never measured: %+v, want present:false", got)
|
||||
}
|
||||
s.SetLineTest(fixedLine{mbps: 90.7, ok: true})
|
||||
got := lineTestBody(t, s)
|
||||
if !got.Present || got.Mbps != 90.7 || got.Method != "iperf3-tunnel" || !strings.HasPrefix(got.MeasuredAt, "2026-10-24T06:31") {
|
||||
t.Fatalf("measured: %+v", got)
|
||||
}
|
||||
}
|
||||
@@ -336,6 +336,7 @@ type Server struct {
|
||||
formatJobs *FormatJobStore // F20-BUG3 detached-format job record (optional)
|
||||
staleLock StaleLockController // F2-b startup stale-lock recovery (optional)
|
||||
partialSweep func(ctx context.Context, vmid int) // R-1006 start-up sweep of dead vzdump leftovers (optional)
|
||||
lineTest LineTestReader // R-1247 weekly line test (optional; nil → present:false)
|
||||
// guestPower (F-REBOOT) is per-guest start-attempt state for the guest-power watchdog.
|
||||
// Guarded by guestPowerMu in guestpower.go; in-memory on purpose (see guestPowerState).
|
||||
guestPower map[int]guestPowerState
|
||||
@@ -573,6 +574,9 @@ func (s *Server) Handler() http.Handler {
|
||||
// R-856 (`09` §3 decision 143): the host crash guard's record of the last HOST boot — the controller
|
||||
// waits ~15 min with app mails after an unclean one. Read-only; a missing/garbled file = present:false.
|
||||
mux.HandleFunc("GET /host/crash-guard", s.withGuest(s.handleCrashGuard))
|
||||
// R-1247: the weekly upload line test's last measurement — the controller's qBittorrent limit and upload watch
|
||||
// read it. Read-only; never measured (or the job not wired) = present:false.
|
||||
mux.HandleFunc("GET /host/line-test", s.withGuest(s.handleLineTest))
|
||||
// Disk management (slice 8C) — self-scoped; format routes through the data-bearing classifier+gate.
|
||||
mux.HandleFunc("GET /disks", s.withGuest(s.handleDisks))
|
||||
mux.HandleFunc("GET /disks/candidates", s.withGuest(s.handleDiskCandidates))
|
||||
|
||||
Reference in New Issue
Block a user