blob: a1ea7ca687960cc596fda291160892a7ec88985d [file] [log] [blame]
Sergiusz Bazanskiff5af692018-08-29 19:20:46 +01001package main
2
3import (
4 "context"
5 "flag"
Serge Bazanskic1515cb2018-10-04 10:36:29 +01006 "fmt"
Sergiusz Bazanskiff5af692018-08-29 19:20:46 +01007 "net"
8 "net/http"
Serge Bazanskic1515cb2018-10-04 10:36:29 +01009 "reflect"
10 "strconv"
11 "strings"
Sergiusz Bazanskiff5af692018-08-29 19:20:46 +010012
13 "code.hackerspace.pl/q3k/hspki"
14 "github.com/golang/glog"
15 "github.com/q3k/statusz"
16 "github.com/ziutek/telnet"
17 "golang.org/x/net/trace"
18 "google.golang.org/grpc"
19 "google.golang.org/grpc/codes"
20 "google.golang.org/grpc/reflection"
21 "google.golang.org/grpc/status"
22
23 pb "code.hackerspace.pl/q3k/m6220-proxy/proto"
Serge Bazanskic1515cb2018-10-04 10:36:29 +010024 tpb "code.hackerspace.pl/q3k/topo/proto/control"
Sergiusz Bazanskiff5af692018-08-29 19:20:46 +010025)
26
27var (
28 flagListenAddress string
29 flagDebugAddress string
30 flagSwitchAddress string
31 flagSwitchUsername string
32 flagSwitchPassword string
33)
34
35func init() {
36 flag.Set("logtostderr", "true")
37}
38
39type service struct {
40 connectionSemaphore chan int
41}
42
43func (s *service) connect() (*cliClient, error) {
44 s.connectionSemaphore <- 1
45 conn, err := telnet.Dial("tcp", flagSwitchAddress)
46 if err != nil {
47 <-s.connectionSemaphore
48 return nil, err
49 }
50
51 cli := newCliClient(conn, flagSwitchUsername, flagSwitchPassword)
52 return cli, nil
53}
54
55func (s *service) disconnect() {
56 <-s.connectionSemaphore
57}
58
59func (s *service) RunCommand(ctx context.Context, req *pb.RunCommandRequest) (*pb.RunCommandResponse, error) {
60 if req.Command == "" {
61 return nil, status.Error(codes.InvalidArgument, "command cannot be null")
62 }
63
64 cli, err := s.connect()
65 if err != nil {
Serge Bazanskic1515cb2018-10-04 10:36:29 +010066 return nil, status.Error(codes.Unavailable, "could not connect to switch")
Sergiusz Bazanskiff5af692018-08-29 19:20:46 +010067 }
68 defer s.disconnect()
69
70 lines, effective, err := cli.runCommand(ctx, req.Command)
71 if err != nil {
72 return nil, err
73 }
74 res := &pb.RunCommandResponse{
75 EffectiveCommand: effective,
76 Lines: lines,
77 }
78 return res, nil
79}
80
Serge Bazanskic1515cb2018-10-04 10:36:29 +010081func (s *service) parseInterfaceStatus(res *tpb.GetPortsResponse, lines []string) error {
82 if len(lines) < 4 {
83 return fmt.Errorf("need at least 4 lines of output, got %d", len(lines))
84 }
85 if lines[0] != "" {
86 return fmt.Errorf("expected first line to be empty, is %q", lines[0])
87 }
88 header1parts := strings.Fields(lines[1])
89 if want := []string{"Port", "Description", "Duplex", "Speed", "Neg", "Link", "Flow", "Control"}; !reflect.DeepEqual(want, header1parts) {
90 return fmt.Errorf("expected header1 to be %v, got %v", want, header1parts)
91 }
92
93 header2parts := strings.Fields(lines[2])
94 if want := []string{"State", "Status"}; !reflect.DeepEqual(want, header2parts) {
95 return fmt.Errorf("expected header2 to be %v, got %v", want, header2parts)
96 }
97
98 if lines[3][0] != '-' {
99 return fmt.Errorf("expected header3 to start with -, got %q", lines[3])
100 }
101
102 for _, line := range lines[4:] {
103 parts := strings.Fields(line)
104 if len(parts) < 6 {
105 break
106 }
107 portName := parts[0]
108 if strings.HasPrefix(portName, "Gi") && strings.HasPrefix(portName, "Ti") {
109 break
110 }
111
112 speedStr := parts[len(parts)-4]
113 stateStr := parts[len(parts)-2]
114
115 port := &tpb.SwitchPort{
116 Name: portName,
117 }
118 if speedStr == "100" {
119 port.Speed = tpb.SwitchPort_SPEED_100M
120 } else if speedStr == "1000" {
121 port.Speed = tpb.SwitchPort_SPEED_1G
122 } else if speedStr == "10000" {
123 port.Speed = tpb.SwitchPort_SPEED_10G
124 }
125 if stateStr == "Up" {
126 port.LinkState = tpb.SwitchPort_LINKSTATE_UP
127 } else if stateStr == "Down" {
128 port.LinkState = tpb.SwitchPort_LINKSTATE_DOWN
129 }
130
131 res.Ports = append(res.Ports, port)
132 }
133
134 return nil
135}
136
137func (s *service) parseInterfaceConfig(port *tpb.SwitchPort, lines []string) error {
138 glog.Infof("%+v", port)
139 for _, line := range lines {
140 glog.Infof("%s: %q", port.Name, line)
141 parts := strings.Fields(line)
142 if len(parts) < 1 {
143 continue
144 }
145
146 if len(parts) >= 2 && parts[0] == "switchport" {
147 if parts[1] == "mode" {
148 if port.PortMode != tpb.SwitchPort_PORTMODE_INVALID {
149 return fmt.Errorf("redefinition of switchport mode")
150 }
151 if parts[2] == "access" {
152 port.PortMode = tpb.SwitchPort_PORTMODE_SWITCHPORT_UNTAGGED
153 } else if parts[2] == "trunk" {
154 port.PortMode = tpb.SwitchPort_PORTMODE_SWITCHPORT_TAGGED
155 } else if parts[2] == "general" {
156 port.PortMode = tpb.SwitchPort_PORTMODE_SWITCHPORT_GENERIC
157 } else {
158 port.PortMode = tpb.SwitchPort_PORTMODE_MANGLED
159 }
160 }
161
162 if parts[1] == "access" {
163 if port.PortMode == tpb.SwitchPort_PORTMODE_INVALID {
164 port.PortMode = tpb.SwitchPort_PORTMODE_SWITCHPORT_UNTAGGED
165 }
166 if len(parts) > 3 && parts[2] == "vlan" {
167 vlan, err := strconv.Atoi(parts[3])
168 if err != nil {
169 return fmt.Errorf("invalid vlan: %q", parts[3])
170 }
171 port.VlanNative = int32(vlan)
172 }
173 }
174
175 if parts[1] == "trunk" {
176 if len(parts) >= 5 && parts[2] == "allowed" && parts[3] == "vlan" {
177 vlans := strings.Split(parts[4], ",")
178 for _, vlan := range vlans {
179 vlanNum, err := strconv.Atoi(vlan)
180 if err != nil {
181 return fmt.Errorf("invalid vlan: %q", parts[3])
182 }
183 port.VlanTagged = append(port.VlanTagged, int32(vlanNum))
184 }
185 }
186 }
187 } else if len(parts) >= 2 && parts[0] == "mtu" {
188 mtu, err := strconv.Atoi(parts[1])
189 if err != nil {
190 return fmt.Errorf("invalid mtu: %q", parts[3])
191 }
192 port.Mtu = int32(mtu)
193 } else if len(parts) >= 2 && parts[0] == "spanning-tree" && parts[1] == "portfast" {
194 port.SpanningTreeMode = tpb.SwitchPort_SPANNING_TREE_MODE_PORTFAST
195 }
196 }
197
198 // no mode -> access
199 if port.PortMode == tpb.SwitchPort_PORTMODE_INVALID {
200 port.PortMode = tpb.SwitchPort_PORTMODE_SWITCHPORT_UNTAGGED
201 }
202
203 // apply defaults
204 if port.Mtu == 0 {
205 port.Mtu = 1500
206 }
207 if port.SpanningTreeMode == tpb.SwitchPort_SPANNING_TREE_MODE_INVALID {
208 port.SpanningTreeMode = tpb.SwitchPort_SPANNING_TREE_MODE_AUTO_PORTFAST
209 }
210
211 // sanitize
212 if port.PortMode == tpb.SwitchPort_PORTMODE_SWITCHPORT_UNTAGGED {
213 port.VlanTagged = []int32{}
214 port.Prefixes = []string{}
215 if port.VlanNative == 0 {
216 port.VlanNative = 1
217 }
218 } else if port.PortMode == tpb.SwitchPort_PORTMODE_SWITCHPORT_TAGGED {
219 port.VlanNative = 0
220 port.Prefixes = []string{}
221 } else if port.PortMode == tpb.SwitchPort_PORTMODE_SWITCHPORT_GENERIC {
222 port.Prefixes = []string{}
223 if port.VlanNative == 0 {
224 port.VlanNative = 1
225 }
226 }
227 return nil
228}
229
230func (s *service) GetPorts(ctx context.Context, req *tpb.GetPortsRequest) (*tpb.GetPortsResponse, error) {
231 cli, err := s.connect()
232 if err != nil {
233 return nil, status.Error(codes.Unavailable, "could not connect to switch")
234 }
235 defer s.disconnect()
236 res := &tpb.GetPortsResponse{}
237
238 statusLines, _, err := cli.runCommand(ctx, "show interface status")
239 if err != nil {
240 return nil, status.Error(codes.Unavailable, "could not get interface status from switch")
241 }
242
243 err = s.parseInterfaceStatus(res, statusLines)
244 if err != nil {
245 return nil, status.Errorf(codes.Unavailable, "could not parse interface status from switch: %v", err)
246 }
247
248 for _, port := range res.Ports {
249 configLines, _, err := cli.runCommand(ctx, "show run interface "+port.Name)
250 if err != nil {
251 return nil, status.Error(codes.Unavailable, "could not get interface config from switch")
252 }
253 err = s.parseInterfaceConfig(port, configLines)
254 if err != nil {
255 return nil, status.Errorf(codes.Unavailable, "could not parse interface config from switch: %v", err)
256 }
257 }
258
259 return res, nil
260}
261
Sergiusz Bazanskiff5af692018-08-29 19:20:46 +0100262func main() {
263 flag.StringVar(&flagListenAddress, "listen_address", "127.0.0.1:42000", "Address to listen on for gRPC")
264 flag.StringVar(&flagDebugAddress, "debug_address", "127.0.0.1:42001", "Address to listen on for Debug HTTP")
265 flag.StringVar(&flagSwitchAddress, "switch_address", "127.0.0.1:23", "Telnet address of M6220")
266 flag.StringVar(&flagSwitchUsername, "switch_username", "admin", "Switch login username")
267 flag.StringVar(&flagSwitchPassword, "switch_password", "admin", "Switch login password")
268 flag.Parse()
269
270 s := &service{
271 connectionSemaphore: make(chan int, 1),
272 }
273
274 grpc.EnableTracing = true
275 grpcLis, err := net.Listen("tcp", flagListenAddress)
276 if err != nil {
277 glog.Exitf("Could not listen on %v: %v", flagListenAddress, err)
278 }
279 grpcSrv := grpc.NewServer(hspki.WithServerHSPKI()...)
280 pb.RegisterM6220ProxyServer(grpcSrv, s)
Serge Bazanskic1515cb2018-10-04 10:36:29 +0100281 tpb.RegisterSwitchControlServer(grpcSrv, s)
Sergiusz Bazanskiff5af692018-08-29 19:20:46 +0100282 reflection.Register(grpcSrv)
283
284 glog.Infof("Starting gRPC on %v", flagListenAddress)
285 go func() {
286 if err := grpcSrv.Serve(grpcLis); err != nil {
287 glog.Exitf("Could not start gRPC: %v", err)
288 }
289 }()
290
291 if flagDebugAddress != "" {
292 glog.Infof("Starting debug on %v", flagDebugAddress)
293 httpMux := http.NewServeMux()
294 httpMux.HandleFunc("/debug/status", statusz.StatusHandler)
295 httpMux.HandleFunc("/debug/requests", trace.Traces)
296 httpMux.HandleFunc("/", statusz.StatusHandler)
297
298 httpLis, err := net.Listen("tcp", flagDebugAddress)
299 if err != nil {
300 glog.Exitf("Could not listen on %v: %v", flagDebugAddress, err)
301 }
302 httpSrv := &http.Server{
303 Addr: flagDebugAddress,
304 Handler: httpMux,
305 }
306
307 go func() {
308 if err := httpSrv.Serve(httpLis); err != nil {
309 glog.Exitf("Could not start HTTP server: %v", err)
310 }
311 }()
312
313 }
314
315 glog.Infof("Running!")
316
317 select {}
318}