| package main |
| |
| import ( |
| "context" |
| "fmt" |
| "net/http" |
| "sort" |
| "strings" |
| |
| "vbom.ml/util/sortorder" |
| |
| "code.hackerspace.pl/hscloud/go/mirko" |
| "code.hackerspace.pl/hscloud/go/statusz" |
| |
| dpb "code.hackerspace.pl/hscloud/dc/proto" |
| "code.hackerspace.pl/hscloud/dc/topo/assets" |
| "code.hackerspace.pl/hscloud/dc/topo/graph" |
| "code.hackerspace.pl/hscloud/dc/topo/state" |
| ) |
| |
| type Service struct { |
| gr *graph.Graph |
| stm *state.StateManager |
| } |
| |
| func NewService(gr *graph.Graph, stm *state.StateManager) *Service { |
| return &Service{ |
| gr: gr, |
| stm: stm, |
| } |
| } |
| |
| const topologyFragment = ` |
| <script src="/assets/viz.js"></script> |
| <script> |
| |
| var viz = new Viz({ workerURL: '/assets/full.render.js' }); |
| var xmlhttp = new XMLHttpRequest(); |
| xmlhttp.onreadystatechange = function() { |
| if (this.readyState == 4 && this.status == 200) { |
| var dot = this.responseText; |
| viz.renderSVGElement(dot) |
| .then(function(element) { |
| document.getElementById("graph").appendChild(element); |
| }); |
| } |
| }; |
| xmlhttp.open('GET', '/debug/graphviz'); |
| xmlhttp.send(); |
| |
| </script> |
| <div id="graph" style="text-align: center;"></div> |
| ` |
| |
| const switchportsFragment = ` |
| <style type="text/css"> |
| .table td,th { |
| background-color: #eee; |
| padding: 0.2em 0.4em 0.2em 0.4em; |
| } |
| .table th { |
| background-color: #c0c0c0; |
| } |
| .table { |
| background-color: #fff; |
| border-spacing: 0.2em; |
| margin-left: auto; |
| margin-right: auto; |
| } |
| </style> |
| <div> |
| <table class="table"> |
| <tr> |
| <th>Switch</th> |
| <th>Port</th> |
| <th>Link State</th> |
| <th>Port Mode</th> |
| <th>MTU</th> |
| <th>Sync Status</th> |
| </tr> |
| {{range .Ports }} |
| {{ if .Managed }} |
| <tr> |
| {{ else }} |
| <tr style="opacity: 0.5"> |
| {{ end}} |
| <td style="text-align: right;">{{ .Switch }}</td> |
| <td>{{ .Name }}</td> |
| {{ if eq .State "DOWN" }} |
| <td style="background-color: #ff3030;">{{ .State }}</td> |
| {{ else }} |
| <td>{{ .State }}</td> |
| {{ end }} |
| <td>{{ .Mode }}</td> |
| <td>{{ .MTU }}</td> |
| {{ if .Managed }} |
| <td style="background-color: #30ff30;">OK</td> |
| {{ else }} |
| <td><i>Unmanaged</i></td> |
| {{ end }} |
| </tr> |
| {{end}} |
| </table> |
| </div> |
| ` |
| |
| func (s *Service) Setup(m *mirko.Mirko) { |
| m.HTTPMux().Handle("/assets/", http.StripPrefix("/assets/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| data, ok := assets.Data[r.RequestURI] |
| if !ok { |
| http.NotFound(w, r) |
| return |
| } |
| |
| if strings.HasSuffix(r.RequestURI, ".js") { |
| w.Header().Set("Content-Type", "text/javascript") |
| } |
| |
| w.Write(data) |
| }))) |
| m.HTTPMux().HandleFunc("/debug/graphviz", s.httpHandleGraphviz) |
| statusz.AddStatusPart("Switch Ports", switchportsFragment, s.statusHandleSwitchports) |
| statusz.AddStatusPart("Topology", topologyFragment, func(ctx context.Context) interface{} { |
| return nil |
| }) |
| } |
| |
| func (s *Service) statusHandleSwitchports(ctx context.Context) interface{} { |
| managedPorts := make(map[string]bool) |
| s.gr.Mu.RLock() |
| for _, sw := range s.gr.Switches { |
| for _, port := range sw.Ports { |
| managedPorts[sw.Name+"|"+port.Name] = true |
| } |
| } |
| s.gr.Mu.RUnlock() |
| |
| s.stm.Mu.RLock() |
| defer s.stm.Mu.RUnlock() |
| |
| res := struct { |
| Ports []*struct { |
| Switch string |
| Name string |
| State string |
| Mode string |
| Managed bool |
| MTU string |
| } |
| }{} |
| for _, sw := range s.stm.Switches { |
| for _, po := range sw.Ports { |
| state := "INVALID" |
| switch po.Proto.LinkState { |
| case dpb.SwitchPort_LINKSTATE_DOWN: |
| state = "DOWN" |
| case dpb.SwitchPort_LINKSTATE_UP: |
| state = "UP" |
| } |
| mode := "INVALID" |
| switch po.Proto.PortMode { |
| case dpb.SwitchPort_PORTMODE_SWITCHPORT_UNTAGGED: |
| mode = fmt.Sprintf("UNTAGGED (%d)", po.Proto.VlanNative) |
| case dpb.SwitchPort_PORTMODE_SWITCHPORT_TAGGED: |
| mode = fmt.Sprintf("TAGGED (%v)", po.Proto.VlanTagged) |
| case dpb.SwitchPort_PORTMODE_SWITCHPORT_GENERIC: |
| mode = "GENERIC" |
| case dpb.SwitchPort_PORTMODE_ROUTED: |
| mode = "ROUTED" |
| case dpb.SwitchPort_PORTMODE_MANGLED: |
| mode = "MANGLED" |
| } |
| |
| managed := managedPorts[sw.Name+"|"+po.Proto.Name] |
| res.Ports = append(res.Ports, &struct { |
| Switch string |
| Name string |
| State string |
| Mode string |
| Managed bool |
| MTU string |
| }{ |
| Switch: sw.Name, |
| Name: po.Proto.Name, |
| State: state, |
| Mode: mode, |
| Managed: managed, |
| MTU: fmt.Sprintf("%d", po.Proto.Mtu), |
| }) |
| } |
| } |
| |
| return res |
| } |
| |
| func (s *Service) httpHandleGraphviz(w http.ResponseWriter, r *http.Request) { |
| fmt.Fprintf(w, "graph G {\n") |
| fmt.Fprintf(w, " ranksep = 2\n") |
| fmt.Fprintf(w, " splines = polyline\n") |
| fmt.Fprintf(w, " rankdir = LR\n") |
| for _, machine := range s.gr.Machines { |
| portNames := []string{} |
| for _, port := range machine.Ports { |
| name := fmt.Sprintf("<%s> %s", port.Name, port.Name) |
| portNames = append(portNames, name) |
| } |
| ports := strings.Join(portNames, "|") |
| fmt.Fprintf(w, " %s [shape=record label=\"{ %s | { %s }}\"]\n", machine.Name, machine.Name, ports) |
| } |
| for _, sw := range s.gr.Switches { |
| portNames := []string{} |
| portsOrdered := []*graph.SwitchPort{} |
| for _, port := range sw.Ports { |
| portsOrdered = append(portsOrdered, port) |
| } |
| sort.Slice(portsOrdered, func(i, j int) bool { |
| return sortorder.NaturalLess(portsOrdered[i].Name, portsOrdered[j].Name) |
| }) |
| for _, port := range portsOrdered { |
| name := fmt.Sprintf("<%s> %s", port.Name, port.Name) |
| portNames = append(portNames, name) |
| } |
| ports := strings.Join(portNames, "|") |
| fmt.Fprintf(w, " %s [shape=record label=\"{{ %s } | %s}\"]\n", sw.Name, ports, sw.Name) |
| } |
| for _, machine := range s.gr.Machines { |
| for _, port := range machine.Ports { |
| if port.OtherEnd == nil { |
| continue |
| } |
| fmt.Fprintf(w, " %s:%q:e -- %s:%q:w\n", machine.Name, port.Name, port.OtherEnd.Switch.Name, port.OtherEnd.Name) |
| } |
| } |
| fmt.Fprintf(w, "}\n") |
| } |