Merge branch 'master' of /home/q3k/Projects/hscloud/go/src/code.hackerspace.pl/q3k/arista-proxy
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
+}