Merge branch 'master' of /home/q3k/Projects/hscloud/go/src/code.hackerspace.pl/q3k/cmc-proxy
diff --git a/cmc-proxy/README.md b/cmc-proxy/README.md
new file mode 100644
index 0000000..1efab29
--- /dev/null
+++ b/cmc-proxy/README.md
@@ -0,0 +1,39 @@
+Dell M1000e gRPC Proxy
+======================
+
+Cursedness level: 6.5/10 (regexp XML parsing, JSONP scraping, limited sessions).
+
+This is a small gRPC proxy to allow programmatic access to a Dell M1000e Chassis Management Controller. It's based on scraping the web interface, as the alternative (WSMAN) is even more ridiculous.
+
+Functionality
+-------------
+
+The only feature supported so far is getting information for an iDRAC KVM console. This can be used to run a iDRAC KVM proxy (to be implemented), or the original client.
+
+Usage
+-----
+
+    ./cmc-proxy -h
+
+Flags are self-explanatory. This is based on [hspki](https://code.hackerspace.pl/q3k/hspki), so you'll need to have compatible (dev) certs to run this. The proxy listens on gRPC and a status HTTP debug server.
+
+Example
+-------
+
+    $ grpc-dev -d '{"blade_num": 6}' cmc.q3k.svc.cluster.local:4200 proto.CMCProxy.GetKVMData
+    {
+      "arguments": [
+        "10.10.10.16:443",
+        "5901",
+        "oojo2obohhaWiu3A",
+        "1",
+        "0",
+        "3668",
+        "3669",
+        "511",
+        "5900",
+        "1",
+        "EN"
+      ]
+    }
+
diff --git a/cmc-proxy/client.go b/cmc-proxy/client.go
new file mode 100644
index 0000000..e22c97c
--- /dev/null
+++ b/cmc-proxy/client.go
@@ -0,0 +1,346 @@
+package main
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"regexp"
+	"strings"
+
+	"code.hackerspace.pl/q3k/mirko"
+	"github.com/cenkalti/backoff"
+	"github.com/golang/glog"
+)
+
+var (
+	reSessionCookie = regexp.MustCompile("'SESSION_COOKIE' : '([^']*)'")
+	reIpmiPriv      = regexp.MustCompile("'IPMI_PRIV' : ([^,]*)")
+	reExtPriv       = regexp.MustCompile("'EXT_PRIV' : ([^,]*)")
+	reSystemModel   = regexp.MustCompile("'SYSTEM_MODEL' : '([^']*)'")
+	reArgument      = regexp.MustCompile("<argument>([^<]*)</argument>")
+)
+
+var (
+	ErrorNoFreeSlot = fmt.Errorf("iDRAC reports no free slot")
+)
+
+type cmcRequestType int
+
+const (
+	cmcRequestKVMDetails cmcRequestType = iota
+)
+
+type cmcResponse struct {
+	data interface{}
+	err  error
+}
+
+type cmcRequest struct {
+	t        cmcRequestType
+	req      interface{}
+	res      chan cmcResponse
+	canceled bool
+}
+
+type KVMDetails struct {
+	arguments []string
+}
+
+type cmcClient struct {
+	session string
+	req     chan *cmcRequest
+}
+
+func (c *cmcClient) RequestKVMDetails(ctx context.Context, slot int) (*KVMDetails, error) {
+	r := &cmcRequest{
+		t:   cmcRequestKVMDetails,
+		req: slot,
+		res: make(chan cmcResponse, 1),
+	}
+	mirko.Trace(ctx, "cmcRequestKVMDetails: requesting...")
+	c.req <- r
+	mirko.Trace(ctx, "cmcRequestKVMDetails: requested.")
+
+	select {
+	case <-ctx.Done():
+		r.canceled = true
+		return nil, context.Canceled
+	case res := <-r.res:
+		mirko.Trace(ctx, "cmcRequestKVMDetails: got response")
+		if res.err != nil {
+			return nil, res.err
+		}
+		return res.data.(*KVMDetails), nil
+	}
+}
+
+func NewCMCClient() *cmcClient {
+	return &cmcClient{
+		req: make(chan *cmcRequest, 4),
+	}
+}
+
+func (c *cmcClient) Run(ctx context.Context) {
+	for {
+		select {
+		case <-ctx.Done():
+			c.logout()
+			return
+		case msg := <-c.req:
+			c.handle(msg)
+		}
+	}
+}
+
+func (c *cmcClient) handle(r *cmcRequest) {
+	switch {
+	case r.t == cmcRequestKVMDetails:
+		var details *KVMDetails
+		slot := r.req.(int)
+		err := backoff.Retry(func() error {
+			if err := c.login(); err != nil {
+				return err
+			}
+			url, err := c.getiDRACURL(slot)
+			if err != nil {
+				return err
+			}
+			details, err = c.getiDRACJNLP(url)
+			return err
+		}, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 2))
+
+		if err != nil {
+			r.res <- cmcResponse{err: err}
+		}
+
+		r.res <- cmcResponse{data: details}
+	default:
+		panic("invalid cmcRequestType")
+	}
+}
+
+func makeUrl(path string) string {
+	if strings.HasSuffix(flagCMCAddress, "/") {
+		return flagCMCAddress + path
+	}
+	return flagCMCAddress + "/" + path
+}
+
+func (c *cmcClient) transport() *http.Transport {
+	return &http.Transport{
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+	}
+}
+
+func (c *cmcClient) addCookies(req *http.Request) {
+	req.AddCookie(&http.Cookie{Name: "custom_domain", Value: ""})
+	req.AddCookie(&http.Cookie{Name: "domain_selected", Value: "This Chassis"})
+	if c.session != "" {
+		glog.Infof("Session cookie: %v", c.session)
+		req.AddCookie(&http.Cookie{Name: "sid", Value: c.session})
+	}
+}
+
+func (c *cmcClient) getiDRACURL(slot int) (string, error) {
+	if c.session == "" {
+		return "", fmt.Errorf("not logged in")
+	}
+
+	url := makeUrl(pathiDRACURL) + fmt.Sprintf("?vKVM=1&serverSlot=%d", slot)
+
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return "", fmt.Errorf("GET prepare to %s failed: %v", pathLogin, err)
+	}
+	c.addCookies(req)
+
+	cl := &http.Client{
+		Transport: c.transport(),
+		CheckRedirect: func(req *http.Request, via []*http.Request) error {
+			return http.ErrUseLastResponse
+		},
+	}
+	resp, err := cl.Do(req)
+	if err != nil {
+		return "", fmt.Errorf("GET to %s failed: %v", pathLogin, err)
+	}
+
+	if resp.StatusCode != 302 {
+		return "", fmt.Errorf("expected 302 on iDRAC URL redirect, got %v instead", resp.Status)
+	}
+
+	loc, _ := resp.Location()
+
+	if !strings.Contains(loc.String(), "cmc_sess_id") {
+		c.session = ""
+		return "", fmt.Errorf("redirect URL contains no session ID - session timed out?")
+	}
+
+	return loc.String(), nil
+}
+
+func (c *cmcClient) getiDRACJNLP(loginUrl string) (*KVMDetails, error) {
+	lurl, err := url.Parse(loginUrl)
+	if err != nil {
+		return nil, err
+	}
+
+	sessid := lurl.Query().Get("cmc_sess_id")
+	if sessid == "" {
+		return nil, fmt.Errorf("no cmc_sess_id in iDRAC login URL")
+	}
+
+	createURL := *lurl
+	createURL.Path = "/Applications/dellUI/RPC/WEBSES/create.asp"
+	createURL.RawQuery = ""
+
+	values := url.Values{}
+	values.Set("WEBVAR_USERNAME", "cmc")
+	values.Set("WEBVAR_PASSWORD", sessid)
+	values.Set("WEBVAR_ISCMCLOGIN", "1")
+	valuesString := values.Encode()
+	req, err := http.NewRequest("POST", createURL.String(), strings.NewReader(valuesString))
+
+	cl := &http.Client{
+		Transport: c.transport(),
+		CheckRedirect: func(req *http.Request, via []*http.Request) error {
+			return http.ErrUseLastResponse
+		},
+	}
+	resp, err := cl.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	data, _ := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	first := func(v [][]byte) string {
+		if len(v) < 1 {
+			return ""
+		}
+		return string(v[1])
+	}
+
+	sessionCookie := first(reSessionCookie.FindSubmatch(data))
+	ipmiPriv := first(reIpmiPriv.FindSubmatch(data))
+	extPriv := first(reExtPriv.FindSubmatch(data))
+	systemModel := first(reSystemModel.FindSubmatch(data))
+
+	if sessionCookie == "Failure_No_Free_Slot" {
+		return nil, ErrorNoFreeSlot
+	}
+
+	jnlpURL := *lurl
+	jnlpURL.Path = "/Applications/dellUI/Java/jviewer.jnlp"
+	jnlpURL.RawQuery = ""
+
+	req, err = http.NewRequest("GET", jnlpURL.String(), nil)
+	for _, cookie := range resp.Cookies() {
+		req.AddCookie(cookie)
+	}
+	req.AddCookie(&http.Cookie{Name: "SessionCookie", Value: sessionCookie})
+	req.AddCookie(&http.Cookie{Name: "SessionCookieUser", Value: "cmc"})
+	req.AddCookie(&http.Cookie{Name: "IPMIPriv", Value: ipmiPriv})
+	req.AddCookie(&http.Cookie{Name: "ExtPriv", Value: extPriv})
+	req.AddCookie(&http.Cookie{Name: "SystemModel", Value: systemModel})
+
+	resp, err = cl.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	data, err = ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	// yes we do parse xml with regex why are you asking
+	matches := reArgument.FindAllSubmatch(data, -1)
+
+	res := &KVMDetails{
+		arguments: []string{},
+	}
+	for _, match := range matches {
+		res.arguments = append(res.arguments, string(match[1]))
+	}
+
+	return res, nil
+}
+
+func (c *cmcClient) login() error {
+	if c.session != "" {
+		return nil
+	}
+
+	values := url.Values{}
+	values.Set("ST2", "NOTSET")
+	values.Set("user", flagCMCUsername)
+	values.Set("user_id", flagCMCUsername)
+	values.Set("password", flagCMCPassword)
+	values.Set("WEBSERVER_timeout", "1800")
+	values.Set("WEBSERVER_timeout_select", "1800")
+	valuesString := values.Encode()
+	req, err := http.NewRequest("POST", makeUrl(pathLogin), strings.NewReader(valuesString))
+	if err != nil {
+		return fmt.Errorf("POST prepare to %s failed: %v", pathLogin, err)
+	}
+	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+	c.addCookies(req)
+
+	cl := &http.Client{
+		Transport: c.transport(),
+		CheckRedirect: func(req *http.Request, via []*http.Request) error {
+			return http.ErrUseLastResponse
+		},
+	}
+	resp, err := cl.Do(req)
+	if err != nil {
+		return fmt.Errorf("POST to %s failed: %v", pathLogin, err)
+	}
+	glog.Infof("Login response: %s", resp.Status)
+	defer resp.Body.Close()
+	for _, cookie := range resp.Cookies() {
+		if cookie.Name == "sid" {
+			c.session = cookie.Value
+			break
+		}
+	}
+	if c.session == "" {
+		return fmt.Errorf("login unsuccesful")
+	}
+	return nil
+}
+
+func (c *cmcClient) logout() {
+	glog.Infof("Killing session..")
+	if c.session == "" {
+		return
+	}
+
+	req, err := http.NewRequest("GET", makeUrl(pathLogout), nil)
+	if err != nil {
+		glog.Errorf("GET prepare to %s failed: %v", pathLogin, err)
+	}
+	c.addCookies(req)
+
+	cl := &http.Client{
+		Transport: c.transport(),
+		CheckRedirect: func(req *http.Request, via []*http.Request) error {
+			return http.ErrUseLastResponse
+		},
+	}
+	resp, err := cl.Do(req)
+	if err != nil {
+		glog.Errorf("GET to %s failed: %v", pathLogin, err)
+	}
+	glog.Infof("Logout response: %s", resp.Status)
+	return
+}
diff --git a/cmc-proxy/main.go b/cmc-proxy/main.go
new file mode 100644
index 0000000..266de97
--- /dev/null
+++ b/cmc-proxy/main.go
@@ -0,0 +1,75 @@
+package main
+
+import (
+	"context"
+	"flag"
+
+	"code.hackerspace.pl/q3k/mirko"
+	"github.com/golang/glog"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+
+	pb "code.hackerspace.pl/q3k/cmc-proxy/proto"
+)
+
+var (
+	flagCMCAddress  string
+	flagCMCUsername string
+	flagCMCPassword string
+)
+
+const (
+	pathLogin    = "cgi-bin/webcgi/login"
+	pathLogout   = "cgi-bin/webcgi/logout"
+	pathiDRACURL = "cgi-bin/webcgi/blade_iDRAC_url"
+)
+
+func init() {
+	flag.Set("logtostderr", "true")
+}
+
+type service struct {
+	cmc *cmcClient
+}
+
+func (s *service) GetKVMData(ctx context.Context, req *pb.GetKVMDataRequest) (*pb.GetKVMDataResponse, error) {
+	if req.BladeNum < 1 || req.BladeNum > 16 {
+		return nil, status.Error(codes.InvalidArgument, "blade_num must be [1,16]")
+	}
+
+	details, err := s.cmc.RequestKVMDetails(ctx, int(req.BladeNum))
+	if err != nil {
+		glog.Errorf("RequestKVMDetails(_, %d): %v", req.BladeNum, err)
+		return nil, status.Error(codes.Unavailable, "CMC unavailable")
+	}
+
+	return &pb.GetKVMDataResponse{
+		Arguments: details.arguments,
+	}, nil
+}
+
+func main() {
+	flag.StringVar(&flagCMCAddress, "cmc_address", "https://10.10.10.10", "URL of Dell M1000e CMC")
+	flag.StringVar(&flagCMCUsername, "cmc_username", "root", "Login username for CMC")
+	flag.StringVar(&flagCMCPassword, "cmc_password", "", "Login password for CMC")
+	flag.Parse()
+
+	m := mirko.New()
+	if err := m.Listen(); err != nil {
+		glog.Exitf("Could not listen: %v", err)
+	}
+
+	s := &service{
+		cmc: NewCMCClient(),
+	}
+	pb.RegisterCMCProxyServer(m.GRPC(), s)
+
+	if err := m.Serve(); err != nil {
+		glog.Exitf("Could not run: %v", err)
+	}
+
+	go s.cmc.Run(m.Context())
+	glog.Info("Running.")
+
+	<-m.Done()
+}
diff --git a/cmc-proxy/proto/generate.go b/cmc-proxy/proto/generate.go
new file mode 100644
index 0000000..fc6193d
--- /dev/null
+++ b/cmc-proxy/proto/generate.go
@@ -0,0 +1,3 @@
+//go:generate protoc -I.. ../proxy.proto --go_out=plugins=grpc:.
+
+package proto
diff --git a/cmc-proxy/proto/proxy.pb.go b/cmc-proxy/proto/proxy.pb.go
new file mode 100644
index 0000000..ff1d00b
--- /dev/null
+++ b/cmc-proxy/proto/proxy.pb.go
@@ -0,0 +1,194 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: proxy.proto
+
+package proto
+
+import (
+	fmt "fmt"
+	proto "github.com/golang/protobuf/proto"
+	context "golang.org/x/net/context"
+	grpc "google.golang.org/grpc"
+	math "math"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
+
+type GetKVMDataRequest struct {
+	BladeNum             int64    `protobuf:"varint,1,opt,name=blade_num,json=bladeNum,proto3" json:"blade_num,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *GetKVMDataRequest) Reset()         { *m = GetKVMDataRequest{} }
+func (m *GetKVMDataRequest) String() string { return proto.CompactTextString(m) }
+func (*GetKVMDataRequest) ProtoMessage()    {}
+func (*GetKVMDataRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_700b50b08ed8dbaf, []int{0}
+}
+
+func (m *GetKVMDataRequest) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_GetKVMDataRequest.Unmarshal(m, b)
+}
+func (m *GetKVMDataRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_GetKVMDataRequest.Marshal(b, m, deterministic)
+}
+func (m *GetKVMDataRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_GetKVMDataRequest.Merge(m, src)
+}
+func (m *GetKVMDataRequest) XXX_Size() int {
+	return xxx_messageInfo_GetKVMDataRequest.Size(m)
+}
+func (m *GetKVMDataRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_GetKVMDataRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_GetKVMDataRequest proto.InternalMessageInfo
+
+func (m *GetKVMDataRequest) GetBladeNum() int64 {
+	if m != nil {
+		return m.BladeNum
+	}
+	return 0
+}
+
+type GetKVMDataResponse struct {
+	Arguments            []string `protobuf:"bytes,1,rep,name=arguments,proto3" json:"arguments,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *GetKVMDataResponse) Reset()         { *m = GetKVMDataResponse{} }
+func (m *GetKVMDataResponse) String() string { return proto.CompactTextString(m) }
+func (*GetKVMDataResponse) ProtoMessage()    {}
+func (*GetKVMDataResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_700b50b08ed8dbaf, []int{1}
+}
+
+func (m *GetKVMDataResponse) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_GetKVMDataResponse.Unmarshal(m, b)
+}
+func (m *GetKVMDataResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_GetKVMDataResponse.Marshal(b, m, deterministic)
+}
+func (m *GetKVMDataResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_GetKVMDataResponse.Merge(m, src)
+}
+func (m *GetKVMDataResponse) XXX_Size() int {
+	return xxx_messageInfo_GetKVMDataResponse.Size(m)
+}
+func (m *GetKVMDataResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_GetKVMDataResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_GetKVMDataResponse proto.InternalMessageInfo
+
+func (m *GetKVMDataResponse) GetArguments() []string {
+	if m != nil {
+		return m.Arguments
+	}
+	return nil
+}
+
+func init() {
+	proto.RegisterType((*GetKVMDataRequest)(nil), "proto.GetKVMDataRequest")
+	proto.RegisterType((*GetKVMDataResponse)(nil), "proto.GetKVMDataResponse")
+}
+
+func init() { proto.RegisterFile("proxy.proto", fileDescriptor_700b50b08ed8dbaf) }
+
+var fileDescriptor_700b50b08ed8dbaf = []byte{
+	// 156 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2e, 0x28, 0xca, 0xaf,
+	0xa8, 0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x05, 0x53, 0x4a, 0x06, 0x5c, 0x82, 0xee,
+	0xa9, 0x25, 0xde, 0x61, 0xbe, 0x2e, 0x89, 0x25, 0x89, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25,
+	0x42, 0xd2, 0x5c, 0x9c, 0x49, 0x39, 0x89, 0x29, 0xa9, 0xf1, 0x79, 0xa5, 0xb9, 0x12, 0x8c, 0x0a,
+	0x8c, 0x1a, 0xcc, 0x41, 0x1c, 0x60, 0x01, 0xbf, 0xd2, 0x5c, 0x25, 0x23, 0x2e, 0x21, 0x64, 0x1d,
+	0xc5, 0x05, 0xf9, 0x79, 0xc5, 0xa9, 0x42, 0x32, 0x5c, 0x9c, 0x89, 0x45, 0xe9, 0xa5, 0xb9, 0xa9,
+	0x79, 0x25, 0xc5, 0x12, 0x8c, 0x0a, 0xcc, 0x1a, 0x9c, 0x41, 0x08, 0x01, 0x23, 0x5f, 0x2e, 0x0e,
+	0x67, 0x5f, 0xe7, 0x00, 0x90, 0xf5, 0x42, 0x8e, 0x5c, 0x5c, 0x08, 0xfd, 0x42, 0x12, 0x10, 0xe7,
+	0xe8, 0x61, 0x38, 0x42, 0x4a, 0x12, 0x8b, 0x0c, 0xc4, 0xb2, 0x24, 0x36, 0xb0, 0x8c, 0x31, 0x20,
+	0x00, 0x00, 0xff, 0xff, 0x09, 0x00, 0x2b, 0x54, 0xd1, 0x00, 0x00, 0x00,
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConn
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion4
+
+// CMCProxyClient is the client API for CMCProxy service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
+type CMCProxyClient interface {
+	GetKVMData(ctx context.Context, in *GetKVMDataRequest, opts ...grpc.CallOption) (*GetKVMDataResponse, error)
+}
+
+type cMCProxyClient struct {
+	cc *grpc.ClientConn
+}
+
+func NewCMCProxyClient(cc *grpc.ClientConn) CMCProxyClient {
+	return &cMCProxyClient{cc}
+}
+
+func (c *cMCProxyClient) GetKVMData(ctx context.Context, in *GetKVMDataRequest, opts ...grpc.CallOption) (*GetKVMDataResponse, error) {
+	out := new(GetKVMDataResponse)
+	err := c.cc.Invoke(ctx, "/proto.CMCProxy/GetKVMData", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// CMCProxyServer is the server API for CMCProxy service.
+type CMCProxyServer interface {
+	GetKVMData(context.Context, *GetKVMDataRequest) (*GetKVMDataResponse, error)
+}
+
+func RegisterCMCProxyServer(s *grpc.Server, srv CMCProxyServer) {
+	s.RegisterService(&_CMCProxy_serviceDesc, srv)
+}
+
+func _CMCProxy_GetKVMData_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(GetKVMDataRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(CMCProxyServer).GetKVMData(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/proto.CMCProxy/GetKVMData",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(CMCProxyServer).GetKVMData(ctx, req.(*GetKVMDataRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+var _CMCProxy_serviceDesc = grpc.ServiceDesc{
+	ServiceName: "proto.CMCProxy",
+	HandlerType: (*CMCProxyServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "GetKVMData",
+			Handler:    _CMCProxy_GetKVMData_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "proxy.proto",
+}
diff --git a/cmc-proxy/proxy.proto b/cmc-proxy/proxy.proto
new file mode 100644
index 0000000..f34b905
--- /dev/null
+++ b/cmc-proxy/proxy.proto
@@ -0,0 +1,15 @@
+syntax = "proto3";
+
+package proto;
+
+message GetKVMDataRequest {
+    int64 blade_num = 1;
+}
+
+message GetKVMDataResponse {
+    repeated string arguments = 1;
+}
+
+service CMCProxy {
+    rpc GetKVMData(GetKVMDataRequest) returns (GetKVMDataResponse);
+}