diff --git a/main.go b/main.go index 8f26e0bf..a1ea7ca6 100644 --- a/main.go +++ b/main.go @@ -3,8 +3,12 @@ package main import ( "context" "flag" + "fmt" "net" "net/http" + "reflect" + "strconv" + "strings" "code.hackerspace.pl/q3k/hspki" "github.com/golang/glog" @@ -17,6 +21,7 @@ import ( "google.golang.org/grpc/status" pb "code.hackerspace.pl/q3k/m6220-proxy/proto" + tpb "code.hackerspace.pl/q3k/topo/proto/control" ) var ( @@ -58,7 +63,7 @@ func (s *service) RunCommand(ctx context.Context, req *pb.RunCommandRequest) (*p cli, err := s.connect() if err != nil { - return nil, status.Error(codes.Unavailable, "could not connect to backend") + return nil, status.Error(codes.Unavailable, "could not connect to switch") } defer s.disconnect() @@ -73,6 +78,187 @@ func (s *service) RunCommand(ctx context.Context, req *pb.RunCommandRequest) (*p return res, nil } +func (s *service) parseInterfaceStatus(res *tpb.GetPortsResponse, lines []string) error { + if len(lines) < 4 { + return fmt.Errorf("need at least 4 lines of output, got %d", len(lines)) + } + if lines[0] != "" { + return fmt.Errorf("expected first line to be empty, is %q", lines[0]) + } + header1parts := strings.Fields(lines[1]) + if want := []string{"Port", "Description", "Duplex", "Speed", "Neg", "Link", "Flow", "Control"}; !reflect.DeepEqual(want, header1parts) { + return fmt.Errorf("expected header1 to be %v, got %v", want, header1parts) + } + + header2parts := strings.Fields(lines[2]) + if want := []string{"State", "Status"}; !reflect.DeepEqual(want, header2parts) { + return fmt.Errorf("expected header2 to be %v, got %v", want, header2parts) + } + + if lines[3][0] != '-' { + return fmt.Errorf("expected header3 to start with -, got %q", lines[3]) + } + + for _, line := range lines[4:] { + parts := strings.Fields(line) + if len(parts) < 6 { + break + } + portName := parts[0] + if strings.HasPrefix(portName, "Gi") && strings.HasPrefix(portName, "Ti") { + break + } + + speedStr := parts[len(parts)-4] + stateStr := parts[len(parts)-2] + + port := &tpb.SwitchPort{ + Name: portName, + } + if speedStr == "100" { + port.Speed = tpb.SwitchPort_SPEED_100M + } else if speedStr == "1000" { + port.Speed = tpb.SwitchPort_SPEED_1G + } else if speedStr == "10000" { + port.Speed = tpb.SwitchPort_SPEED_10G + } + if stateStr == "Up" { + port.LinkState = tpb.SwitchPort_LINKSTATE_UP + } else if stateStr == "Down" { + port.LinkState = tpb.SwitchPort_LINKSTATE_DOWN + } + + res.Ports = append(res.Ports, port) + } + + return nil +} + +func (s *service) parseInterfaceConfig(port *tpb.SwitchPort, lines []string) error { + glog.Infof("%+v", port) + for _, line := range lines { + glog.Infof("%s: %q", port.Name, line) + parts := strings.Fields(line) + if len(parts) < 1 { + continue + } + + if len(parts) >= 2 && parts[0] == "switchport" { + if parts[1] == "mode" { + if port.PortMode != tpb.SwitchPort_PORTMODE_INVALID { + return fmt.Errorf("redefinition of switchport mode") + } + if parts[2] == "access" { + port.PortMode = tpb.SwitchPort_PORTMODE_SWITCHPORT_UNTAGGED + } else if parts[2] == "trunk" { + port.PortMode = tpb.SwitchPort_PORTMODE_SWITCHPORT_TAGGED + } else if parts[2] == "general" { + port.PortMode = tpb.SwitchPort_PORTMODE_SWITCHPORT_GENERIC + } else { + port.PortMode = tpb.SwitchPort_PORTMODE_MANGLED + } + } + + if parts[1] == "access" { + if port.PortMode == tpb.SwitchPort_PORTMODE_INVALID { + port.PortMode = tpb.SwitchPort_PORTMODE_SWITCHPORT_UNTAGGED + } + if len(parts) > 3 && parts[2] == "vlan" { + vlan, err := strconv.Atoi(parts[3]) + if err != nil { + return fmt.Errorf("invalid vlan: %q", parts[3]) + } + port.VlanNative = int32(vlan) + } + } + + if parts[1] == "trunk" { + if len(parts) >= 5 && parts[2] == "allowed" && parts[3] == "vlan" { + vlans := strings.Split(parts[4], ",") + for _, vlan := range vlans { + vlanNum, err := strconv.Atoi(vlan) + if err != nil { + return fmt.Errorf("invalid vlan: %q", parts[3]) + } + port.VlanTagged = append(port.VlanTagged, int32(vlanNum)) + } + } + } + } else if len(parts) >= 2 && parts[0] == "mtu" { + mtu, err := strconv.Atoi(parts[1]) + if err != nil { + return fmt.Errorf("invalid mtu: %q", parts[3]) + } + port.Mtu = int32(mtu) + } else if len(parts) >= 2 && parts[0] == "spanning-tree" && parts[1] == "portfast" { + port.SpanningTreeMode = tpb.SwitchPort_SPANNING_TREE_MODE_PORTFAST + } + } + + // no mode -> access + if port.PortMode == tpb.SwitchPort_PORTMODE_INVALID { + port.PortMode = tpb.SwitchPort_PORTMODE_SWITCHPORT_UNTAGGED + } + + // apply defaults + if port.Mtu == 0 { + port.Mtu = 1500 + } + if port.SpanningTreeMode == tpb.SwitchPort_SPANNING_TREE_MODE_INVALID { + port.SpanningTreeMode = tpb.SwitchPort_SPANNING_TREE_MODE_AUTO_PORTFAST + } + + // sanitize + if port.PortMode == tpb.SwitchPort_PORTMODE_SWITCHPORT_UNTAGGED { + port.VlanTagged = []int32{} + port.Prefixes = []string{} + if port.VlanNative == 0 { + port.VlanNative = 1 + } + } else if port.PortMode == tpb.SwitchPort_PORTMODE_SWITCHPORT_TAGGED { + port.VlanNative = 0 + port.Prefixes = []string{} + } else if port.PortMode == tpb.SwitchPort_PORTMODE_SWITCHPORT_GENERIC { + port.Prefixes = []string{} + if port.VlanNative == 0 { + port.VlanNative = 1 + } + } + return nil +} + +func (s *service) GetPorts(ctx context.Context, req *tpb.GetPortsRequest) (*tpb.GetPortsResponse, error) { + cli, err := s.connect() + if err != nil { + return nil, status.Error(codes.Unavailable, "could not connect to switch") + } + defer s.disconnect() + res := &tpb.GetPortsResponse{} + + statusLines, _, err := cli.runCommand(ctx, "show interface status") + if err != nil { + return nil, status.Error(codes.Unavailable, "could not get interface status from switch") + } + + err = s.parseInterfaceStatus(res, statusLines) + if err != nil { + return nil, status.Errorf(codes.Unavailable, "could not parse interface status from switch: %v", err) + } + + for _, port := range res.Ports { + configLines, _, err := cli.runCommand(ctx, "show run interface "+port.Name) + if err != nil { + return nil, status.Error(codes.Unavailable, "could not get interface config from switch") + } + err = s.parseInterfaceConfig(port, configLines) + if err != nil { + return nil, status.Errorf(codes.Unavailable, "could not parse interface config from switch: %v", err) + } + } + + return res, nil +} + func main() { flag.StringVar(&flagListenAddress, "listen_address", "127.0.0.1:42000", "Address to listen on for gRPC") flag.StringVar(&flagDebugAddress, "debug_address", "127.0.0.1:42001", "Address to listen on for Debug HTTP") @@ -92,6 +278,7 @@ func main() { } grpcSrv := grpc.NewServer(hspki.WithServerHSPKI()...) pb.RegisterM6220ProxyServer(grpcSrv, s) + tpb.RegisterSwitchControlServer(grpcSrv, s) reflection.Register(grpcSrv) glog.Infof("Starting gRPC on %v", flagListenAddress)