go/svc/(dc stuff) -> dc/
We want to start keeping codebases separated per 'team'/intent, to then
have simple OWNER files/trees to specify review rules.
This means dc/ stuff can all be OWNED by q3k, and review will only
involve a +1 for style/readability, instead of a +2 for approval.
Change-Id: I05afbc4e1018944b841ec0d88cd24cc95bec8bf1
diff --git a/dc/cmc-proxy/BUILD.bazel b/dc/cmc-proxy/BUILD.bazel
new file mode 100644
index 0000000..b2f68ca
--- /dev/null
+++ b/dc/cmc-proxy/BUILD.bazel
@@ -0,0 +1,25 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = [
+ "client.go",
+ "main.go",
+ ],
+ importpath = "code.hackerspace.pl/hscloud/dc/cmc-proxy",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//dc/cmc-proxy/proto:go_default_library",
+ "//go/mirko:go_default_library",
+ "@com_github_cenkalti_backoff//:go_default_library",
+ "@com_github_golang_glog//:go_default_library",
+ "@org_golang_google_grpc//codes:go_default_library",
+ "@org_golang_google_grpc//status:go_default_library",
+ ],
+)
+
+go_binary(
+ name = "cmc-proxy",
+ embed = [":go_default_library"],
+ visibility = ["//visibility:public"],
+)
diff --git a/dc/cmc-proxy/README.md b/dc/cmc-proxy/README.md
new file mode 100644
index 0000000..6e063f2
--- /dev/null
+++ b/dc/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. 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/dc/cmc-proxy/client.go b/dc/cmc-proxy/client.go
new file mode 100644
index 0000000..345a7b4
--- /dev/null
+++ b/dc/cmc-proxy/client.go
@@ -0,0 +1,372 @@
+package main
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+
+ "code.hackerspace.pl/hscloud/go/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.logout()
+ 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]))
+ }
+
+ logoutURL := *lurl
+ logoutURL.Path = "/Applications/dellUI/RPC/WEBSES/logout.asp"
+ logoutURL.RawQuery = ""
+
+ req, err = http.NewRequest("GET", logoutURL.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
+ }
+
+ 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/dc/cmc-proxy/main.go b/dc/cmc-proxy/main.go
new file mode 100644
index 0000000..dc0cfb4
--- /dev/null
+++ b/dc/cmc-proxy/main.go
@@ -0,0 +1,75 @@
+package main
+
+import (
+ "context"
+ "flag"
+
+ "code.hackerspace.pl/hscloud/go/mirko"
+ "github.com/golang/glog"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+
+ pb "code.hackerspace.pl/hscloud/dc/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/dc/cmc-proxy/proto/.gitignore b/dc/cmc-proxy/proto/.gitignore
new file mode 100644
index 0000000..3cf12ab
--- /dev/null
+++ b/dc/cmc-proxy/proto/.gitignore
@@ -0,0 +1 @@
+proxy.pb.go
diff --git a/dc/cmc-proxy/proto/BUILD.bazel b/dc/cmc-proxy/proto/BUILD.bazel
new file mode 100644
index 0000000..14b0569
--- /dev/null
+++ b/dc/cmc-proxy/proto/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+ name = "proto_proto",
+ srcs = ["proxy.proto"],
+ visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+ name = "proto_go_proto",
+ compilers = ["@io_bazel_rules_go//proto:go_grpc"],
+ importpath = "code.hackerspace.pl/hscloud/dc/cmc-proxy/proto",
+ proto = ":proto_proto",
+ visibility = ["//visibility:public"],
+)
+
+go_library(
+ name = "go_default_library",
+ embed = [":proto_go_proto"],
+ importpath = "code.hackerspace.pl/hscloud/dc/cmc-proxy/proto",
+ visibility = ["//visibility:public"],
+)
diff --git a/dc/cmc-proxy/proto/proxy.proto b/dc/cmc-proxy/proto/proxy.proto
new file mode 100644
index 0000000..5afe6b9
--- /dev/null
+++ b/dc/cmc-proxy/proto/proxy.proto
@@ -0,0 +1,15 @@
+syntax = "proto3";
+package proto;
+option go_package = "code.hackerspace.pl/hscloud/dc/cmc-proxy/proto";
+
+message GetKVMDataRequest {
+ int64 blade_num = 1;
+}
+
+message GetKVMDataResponse {
+ repeated string arguments = 1;
+}
+
+service CMCProxy {
+ rpc GetKVMData(GetKVMDataRequest) returns (GetKVMDataResponse);
+}