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