blob: e22c97ca913c7ef463f8a62145b86f4bdfeb44ef [file] [log] [blame]
Serge Bazanski753d63f2018-10-14 15:21:46 -07001package main
2
3import (
4 "context"
5 "crypto/tls"
6 "fmt"
7 "io/ioutil"
8 "net/http"
9 "net/url"
10 "regexp"
11 "strings"
12
13 "code.hackerspace.pl/q3k/mirko"
14 "github.com/cenkalti/backoff"
15 "github.com/golang/glog"
16)
17
18var (
19 reSessionCookie = regexp.MustCompile("'SESSION_COOKIE' : '([^']*)'")
20 reIpmiPriv = regexp.MustCompile("'IPMI_PRIV' : ([^,]*)")
21 reExtPriv = regexp.MustCompile("'EXT_PRIV' : ([^,]*)")
22 reSystemModel = regexp.MustCompile("'SYSTEM_MODEL' : '([^']*)'")
23 reArgument = regexp.MustCompile("<argument>([^<]*)</argument>")
24)
25
26var (
27 ErrorNoFreeSlot = fmt.Errorf("iDRAC reports no free slot")
28)
29
30type cmcRequestType int
31
32const (
33 cmcRequestKVMDetails cmcRequestType = iota
34)
35
36type cmcResponse struct {
37 data interface{}
38 err error
39}
40
41type cmcRequest struct {
42 t cmcRequestType
43 req interface{}
44 res chan cmcResponse
45 canceled bool
46}
47
48type KVMDetails struct {
49 arguments []string
50}
51
52type cmcClient struct {
53 session string
54 req chan *cmcRequest
55}
56
57func (c *cmcClient) RequestKVMDetails(ctx context.Context, slot int) (*KVMDetails, error) {
58 r := &cmcRequest{
59 t: cmcRequestKVMDetails,
60 req: slot,
61 res: make(chan cmcResponse, 1),
62 }
63 mirko.Trace(ctx, "cmcRequestKVMDetails: requesting...")
64 c.req <- r
65 mirko.Trace(ctx, "cmcRequestKVMDetails: requested.")
66
67 select {
68 case <-ctx.Done():
69 r.canceled = true
70 return nil, context.Canceled
71 case res := <-r.res:
72 mirko.Trace(ctx, "cmcRequestKVMDetails: got response")
73 if res.err != nil {
74 return nil, res.err
75 }
76 return res.data.(*KVMDetails), nil
77 }
78}
79
80func NewCMCClient() *cmcClient {
81 return &cmcClient{
82 req: make(chan *cmcRequest, 4),
83 }
84}
85
86func (c *cmcClient) Run(ctx context.Context) {
87 for {
88 select {
89 case <-ctx.Done():
90 c.logout()
91 return
92 case msg := <-c.req:
93 c.handle(msg)
94 }
95 }
96}
97
98func (c *cmcClient) handle(r *cmcRequest) {
99 switch {
100 case r.t == cmcRequestKVMDetails:
101 var details *KVMDetails
102 slot := r.req.(int)
103 err := backoff.Retry(func() error {
104 if err := c.login(); err != nil {
105 return err
106 }
107 url, err := c.getiDRACURL(slot)
108 if err != nil {
109 return err
110 }
111 details, err = c.getiDRACJNLP(url)
112 return err
113 }, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 2))
114
115 if err != nil {
116 r.res <- cmcResponse{err: err}
117 }
118
119 r.res <- cmcResponse{data: details}
120 default:
121 panic("invalid cmcRequestType")
122 }
123}
124
125func makeUrl(path string) string {
126 if strings.HasSuffix(flagCMCAddress, "/") {
127 return flagCMCAddress + path
128 }
129 return flagCMCAddress + "/" + path
130}
131
132func (c *cmcClient) transport() *http.Transport {
133 return &http.Transport{
134 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
135 }
136}
137
138func (c *cmcClient) addCookies(req *http.Request) {
139 req.AddCookie(&http.Cookie{Name: "custom_domain", Value: ""})
140 req.AddCookie(&http.Cookie{Name: "domain_selected", Value: "This Chassis"})
141 if c.session != "" {
Serge Bazanski4ffc3812018-10-14 23:26:04 +0100142 glog.Infof("Session cookie: %v", c.session)
Serge Bazanski753d63f2018-10-14 15:21:46 -0700143 req.AddCookie(&http.Cookie{Name: "sid", Value: c.session})
144 }
145}
146
147func (c *cmcClient) getiDRACURL(slot int) (string, error) {
148 if c.session == "" {
149 return "", fmt.Errorf("not logged in")
150 }
151
152 url := makeUrl(pathiDRACURL) + fmt.Sprintf("?vKVM=1&serverSlot=%d", slot)
153
154 req, err := http.NewRequest("GET", url, nil)
155 if err != nil {
156 return "", fmt.Errorf("GET prepare to %s failed: %v", pathLogin, err)
157 }
158 c.addCookies(req)
159
160 cl := &http.Client{
161 Transport: c.transport(),
162 CheckRedirect: func(req *http.Request, via []*http.Request) error {
163 return http.ErrUseLastResponse
164 },
165 }
166 resp, err := cl.Do(req)
167 if err != nil {
168 return "", fmt.Errorf("GET to %s failed: %v", pathLogin, err)
169 }
170
171 if resp.StatusCode != 302 {
172 return "", fmt.Errorf("expected 302 on iDRAC URL redirect, got %v instead", resp.Status)
173 }
174
175 loc, _ := resp.Location()
176
177 if !strings.Contains(loc.String(), "cmc_sess_id") {
178 c.session = ""
179 return "", fmt.Errorf("redirect URL contains no session ID - session timed out?")
180 }
181
182 return loc.String(), nil
183}
184
185func (c *cmcClient) getiDRACJNLP(loginUrl string) (*KVMDetails, error) {
186 lurl, err := url.Parse(loginUrl)
187 if err != nil {
188 return nil, err
189 }
190
191 sessid := lurl.Query().Get("cmc_sess_id")
192 if sessid == "" {
193 return nil, fmt.Errorf("no cmc_sess_id in iDRAC login URL")
194 }
195
196 createURL := *lurl
197 createURL.Path = "/Applications/dellUI/RPC/WEBSES/create.asp"
198 createURL.RawQuery = ""
199
200 values := url.Values{}
201 values.Set("WEBVAR_USERNAME", "cmc")
202 values.Set("WEBVAR_PASSWORD", sessid)
203 values.Set("WEBVAR_ISCMCLOGIN", "1")
204 valuesString := values.Encode()
205 req, err := http.NewRequest("POST", createURL.String(), strings.NewReader(valuesString))
206
207 cl := &http.Client{
208 Transport: c.transport(),
209 CheckRedirect: func(req *http.Request, via []*http.Request) error {
210 return http.ErrUseLastResponse
211 },
212 }
213 resp, err := cl.Do(req)
214 if err != nil {
215 return nil, err
216 }
217 defer resp.Body.Close()
218
219 data, _ := ioutil.ReadAll(resp.Body)
220 if err != nil {
221 return nil, err
222 }
223
224 first := func(v [][]byte) string {
225 if len(v) < 1 {
226 return ""
227 }
228 return string(v[1])
229 }
230
231 sessionCookie := first(reSessionCookie.FindSubmatch(data))
232 ipmiPriv := first(reIpmiPriv.FindSubmatch(data))
233 extPriv := first(reExtPriv.FindSubmatch(data))
234 systemModel := first(reSystemModel.FindSubmatch(data))
235
236 if sessionCookie == "Failure_No_Free_Slot" {
237 return nil, ErrorNoFreeSlot
238 }
239
240 jnlpURL := *lurl
241 jnlpURL.Path = "/Applications/dellUI/Java/jviewer.jnlp"
242 jnlpURL.RawQuery = ""
243
244 req, err = http.NewRequest("GET", jnlpURL.String(), nil)
245 for _, cookie := range resp.Cookies() {
Serge Bazanski753d63f2018-10-14 15:21:46 -0700246 req.AddCookie(cookie)
247 }
248 req.AddCookie(&http.Cookie{Name: "SessionCookie", Value: sessionCookie})
249 req.AddCookie(&http.Cookie{Name: "SessionCookieUser", Value: "cmc"})
250 req.AddCookie(&http.Cookie{Name: "IPMIPriv", Value: ipmiPriv})
251 req.AddCookie(&http.Cookie{Name: "ExtPriv", Value: extPriv})
252 req.AddCookie(&http.Cookie{Name: "SystemModel", Value: systemModel})
253
254 resp, err = cl.Do(req)
255 if err != nil {
256 return nil, err
257 }
258 defer resp.Body.Close()
259
260 data, err = ioutil.ReadAll(resp.Body)
261 if err != nil {
262 return nil, err
263 }
264
265 // yes we do parse xml with regex why are you asking
266 matches := reArgument.FindAllSubmatch(data, -1)
267
268 res := &KVMDetails{
269 arguments: []string{},
270 }
271 for _, match := range matches {
272 res.arguments = append(res.arguments, string(match[1]))
273 }
274
275 return res, nil
276}
277
278func (c *cmcClient) login() error {
279 if c.session != "" {
280 return nil
281 }
282
283 values := url.Values{}
284 values.Set("ST2", "NOTSET")
285 values.Set("user", flagCMCUsername)
286 values.Set("user_id", flagCMCUsername)
287 values.Set("password", flagCMCPassword)
288 values.Set("WEBSERVER_timeout", "1800")
289 values.Set("WEBSERVER_timeout_select", "1800")
290 valuesString := values.Encode()
Serge Bazanski753d63f2018-10-14 15:21:46 -0700291 req, err := http.NewRequest("POST", makeUrl(pathLogin), strings.NewReader(valuesString))
292 if err != nil {
293 return fmt.Errorf("POST prepare to %s failed: %v", pathLogin, err)
294 }
295 req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
296 c.addCookies(req)
297
298 cl := &http.Client{
299 Transport: c.transport(),
300 CheckRedirect: func(req *http.Request, via []*http.Request) error {
301 return http.ErrUseLastResponse
302 },
303 }
304 resp, err := cl.Do(req)
305 if err != nil {
306 return fmt.Errorf("POST to %s failed: %v", pathLogin, err)
307 }
308 glog.Infof("Login response: %s", resp.Status)
309 defer resp.Body.Close()
310 for _, cookie := range resp.Cookies() {
311 if cookie.Name == "sid" {
312 c.session = cookie.Value
313 break
314 }
315 }
316 if c.session == "" {
317 return fmt.Errorf("login unsuccesful")
318 }
319 return nil
320}
321
322func (c *cmcClient) logout() {
323 glog.Infof("Killing session..")
324 if c.session == "" {
325 return
326 }
327
328 req, err := http.NewRequest("GET", makeUrl(pathLogout), nil)
329 if err != nil {
330 glog.Errorf("GET prepare to %s failed: %v", pathLogin, err)
331 }
332 c.addCookies(req)
333
334 cl := &http.Client{
335 Transport: c.transport(),
336 CheckRedirect: func(req *http.Request, via []*http.Request) error {
337 return http.ErrUseLastResponse
338 },
339 }
340 resp, err := cl.Do(req)
341 if err != nil {
342 glog.Errorf("GET to %s failed: %v", pathLogin, err)
343 }
344 glog.Infof("Logout response: %s", resp.Status)
345 return
346}