tstest/natlab/vmtest: add web UI for watching VM tests live
Add an optional --vmtest-web flag that starts an HTTP server showing a live dashboard for vmtest runs. The dashboard includes: - Step progress tracker showing all test phases (compile, image prep, QEMU launch, agent connect, tailscale up, test-specific steps) with status icons and elapsed times - Per-VM "virtual monitor" cards showing serial console output streamed in realtime via WebSocket - Per-NIC DHCP status (supporting multi-homed VMs like subnet routers) - Per-node Tailscale status (hidden for non-tailnet VMs) - Test status badge (Running/Passed/Failed) with live elapsed timer - Event log showing all lifecycle events chronologically Architecture follows the existing util/eventbus HTMX+WebSocket pattern: the server pushes HTML fragments with hx-swap-oob attributes over a WebSocket, and HTMX routes them to the correct DOM elements by ID. Key components: - vmstatus.go: Step tracker (Begin/End lifecycle), EventBus (pub/sub with history for late joiners), VMEvent types, NodeStatus tracking - web.go: HTTP server, WebSocket handler, template loading, ANSI-to-HTML conversion via robert-nix/ansihtml, deterministic port selection - assets/: HTML templates, CSS, HTMX library (copied from eventbus) - vnet/vnet.go: DHCP event callback on Server for observing DHCP lifecycle - qemu.go: Console log file tailing with manual offset-based reading Usage: go test ./tstest/natlab/vmtest/ --run-vm-tests --vmtest-web=:0 -v When using :0, a deterministic port based on the test name is tried first so re-runs get the same URL, falling back to OS-assigned on conflict. Updates #13038 Change-Id: I45281347b3d7af78ed9f4ff896033984f84dcb4d Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
committed by
Brad Fitzpatrick
parent
0ac09721df
commit
b9eac14ef9
@@ -751,6 +751,10 @@ type Server struct {
|
||||
|
||||
cloudInitData map[int]*CloudInitData // node num → cloud-init config
|
||||
fileContents map[string][]byte // filename → file bytes
|
||||
|
||||
// onDHCPEvent, if non-nil, is called when DHCP messages are processed.
|
||||
// Parameters are: source MAC, node number, DHCP message type, assigned IP.
|
||||
onDHCPEvent func(nodeMAC MAC, nodeNum int, msgType layers.DHCPMsgType, assignedIP netip.Addr)
|
||||
}
|
||||
|
||||
func (s *Server) logf(format string, args ...any) {
|
||||
@@ -765,6 +769,13 @@ func (s *Server) SetLoggerForTest(logf func(format string, args ...any)) {
|
||||
s.optLogf = logf
|
||||
}
|
||||
|
||||
// SetDHCPCallback registers a function to be called when DHCP messages are
|
||||
// processed. The callback receives the source MAC, node number, DHCP message
|
||||
// type (Discover, Offer, Request, Ack), and the assigned IP address.
|
||||
func (s *Server) SetDHCPCallback(fn func(MAC, int, layers.DHCPMsgType, netip.Addr)) {
|
||||
s.onDHCPEvent = fn
|
||||
}
|
||||
|
||||
var derpMap = &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
@@ -1990,6 +2001,10 @@ func (s *Server) createDHCPResponse(request gopacket.Packet) ([]byte, error) {
|
||||
Length: 4,
|
||||
},
|
||||
)
|
||||
if s.onDHCPEvent != nil {
|
||||
s.onDHCPEvent(srcMAC, node.num, layers.DHCPMsgTypeDiscover, clientIP)
|
||||
s.onDHCPEvent(srcMAC, node.num, layers.DHCPMsgTypeOffer, clientIP)
|
||||
}
|
||||
case layers.DHCPMsgTypeRequest:
|
||||
response.Options = append(response.Options,
|
||||
layers.DHCPOption{
|
||||
@@ -2018,6 +2033,10 @@ func (s *Server) createDHCPResponse(request gopacket.Packet) ([]byte, error) {
|
||||
Length: 4,
|
||||
},
|
||||
)
|
||||
if s.onDHCPEvent != nil {
|
||||
s.onDHCPEvent(srcMAC, node.num, layers.DHCPMsgTypeRequest, clientIP)
|
||||
s.onDHCPEvent(srcMAC, node.num, layers.DHCPMsgTypeAck, clientIP)
|
||||
}
|
||||
}
|
||||
|
||||
eth := &layers.Ethernet{
|
||||
|
||||
Reference in New Issue
Block a user