forked from hswaw/hscloud
Initial Commit
This commit is contained in:
commit
e08e6da775
11 changed files with 404 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
arista-proxy
|
||||||
|
*swp
|
24
arista.proto
Normal file
24
arista.proto
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
service AristaProxy {
|
||||||
|
rpc ShowVersion(ShowVersionRequest) returns (ShowVersionResponse);
|
||||||
|
};
|
143
grpc.go
Normal file
143
grpc.go
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
"github.com/q3k/statusz"
|
||||||
|
"golang.org/x/net/trace"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/reflection"
|
||||||
|
|
||||||
|
pb "code.hackerspace.pl/q3k/arista-proxy/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serverOpts struct {
|
||||||
|
listenAddress string
|
||||||
|
debugAddress string
|
||||||
|
tlsCAPath string
|
||||||
|
tlsCertificatePath string
|
||||||
|
tlsKeyPath string
|
||||||
|
pkiRealm string
|
||||||
|
}
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
arista *aristaClient
|
||||||
|
opts *serverOpts
|
||||||
|
|
||||||
|
grpc struct {
|
||||||
|
listen net.Listener
|
||||||
|
server *grpc.Server
|
||||||
|
}
|
||||||
|
http struct {
|
||||||
|
listen net.Listener
|
||||||
|
server *http.Server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServer(opts *serverOpts, arista *aristaClient) (*server, error) {
|
||||||
|
return &server{
|
||||||
|
opts: opts,
|
||||||
|
arista: arista,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) trace(ctx context.Context, f string, args ...interface{}) {
|
||||||
|
tr, ok := trace.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
fmtd := fmt.Sprintf(f, args...)
|
||||||
|
glog.Warningf("No trace in %v: %s", ctx, fmtd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tr.LazyPrintf(f, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) setupGRPC(options ...grpc.ServerOption) error {
|
||||||
|
serverCert, err := tls.LoadX509KeyPair(s.opts.tlsCertificatePath, s.opts.tlsKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("while loading keypair: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certPool := x509.NewCertPool()
|
||||||
|
ca, err := ioutil.ReadFile(s.opts.tlsCAPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("while loading ca certificate: %v", err)
|
||||||
|
}
|
||||||
|
if ok := certPool.AppendCertsFromPEM(ca); !ok {
|
||||||
|
return fmt.Errorf("while appending ca certificate to pool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", s.opts.listenAddress)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("while listening on main port: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
creds := credentials.NewTLS(&tls.Config{
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
Certificates: []tls.Certificate{serverCert},
|
||||||
|
ClientCAs: certPool,
|
||||||
|
})
|
||||||
|
|
||||||
|
s.grpc.listen = lis
|
||||||
|
options = append([]grpc.ServerOption{grpc.Creds(creds)}, options...)
|
||||||
|
s.grpc.server = grpc.NewServer(options...)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) setupDebugHTTP(mux http.Handler) error {
|
||||||
|
lis, err := net.Listen("tcp", s.opts.debugAddress)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("while listening on main port: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.http.listen = lis
|
||||||
|
s.http.server = &http.Server{
|
||||||
|
Addr: s.opts.debugAddress,
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) serveForever() {
|
||||||
|
grpc.EnableTracing = true
|
||||||
|
|
||||||
|
if err := s.setupGRPC(grpc.UnaryInterceptor(s.unaryInterceptor)); err != nil {
|
||||||
|
glog.Exitf("Could not setup GRPC server: %v", err)
|
||||||
|
}
|
||||||
|
pb.RegisterAristaProxyServer(s.grpc.server, s)
|
||||||
|
reflection.Register(s.grpc.server)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := s.grpc.server.Serve(s.grpc.listen); err != nil {
|
||||||
|
glog.Exitf("Could not start GRPC server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
glog.Infof("Listening for GRPC on %v", s.opts.listenAddress)
|
||||||
|
|
||||||
|
httpMux := http.NewServeMux()
|
||||||
|
httpMux.HandleFunc("/debug/status", statusz.StatusHandler)
|
||||||
|
httpMux.HandleFunc("/debug/requests", trace.Traces)
|
||||||
|
httpMux.HandleFunc("/", statusz.StatusHandler)
|
||||||
|
|
||||||
|
if err := s.setupDebugHTTP(httpMux); err != nil {
|
||||||
|
glog.Exitf("Could not setup HTTP server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := s.http.server.Serve(s.http.listen); err != nil {
|
||||||
|
glog.Exitf("Could not start HTTP server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
glog.Infof("Listening for HTTP on %v", s.opts.debugAddress)
|
||||||
|
|
||||||
|
select {}
|
||||||
|
}
|
73
main.go
Normal file
73
main.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
"github.com/ybbus/jsonrpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagAristaAPI string
|
||||||
|
flagListenAddress string
|
||||||
|
flagDebugAddress string
|
||||||
|
flagCAPath string
|
||||||
|
flagCertificatePath string
|
||||||
|
flagKeyPath string
|
||||||
|
flagPKIRealm 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&flagAristaAPI, "arista_api", "http://admin:password@1.2.3.4:80/command-api", "Arista remote endpoint")
|
||||||
|
flag.StringVar(&flagListenAddress, "listen_address", "127.0.0.1:8080", "gRPC listen address")
|
||||||
|
flag.StringVar(&flagDebugAddress, "debug_address", "127.0.0.1:8081", "Debug HTTP listen address, or empty to disable")
|
||||||
|
flag.StringVar(&flagCAPath, "tls_ca_path", "pki/ca.pem", "Path to PKI CA certificate")
|
||||||
|
flag.StringVar(&flagCertificatePath, "tls_certificate_path", "pki/service.pem", "Path to PKI service certificate")
|
||||||
|
flag.StringVar(&flagKeyPath, "tls_key_path", "pki/service-key.pem", "Path to PKI service private key")
|
||||||
|
flag.StringVar(&flagPKIRealm, "pki_realm", "svc.cluster.local", "PKI realm")
|
||||||
|
flag.Set("logtostderr", "true")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
arista := &aristaClient{
|
||||||
|
rpc: jsonrpc.NewClient(flagAristaAPI),
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &serverOpts{
|
||||||
|
listenAddress: flagListenAddress,
|
||||||
|
debugAddress: flagDebugAddress,
|
||||||
|
tlsCAPath: flagCAPath,
|
||||||
|
tlsCertificatePath: flagCertificatePath,
|
||||||
|
tlsKeyPath: flagKeyPath,
|
||||||
|
pkiRealm: flagPKIRealm,
|
||||||
|
}
|
||||||
|
server, err := newServer(opts, arista)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("Could not create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
glog.Info("Starting up...")
|
||||||
|
server.serveForever()
|
||||||
|
}
|
88
pki.go
Normal file
88
pki.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/trace"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/peer"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type clientPKIInfo struct {
|
||||||
|
realm string
|
||||||
|
principal string
|
||||||
|
job string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientPKIInfo) String() string {
|
||||||
|
return fmt.Sprintf("job=%q, principal=%q, realm=%q", c.job, c.principal, c.realm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseClientName(realm, name string) (*clientPKIInfo, error) {
|
||||||
|
if !strings.HasSuffix(name, "."+realm) {
|
||||||
|
return nil, fmt.Errorf("invalid realm")
|
||||||
|
}
|
||||||
|
service := strings.TrimSuffix(name, "."+realm)
|
||||||
|
parts := strings.Split(service, ".")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, fmt.Errorf("invalid service")
|
||||||
|
}
|
||||||
|
return &clientPKIInfo{
|
||||||
|
realm: realm,
|
||||||
|
principal: parts[1],
|
||||||
|
job: parts[0],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ctxKeyPKIInfo = "hscloud-pki-info"
|
||||||
|
)
|
||||||
|
|
||||||
|
func withPKIInfo(ctx context.Context, c *clientPKIInfo) context.Context {
|
||||||
|
tr, ok := trace.FromContext(ctx)
|
||||||
|
if ok {
|
||||||
|
tr.LazyPrintf("PKI Peer: %s", c.String())
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, ctxKeyPKIInfo, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
|
||||||
|
peer, ok := peer.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
s.trace(ctx, "Could not establish identity of peer.")
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "no peer info")
|
||||||
|
}
|
||||||
|
|
||||||
|
authInfo, ok := peer.AuthInfo.(credentials.TLSInfo)
|
||||||
|
if !ok {
|
||||||
|
s.trace(ctx, "Could not establish TLS identity of peer.")
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "no TLS peer info")
|
||||||
|
}
|
||||||
|
|
||||||
|
chains := authInfo.State.VerifiedChains
|
||||||
|
if len(chains) != 1 {
|
||||||
|
s.trace(ctx, "No trusted chain found.")
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "invalid TLS certificate")
|
||||||
|
}
|
||||||
|
chain := chains[0]
|
||||||
|
|
||||||
|
certDNs := make([]string, len(chain))
|
||||||
|
for i, cert := range chain {
|
||||||
|
certDNs[i] = cert.Subject.String()
|
||||||
|
}
|
||||||
|
s.trace(ctx, "TLS chain: %s", strings.Join(certDNs, ", "))
|
||||||
|
|
||||||
|
clientInfo, err := parseClientName(s.opts.pkiRealm, chain[0].Subject.CommonName)
|
||||||
|
if err != nil {
|
||||||
|
s.trace(ctx, "Could not parse certificate DN: %v", err)
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "invalid TLS CommonName")
|
||||||
|
}
|
||||||
|
ctx = withPKIInfo(ctx, clientInfo)
|
||||||
|
|
||||||
|
return handler(ctx, req)
|
||||||
|
}
|
3
pki/.gitignore
vendored
Normal file
3
pki/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
*csr
|
||||||
|
*pem
|
||||||
|
*json
|
6
pki/clean.sh
Executable file
6
pki/clean.sh
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e -x
|
||||||
|
|
||||||
|
rm *pem
|
||||||
|
rm *csr
|
7
pki/gen.sh
Executable file
7
pki/gen.sh
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e -x
|
||||||
|
|
||||||
|
test -f ca.pem || ( cfssl gencert -initca ca_csr.json | cfssljson -bare ca )
|
||||||
|
test -f service.pem || ( cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca_config.json -profile=test service_csr.json | cfssljson -bare service )
|
||||||
|
test -f client.pem || ( cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca_config.json -profile=test client_csr.json | cfssljson -bare client )
|
1
proto/.gitignore
vendored
Normal file
1
proto/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
arista.pb.go
|
3
proto/generate.go
Normal file
3
proto/generate.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
//go:generate protoc -I.. ../arista.proto --go_out=plugins=grpc:.
|
||||||
|
|
||||||
|
package proto
|
54
service.go
Normal file
54
service.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
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
|
||||||
|
}
|
Loading…
Reference in a new issue