blob: 345a7b4cd5a8f86ea3dcd087d2a28f69e1807041 [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
Serge Bazanski965b78a2018-10-25 12:22:37 +010013 "code.hackerspace.pl/hscloud/go/mirko"
Serge Bazanski753d63f2018-10-14 15:21:46 -070014 "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") {
Piotr Dobrowolski11603cb2019-02-10 15:31:48 +0100178 c.logout()
Serge Bazanski753d63f2018-10-14 15:21:46 -0700179 c.session = ""
180 return "", fmt.Errorf("redirect URL contains no session ID - session timed out?")
181 }
182
183 return loc.String(), nil
184}
185
186func (c *cmcClient) getiDRACJNLP(loginUrl string) (*KVMDetails, error) {
187 lurl, err := url.Parse(loginUrl)
188 if err != nil {
189 return nil, err
190 }
191
192 sessid := lurl.Query().Get("cmc_sess_id")
193 if sessid == "" {
194 return nil, fmt.Errorf("no cmc_sess_id in iDRAC login URL")
195 }
196
197 createURL := *lurl
198 createURL.Path = "/Applications/dellUI/RPC/WEBSES/create.asp"
199 createURL.RawQuery = ""
200
201 values := url.Values{}
202 values.Set("WEBVAR_USERNAME", "cmc")
203 values.Set("WEBVAR_PASSWORD", sessid)
204 values.Set("WEBVAR_ISCMCLOGIN", "1")
205 valuesString := values.Encode()
206 req, err := http.NewRequest("POST", createURL.String(), strings.NewReader(valuesString))
207
208 cl := &http.Client{
209 Transport: c.transport(),
210 CheckRedirect: func(req *http.Request, via []*http.Request) error {
211 return http.ErrUseLastResponse
212 },
213 }
214 resp, err := cl.Do(req)
215 if err != nil {
216 return nil, err
217 }
218 defer resp.Body.Close()
219
220 data, _ := ioutil.ReadAll(resp.Body)
221 if err != nil {
222 return nil, err
223 }
224
225 first := func(v [][]byte) string {
226 if len(v) < 1 {
227 return ""
228 }
229 return string(v[1])
230 }
231
232 sessionCookie := first(reSessionCookie.FindSubmatch(data))
233 ipmiPriv := first(reIpmiPriv.FindSubmatch(data))
234 extPriv := first(reExtPriv.FindSubmatch(data))
235 systemModel := first(reSystemModel.FindSubmatch(data))
236
237 if sessionCookie == "Failure_No_Free_Slot" {
238 return nil, ErrorNoFreeSlot
239 }
240
241 jnlpURL := *lurl
242 jnlpURL.Path = "/Applications/dellUI/Java/jviewer.jnlp"
243 jnlpURL.RawQuery = ""
244
245 req, err = http.NewRequest("GET", jnlpURL.String(), nil)
246 for _, cookie := range resp.Cookies() {
Serge Bazanski753d63f2018-10-14 15:21:46 -0700247 req.AddCookie(cookie)
248 }
249 req.AddCookie(&http.Cookie{Name: "SessionCookie", Value: sessionCookie})
250 req.AddCookie(&http.Cookie{Name: "SessionCookieUser", Value: "cmc"})
251 req.AddCookie(&http.Cookie{Name: "IPMIPriv", Value: ipmiPriv})
252 req.AddCookie(&http.Cookie{Name: "ExtPriv", Value: extPriv})
253 req.AddCookie(&http.Cookie{Name: "SystemModel", Value: systemModel})
254
255 resp, err = cl.Do(req)
256 if err != nil {
257 return nil, err
258 }
259 defer resp.Body.Close()
260
261 data, err = ioutil.ReadAll(resp.Body)
262 if err != nil {
263 return nil, err
264 }
265
266 // yes we do parse xml with regex why are you asking
267 matches := reArgument.FindAllSubmatch(data, -1)
268
269 res := &KVMDetails{
270 arguments: []string{},
271 }
272 for _, match := range matches {
273 res.arguments = append(res.arguments, string(match[1]))
274 }
275
Piotr Dobrowolski11603cb2019-02-10 15:31:48 +0100276 logoutURL := *lurl
277 logoutURL.Path = "/Applications/dellUI/RPC/WEBSES/logout.asp"
278 logoutURL.RawQuery = ""
279
280 req, err = http.NewRequest("GET", logoutURL.String(), nil)
281 for _, cookie := range resp.Cookies() {
282 req.AddCookie(cookie)
283 }
284 req.AddCookie(&http.Cookie{Name: "SessionCookie", Value: sessionCookie})
285 req.AddCookie(&http.Cookie{Name: "SessionCookieUser", Value: "cmc"})
286 req.AddCookie(&http.Cookie{Name: "IPMIPriv", Value: ipmiPriv})
287 req.AddCookie(&http.Cookie{Name: "ExtPriv", Value: extPriv})
288 req.AddCookie(&http.Cookie{Name: "SystemModel", Value: systemModel})
289
290 resp, err = cl.Do(req)
291 if err != nil {
292 return nil, err
293 }
294 defer resp.Body.Close()
295
296 data, err = ioutil.ReadAll(resp.Body)
297 if err != nil {
298 return nil, err
299 }
300
Serge Bazanski753d63f2018-10-14 15:21:46 -0700301 return res, nil
302}
303
304func (c *cmcClient) login() error {
305 if c.session != "" {
306 return nil
307 }
308
309 values := url.Values{}
310 values.Set("ST2", "NOTSET")
311 values.Set("user", flagCMCUsername)
312 values.Set("user_id", flagCMCUsername)
313 values.Set("password", flagCMCPassword)
314 values.Set("WEBSERVER_timeout", "1800")
315 values.Set("WEBSERVER_timeout_select", "1800")
316 valuesString := values.Encode()
Serge Bazanski753d63f2018-10-14 15:21:46 -0700317 req, err := http.NewRequest("POST", makeUrl(pathLogin), strings.NewReader(valuesString))
318 if err != nil {
319 return fmt.Errorf("POST prepare to %s failed: %v", pathLogin, err)
320 }
321 req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
322 c.addCookies(req)
323
324 cl := &http.Client{
325 Transport: c.transport(),
326 CheckRedirect: func(req *http.Request, via []*http.Request) error {
327 return http.ErrUseLastResponse
328 },
329 }
330 resp, err := cl.Do(req)
331 if err != nil {
332 return fmt.Errorf("POST to %s failed: %v", pathLogin, err)
333 }
334 glog.Infof("Login response: %s", resp.Status)
335 defer resp.Body.Close()
336 for _, cookie := range resp.Cookies() {
337 if cookie.Name == "sid" {
338 c.session = cookie.Value
339 break
340 }
341 }
342 if c.session == "" {
343 return fmt.Errorf("login unsuccesful")
344 }
345 return nil
346}
347
348func (c *cmcClient) logout() {
349 glog.Infof("Killing session..")
350 if c.session == "" {
351 return
352 }
353
354 req, err := http.NewRequest("GET", makeUrl(pathLogout), nil)
355 if err != nil {
356 glog.Errorf("GET prepare to %s failed: %v", pathLogin, err)
357 }
358 c.addCookies(req)
359
360 cl := &http.Client{
361 Transport: c.transport(),
362 CheckRedirect: func(req *http.Request, via []*http.Request) error {
363 return http.ErrUseLastResponse
364 },
365 }
366 resp, err := cl.Do(req)
367 if err != nil {
368 glog.Errorf("GET to %s failed: %v", pathLogin, err)
369 }
370 glog.Infof("Logout response: %s", resp.Status)
371 return
372}