diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..045c22e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +arista-proxy +*swp diff --git a/arista-proxy/README.md b/arista-proxy/README.md new file mode 100644 index 0000000..ed90285 --- /dev/null +++ b/arista-proxy/README.md @@ -0,0 +1,49 @@ +Old Shitty Arista eAPI/Capi <-> gRPC proxy +========================================== + +Our Arista 7148S does not support gRPC/OpenConfig, so we have to make our own damn gRPC proxy. + +The schema is supposed to be 1:1 mapped to the JSON-RPC EAPI. This is just a dumb proxy. + +PKI +--- + +This service uses [HSPKI](https://code.hackerspace.pl/q3k/hspki), you will need to generate development TLS certificates for local use. + +Getting and Building +-------------------- + + go get -d -u code.hackerspace.pl/q3k/arista-proxy + go generate code.hackerspace.pl/q3k/arista-proxy/proto + go build code.hackerspace.pl/q3k/arista-proxy + +Debug Status Page +----------------- + +The `debug_address` flag controls spawning an HTTP server useful for debugging. You can use it to inspect gRPC request and view general status information of the proxy. + +Flags +----- + + ./arista-proxy -help + Usage of ./arista-proxy: + -alsologtostderr + log to standard error as well as files + -arista_api string + Arista remote endpoint (default "http://admin:password@1.2.3.4:80/command-api") + -debug_address string + Debug HTTP listen address, or empty to disable (default "127.0.0.1:42000") + -listen_address string + gRPC listen address (default "127.0.0.1:43001") + -log_backtrace_at value + when logging hits line file:N, emit a stack trace + -log_dir string + If non-empty, write log files in this directory + -logtostderr + log to standard error instead of files + -stderrthreshold value + logs at or above this threshold go to stderr + -v value + log level for V logs + -vmodule value + comma-separated list of pattern=N settings for file-filtered logging diff --git a/arista-proxy/arista.proto b/arista-proxy/arista.proto new file mode 100644 index 0000000..d6bf105 --- /dev/null +++ b/arista-proxy/arista.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package proto; + +message ShowVersionRequest { +}; + +message ShowVersionResponse { + string model_name = 1; + string internal_version = 2; + string system_mac_address = 3; + string serial_number = 4; + int64 mem_total = 5; + double bootup_timestamp = 6; + int64 mem_free = 7; + string version = 8; + string architecture = 9; + string internal_build_id = 10; + string hardware_revision = 11; +}; + +message ShowEnvironmentTemperatureRequest { +}; + +message ShowEnvironmentTemperatureResponse { +}; + +service AristaProxy { + rpc ShowVersion(ShowVersionRequest) returns (ShowVersionResponse); + rpc ShowEnvironmentTemperature(ShowEnvironmentTemperatureRequest) returns (ShowEnvironmentTemperatureResponse); +}; diff --git a/arista-proxy/main.go b/arista-proxy/main.go new file mode 100644 index 0000000..f417f1f --- /dev/null +++ b/arista-proxy/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "flag" + "fmt" + + "code.hackerspace.pl/q3k/mirko" + "github.com/golang/glog" + "github.com/ybbus/jsonrpc" + + pb "code.hackerspace.pl/q3k/arista-proxy/proto" +) + +var ( + flagAristaAPI string +) + +type aristaClient struct { + rpc jsonrpc.RPCClient +} + +func (c *aristaClient) structuredCall(res interface{}, command ...string) error { + cmd := struct { + Version int `json:"version"` + Cmds []string `json:"cmds"` + Format string `json:"format"` + }{ + Version: 1, + Cmds: command, + Format: "json", + } + + err := c.rpc.CallFor(res, "runCmds", cmd) + if err != nil { + return fmt.Errorf("could not execute structured call: %v", err) + } + return nil +} + +type server struct { + arista *aristaClient +} + +func main() { + flag.StringVar(&flagAristaAPI, "arista_api", "http://admin:password@1.2.3.4:80/command-api", "Arista remote endpoint") + flag.Parse() + + arista := &aristaClient{ + rpc: jsonrpc.NewClient(flagAristaAPI), + } + + m := mirko.New() + if err := m.Listen(); err != nil { + glog.Exitf("Listen(): %v", err) + } + + s := &server{ + arista: arista, + } + pb.RegisterAristaProxyServer(m.GRPC(), s) + + if err := m.Serve(); err != nil { + glog.Exitf("Serve(): %v", err) + } + + select {} +} diff --git a/arista-proxy/proto/.gitignore b/arista-proxy/proto/.gitignore new file mode 100644 index 0000000..46ddcab --- /dev/null +++ b/arista-proxy/proto/.gitignore @@ -0,0 +1 @@ +arista.pb.go diff --git a/arista-proxy/proto/generate.go b/arista-proxy/proto/generate.go new file mode 100644 index 0000000..92f2720 --- /dev/null +++ b/arista-proxy/proto/generate.go @@ -0,0 +1,3 @@ +//go:generate protoc -I.. ../arista.proto --go_out=plugins=grpc:. + +package proto diff --git a/arista-proxy/service.go b/arista-proxy/service.go new file mode 100644 index 0000000..62d68ea --- /dev/null +++ b/arista-proxy/service.go @@ -0,0 +1,97 @@ +package main + +import ( + "context" + + "github.com/golang/glog" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + pb "code.hackerspace.pl/q3k/arista-proxy/proto" +) + +func (s *server) ShowVersion(ctx context.Context, req *pb.ShowVersionRequest) (*pb.ShowVersionResponse, error) { + var version []struct { + ModelName string `json:"modelName"` + InternalVersion string `json:"internalVersion"` + SystemMacAddress string `json:"systemMacAddress"` + SerialNumber string `json:"serialNumber"` + MemTotal int64 `json:"memTotal"` + BootupTimestamp float64 `json:"bootupTimestamp"` + MemFree int64 `json:"memFree"` + Version string `json:"version"` + Architecture string `json:"architecture"` + InternalBuildId string `json:"internalBuildId"` + HardwareRevision string `json:"hardwareRevision"` + } + + err := s.arista.structuredCall(&version, "show version") + if err != nil { + glog.Errorf("EOS Capi: show version: %v", err) + return nil, status.Error(codes.Unavailable, "EOS Capi call failed") + } + + if len(version) != 1 { + glog.Errorf("Expected 1-length result, got %d", len(version)) + return nil, status.Error(codes.Internal, "Internal error") + } + + d := version[0] + + return &pb.ShowVersionResponse{ + ModelName: d.ModelName, + InternalVersion: d.InternalVersion, + SystemMacAddress: d.SystemMacAddress, + SerialNumber: d.SerialNumber, + MemTotal: d.MemTotal, + BootupTimestamp: d.BootupTimestamp, + MemFree: d.MemFree, + Version: d.Version, + Architecture: d.Architecture, + InternalBuildId: d.InternalBuildId, + HardwareRevision: d.HardwareRevision, + }, nil +} + +type temperatureSensor struct { + InAlertState bool `json:"inAlertState"` + MaxTemperature float64 `json:"maxTemperature"` + RelPos int64 `json:"relPos"` + Description string `json:"description"` + Name string `json:"name"` + AlertCount int64 `json:"alertCount"` + CurrentTemperature float64 `json:"currentTemperature"` + OverheatThreshold float64 `json:"overheatThreshold"` + CriticalThreshold float64 `json:"criticalThreshold"` + HwStatus string `json:"hwStatus"` +} + +func (s *server) ShowEnvironmentTemperature(ctx context.Context, req *pb.ShowEnvironmentTemperatureRequest) (*pb.ShowEnvironmentTemperatureResponse, error) { + var response []struct { + PowerSuppplySlots []struct { + TempSensors []temperatureSensor `json:"tempSensors"` + EntPhysicalClass string `json:"entPhysicalClass"` + RelPos int64 `json:"relPos"` + } `json:"powerSupplySlots"` + + ShutdownOnOverheat bool `json:"shutdownOnOverheat"` + TempSensors []temperatureSensor `json:"tempSensors"` + SystemStatus string `json:"systemStatus"` + } + + err := s.arista.structuredCall(&response, "show environment temperature") + if err != nil { + glog.Errorf("EOS Capi: show environment temperature: %v", err) + return nil, status.Error(codes.Unavailable, "EOS Capi call failed") + } + + if len(response) != 1 { + glog.Errorf("Expected 1-length result, got %d", len(response)) + return nil, status.Error(codes.Internal, "Internal error") + } + + d := response[0] + glog.Infof("%+v", d) + + return &pb.ShowEnvironmentTemperatureResponse{}, nil +}