Merge branch 'master' of /home/q3k/Projects/hscloud/go/src/code.hackerspace.pl/q3k/cmc-proxy

master
Serge Bazanski 2018-10-25 12:20:19 +01:00
commit e032a706b2
6 changed files with 672 additions and 0 deletions

39
cmc-proxy/README.md Normal file
View File

@ -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"
]
}

346
cmc-proxy/client.go Normal file
View File

@ -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
}

75
cmc-proxy/main.go Normal file
View File

@ -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()
}

View File

@ -0,0 +1,3 @@
//go:generate protoc -I.. ../proxy.proto --go_out=plugins=grpc:.
package proto

194
cmc-proxy/proto/proxy.pb.go Normal file
View File

@ -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",
}

15
cmc-proxy/proxy.proto Normal file
View File

@ -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);
}