forked from hswaw/hscloud
Sergiusz Bazanski
61594bb088
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
372 lines
8.6 KiB
Go
372 lines
8.6 KiB
Go
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
|
|
}
|