idrac 0.1.38 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/idrac/client.rb CHANGED
@@ -9,23 +9,29 @@ require 'colorize'
9
9
 
10
10
  module IDRAC
11
11
  class Client
12
- attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl, :auto_delete_sessions
13
- attr_accessor :direct_mode
12
+ attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl, :auto_delete_sessions, :session, :web
13
+ attr_accessor :direct_mode, :verbosity
14
+
15
+ include PowerMethods
16
+ include SessionMethods
17
+ include Debuggable
18
+ include JobMethods
19
+ include LifecycleMethods
14
20
 
15
- def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: true, direct_mode: false, auto_delete_sessions: true)
21
+ def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: false, direct_mode: false, auto_delete_sessions: true)
16
22
  @host = host
17
23
  @username = username
18
24
  @password = password
19
25
  @port = port
20
26
  @use_ssl = use_ssl
21
27
  @verify_ssl = verify_ssl
22
- @session_id = nil
23
- @cookies = nil
24
- @x_auth_token = nil
25
28
  @direct_mode = direct_mode
26
- @sessions_maxed = false
27
- @tried_clearing_sessions = false
28
29
  @auto_delete_sessions = auto_delete_sessions
30
+ @verbosity = 0
31
+
32
+ # Initialize the session and web classes
33
+ @session = Session.new(self)
34
+ @web = Web.new(self)
29
35
  end
30
36
 
31
37
  def connection
@@ -33,353 +39,13 @@ module IDRAC
33
39
  faraday.request :multipart
34
40
  faraday.request :url_encoded
35
41
  faraday.adapter Faraday.default_adapter
36
- end
37
- end
38
-
39
- # Force clear all sessions by directly using Basic Auth
40
- def force_clear_sessions
41
- puts "Attempting to force clear all sessions...".light_cyan
42
-
43
- # Try to delete sessions directly using Basic Auth
44
- if delete_all_sessions_with_basic_auth
45
- puts "Successfully cleared sessions using Basic Auth".green
46
- return true
47
- else
48
- puts "Failed to clear sessions using Basic Auth".red
49
- return false
50
- end
51
- end
52
-
53
- # Delete all sessions using Basic Authentication
54
- def delete_all_sessions_with_basic_auth
55
- puts "Attempting to delete all sessions using Basic Authentication...".light_cyan
56
-
57
- # First, get the list of sessions
58
- sessions_url = '/redfish/v1/SessionService/Sessions'
59
-
60
- # Create a connection with Basic Auth
61
- basic_auth_headers = {
62
- 'Authorization' => "Basic #{Base64.strict_encode64("#{username}:#{password}")}",
63
- 'Content-Type' => 'application/json'
64
- }
65
-
66
- begin
67
- # Get the list of sessions
68
- response = connection.get(sessions_url) do |req|
69
- req.headers.merge!(basic_auth_headers)
70
- end
71
-
72
- if response.status != 200
73
- puts "Failed to get sessions list: #{response.status} - #{response.body}".red
74
- return false
75
- end
76
-
77
- # Parse the response to get session IDs
78
- begin
79
- sessions_data = JSON.parse(response.body)
80
-
81
- if sessions_data['Members'] && sessions_data['Members'].any?
82
- puts "Found #{sessions_data['Members'].count} active sessions".light_yellow
83
-
84
- # Delete each session
85
- success = true
86
- sessions_data['Members'].each do |session|
87
- session_url = session['@odata.id']
88
-
89
- # Skip if no URL
90
- next unless session_url
91
-
92
- # Delete the session
93
- delete_response = connection.delete(session_url) do |req|
94
- req.headers.merge!(basic_auth_headers)
95
- end
96
-
97
- if delete_response.status == 200 || delete_response.status == 204
98
- puts "Successfully deleted session: #{session_url}".green
99
- else
100
- puts "Failed to delete session #{session_url}: #{delete_response.status}".red
101
- success = false
102
- end
103
-
104
- # Small delay between deletions
105
- sleep(1)
106
- end
107
-
108
- return success
109
- else
110
- puts "No active sessions found".light_yellow
111
- return true
42
+ # Add request/response logging based on verbosity
43
+ if @verbosity > 0
44
+ faraday.response :logger, Logger.new(STDOUT), bodies: @verbosity >= 2 do |logger|
45
+ logger.filter(/(Authorization: Basic )([^,\n]+)/, '\1[FILTERED]')
46
+ logger.filter(/(Password"=>"?)([^,"]+)/, '\1[FILTERED]')
112
47
  end
113
- rescue JSON::ParserError => e
114
- puts "Error parsing sessions response: #{e.message}".red.bold
115
- return false
116
48
  end
117
- rescue => e
118
- puts "Error during session deletion with Basic Auth: #{e.message}".red.bold
119
- return false
120
- end
121
- end
122
-
123
- # Create a Redfish session
124
- def create_redfish_session
125
- # Skip if we're in direct mode
126
- if @direct_mode
127
- puts "Skipping Redfish session creation (direct mode)".light_yellow
128
- return false
129
- end
130
-
131
- url = '/redfish/v1/SessionService/Sessions'
132
- payload = { "UserName" => username, "Password" => password }
133
-
134
- # Try first with just Content-Type header (no Basic Auth)
135
- begin
136
- response = connection.post(url) do |req|
137
- req.headers['Content-Type'] = 'application/json'
138
- req.body = payload.to_json
139
- end
140
-
141
- if response.status == 201 || response.status == 200
142
- # Extract X-Auth-Token from response headers
143
- @x_auth_token = response.headers['X-Auth-Token']
144
-
145
- # Extract session location from response headers
146
- @session_location = response.headers['Location']
147
-
148
- puts "Redfish session created successfully".green
149
- @sessions_maxed = false
150
- return true
151
- end
152
- rescue => e
153
- puts "First session creation attempt failed: #{e.message}".light_red
154
- end
155
-
156
- # If that fails, try with Basic Auth
157
- begin
158
- # Use Basic Auth for the session creation
159
- basic_auth_headers = {
160
- 'Authorization' => "Basic #{Base64.strict_encode64("#{username}:#{password}")}",
161
- 'Content-Type' => 'application/json'
162
- }
163
-
164
- response = connection.post(url) do |req|
165
- req.headers.merge!(basic_auth_headers)
166
- req.body = payload.to_json
167
- end
168
-
169
- if response.status == 201 || response.status == 200
170
- # Extract X-Auth-Token from response headers
171
- @x_auth_token = response.headers['X-Auth-Token']
172
-
173
- # Extract session location from response headers
174
- @session_location = response.headers['Location']
175
-
176
- puts "Redfish session created successfully with Basic Auth".green
177
- @sessions_maxed = false
178
- return true
179
- elsif response.status == 400 && response.body.include?("maximum number of user sessions")
180
- puts "Maximum sessions reached during Redfish session creation".light_red
181
- @sessions_maxed = true
182
-
183
- # Try to clear sessions if auto_delete_sessions is enabled
184
- if @auto_delete_sessions
185
- puts "Auto-delete sessions is enabled, attempting to clear sessions".light_cyan
186
- if force_clear_sessions
187
- puts "Successfully cleared sessions, trying to create a new session".green
188
-
189
- # Try one more time after clearing
190
- response = connection.post(url) do |req|
191
- req.headers.merge!(basic_auth_headers)
192
- req.body = payload.to_json
193
- end
194
-
195
- if response.status == 201 || response.status == 200
196
- @x_auth_token = response.headers['X-Auth-Token']
197
- @session_location = response.headers['Location']
198
- puts "Redfish session created successfully after clearing sessions".green
199
- @sessions_maxed = false
200
- return true
201
- else
202
- puts "Failed to create Redfish session after clearing: #{response.status} - #{response.body}".red
203
- # If we still can't create a session, switch to direct mode
204
- @direct_mode = true
205
- return false
206
- end
207
- else
208
- puts "Failed to clear sessions, switching to direct mode".light_yellow
209
- @direct_mode = true
210
- return false
211
- end
212
- else
213
- puts "Auto-delete sessions is disabled, switching to direct mode".light_yellow
214
- @direct_mode = true
215
- return false
216
- end
217
- else
218
- puts "Failed to create Redfish session: #{response.status} - #{response.body}".red
219
-
220
- # If we get a 415 error, try with form-urlencoded
221
- if response.status == 415
222
- puts "Trying with form-urlencoded content type".light_cyan
223
- response = connection.post(url) do |req|
224
- req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
225
- req.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
226
- req.body = "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}"
227
- end
228
-
229
- if response.status == 201 || response.status == 200
230
- @x_auth_token = response.headers['X-Auth-Token']
231
- @session_location = response.headers['Location']
232
- puts "Redfish session created successfully with form-urlencoded".green
233
- @sessions_maxed = false
234
- return true
235
- else
236
- puts "Failed with form-urlencoded too: #{response.status} - #{response.body}".red
237
- @direct_mode = true
238
- return false
239
- end
240
- else
241
- @direct_mode = true
242
- return false
243
- end
244
- end
245
- rescue => e
246
- puts "Error during Redfish session creation: #{e.message}".red.bold
247
- @direct_mode = true
248
- return false
249
- end
250
- end
251
-
252
- # Delete the Redfish session
253
- def delete_redfish_session
254
- return unless @x_auth_token && @session_location
255
-
256
- begin
257
- puts "Deleting Redfish session...".light_cyan
258
-
259
- # Use the X-Auth-Token for authentication
260
- headers = { 'X-Auth-Token' => @x_auth_token }
261
-
262
- response = connection.delete(@session_location) do |req|
263
- req.headers.merge!(headers)
264
- end
265
-
266
- if response.status == 200 || response.status == 204
267
- puts "Redfish session deleted successfully".green
268
- @x_auth_token = nil
269
- @session_location = nil
270
- return true
271
- else
272
- puts "Failed to delete Redfish session: #{response.status} - #{response.body}".red
273
- return false
274
- end
275
- rescue => e
276
- puts "Error during Redfish session deletion: #{e.message}".red.bold
277
- return false
278
- end
279
- end
280
-
281
- # Login to the WebUI (for screenshot functionality)
282
- def webui_login(retry_count = 0)
283
- # Limit retries to prevent infinite loops
284
- if retry_count >= 3
285
- puts "Maximum retry count reached for WebUI login".red
286
- return false
287
- end
288
-
289
- # Skip if we already have a session ID
290
- return true if @session_id
291
-
292
- begin
293
- puts "Logging in to WebUI...".light_cyan
294
-
295
- # Create the login URL
296
- login_url = "#{base_url}/data/login"
297
-
298
- # Create the login payload
299
- payload = {
300
- 'user' => username,
301
- 'password' => password
302
- }
303
-
304
- # Make the login request
305
- response = HTTParty.post(
306
- login_url,
307
- body: payload,
308
- verify: verify_ssl,
309
- headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }
310
- )
311
-
312
- # Check if the login was successful
313
- if response.code == 200
314
- # Extract the session ID from the response
315
- if response.body.include?('ST2')
316
- @session_id = response.body.match(/ST2=([^;]+)/)[1]
317
- @cookies = response.headers['set-cookie']
318
- puts "WebUI login successful".green
319
- return true
320
- else
321
- puts "WebUI login failed: No session ID found in response".red
322
- return false
323
- end
324
- elsif response.code == 400 && response.body.include?("maximum number of user sessions")
325
- puts "Maximum sessions reached during WebUI login".light_red
326
-
327
- # Try to clear sessions if auto_delete_sessions is enabled
328
- if @auto_delete_sessions && !@tried_clearing_sessions
329
- puts "Auto-delete sessions is enabled, attempting to clear sessions".light_cyan
330
- @tried_clearing_sessions = true
331
-
332
- if force_clear_sessions
333
- puts "Successfully cleared sessions, trying WebUI login again".green
334
- return webui_login(retry_count + 1)
335
- else
336
- puts "Failed to clear sessions for WebUI login".red
337
- return false
338
- end
339
- else
340
- puts "Auto-delete sessions is disabled or already tried clearing".light_yellow
341
- return false
342
- end
343
- else
344
- puts "WebUI login failed: #{response.code} - #{response.body}".red
345
- return false
346
- end
347
- rescue => e
348
- puts "Error during WebUI login: #{e.message}".red.bold
349
- return false
350
- end
351
- end
352
-
353
- # Logout from the WebUI
354
- def webui_logout
355
- return unless @session_id
356
-
357
- begin
358
- puts "Logging out from WebUI...".light_cyan
359
-
360
- # Create the logout URL
361
- logout_url = "#{base_url}/data/logout"
362
-
363
- # Make the logout request
364
- response = HTTParty.get(
365
- logout_url,
366
- verify: verify_ssl,
367
- headers: { 'Cookie' => @cookies }
368
- )
369
-
370
- # Check if the logout was successful
371
- if response.code == 200
372
- puts "WebUI logout successful".green
373
- @session_id = nil
374
- @cookies = nil
375
- return true
376
- else
377
- puts "WebUI logout failed: #{response.code} - #{response.body}".red
378
- return false
379
- end
380
- rescue => e
381
- puts "Error during WebUI logout: #{e.message}".red.bold
382
- return false
383
49
  end
384
50
  end
385
51
 
@@ -392,16 +58,11 @@ module IDRAC
392
58
  end
393
59
 
394
60
  # Try to create a Redfish session
395
- if create_redfish_session
61
+ if session.create
396
62
  puts "Successfully logged in to iDRAC using Redfish session".green
397
63
  return true
398
64
  else
399
- if @sessions_maxed
400
- puts "Maximum sessions reached and could not clear sessions".light_red
401
- else
402
- puts "Failed to create Redfish session, falling back to direct mode".light_yellow
403
- end
404
-
65
+ puts "Failed to create Redfish session, falling back to direct mode".light_yellow
405
66
  @direct_mode = true
406
67
  return true
407
68
  end
@@ -409,14 +70,8 @@ module IDRAC
409
70
 
410
71
  # Logout from iDRAC
411
72
  def logout
412
- if @x_auth_token
413
- delete_redfish_session
414
- end
415
-
416
- if @session_id
417
- webui_logout
418
- end
419
-
73
+ session.delete if session.x_auth_token
74
+ web.logout if web.session_id
420
75
  puts "Logged out from iDRAC".green
421
76
  return true
422
77
  end
@@ -429,6 +84,8 @@ module IDRAC
429
84
  raise Error, "Maximum retry count reached for authenticated request"
430
85
  end
431
86
 
87
+ debug "Authenticated request: #{method.to_s.upcase} #{path}", 1
88
+
432
89
  # If we're in direct mode, use Basic Auth
433
90
  if @direct_mode
434
91
  # Create Basic Auth header
@@ -438,6 +95,8 @@ module IDRAC
438
95
  options[:headers] ||= {}
439
96
  options[:headers]['Authorization'] = auth_header
440
97
 
98
+ debug "Using Basic Auth for request", 2
99
+
441
100
  # Make the request
442
101
  begin
443
102
  response = connection.send(method, path) do |req|
@@ -445,6 +104,10 @@ module IDRAC
445
104
  req.body = options[:body] if options[:body]
446
105
  end
447
106
 
107
+ debug "Response status: #{response.status}", 1
108
+ debug "Response headers: #{response.headers.inspect}", 2
109
+ debug "Response body: #{response.body}", 3 if response.body
110
+
448
111
  return response
449
112
  rescue => e
450
113
  puts "Error during authenticated request (direct mode): #{e.message}".red.bold
@@ -452,10 +115,12 @@ module IDRAC
452
115
  end
453
116
  else
454
117
  # Use X-Auth-Token if available
455
- if @x_auth_token
118
+ if session.x_auth_token
456
119
  # Add the X-Auth-Token header to the request
457
120
  options[:headers] ||= {}
458
- options[:headers]['X-Auth-Token'] = @x_auth_token
121
+ options[:headers]['X-Auth-Token'] = session.x_auth_token
122
+
123
+ debug "Using X-Auth-Token for request", 2
459
124
 
460
125
  # Make the request
461
126
  begin
@@ -464,12 +129,16 @@ module IDRAC
464
129
  req.body = options[:body] if options[:body]
465
130
  end
466
131
 
132
+ debug "Response status: #{response.status}", 1
133
+ debug "Response headers: #{response.headers.inspect}", 2
134
+ debug "Response body: #{response.body}", 3 if response.body
135
+
467
136
  # Check if the session is still valid
468
137
  if response.status == 401 || response.status == 403
469
138
  puts "Session expired or invalid, attempting to create a new session...".light_yellow
470
139
 
471
140
  # Try to create a new session
472
- if create_redfish_session
141
+ if session.create
473
142
  puts "Successfully created a new session, retrying request...".green
474
143
  return authenticated_request(method, path, options, retry_count + 1)
475
144
  else
@@ -484,7 +153,7 @@ module IDRAC
484
153
  puts "Error during authenticated request (token mode): #{e.message}".red.bold
485
154
 
486
155
  # Try to create a new session
487
- if create_redfish_session
156
+ if session.create
488
157
  puts "Successfully created a new session after error, retrying request...".green
489
158
  return authenticated_request(method, path, options, retry_count + 1)
490
159
  else
@@ -495,7 +164,7 @@ module IDRAC
495
164
  end
496
165
  else
497
166
  # If we don't have a token, try to create a session
498
- if create_redfish_session
167
+ if session.create
499
168
  puts "Successfully created a new session, making request...".green
500
169
  return authenticated_request(method, path, options, retry_count + 1)
501
170
  else
@@ -509,40 +178,61 @@ module IDRAC
509
178
 
510
179
  def get(path:, headers: {})
511
180
  # For screenshot functionality, we need to use the WebUI cookies
512
- if @cookies.nil? && path.include?('screen/screen.jpg')
513
- webui_login unless @session_id
181
+ if web.cookies.nil? && path.include?('screen/screen.jpg')
182
+ web.login unless web.session_id
514
183
  end
515
184
 
185
+ debug "GET request to #{base_url}/#{path}", 1
186
+
516
187
  headers_to_use = {
517
188
  "User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
518
189
  "Accept-Encoding" => "deflate, gzip"
519
190
  }
520
191
 
521
- if @cookies
522
- headers_to_use["Cookie"] = @cookies
192
+ if web.cookies
193
+ headers_to_use["Cookie"] = web.cookies
194
+ debug "Using WebUI cookies for request", 2
523
195
  elsif @direct_mode
524
196
  # In direct mode, use Basic Auth
525
197
  headers_to_use["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
526
- elsif @x_auth_token
527
- headers_to_use["X-Auth-Token"] = @x_auth_token
198
+ debug "Using Basic Auth for GET request", 2
199
+ elsif session.x_auth_token
200
+ headers_to_use["X-Auth-Token"] = session.x_auth_token
201
+ debug "Using X-Auth-Token for GET request", 2
528
202
  end
529
203
 
204
+ debug "Request headers: #{headers_to_use.merge(headers).inspect}", 3
205
+
530
206
  response = HTTParty.get(
531
207
  "#{base_url}/#{path}",
532
208
  headers: headers_to_use.merge(headers),
533
209
  verify: false
534
210
  )
211
+
212
+ debug "Response status: #{response.code}", 1
213
+ debug "Response headers: #{response.headers.inspect}", 2
214
+ debug "Response body: #{response.body.to_s[0..500]}#{response.body.to_s.length > 500 ? '...' : ''}", 3 if response.body
215
+
216
+ response
535
217
  end
536
218
 
537
219
  def screenshot
538
- # Create a Screenshot instance and capture a screenshot
539
- screenshot_instance = Screenshot.new(self)
540
- screenshot_instance.capture
220
+ web.capture_screenshot
541
221
  end
542
222
 
543
223
  def base_url
544
224
  protocol = use_ssl ? 'https' : 'http'
545
225
  "#{protocol}://#{host}:#{port}"
546
226
  end
227
+
228
+ def redfish_version
229
+ response = authenticated_request(:get, "/redfish/v1")
230
+ if response.status == 200
231
+ data = JSON.parse(response.body)
232
+ data["RedfishVersion"]
233
+ else
234
+ raise Error, "Failed to get Redfish version: #{response.status} - #{response.body}"
235
+ end
236
+ end
547
237
  end
548
238
  end
@@ -8,6 +8,8 @@ require 'securerandom'
8
8
  require 'set'
9
9
  require 'colorize'
10
10
  require_relative 'firmware_catalog'
11
+ require 'faraday'
12
+ require 'faraday/multipart'
11
13
 
12
14
  module IDRAC
13
15
  class Firmware
@@ -427,6 +429,24 @@ module IDRAC
427
429
  end
428
430
  end
429
431
 
432
+ def get_power_state
433
+ # Ensure we have a client
434
+ raise Error, "Client is required for power management" unless client
435
+
436
+ # Login to iDRAC if needed
437
+ client.login unless client.instance_variable_get(:@session_id)
438
+
439
+ # Get system information
440
+ response = client.authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
441
+
442
+ if response.status == 200
443
+ system_data = JSON.parse(response.body)
444
+ return system_data["PowerState"]
445
+ else
446
+ raise Error, "Failed to get power state. Status code: #{response.status}"
447
+ end
448
+ end
449
+
430
450
  private
431
451
 
432
452
  def upload_firmware(firmware_path)
@@ -478,16 +498,20 @@ module IDRAC
478
498
  file_content = File.read(firmware_path)
479
499
 
480
500
  headers = {
481
- 'Content-Type' => 'application/octet-stream',
501
+ 'Content-Type' => 'multipart/form-data',
482
502
  'If-Match' => etag
483
503
  }
484
504
 
505
+ # Create a temp file for multipart upload
506
+ upload_io = Faraday::UploadIO.new(firmware_path, 'application/octet-stream')
507
+ payload = { :file => upload_io }
508
+
485
509
  upload_response = client.authenticated_request(
486
510
  :post,
487
511
  http_push_uri,
488
512
  {
489
513
  headers: headers,
490
- body: file_content
514
+ body: payload,
491
515
  }
492
516
  )
493
517