package main import ( "context" "flag" "fmt" "reflect" "strconv" "strings" "code.hackerspace.pl/hscloud/go/mirko" "github.com/golang/glog" "github.com/ziutek/telnet" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ipb "code.hackerspace.pl/hscloud/go/proto/infra" pb "code.hackerspace.pl/hscloud/go/svc/m6220-proxy/proto" ) var ( flagSwitchAddress string flagSwitchUsername string flagSwitchPassword string ) func init() { flag.Set("logtostderr", "true") } type service struct { connectionSemaphore chan int } func (s *service) connect() (*cliClient, error) { s.connectionSemaphore <- 1 conn, err := telnet.Dial("tcp", flagSwitchAddress) if err != nil { <-s.connectionSemaphore return nil, err } cli := newCliClient(conn, flagSwitchUsername, flagSwitchPassword) return cli, nil } func (s *service) disconnect() { <-s.connectionSemaphore } func (s *service) RunCommand(ctx context.Context, req *pb.RunCommandRequest) (*pb.RunCommandResponse, error) { if req.Command == "" { return nil, status.Error(codes.InvalidArgument, "command cannot be null") } cli, err := s.connect() if err != nil { return nil, status.Error(codes.Unavailable, "could not connect to switch") } defer s.disconnect() lines, effective, err := cli.runCommand(ctx, req.Command) if err != nil { return nil, err } res := &pb.RunCommandResponse{ EffectiveCommand: effective, Lines: lines, } return res, nil } func (s *service) parseInterfaceStatus(res *ipb.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 := &ipb.SwitchPort{ Name: portName, } if speedStr == "100" { port.Speed = ipb.SwitchPort_SPEED_100M } else if speedStr == "1000" { port.Speed = ipb.SwitchPort_SPEED_1G } else if speedStr == "10000" { port.Speed = ipb.SwitchPort_SPEED_10G } if stateStr == "Up" { port.LinkState = ipb.SwitchPort_LINKSTATE_UP } else if stateStr == "Down" { port.LinkState = ipb.SwitchPort_LINKSTATE_DOWN } res.Ports = append(res.Ports, port) } return nil } func (s *service) parseInterfaceConfig(port *ipb.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 != ipb.SwitchPort_PORTMODE_INVALID { return fmt.Errorf("redefinition of switchport mode") } if parts[2] == "access" { port.PortMode = ipb.SwitchPort_PORTMODE_SWITCHPORT_UNTAGGED } else if parts[2] == "trunk" { port.PortMode = ipb.SwitchPort_PORTMODE_SWITCHPORT_TAGGED } else if parts[2] == "general" { port.PortMode = ipb.SwitchPort_PORTMODE_SWITCHPORT_GENERIC } else { port.PortMode = ipb.SwitchPort_PORTMODE_MANGLED } } if parts[1] == "access" { if port.PortMode == ipb.SwitchPort_PORTMODE_INVALID { port.PortMode = ipb.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 = ipb.SwitchPort_SPANNING_TREE_MODE_PORTFAST } } // no mode -> access if port.PortMode == ipb.SwitchPort_PORTMODE_INVALID { port.PortMode = ipb.SwitchPort_PORTMODE_SWITCHPORT_UNTAGGED } // apply defaults if port.Mtu == 0 { port.Mtu = 1500 } if port.SpanningTreeMode == ipb.SwitchPort_SPANNING_TREE_MODE_INVALID { port.SpanningTreeMode = ipb.SwitchPort_SPANNING_TREE_MODE_AUTO_PORTFAST } // sanitize if port.PortMode == ipb.SwitchPort_PORTMODE_SWITCHPORT_UNTAGGED { port.VlanTagged = []int32{} port.Prefixes = []string{} if port.VlanNative == 0 { port.VlanNative = 1 } } else if port.PortMode == ipb.SwitchPort_PORTMODE_SWITCHPORT_TAGGED { port.VlanNative = 0 port.Prefixes = []string{} } else if port.PortMode == ipb.SwitchPort_PORTMODE_SWITCHPORT_GENERIC { port.Prefixes = []string{} if port.VlanNative == 0 { port.VlanNative = 1 } } return nil } func (s *service) GetPorts(ctx context.Context, req *ipb.GetPortsRequest) (*ipb.GetPortsResponse, error) { cli, err := s.connect() if err != nil { return nil, status.Error(codes.Unavailable, "could not connect to switch") } defer s.disconnect() res := &ipb.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(&flagSwitchAddress, "switch_address", "127.0.0.1:23", "Telnet address of M6220") flag.StringVar(&flagSwitchUsername, "switch_username", "admin", "Switch login username") flag.StringVar(&flagSwitchPassword, "switch_password", "admin", "Switch login password") flag.Parse() s := &service{ connectionSemaphore: make(chan int, 1), } m := mirko.New() if err := m.Listen(); err != nil { glog.Exitf("Listen(): %v", err) } pb.RegisterM6220ProxyServer(m.GRPC(), s) ipb.RegisterSwitchControlServer(m.GRPC(), s) if err := m.Serve(); err != nil { glog.Exitf("Serve(): %v", err) } select {} }