hub 0.177.0: Reboot guest on the host page (D65 = A, reboot_guest in the agent's closed list, 30-minute expiry); R-1346: the slow-request line and the vouch save's per-leg timing
gates / gates (push) Successful in 1m52s
gates / gates (push) Successful in 1m52s
This commit is contained in:
1 parent
dd3e4137ee
commit
636022dc31
10 files changed
+223
-16
No files matched your search
@@ -1,9 +1,12 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -80,3 +83,19 @@ func TestVouchGoldenChangedToOlderIsStillRefused(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// R-1346: a vouch save names the time of each leg in one line, so the next slow save says where its time went.
|
||||
// RED-PROOF: drop the timing line → FAILS.
|
||||
func TestVouchSaveLogsItsTiming(t *testing.T) {
|
||||
s, st := newTestServer(t)
|
||||
var logs bytes.Buffer
|
||||
s.logger = log.New(&logs, "", 0)
|
||||
seedReport(t, st, "demo", "0.335.0")
|
||||
w := postArtifacts(t, s, url.Values{"agent_version": {"0.160.0"}, "agent_sha256": {strings.Repeat("c", 64)}})
|
||||
if loc := w.Header().Get("Location"); !strings.Contains(loc, "flash=artifacts_set") {
|
||||
t.Fatalf("vouch refused: %q", loc)
|
||||
}
|
||||
if !regexp.MustCompile(`artifact vouch timing \(R-1346\): registry probes \d+ms, sha lookups \d+ms, bundle sha \d+ms, fleet check \+ save \d+ms, total \d+ms`).MatchString(logs.String()) {
|
||||
t.Fatalf("no timing line:\n%s", logs.String())
|
||||
}
|
||||
}
|
||||
@@ -1440,6 +1440,9 @@ func (s *Server) handleSetArtifacts(w http.ResponseWriter, r *http.Request) {
|
||||
// read as a success, and this project has the scars; so an unreachable registry refuses too,
|
||||
// with its own message. There is NO OVERRIDE: the registry is the operator's own server, and if
|
||||
// it cannot be reached then vouching is moot rather than urgent.
|
||||
// R-1346: each leg's time, in one line at the save — the save took 37–170 s on four days (the hub's access log) while
|
||||
// every registry call measured under 0.3 s, so the next slow vouch must name its leg.
|
||||
began := time.Now()
|
||||
if s.gitea != nil {
|
||||
ctx := r.Context()
|
||||
type probeTarget struct{ label, pkg, file, version, repo string }
|
||||
@@ -1484,6 +1487,7 @@ func (s *Server) handleSetArtifacts(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
probesDone := time.Now()
|
||||
// R-292: each failure names its own cause — version missing, registry unreachable, no sha listed, or a
|
||||
// bad typed sha — instead of one flash that conflated all of them.
|
||||
agentSHA, flashAS := s.resolveArtifactSHA(r.Context(), pkgAgent, fileAgent, agentVer, r.FormValue("agent_sha256"))
|
||||
@@ -1492,6 +1496,7 @@ func (s *Server) handleSetArtifacts(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/configuration?flash="+flash, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
shaDone := time.Now()
|
||||
// R-840: the vouched agent's CONFIG BUNDLE is resolved from the registry by exact name, like the binary. A version
|
||||
// without one (older than v0.143.0) vouches none — the installer then falls back to its per-file fetches.
|
||||
bundleSHA := ""
|
||||
@@ -1504,6 +1509,7 @@ func (s *Server) handleSetArtifacts(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
bundleSHA = b
|
||||
}
|
||||
bundleDone := time.Now()
|
||||
// R-50b(a): the PBS-DR wrapper hash is operator-typed, not resolved from the package registry —
|
||||
// unlike the agent binary and the golden, this artifact is not published there at all. It is
|
||||
// installed from raw/branch/main, which is exactly the drift this field makes visible.
|
||||
@@ -1575,6 +1581,9 @@ func (s *Server) handleSetArtifacts(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] Artifact manifest set: agent=%s golden=%s min_agent=%q wrapper_sha=%t bundle_sha=%q", agentVer, goldenVer, minAgent, wrapperSHA != "", bundleSHA)
|
||||
s.logger.Printf("[INFO] artifact vouch timing (R-1346): registry probes %dms, sha lookups %dms, bundle sha %dms, fleet check + save %dms, total %dms",
|
||||
probesDone.Sub(began).Milliseconds(), shaDone.Sub(probesDone).Milliseconds(), bundleDone.Sub(shaDone).Milliseconds(),
|
||||
time.Since(bundleDone).Milliseconds(), time.Since(began).Milliseconds())
|
||||
// Agent-plane immediate-sync (Direction-2a, v0.59.0): a MinAgent-floor / vouched-agent change is
|
||||
// a fleet-wide agent-plane intent shift. Fire-and-forget nudge every box so it re-reports at
|
||||
// once (the self-update train's signed op / floor re-evaluation lands in seconds, not ≤15 min).
|
||||
|
||||
@@ -84,7 +84,8 @@ func TestHostPage_RestartAndClearHoldPosts(t *testing.T) {
|
||||
{"action": {"clear_hold"}, "arg": {"vikunja"}}, // not held
|
||||
{"action": {"restart_controller"}, "arg": {"9202"}}, // not a guest of this host
|
||||
{"action": {"restart_controller"}, "arg": {"abc"}},
|
||||
{"action": {"reboot_guest"}, "arg": {"9201"}},
|
||||
{"action": {"reboot_host"}, "arg": {"9201"}},
|
||||
{"action": {"reboot_guest"}, "arg": {"9202"}}, // not a guest of this host
|
||||
} {
|
||||
if rr := postOperatorAction(t, s, "h1", f); rr.Code != http.StatusBadRequest {
|
||||
t.Errorf("%v: status %d, want 400", f, rr.Code)
|
||||
@@ -122,3 +123,40 @@ func TestOperatorActor_NamesTheAccessToken(t *testing.T) {
|
||||
t.Fatalf("actor = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// D65 = A: „Reboot guest" beside „Restart controller", behind a confirm step that says the apps stop for about a minute;
|
||||
// the press stores a row for THIS host's agent only. RED-PROOF: seen failing before the button and the route existed.
|
||||
func TestHostPage_RebootGuestRendersAndPosts(t *testing.T) {
|
||||
s, st := heldFixture(t)
|
||||
body := renderHost(t, s, "h1")
|
||||
for _, want := range []string{`data-reboot-guest="9201"`, `name="action" value="reboot_guest"`,
|
||||
"Reboot guest 9201 on h1 (c1)?", "every app of the household stops for about a minute"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("host page lacks %q", want)
|
||||
}
|
||||
}
|
||||
if rr := postOperatorAction(t, s, "h1", url.Values{"action": {"reboot_guest"}, "arg": {"9201"}}); rr.Code != http.StatusSeeOther {
|
||||
t.Fatalf("reboot: %d %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
if a, _ := st.PendingHostOperatorActions("h1"); len(a) != 1 || a[0].Action != "reboot_guest" || a[0].Arg != "9201" {
|
||||
t.Fatalf("the agent's reply would list %+v", a)
|
||||
}
|
||||
if c, _ := st.PendingOperatorActions("c1"); len(c) != 0 {
|
||||
t.Fatalf("the reboot reached the controller's ACK: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
// A host with no customer has no buttons (no controller, no household) — the other branch of the template gate.
|
||||
func TestHostPage_NoRebootWithoutACustomer(t *testing.T) {
|
||||
s, st := newTestServer(t)
|
||||
if err := st.UpsertHost(&store.Host{HostID: "h9", APIKey: "k"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now := time.Now()
|
||||
if err := st.UpsertGuestFromReport(&store.Guest{GuestID: "h9-9201", HostID: "h9", VMID: 9201, Status: "running", LastSeenAt: &now}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body := renderHost(t, s, "h9"); strings.Contains(body, "data-reboot-guest") {
|
||||
t.Fatal("a host with no customer shows Reboot guest")
|
||||
}
|
||||
}
|
||||
@@ -176,12 +176,15 @@ func TestHandleHostDetail(t *testing.T) {
|
||||
// remains absent: 2 log requests + 4 operator actions, and the 4 all post to /operator-action.
|
||||
// 2026-10-29 (operator brief, Part 1): plus ONE „Restart controller" per guest of a customer host (the AGENT's closed
|
||||
// list, behind a confirm step; this fixture has one guest). „Clear hold" appears only per held app (none
|
||||
// here). No other button.
|
||||
if got := strings.Count(strings.ToLower(body), "<button"); got != 7 {
|
||||
t.Errorf("host detail has %d buttons, want exactly the 2 log-request + 4 operator-action + 1 restart-controller buttons", got)
|
||||
// here). D65 = A (2026-10-29): plus ONE „Reboot guest" per guest, beside it. No other button.
|
||||
if got := strings.Count(strings.ToLower(body), "<button"); got != 8 {
|
||||
t.Errorf("host detail has %d buttons, want exactly the 2 log-request + 4 operator-action + 1 restart-controller + 1 reboot-guest buttons", got)
|
||||
}
|
||||
if got := strings.Count(body, `action="/hosts/demo-felhom-01/operator-action"`); got != 5 {
|
||||
t.Errorf("operator-action forms = %d, want 5", got)
|
||||
if got := strings.Count(body, `action="/hosts/demo-felhom-01/operator-action"`); got != 6 {
|
||||
t.Errorf("operator-action forms = %d, want 6", got)
|
||||
}
|
||||
if got := strings.Count(body, `value="reboot_guest"`); got != 1 {
|
||||
t.Errorf("reboot_guest forms = %d, want 1 (one guest)", got)
|
||||
}
|
||||
if got := strings.Count(body, `value="restart_controller"`); got != 1 {
|
||||
t.Errorf("restart_controller forms = %d, want 1 (one guest)", got)
|
||||
|
||||
@@ -144,6 +144,17 @@
|
||||
<p>Restart the controller of guest {{.VMID}} on {{$.HostID}} ({{$.CustomerID}})? The box's agent restarts it, even when the controller does not answer. The household's dashboard is away for about a minute. At most 3 restarts in 15 minutes; the result shows under Operator Actions.</p>
|
||||
<button type="submit" class="btn btn-sm btn-danger">Restart the controller</button>
|
||||
</form>
|
||||
</details>
|
||||
{{/* D65 = A (2026-10-29): the AGENT reboots this guest (pct reboot — never the host). Refused during a backup, restore, update or swap, more than once an hour, or under the restart brake; the result says when the guest and the controller were back. */}}
|
||||
<details class="op-confirm" data-reboot-guest="{{.VMID}}">
|
||||
<summary class="btn btn-sm btn-outline">Reboot guest</summary>
|
||||
<form method="POST" action="/hosts/{{$.HostID}}/operator-action">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
|
||||
<input type="hidden" name="action" value="reboot_guest">
|
||||
<input type="hidden" name="arg" value="{{.VMID}}">
|
||||
<p>Reboot guest {{.VMID}} on {{$.HostID}} ({{$.CustomerID}})? The box's agent reboots the whole guest — use it when Docker or the network inside the box is stuck. While it reboots, every app of the household stops for about a minute; the dashboard then says Felhom support restarted the box. Refused during a backup, a restore, an update or a controller swap, and more than once an hour. The result shows under Operator Actions.</p>
|
||||
<button type="submit" class="btn btn-sm btn-danger">Reboot the guest</button>
|
||||
</form>
|
||||
</details>{{end}}
|
||||
</td>
|
||||
<td>{{timeAgoPtr .LastSeenAt}}</td>
|
||||
|
||||
Reference in new issue
Block a user