idrac 0.1.38 → 0.1.40

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 462eec3a8d4efc8ef0c8b79247cb3b5624f96c97b1cb2b98e10311939fcea521
4
- data.tar.gz: 91793e111a24c4d71ef4ab9c38ea6291630b4314e754f108068a6cded9c23b70
3
+ metadata.gz: 822357c4c005e77d857696867b06be46e3f1f473a239e2936119aec1e68c5eb9
4
+ data.tar.gz: 6ca9c128259d2d94518b2b22ee0d8bfff02624a8f8b15fbabd816a2e08351f8f
5
5
  SHA512:
6
- metadata.gz: 0e6ecd835d5f68d3e4230944843131d70cc5e239bfc546127af7fed5d9fe83a82c4c5421f68fd88ea76f6f94fc9df8193405c936f20ee1a4905cc66e3d457bd7
7
- data.tar.gz: e301acc6c69c352fb3ae4bc2503a7501ab01fe4dd3af9597a2b276aa468c98cb9a9e03c649e55271b26edc8d738b7fcf758e46ab5adef4100cbd6553583f1371
6
+ metadata.gz: a50e1c605aaaa5c4cfe72a49a60d41c63765c20e6bfc3d7d43063a0e53cd29804797094e38287e63ee3cddbcd1335ba543417ec75f089541a950ef55654042df
7
+ data.tar.gz: d3306667a4271507a7923a95b78b557ca55abe78900fee320c36bfb98eda0cafbd268afaaa4bf4fee4a2241b344b634ce445900bae9a768132862995553dd813
data/README.md CHANGED
@@ -92,15 +92,10 @@ client = IDRAC.new(
92
92
  # The client automatically handles session expiration (401 errors)
93
93
  # by re-authenticating and retrying the request
94
94
 
95
- # Take a screenshot (using the client convenience method)
95
+ # Take a screenshot (using the client method)
96
96
  filename = client.screenshot
97
97
  puts "Screenshot saved to: #{filename}"
98
98
 
99
- # Or use the Screenshot class directly for more control
100
- screenshot = IDRAC::Screenshot.new(client)
101
- filename = screenshot.capture
102
- puts "Screenshot saved to: #{filename}"
103
-
104
99
  # Firmware operations
105
100
  firmware = IDRAC::Firmware.new(client)
106
101
 
data/bin/idrac CHANGED
@@ -185,9 +185,8 @@ module IDRAC
185
185
  client = create_client
186
186
 
187
187
  begin
188
- # Create a Screenshot instance directly
189
- screenshot = IDRAC::Screenshot.new(client)
190
- filename = screenshot.capture
188
+ # Capture a screenshot using the client
189
+ filename = client.screenshot
191
190
 
192
191
  # Rename the file if output option is provided
193
192
  if options[:output]
data/lib/idrac/client.rb CHANGED
@@ -9,7 +9,7 @@ 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
12
+ attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl, :auto_delete_sessions, :session, :web
13
13
  attr_accessor :direct_mode
14
14
 
15
15
  def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: true, direct_mode: false, auto_delete_sessions: true)
@@ -19,13 +19,12 @@ module IDRAC
19
19
  @port = port
20
20
  @use_ssl = use_ssl
21
21
  @verify_ssl = verify_ssl
22
- @session_id = nil
23
- @cookies = nil
24
- @x_auth_token = nil
25
22
  @direct_mode = direct_mode
26
- @sessions_maxed = false
27
- @tried_clearing_sessions = false
28
23
  @auto_delete_sessions = auto_delete_sessions
24
+
25
+ # Initialize the session and web classes
26
+ @session = Session.new(self)
27
+ @web = Web.new(self)
29
28
  end
30
29
 
31
30
  def connection
@@ -36,353 +35,6 @@ module IDRAC
36
35
  end
37
36
  end
38
37
 
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
112
- end
113
- rescue JSON::ParserError => e
114
- puts "Error parsing sessions response: #{e.message}".red.bold
115
- return false
116
- 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
- end
384
- end
385
-
386
38
  # Login to iDRAC
387
39
  def login
388
40
  # If we're in direct mode, skip login attempts
@@ -392,16 +44,11 @@ module IDRAC
392
44
  end
393
45
 
394
46
  # Try to create a Redfish session
395
- if create_redfish_session
47
+ if session.create
396
48
  puts "Successfully logged in to iDRAC using Redfish session".green
397
49
  return true
398
50
  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
-
51
+ puts "Failed to create Redfish session, falling back to direct mode".light_yellow
405
52
  @direct_mode = true
406
53
  return true
407
54
  end
@@ -409,14 +56,8 @@ module IDRAC
409
56
 
410
57
  # Logout from iDRAC
411
58
  def logout
412
- if @x_auth_token
413
- delete_redfish_session
414
- end
415
-
416
- if @session_id
417
- webui_logout
418
- end
419
-
59
+ session.delete if session.x_auth_token
60
+ web.logout if web.session_id
420
61
  puts "Logged out from iDRAC".green
421
62
  return true
422
63
  end
@@ -452,10 +93,10 @@ module IDRAC
452
93
  end
453
94
  else
454
95
  # Use X-Auth-Token if available
455
- if @x_auth_token
96
+ if session.x_auth_token
456
97
  # Add the X-Auth-Token header to the request
457
98
  options[:headers] ||= {}
458
- options[:headers]['X-Auth-Token'] = @x_auth_token
99
+ options[:headers]['X-Auth-Token'] = session.x_auth_token
459
100
 
460
101
  # Make the request
461
102
  begin
@@ -469,7 +110,7 @@ module IDRAC
469
110
  puts "Session expired or invalid, attempting to create a new session...".light_yellow
470
111
 
471
112
  # Try to create a new session
472
- if create_redfish_session
113
+ if session.create
473
114
  puts "Successfully created a new session, retrying request...".green
474
115
  return authenticated_request(method, path, options, retry_count + 1)
475
116
  else
@@ -484,7 +125,7 @@ module IDRAC
484
125
  puts "Error during authenticated request (token mode): #{e.message}".red.bold
485
126
 
486
127
  # Try to create a new session
487
- if create_redfish_session
128
+ if session.create
488
129
  puts "Successfully created a new session after error, retrying request...".green
489
130
  return authenticated_request(method, path, options, retry_count + 1)
490
131
  else
@@ -495,7 +136,7 @@ module IDRAC
495
136
  end
496
137
  else
497
138
  # If we don't have a token, try to create a session
498
- if create_redfish_session
139
+ if session.create
499
140
  puts "Successfully created a new session, making request...".green
500
141
  return authenticated_request(method, path, options, retry_count + 1)
501
142
  else
@@ -509,8 +150,8 @@ module IDRAC
509
150
 
510
151
  def get(path:, headers: {})
511
152
  # 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
153
+ if web.cookies.nil? && path.include?('screen/screen.jpg')
154
+ web.login unless web.session_id
514
155
  end
515
156
 
516
157
  headers_to_use = {
@@ -518,16 +159,16 @@ module IDRAC
518
159
  "Accept-Encoding" => "deflate, gzip"
519
160
  }
520
161
 
521
- if @cookies
522
- headers_to_use["Cookie"] = @cookies
162
+ if web.cookies
163
+ headers_to_use["Cookie"] = web.cookies
523
164
  elsif @direct_mode
524
165
  # In direct mode, use Basic Auth
525
166
  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
167
+ elsif session.x_auth_token
168
+ headers_to_use["X-Auth-Token"] = session.x_auth_token
528
169
  end
529
170
 
530
- response = HTTParty.get(
171
+ HTTParty.get(
531
172
  "#{base_url}/#{path}",
532
173
  headers: headers_to_use.merge(headers),
533
174
  verify: false
@@ -535,9 +176,7 @@ module IDRAC
535
176
  end
536
177
 
537
178
  def screenshot
538
- # Create a Screenshot instance and capture a screenshot
539
- screenshot_instance = Screenshot.new(self)
540
- screenshot_instance.capture
179
+ web.capture_screenshot
541
180
  end
542
181
 
543
182
  def base_url
@@ -0,0 +1,278 @@
1
+ require 'faraday'
2
+ require 'base64'
3
+ require 'json'
4
+ require 'colorize'
5
+ require 'uri'
6
+
7
+ module IDRAC
8
+ class Session
9
+ attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl,
10
+ :x_auth_token, :session_location, :direct_mode, :auto_delete_sessions
11
+
12
+ def initialize(client)
13
+ @client = client
14
+ @host = client.host
15
+ @username = client.username
16
+ @password = client.password
17
+ @port = client.port
18
+ @use_ssl = client.use_ssl
19
+ @verify_ssl = client.verify_ssl
20
+ @x_auth_token = nil
21
+ @session_location = nil
22
+ @direct_mode = client.direct_mode
23
+ @sessions_maxed = false
24
+ @auto_delete_sessions = client.auto_delete_sessions
25
+ end
26
+
27
+ def connection
28
+ @connection ||= Faraday.new(url: base_url, ssl: { verify: verify_ssl }) do |faraday|
29
+ faraday.request :multipart
30
+ faraday.request :url_encoded
31
+ faraday.adapter Faraday.default_adapter
32
+ end
33
+ end
34
+
35
+ # Force clear all sessions by directly using Basic Auth
36
+ def force_clear_sessions
37
+ puts "Attempting to force clear all sessions...".light_cyan
38
+
39
+ if delete_all_sessions_with_basic_auth
40
+ puts "Successfully cleared sessions using Basic Auth".green
41
+ true
42
+ else
43
+ puts "Failed to clear sessions using Basic Auth".red
44
+ false
45
+ end
46
+ end
47
+
48
+ # Delete all sessions using Basic Authentication
49
+ def delete_all_sessions_with_basic_auth
50
+ puts "Attempting to delete all sessions using Basic Authentication...".light_cyan
51
+
52
+ # First, get the list of sessions
53
+ sessions_url = '/redfish/v1/SessionService/Sessions'
54
+
55
+ begin
56
+ # Get the list of sessions
57
+ response = request_with_basic_auth(:get, sessions_url)
58
+
59
+ if response.status != 200
60
+ puts "Failed to get sessions list: #{response.status} - #{response.body}".red
61
+ return false
62
+ end
63
+
64
+ # Parse the response to get session IDs
65
+ begin
66
+ sessions_data = JSON.parse(response.body)
67
+
68
+ if sessions_data['Members'] && sessions_data['Members'].any?
69
+ puts "Found #{sessions_data['Members'].count} active sessions".light_yellow
70
+
71
+ # Delete each session
72
+ success = true
73
+ sessions_data['Members'].each do |session|
74
+ session_url = session['@odata.id']
75
+
76
+ # Skip if no URL
77
+ next unless session_url
78
+
79
+ # Delete the session
80
+ delete_response = request_with_basic_auth(:delete, session_url)
81
+
82
+ if delete_response.status == 200 || delete_response.status == 204
83
+ puts "Successfully deleted session: #{session_url}".green
84
+ else
85
+ puts "Failed to delete session #{session_url}: #{delete_response.status}".red
86
+ success = false
87
+ end
88
+
89
+ # Small delay between deletions
90
+ sleep(1)
91
+ end
92
+
93
+ return success
94
+ else
95
+ puts "No active sessions found".light_yellow
96
+ return true
97
+ end
98
+ rescue JSON::ParserError => e
99
+ puts "Error parsing sessions response: #{e.message}".red.bold
100
+ return false
101
+ end
102
+ rescue => e
103
+ puts "Error during session deletion with Basic Auth: #{e.message}".red.bold
104
+ return false
105
+ end
106
+ end
107
+
108
+ # Create a Redfish session
109
+ def create
110
+ # Skip if we're in direct mode
111
+ if @direct_mode
112
+ puts "Skipping Redfish session creation (direct mode)".light_yellow
113
+ return false
114
+ end
115
+
116
+ url = '/redfish/v1/SessionService/Sessions'
117
+ payload = { "UserName" => username, "Password" => password }
118
+
119
+ # Try creation methods in sequence
120
+ return true if create_session_with_content_type(url, payload)
121
+ return true if create_session_with_basic_auth(url, payload)
122
+ return true if handle_max_sessions_and_retry(url, payload)
123
+ return true if create_session_with_form_urlencoded(url, payload)
124
+
125
+ # If all attempts fail, switch to direct mode
126
+ @direct_mode = true
127
+ false
128
+ end
129
+
130
+ # Delete the Redfish session
131
+ def delete
132
+ return unless @x_auth_token && @session_location
133
+
134
+ begin
135
+ puts "Deleting Redfish session...".light_cyan
136
+
137
+ # Use the X-Auth-Token for authentication
138
+ headers = { 'X-Auth-Token' => @x_auth_token }
139
+
140
+ response = connection.delete(@session_location) do |req|
141
+ req.headers.merge!(headers)
142
+ end
143
+
144
+ if response.status == 200 || response.status == 204
145
+ puts "Redfish session deleted successfully".green
146
+ @x_auth_token = nil
147
+ @session_location = nil
148
+ return true
149
+ else
150
+ puts "Failed to delete Redfish session: #{response.status} - #{response.body}".red
151
+ return false
152
+ end
153
+ rescue => e
154
+ puts "Error during Redfish session deletion: #{e.message}".red.bold
155
+ return false
156
+ end
157
+ end
158
+
159
+ private
160
+
161
+ def base_url
162
+ protocol = use_ssl ? 'https' : 'http'
163
+ "#{protocol}://#{host}:#{port}"
164
+ end
165
+
166
+ def basic_auth_headers
167
+ {
168
+ 'Authorization' => "Basic #{Base64.strict_encode64("#{username}:#{password}")}",
169
+ 'Content-Type' => 'application/json'
170
+ }
171
+ end
172
+
173
+ def request_with_basic_auth(method, url, body = nil)
174
+ connection.send(method, url) do |req|
175
+ req.headers.merge!(basic_auth_headers)
176
+ req.body = body if body
177
+ end
178
+ rescue => e
179
+ puts "Error during #{method} request with Basic Auth: #{e.message}".red.bold
180
+ raise e
181
+ end
182
+
183
+ def process_session_response(response)
184
+ if response.status == 201 || response.status == 200
185
+ @x_auth_token = response.headers['X-Auth-Token']
186
+ @session_location = response.headers['Location']
187
+ @sessions_maxed = false
188
+ true
189
+ else
190
+ false
191
+ end
192
+ end
193
+
194
+ def create_session_with_content_type(url, payload)
195
+ begin
196
+ response = connection.post(url) do |req|
197
+ req.headers['Content-Type'] = 'application/json'
198
+ req.body = payload.to_json
199
+ end
200
+
201
+ if process_session_response(response)
202
+ puts "Redfish session created successfully".green
203
+ return true
204
+ end
205
+ rescue => e
206
+ puts "First session creation attempt failed: #{e.message}".light_red
207
+ end
208
+ false
209
+ end
210
+
211
+ def create_session_with_basic_auth(url, payload)
212
+ begin
213
+ response = request_with_basic_auth(:post, url, payload.to_json)
214
+
215
+ if process_session_response(response)
216
+ puts "Redfish session created successfully with Basic Auth".green
217
+ return true
218
+ elsif response.status == 400 && response.body.include?("maximum number of user sessions")
219
+ puts "Maximum sessions reached during Redfish session creation".light_red
220
+ @sessions_maxed = true
221
+ return false
222
+ else
223
+ puts "Failed to create Redfish session: #{response.status} - #{response.body}".red
224
+ return false
225
+ end
226
+ rescue => e
227
+ puts "Error during Redfish session creation with Basic Auth: #{e.message}".red.bold
228
+ return false
229
+ end
230
+ end
231
+
232
+ def handle_max_sessions_and_retry(url, payload)
233
+ return false unless @sessions_maxed && @auto_delete_sessions
234
+
235
+ puts "Auto-delete sessions is enabled, attempting to clear sessions".light_cyan
236
+ if force_clear_sessions
237
+ puts "Successfully cleared sessions, trying to create a new session".green
238
+
239
+ # Try one more time after clearing
240
+ response = request_with_basic_auth(:post, url, payload.to_json)
241
+
242
+ if process_session_response(response)
243
+ puts "Redfish session created successfully after clearing sessions".green
244
+ return true
245
+ else
246
+ puts "Failed to create Redfish session after clearing sessions: #{response.status} - #{response.body}".red
247
+ return false
248
+ end
249
+ else
250
+ puts "Failed to clear sessions, switching to direct mode".light_yellow
251
+ return false
252
+ end
253
+ end
254
+
255
+ def create_session_with_form_urlencoded(url, payload)
256
+ # Only try with form-urlencoded if we had a 415 error previously
257
+ begin
258
+ puts "Trying with form-urlencoded content type".light_cyan
259
+ response = connection.post(url) do |req|
260
+ req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
261
+ req.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
262
+ req.body = "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}"
263
+ end
264
+
265
+ if process_session_response(response)
266
+ puts "Redfish session created successfully with form-urlencoded".green
267
+ return true
268
+ else
269
+ puts "Failed with form-urlencoded too: #{response.status} - #{response.body}".red
270
+ return false
271
+ end
272
+ rescue => e
273
+ puts "Error during form-urlencoded session creation: #{e.message}".red.bold
274
+ return false
275
+ end
276
+ end
277
+ end
278
+ end
data/lib/idrac/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IDRAC
4
- VERSION = "0.1.38"
4
+ VERSION = "0.1.40"
5
5
  end
data/lib/idrac/web.rb ADDED
@@ -0,0 +1,187 @@
1
+ require 'httparty'
2
+ require 'nokogiri'
3
+ require 'uri'
4
+ require 'colorize'
5
+
6
+ module IDRAC
7
+ class Web
8
+ attr_reader :client, :session_id, :cookies
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ @session_id = nil
13
+ @cookies = nil
14
+ @tried_clearing_sessions = false
15
+ end
16
+
17
+ # Login to the WebUI
18
+ def login(retry_count = 0)
19
+ # Limit retries to prevent infinite loops
20
+ if retry_count >= 3
21
+ puts "Maximum retry count reached for WebUI login".red
22
+ return false
23
+ end
24
+
25
+ # Skip if we already have a session ID
26
+ return true if @session_id
27
+
28
+ begin
29
+ puts "Logging in to WebUI...".light_cyan
30
+
31
+ # Create the login URL
32
+ login_url = "#{base_url}/data/login"
33
+
34
+ # Create the login payload
35
+ payload = {
36
+ 'user' => client.username,
37
+ 'password' => client.password
38
+ }
39
+
40
+ # Make the login request
41
+ response = HTTParty.post(
42
+ login_url,
43
+ body: payload,
44
+ verify: client.verify_ssl,
45
+ headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }
46
+ )
47
+
48
+ # Check if the login was successful
49
+ if response.code == 200
50
+ # Extract the session ID from the response
51
+ if response.body.include?('ST2')
52
+ @session_id = response.body.match(/ST2=([^;]+)/)[1]
53
+ @cookies = response.headers['set-cookie']
54
+ puts "WebUI login successful".green
55
+ return response.body
56
+ else
57
+ puts "WebUI login failed: No session ID found in response".red
58
+ return false
59
+ end
60
+ elsif response.code == 400 && response.body.include?("maximum number of user sessions")
61
+ puts "Maximum sessions reached during WebUI login".light_red
62
+
63
+ # Try to clear sessions if auto_delete_sessions is enabled
64
+ if client.auto_delete_sessions && !@tried_clearing_sessions
65
+ puts "Auto-delete sessions is enabled, attempting to clear sessions".light_cyan
66
+ @tried_clearing_sessions = true
67
+
68
+ if client.session.force_clear_sessions
69
+ puts "Successfully cleared sessions, trying WebUI login again".green
70
+ return login(retry_count + 1)
71
+ else
72
+ puts "Failed to clear sessions for WebUI login".red
73
+ return false
74
+ end
75
+ else
76
+ puts "Auto-delete sessions is disabled or already tried clearing".light_yellow
77
+ return false
78
+ end
79
+ else
80
+ puts "WebUI login failed: #{response.code} - #{response.body}".red
81
+ return false
82
+ end
83
+ rescue => e
84
+ puts "Error during WebUI login: #{e.message}".red.bold
85
+ return false
86
+ end
87
+ end
88
+
89
+ # Logout from the WebUI
90
+ def logout
91
+ return unless @session_id
92
+
93
+ begin
94
+ puts "Logging out from WebUI...".light_cyan
95
+
96
+ # Create the logout URL
97
+ logout_url = "#{base_url}/data/logout"
98
+
99
+ # Make the logout request
100
+ response = HTTParty.get(
101
+ logout_url,
102
+ verify: client.verify_ssl,
103
+ headers: { 'Cookie' => @cookies }
104
+ )
105
+
106
+ # Check if the logout was successful
107
+ if response.code == 200
108
+ puts "WebUI logout successful".green
109
+ @session_id = nil
110
+ @cookies = nil
111
+ return true
112
+ else
113
+ puts "WebUI logout failed: #{response.code} - #{response.body}".red
114
+ return false
115
+ end
116
+ rescue => e
117
+ puts "Error during WebUI logout: #{e.message}".red.bold
118
+ return false
119
+ end
120
+ end
121
+
122
+ # Capture a screenshot
123
+ def capture_screenshot
124
+ # Login to get the forward URL and cookies
125
+ forward_url = login
126
+ return nil unless forward_url
127
+
128
+ # Extract the key-value pairs from the forward URL (format: index?ST1=ABC,ST2=DEF)
129
+ tokens = forward_url.split("?").last.split(",").inject({}) do |acc, kv|
130
+ k, v = kv.split("=")
131
+ acc[k] = v
132
+ acc
133
+ end
134
+
135
+ # Generate a timestamp for the request
136
+ timestamp_ms = (Time.now.to_f * 1000).to_i
137
+
138
+ # First request to trigger the screenshot capture
139
+ path = "data?get=consolepreview[manual%20#{timestamp_ms}]"
140
+ res = get(path: path, headers: tokens)
141
+ raise Error, "Failed to trigger screenshot capture." unless res.code.between?(200, 299)
142
+
143
+ # Wait for the screenshot to be generated
144
+ sleep 2
145
+
146
+ # Second request to get the actual screenshot image
147
+ path = "capconsole/scapture0.png?#{timestamp_ms}"
148
+ res = get(path: path, headers: tokens)
149
+ raise Error, "Failed to retrieve screenshot image." unless res.code.between?(200, 299)
150
+
151
+ # Save the screenshot to a file
152
+ filename = "idrac_screenshot_#{timestamp_ms}.png"
153
+ File.open(filename, "wb") { |f| f.write(res.body) }
154
+
155
+ # Return the filename
156
+ filename
157
+ end
158
+
159
+ # HTTP GET request for WebUI operations
160
+ def get(path:, headers: {})
161
+ headers_to_use = {
162
+ "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",
163
+ "Accept-Encoding" => "deflate, gzip"
164
+ }
165
+
166
+ if @cookies
167
+ headers_to_use["Cookie"] = @cookies
168
+ elsif client.direct_mode
169
+ # In direct mode, use Basic Auth
170
+ headers_to_use["Authorization"] = "Basic #{Base64.strict_encode64("#{client.username}:#{client.password}")}"
171
+ end
172
+
173
+ HTTParty.get(
174
+ "#{base_url}/#{path}",
175
+ headers: headers_to_use.merge(headers),
176
+ verify: false
177
+ )
178
+ end
179
+
180
+ private
181
+
182
+ def base_url
183
+ protocol = client.use_ssl ? 'https' : 'http'
184
+ "#{protocol}://#{client.host}:#{client.port}"
185
+ end
186
+ end
187
+ end
data/lib/idrac.rb CHANGED
@@ -12,8 +12,9 @@ require 'debug' if ENV['RUBY_ENV'] == 'development'
12
12
 
13
13
  require_relative "idrac/version"
14
14
  require_relative "idrac/error"
15
+ require_relative "idrac/session"
16
+ require_relative "idrac/web"
15
17
  require_relative "idrac/client"
16
- require_relative "idrac/screenshot"
17
18
  require_relative "idrac/firmware"
18
19
  require_relative "idrac/firmware_catalog"
19
20
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idrac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.38
4
+ version: 0.1.40
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Siegel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-02 00:00:00.000000000 Z
11
+ date: 2025-04-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -224,8 +224,9 @@ files:
224
224
  - lib/idrac/error.rb
225
225
  - lib/idrac/firmware.rb
226
226
  - lib/idrac/firmware_catalog.rb
227
- - lib/idrac/screenshot.rb
227
+ - lib/idrac/session.rb
228
228
  - lib/idrac/version.rb
229
+ - lib/idrac/web.rb
229
230
  homepage: http://github.com
230
231
  licenses:
231
232
  - MIT
@@ -246,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
246
247
  - !ruby/object:Gem::Version
247
248
  version: '0'
248
249
  requirements: []
249
- rubygems_version: 3.4.19
250
+ rubygems_version: 3.5.16
250
251
  signing_key:
251
252
  specification_version: 4
252
253
  summary: API Client for Dell iDRAC
@@ -1,49 +0,0 @@
1
- require 'httparty'
2
- require 'nokogiri'
3
-
4
- module IDRAC
5
- # Reverse engineered screenshot functionality for iDRAC
6
- # This uses introspection on how the web UI creates screenshots rather than the Redfish API
7
- class Screenshot
8
- attr_reader :client
9
-
10
- def initialize(client)
11
- @client = client
12
- end
13
-
14
- def capture
15
- # Login to get the forward URL and cookies
16
- forward_url = client.login
17
-
18
- # Extract the key-value pairs from the forward URL (format: index?ST1=ABC,ST2=DEF)
19
- tokens = forward_url.split("?").last.split(",").inject({}) do |acc, kv|
20
- k, v = kv.split("=")
21
- acc[k] = v
22
- acc
23
- end
24
-
25
- # Generate a timestamp for the request
26
- timestamp_ms = (Time.now.to_f * 1000).to_i
27
-
28
- # First request to trigger the screenshot capture
29
- path = "data?get=consolepreview[manual%20#{timestamp_ms}]"
30
- res = client.get(path: path, headers: tokens)
31
- raise Error, "Failed to trigger screenshot capture." unless res.code.between?(200, 299)
32
-
33
- # Wait for the screenshot to be generated
34
- sleep 2
35
-
36
- # Second request to get the actual screenshot image
37
- path = "capconsole/scapture0.png?#{timestamp_ms}"
38
- res = client.get(path: path, headers: tokens)
39
- raise Error, "Failed to retrieve screenshot image." unless res.code.between?(200, 299)
40
-
41
- # Save the screenshot to a file
42
- filename = "idrac_screenshot_#{timestamp_ms}.png"
43
- File.open(filename, "wb") { |f| f.write(res.body) }
44
-
45
- # Return the filename
46
- filename
47
- end
48
- end
49
- end