The control plane asks one node on each physical fabric to run
ibnetdiscover every five minutes and parses the text itself, so a
format the parser has not met is a control plane upgrade, not one on
every node. From the map it works out the switch tiers (leaf, spine,
core, by distance from the adapters), which adapter is which node's,
which cables trained below the rate the rest of their kind run at, and
whether the rails line up: every node's Nth adapter on its own leaf.
A page per fabric (/admin/fabrics) draws it: switches in their tiers,
adapters fanned out under their leaves, cables coloured by rate and
bundled, degraded ones marching red with a marker, and the cluster's
own adapters live - down ports glowing red, and particles running each
cable as fast as its adapter is sending and receiving, from the
counters. It zooms, pans, fades what a hovered switch or adapter does
not touch, opens a panel of detail on a click, colours by rail, finds a
node, and refreshes every fifteen seconds. The data is also at
/api/v1/fabrics/{id}/topology.
Placement reads the map: a replica spanning nodes takes nodes of one
leaf together when it can, and nodes whose cable trained short last.
Static assets are named by their content, as the stylesheet already
was.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
83 lines
2.1 KiB
Go
83 lines
2.1 KiB
Go
package web
|
|
|
|
import (
|
|
"encoding/json"
|
|
"html/template"
|
|
"net/http"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
|
|
)
|
|
|
|
// TopologySource is a Source that can map its fabrics.
|
|
type TopologySource interface {
|
|
Topologies() []adminapi.TopologyView
|
|
Topology(id string) (adminapi.TopologyView, bool)
|
|
}
|
|
|
|
type fabricPageData struct {
|
|
Nav Nav
|
|
OnAdminListener bool
|
|
Base string
|
|
List []adminapi.TopologyView
|
|
Current *adminapi.TopologyView
|
|
// Data is the current fabric as JSON, for the drawing to start from
|
|
// without a round trip. json.Marshal escapes <, > and &, so a switch
|
|
// description cannot close the script it sits in.
|
|
Data template.JS
|
|
Version string
|
|
Now string
|
|
}
|
|
|
|
// FabricsPage shows every physical fabric and, for one, its map.
|
|
func FabricsPage(src Source, onAdminListener bool, version string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ts, ok := src.(TopologySource)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
d := fabricPageData{Nav: navFrom(r, "cluster"), OnAdminListener: onAdminListener, Base: "/admin/fabrics",
|
|
List: ts.Topologies(), Version: version, Now: time.Now().UTC().Format(time.RFC3339)}
|
|
if onAdminListener {
|
|
d.Base = "/fabrics"
|
|
}
|
|
id := r.PathValue("id")
|
|
if id == "" && len(d.List) > 0 {
|
|
id = d.List[0].ID
|
|
}
|
|
if id != "" {
|
|
t, ok := ts.Topology(id)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
d.Current = &t
|
|
b, _ := json.Marshal(t)
|
|
d.Data = template.JS(b) //nolint:gosec // json.Marshal escapes <, > and &
|
|
}
|
|
noStore(w)
|
|
render(w, "fabrics", d)
|
|
}
|
|
}
|
|
|
|
// FabricTopologyJSON is one fabric's map and live state, for the drawing to
|
|
// refresh from.
|
|
func FabricTopologyJSON(src Source) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ts, ok := src.(TopologySource)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
t, ok := ts.Topology(r.PathValue("id"))
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
noStore(w)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(t)
|
|
}
|
|
}
|