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.
- checksums.yaml +4 -4
- data/README.md +76 -14
- data/bin/idrac +246 -89
- data/lib/idrac/client.rb +72 -382
- data/lib/idrac/firmware.rb +26 -2
- data/lib/idrac/jobs.rb +212 -0
- data/lib/idrac/lifecycle.rb +300 -0
- data/lib/idrac/power.rb +195 -0
- data/lib/idrac/session.rb +717 -0
- data/lib/idrac/version.rb +1 -1
- data/lib/idrac/web.rb +187 -0
- data/lib/idrac.rb +29 -7
- metadata +8 -4
- data/lib/idrac/screenshot.rb +0 -49
@@ -0,0 +1,717 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'base64'
|
3
|
+
require 'json'
|
4
|
+
require 'colorize'
|
5
|
+
require 'uri'
|
6
|
+
require 'logger'
|
7
|
+
require 'socket'
|
8
|
+
|
9
|
+
module IDRAC
|
10
|
+
class Session
|
11
|
+
attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl,
|
12
|
+
:x_auth_token, :session_location, :direct_mode, :auto_delete_sessions
|
13
|
+
attr_accessor :verbosity
|
14
|
+
|
15
|
+
include Debuggable
|
16
|
+
|
17
|
+
def initialize(client)
|
18
|
+
@client = client
|
19
|
+
@host = client.host
|
20
|
+
@username = client.username
|
21
|
+
@password = client.password
|
22
|
+
@port = client.port
|
23
|
+
@use_ssl = client.use_ssl
|
24
|
+
@verify_ssl = client.verify_ssl
|
25
|
+
@x_auth_token = nil
|
26
|
+
@session_location = nil
|
27
|
+
@direct_mode = client.direct_mode
|
28
|
+
@sessions_maxed = false
|
29
|
+
@auto_delete_sessions = client.auto_delete_sessions
|
30
|
+
@verbosity = client.respond_to?(:verbosity) ? client.verbosity : 0
|
31
|
+
end
|
32
|
+
|
33
|
+
def connection
|
34
|
+
@connection ||= Faraday.new(url: base_url, ssl: {
|
35
|
+
verify: verify_ssl
|
36
|
+
# Keep SSL settings minimal for cross-version compatibility
|
37
|
+
}) do |faraday|
|
38
|
+
faraday.request :multipart
|
39
|
+
faraday.request :url_encoded
|
40
|
+
faraday.adapter Faraday.default_adapter
|
41
|
+
# Add request/response logging
|
42
|
+
if @verbosity > 0
|
43
|
+
faraday.response :logger, Logger.new(STDOUT), bodies: @verbosity >= 2 do |logger|
|
44
|
+
logger.filter(/(Authorization: Basic )([^,\n]+)/, '\1[FILTERED]')
|
45
|
+
logger.filter(/(Password"=>"?)([^,"]+)/, '\1[FILTERED]')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Force clear all sessions by directly using Basic Auth
|
52
|
+
def force_clear_sessions
|
53
|
+
debug "Attempting to force clear all sessions...", 0
|
54
|
+
|
55
|
+
max_retries = 3
|
56
|
+
retry_count = 0
|
57
|
+
|
58
|
+
while retry_count < max_retries
|
59
|
+
if delete_all_sessions_with_basic_auth
|
60
|
+
debug "Successfully cleared sessions using Basic Auth", 0, :green
|
61
|
+
return true
|
62
|
+
else
|
63
|
+
retry_count += 1
|
64
|
+
if retry_count < max_retries
|
65
|
+
# Exponential backoff
|
66
|
+
sleep_time = 2 ** retry_count
|
67
|
+
debug "Retrying session clear after #{sleep_time} seconds (attempt #{retry_count+1}/#{max_retries})", 0, :light_yellow
|
68
|
+
sleep(sleep_time)
|
69
|
+
else
|
70
|
+
debug "Failed to clear sessions after #{max_retries} attempts", 0, :red
|
71
|
+
return false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
# Delete all sessions using Basic Authentication
|
80
|
+
def delete_all_sessions_with_basic_auth
|
81
|
+
debug "Attempting to delete all sessions using Basic Authentication...", 0
|
82
|
+
|
83
|
+
# First, get the list of sessions
|
84
|
+
sessions_url = determine_session_endpoint
|
85
|
+
|
86
|
+
begin
|
87
|
+
# Get the list of sessions
|
88
|
+
response = request_with_basic_auth(:get, sessions_url, nil, 'application/json')
|
89
|
+
|
90
|
+
if response.status != 200
|
91
|
+
debug "Failed to get sessions list: #{response.status} - #{response.body}", 0, :red
|
92
|
+
# If we received HTML error, assume we can't get sessions and try direct session deletion
|
93
|
+
if response.headers['content-type']&.include?('text/html') || response.body.to_s.include?('DOCTYPE html')
|
94
|
+
debug "Received HTML error response, trying direct session deletion", 0, :light_yellow
|
95
|
+
return try_delete_latest_sessions
|
96
|
+
end
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
|
100
|
+
# Parse the response to get session IDs
|
101
|
+
begin
|
102
|
+
sessions_data = JSON.parse(response.body)
|
103
|
+
|
104
|
+
if sessions_data['Members'] && sessions_data['Members'].any?
|
105
|
+
debug "Found #{sessions_data['Members'].count} active sessions", 0, :light_yellow
|
106
|
+
|
107
|
+
# Delete each session
|
108
|
+
success = true
|
109
|
+
sessions_data['Members'].each do |session|
|
110
|
+
session_url = session['@odata.id']
|
111
|
+
|
112
|
+
# Skip if no URL
|
113
|
+
next unless session_url
|
114
|
+
|
115
|
+
# Delete the session
|
116
|
+
delete_response = request_with_basic_auth(:delete, session_url, nil, 'application/json')
|
117
|
+
|
118
|
+
if delete_response.status == 200 || delete_response.status == 204
|
119
|
+
debug "Successfully deleted session: #{session_url}", 1, :green
|
120
|
+
else
|
121
|
+
debug "Failed to delete session #{session_url}: #{delete_response.status}", 0, :red
|
122
|
+
success = false
|
123
|
+
end
|
124
|
+
|
125
|
+
# Small delay between deletions
|
126
|
+
sleep(1)
|
127
|
+
end
|
128
|
+
|
129
|
+
return success
|
130
|
+
else
|
131
|
+
debug "No active sessions found", 0, :light_yellow
|
132
|
+
return true
|
133
|
+
end
|
134
|
+
rescue JSON::ParserError => e
|
135
|
+
debug "Error parsing sessions response: #{e.message}", 0, :red
|
136
|
+
debug "Trying direct session deletion", 0, :light_yellow
|
137
|
+
return try_delete_latest_sessions
|
138
|
+
end
|
139
|
+
rescue => e
|
140
|
+
debug "Error during session deletion with Basic Auth: #{e.message}", 0, :red
|
141
|
+
debug "Trying direct session deletion", 0, :light_yellow
|
142
|
+
return try_delete_latest_sessions
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Try to delete sessions by direct URL when we can't list sessions
|
147
|
+
def try_delete_latest_sessions
|
148
|
+
# Try to delete sessions by direct URL when we can't list sessions
|
149
|
+
debug "Attempting to delete recent sessions directly...", 0
|
150
|
+
base_url = determine_session_endpoint
|
151
|
+
success = false
|
152
|
+
|
153
|
+
# Try session IDs 1-10 (common for iDRAC)
|
154
|
+
(1..10).each do |id|
|
155
|
+
session_url = "#{base_url}/#{id}"
|
156
|
+
begin
|
157
|
+
delete_response = request_with_basic_auth(:delete, session_url, nil, 'application/json')
|
158
|
+
|
159
|
+
if delete_response.status == 200 || delete_response.status == 204
|
160
|
+
debug "Successfully deleted session: #{session_url}", 1, :green
|
161
|
+
success = true
|
162
|
+
else
|
163
|
+
debug "Failed to delete session #{session_url}: #{delete_response.status}", 1, :red
|
164
|
+
end
|
165
|
+
rescue => e
|
166
|
+
debug "Error deleting session #{session_url}: #{e.message}", 1, :red
|
167
|
+
end
|
168
|
+
|
169
|
+
# Small delay between deletions
|
170
|
+
sleep(0.5)
|
171
|
+
end
|
172
|
+
|
173
|
+
return success
|
174
|
+
end
|
175
|
+
|
176
|
+
# Create a Redfish session
|
177
|
+
def create
|
178
|
+
# Skip if we're in direct mode
|
179
|
+
if @direct_mode
|
180
|
+
debug "Skipping Redfish session creation (direct mode)", 0, :light_yellow
|
181
|
+
return false
|
182
|
+
end
|
183
|
+
|
184
|
+
# Determine the correct session endpoint based on Redfish version
|
185
|
+
session_endpoint = determine_session_endpoint
|
186
|
+
|
187
|
+
payload = { "UserName" => username, "Password" => password }
|
188
|
+
|
189
|
+
debug "Attempting to create Redfish session at #{base_url}#{session_endpoint}", 0
|
190
|
+
debug "SSL verification: #{verify_ssl ? 'Enabled' : 'Disabled'}", 1
|
191
|
+
print_connection_debug_info if @verbosity >= 2
|
192
|
+
|
193
|
+
# Try creation methods in sequence
|
194
|
+
return true if create_session_with_content_type(session_endpoint, payload)
|
195
|
+
return true if create_session_with_basic_auth(session_endpoint, payload)
|
196
|
+
return true if handle_max_sessions_and_retry(session_endpoint, payload)
|
197
|
+
return true if create_session_with_form_urlencoded(session_endpoint, payload)
|
198
|
+
|
199
|
+
# If all attempts fail, switch to direct mode
|
200
|
+
@direct_mode = true
|
201
|
+
false
|
202
|
+
end
|
203
|
+
|
204
|
+
# Delete the Redfish session
|
205
|
+
def delete
|
206
|
+
return unless @x_auth_token && @session_location
|
207
|
+
|
208
|
+
begin
|
209
|
+
debug "Deleting Redfish session...", 0
|
210
|
+
|
211
|
+
# Use the X-Auth-Token for authentication
|
212
|
+
headers = { 'X-Auth-Token' => @x_auth_token }
|
213
|
+
|
214
|
+
response = connection.delete(@session_location) do |req|
|
215
|
+
req.headers.merge!(headers)
|
216
|
+
end
|
217
|
+
|
218
|
+
if response.status == 200 || response.status == 204
|
219
|
+
debug "Redfish session deleted successfully", 0, :green
|
220
|
+
@x_auth_token = nil
|
221
|
+
@session_location = nil
|
222
|
+
return true
|
223
|
+
else
|
224
|
+
debug "Failed to delete Redfish session: #{response.status} - #{response.body}", 0, :red
|
225
|
+
return false
|
226
|
+
end
|
227
|
+
rescue => e
|
228
|
+
debug "Error during Redfish session deletion: #{e.message}", 0, :red
|
229
|
+
return false
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
def base_url
|
236
|
+
protocol = use_ssl ? 'https' : 'http'
|
237
|
+
"#{protocol}://#{host}:#{port}"
|
238
|
+
end
|
239
|
+
|
240
|
+
def print_connection_debug_info
|
241
|
+
begin
|
242
|
+
debug "=== Connection Debug Info ===", 2, :yellow
|
243
|
+
debug "Host: #{host}, Port: #{port}, SSL: #{use_ssl}", 2
|
244
|
+
debug "Ruby version: #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}", 2
|
245
|
+
|
246
|
+
begin
|
247
|
+
debug "OpenSSL version: #{OpenSSL::OPENSSL_VERSION}", 2
|
248
|
+
rescue => e
|
249
|
+
debug "Could not determine OpenSSL version: #{e.message}", 2
|
250
|
+
end
|
251
|
+
|
252
|
+
# Test basic TCP connection first
|
253
|
+
begin
|
254
|
+
socket = TCPSocket.new(host, port)
|
255
|
+
debug "TCP connection successful", 2, :green
|
256
|
+
socket.close
|
257
|
+
rescue => e
|
258
|
+
debug "TCP connection failed: #{e.class.name}: #{e.message}", 2, :red
|
259
|
+
debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
|
260
|
+
end
|
261
|
+
|
262
|
+
# Try SSL connection if using SSL
|
263
|
+
if use_ssl
|
264
|
+
begin
|
265
|
+
tcp_client = TCPSocket.new(host, port)
|
266
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
267
|
+
ssl_context.verify_mode = verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
268
|
+
ssl_client = OpenSSL::SSL::SSLSocket.new(tcp_client, ssl_context)
|
269
|
+
ssl_client.connect
|
270
|
+
debug "SSL connection successful", 2, :green
|
271
|
+
debug "SSL protocol: #{ssl_client.ssl_version}", 2
|
272
|
+
debug "SSL cipher: #{ssl_client.cipher.join(', ')}", 2
|
273
|
+
|
274
|
+
if @verbosity >= 3
|
275
|
+
cert = ssl_client.peer_cert
|
276
|
+
if cert
|
277
|
+
debug "Server certificate:", 3
|
278
|
+
debug " Subject: #{cert.subject}", 3
|
279
|
+
debug " Issuer: #{cert.issuer}", 3
|
280
|
+
debug " Validity: #{cert.not_before} to #{cert.not_after}", 3
|
281
|
+
debug " Fingerprint: #{OpenSSL::Digest::SHA256.new(cert.to_der).to_s}", 3
|
282
|
+
else
|
283
|
+
debug "No server certificate available", 3, :yellow
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
ssl_client.close
|
288
|
+
tcp_client.close
|
289
|
+
rescue => e
|
290
|
+
debug "SSL connection failed: #{e.class.name}: #{e.message}", 2, :red
|
291
|
+
debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
|
292
|
+
end
|
293
|
+
end
|
294
|
+
debug "===========================", 2, :yellow
|
295
|
+
rescue => e
|
296
|
+
debug "Error during connection debugging: #{e.class.name}: #{e.message}", 2, :red
|
297
|
+
debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def basic_auth_headers(content_type = 'application/json')
|
302
|
+
{
|
303
|
+
'Authorization' => "Basic #{Base64.strict_encode64("#{username}:#{password}")}",
|
304
|
+
'Content-Type' => content_type
|
305
|
+
}
|
306
|
+
end
|
307
|
+
|
308
|
+
def request_with_basic_auth(method, url, body = nil, content_type = 'application/json')
|
309
|
+
debug "Basic Auth request: #{method.to_s.upcase} #{url}", 1
|
310
|
+
debug "Request body size: #{body.to_s.size} bytes", 2 if body
|
311
|
+
|
312
|
+
connection.send(method, url) do |req|
|
313
|
+
req.headers.merge!(basic_auth_headers(content_type))
|
314
|
+
req.body = body if body
|
315
|
+
debug "Request headers: #{req.headers.reject { |k,v| k =~ /auth/i }.to_json}", 2
|
316
|
+
end
|
317
|
+
rescue Faraday::SSLError => e
|
318
|
+
debug "SSL Error in Basic Auth request: #{e.message}", 0, :red
|
319
|
+
debug "OpenSSL version: #{OpenSSL::OPENSSL_VERSION}", 1
|
320
|
+
debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
|
321
|
+
raise e
|
322
|
+
rescue => e
|
323
|
+
debug "Error during #{method} request with Basic Auth: #{e.class.name}: #{e.message}", 0, :red
|
324
|
+
debug e.backtrace.join("\n"), 2 if e.backtrace && @verbosity >= 2
|
325
|
+
raise e
|
326
|
+
end
|
327
|
+
|
328
|
+
def process_session_response(response)
|
329
|
+
if response.status == 201 || response.status == 200
|
330
|
+
@x_auth_token = response.headers['X-Auth-Token']
|
331
|
+
@session_location = response.headers['Location']
|
332
|
+
@sessions_maxed = false
|
333
|
+
true
|
334
|
+
else
|
335
|
+
false
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def create_session_with_content_type(url, payload)
|
340
|
+
begin
|
341
|
+
debug "Creating session with Content-Type: application/json", 0
|
342
|
+
|
343
|
+
response = connection.post(url) do |req|
|
344
|
+
req.headers['Content-Type'] = 'application/json'
|
345
|
+
req.headers['Accept'] = 'application/json'
|
346
|
+
req.body = payload.to_json
|
347
|
+
debug "Request headers: #{req.headers.reject { |k,v| k =~ /auth/i }.to_json}", 2
|
348
|
+
debug "Request body: #{req.body}", 2
|
349
|
+
end
|
350
|
+
|
351
|
+
debug "Response status: #{response.status}", 1
|
352
|
+
debug "Response headers: #{response.headers.to_json}", 2
|
353
|
+
debug "Response body: #{response.body}", 2
|
354
|
+
|
355
|
+
if response.status == 405
|
356
|
+
debug "405 Method Not Allowed: Check if the endpoint supports POST requests and verify the request format.", 0, :red
|
357
|
+
return false
|
358
|
+
end
|
359
|
+
|
360
|
+
if process_session_response(response)
|
361
|
+
debug "Redfish session created successfully", 0, :green
|
362
|
+
return true
|
363
|
+
end
|
364
|
+
|
365
|
+
# If the response status is 415 (Unsupported Media Type), try with different Content-Type
|
366
|
+
if response.status == 415 || (response.body.to_s.include?("unsupported media type"))
|
367
|
+
debug "415 Unsupported Media Type, trying alternate content type", 0, :yellow
|
368
|
+
|
369
|
+
# Try with no content-type header, just the payload
|
370
|
+
alt_response = connection.post(url) do |req|
|
371
|
+
# No Content-Type header
|
372
|
+
req.headers['Accept'] = '*/*'
|
373
|
+
req.body = payload.to_json
|
374
|
+
end
|
375
|
+
|
376
|
+
if process_session_response(alt_response)
|
377
|
+
debug "Redfish session created successfully with alternate content type", 0, :green
|
378
|
+
return true
|
379
|
+
end
|
380
|
+
end
|
381
|
+
rescue Faraday::SSLError => e
|
382
|
+
debug "SSL Error: #{e.message}", 0, :red
|
383
|
+
debug "OpenSSL version: #{OpenSSL::OPENSSL_VERSION}", 1
|
384
|
+
debug "Connection URL: #{base_url}#{url}", 1
|
385
|
+
debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
|
386
|
+
return false
|
387
|
+
rescue => e
|
388
|
+
debug "First session creation attempt failed: #{e.class.name}: #{e.message}", 0, :light_red
|
389
|
+
debug e.backtrace.join("\n"), 2 if e.backtrace && @verbosity >= 2
|
390
|
+
end
|
391
|
+
false
|
392
|
+
end
|
393
|
+
|
394
|
+
def create_session_with_basic_auth(url, payload)
|
395
|
+
begin
|
396
|
+
debug "Creating session with Basic Auth", 0
|
397
|
+
|
398
|
+
# Try first with JSON format
|
399
|
+
response = request_with_basic_auth(:post, url, payload.to_json, 'application/json')
|
400
|
+
|
401
|
+
debug "Response status: #{response.status}", 1
|
402
|
+
debug "Response body size: #{response.body.to_s.size} bytes", 2
|
403
|
+
|
404
|
+
if @verbosity >= 2 || response.status >= 400
|
405
|
+
debug "Response body (first 500 chars): #{response.body.to_s[0..500]}", 2
|
406
|
+
end
|
407
|
+
|
408
|
+
if process_session_response(response)
|
409
|
+
debug "Redfish session created successfully with Basic Auth (JSON)", 0, :green
|
410
|
+
return true
|
411
|
+
end
|
412
|
+
|
413
|
+
# If that fails, try with form-urlencoded
|
414
|
+
if response.status == 415 || (response.body.to_s.include?("unsupported media type"))
|
415
|
+
debug "415 Unsupported Media Type with JSON, trying form-urlencoded", 0, :yellow
|
416
|
+
|
417
|
+
form_data = "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}"
|
418
|
+
form_response = request_with_basic_auth(:post, url, form_data, 'application/x-www-form-urlencoded')
|
419
|
+
|
420
|
+
if process_session_response(form_response)
|
421
|
+
debug "Redfish session created successfully with Basic Auth (form-urlencoded)", 0, :green
|
422
|
+
return true
|
423
|
+
elsif form_response.status == 400
|
424
|
+
# Check for maximum sessions error
|
425
|
+
if (form_response.body.include?("maximum number of user sessions") ||
|
426
|
+
form_response.body.include?("RAC0218") ||
|
427
|
+
form_response.body.include?("Internal Server Error"))
|
428
|
+
debug "Maximum sessions reached detected during session creation", 0, :light_red
|
429
|
+
@sessions_maxed = true
|
430
|
+
return false
|
431
|
+
end
|
432
|
+
end
|
433
|
+
elsif response.status == 400
|
434
|
+
# Check for maximum sessions error
|
435
|
+
if (response.body.include?("maximum number of user sessions") ||
|
436
|
+
response.body.include?("RAC0218") ||
|
437
|
+
response.body.include?("Internal Server Error"))
|
438
|
+
debug "Maximum sessions reached detected during session creation", 0, :light_red
|
439
|
+
@sessions_maxed = true
|
440
|
+
return false
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# Try one more approach with no Content-Type header
|
445
|
+
debug "Trying Basic Auth with no Content-Type header", 0, :yellow
|
446
|
+
no_content_type_response = connection.post(url) do |req|
|
447
|
+
req.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
|
448
|
+
req.headers['Accept'] = '*/*'
|
449
|
+
req.body = payload.to_json
|
450
|
+
end
|
451
|
+
|
452
|
+
if process_session_response(no_content_type_response)
|
453
|
+
debug "Redfish session created successfully with Basic Auth (no content type)", 0, :green
|
454
|
+
return true
|
455
|
+
end
|
456
|
+
|
457
|
+
debug "Failed to create Redfish session: #{response.status} - #{response.body}", 0, :red
|
458
|
+
return false
|
459
|
+
rescue Faraday::SSLError => e
|
460
|
+
debug "SSL Error in Basic Auth request: #{e.message}", 0, :red
|
461
|
+
debug "OpenSSL version: #{OpenSSL::OPENSSL_VERSION}", 1
|
462
|
+
debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
|
463
|
+
return false
|
464
|
+
rescue => e
|
465
|
+
debug "Error during Redfish session creation with Basic Auth: #{e.class.name}: #{e.message}", 0, :red
|
466
|
+
debug e.backtrace.join("\n"), 2 if e.backtrace && @verbosity >= 2
|
467
|
+
return false
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
def handle_max_sessions_and_retry(url, payload)
|
472
|
+
return false unless @sessions_maxed
|
473
|
+
|
474
|
+
debug "Maximum sessions reached, attempting to clear sessions", 0
|
475
|
+
if @auto_delete_sessions
|
476
|
+
if force_clear_sessions
|
477
|
+
debug "Successfully cleared sessions, trying to create a new session", 0, :green
|
478
|
+
|
479
|
+
# Give the iDRAC a moment to process the session deletions
|
480
|
+
sleep(3)
|
481
|
+
|
482
|
+
# Try one more time after clearing with form-urlencoded
|
483
|
+
begin
|
484
|
+
response = connection.post(url) do |req|
|
485
|
+
req.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
|
486
|
+
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
487
|
+
req.body = "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}"
|
488
|
+
end
|
489
|
+
|
490
|
+
if process_session_response(response)
|
491
|
+
debug "Redfish session created successfully after clearing sessions", 0, :green
|
492
|
+
return true
|
493
|
+
else
|
494
|
+
debug "Failed to create Redfish session after clearing sessions: #{response.status} - #{response.body}", 0, :red
|
495
|
+
# If still failing, try direct mode
|
496
|
+
debug "Falling back to direct mode", 0, :light_yellow
|
497
|
+
@direct_mode = true
|
498
|
+
return false
|
499
|
+
end
|
500
|
+
rescue => e
|
501
|
+
debug "Error during session creation after clearing: #{e.class.name}: #{e.message}", 0, :red
|
502
|
+
debug "Falling back to direct mode", 0, :light_yellow
|
503
|
+
@direct_mode = true
|
504
|
+
return false
|
505
|
+
end
|
506
|
+
else
|
507
|
+
debug "Failed to clear sessions, switching to direct mode", 0, :light_yellow
|
508
|
+
@direct_mode = true
|
509
|
+
return false
|
510
|
+
end
|
511
|
+
else
|
512
|
+
debug "Auto delete sessions is disabled, switching to direct mode", 0, :light_yellow
|
513
|
+
@direct_mode = true
|
514
|
+
return false
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
def create_session_with_form_urlencoded(url, payload)
|
519
|
+
# Only try with form-urlencoded if we had a 415 error previously
|
520
|
+
begin
|
521
|
+
debug "Trying with form-urlencoded content type", 0
|
522
|
+
debug "URL: #{base_url}#{url}", 1
|
523
|
+
|
524
|
+
# Try first without any authorization header
|
525
|
+
response = connection.post(url) do |req|
|
526
|
+
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
527
|
+
req.headers['Accept'] = '*/*'
|
528
|
+
req.body = "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}"
|
529
|
+
debug "Request headers: #{req.headers.reject { |k,v| k =~ /auth/i }.to_json}", 2
|
530
|
+
end
|
531
|
+
|
532
|
+
debug "Response status: #{response.status}", 1
|
533
|
+
debug "Response headers: #{response.headers.to_json}", 2
|
534
|
+
debug "Response body: #{response.body}", 3
|
535
|
+
|
536
|
+
if process_session_response(response)
|
537
|
+
debug "Redfish session created successfully with form-urlencoded", 0, :green
|
538
|
+
return true
|
539
|
+
end
|
540
|
+
|
541
|
+
# If that fails, try with Basic Auth + form-urlencoded
|
542
|
+
debug "Trying form-urlencoded with Basic Auth", 0
|
543
|
+
auth_response = request_with_basic_auth(:post, url, "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}", 'application/x-www-form-urlencoded')
|
544
|
+
|
545
|
+
if process_session_response(auth_response)
|
546
|
+
debug "Redfish session created successfully with form-urlencoded + Basic Auth", 0, :green
|
547
|
+
return true
|
548
|
+
end
|
549
|
+
|
550
|
+
# Last resort: try with both headers (some iDRAC versions need this)
|
551
|
+
debug "Trying with both Content-Type headers", 0
|
552
|
+
both_response = connection.post(url) do |req|
|
553
|
+
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
554
|
+
req.headers['Accept'] = 'application/json'
|
555
|
+
req.headers['X-Requested-With'] = 'XMLHttpRequest'
|
556
|
+
req.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
|
557
|
+
req.body = "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}"
|
558
|
+
end
|
559
|
+
|
560
|
+
if process_session_response(both_response)
|
561
|
+
debug "Redfish session created successfully with multiple content types", 0, :green
|
562
|
+
return true
|
563
|
+
else
|
564
|
+
debug "Failed with form-urlencoded too: #{response.status} - #{response.body}", 0, :red
|
565
|
+
return false
|
566
|
+
end
|
567
|
+
rescue Faraday::SSLError => e
|
568
|
+
debug "SSL Error in form-urlencoded request: #{e.message}", 0, :red
|
569
|
+
debug "OpenSSL version: #{OpenSSL::OPENSSL_VERSION}", 1
|
570
|
+
debug "Connection URL: #{base_url}#{url}", 1
|
571
|
+
debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
|
572
|
+
return false
|
573
|
+
rescue => e
|
574
|
+
debug "Error during form-urlencoded session creation: #{e.class.name}: #{e.message}", 0, :red
|
575
|
+
debug e.backtrace.join("\n"), 2 if e.backtrace && @verbosity >= 2
|
576
|
+
return false
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
# Determine the correct session endpoint based on Redfish version
|
581
|
+
def determine_session_endpoint
|
582
|
+
begin
|
583
|
+
debug "Checking Redfish version to determine session endpoint...", 1
|
584
|
+
|
585
|
+
response = connection.get('/redfish/v1') do |req|
|
586
|
+
req.headers['Accept'] = 'application/json'
|
587
|
+
end
|
588
|
+
|
589
|
+
if response.status == 200
|
590
|
+
begin
|
591
|
+
data = JSON.parse(response.body)
|
592
|
+
redfish_version = data['RedfishVersion']
|
593
|
+
|
594
|
+
if redfish_version
|
595
|
+
debug "Detected Redfish version: #{redfish_version}", 1
|
596
|
+
|
597
|
+
# For version 1.17.0 and below, use the /redfish/v1/Sessions endpoint
|
598
|
+
# For newer versions, use /redfish/v1/SessionService/Sessions
|
599
|
+
if Gem::Version.new(redfish_version) <= Gem::Version.new('1.17.0')
|
600
|
+
endpoint = '/redfish/v1/Sessions'
|
601
|
+
debug "Using endpoint #{endpoint} for Redfish version #{redfish_version}", 1
|
602
|
+
return endpoint
|
603
|
+
else
|
604
|
+
endpoint = '/redfish/v1/SessionService/Sessions'
|
605
|
+
debug "Using endpoint #{endpoint} for Redfish version #{redfish_version}", 1
|
606
|
+
return endpoint
|
607
|
+
end
|
608
|
+
end
|
609
|
+
rescue JSON::ParserError => e
|
610
|
+
debug "Error parsing Redfish version: #{e.message}", 0, :red
|
611
|
+
debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
|
612
|
+
rescue => e
|
613
|
+
debug "Error determining Redfish version: #{e.message}", 0, :red
|
614
|
+
debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
|
615
|
+
end
|
616
|
+
end
|
617
|
+
rescue => e
|
618
|
+
debug "Error checking Redfish version: #{e.message}", 0, :red
|
619
|
+
debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
|
620
|
+
end
|
621
|
+
|
622
|
+
# Default to /redfish/v1/Sessions if we can't determine version
|
623
|
+
default_endpoint = '/redfish/v1/Sessions'
|
624
|
+
debug "Defaulting to endpoint #{default_endpoint}", 0, :light_yellow
|
625
|
+
default_endpoint
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
# Module containing extracted session methods to be included in Client
|
630
|
+
module SessionMethods
|
631
|
+
def force_clear_sessions
|
632
|
+
debug = ->(msg, level=0, color=:light_cyan) {
|
633
|
+
verbosity = respond_to?(:verbosity) ? verbosity : 0
|
634
|
+
return unless verbosity >= level
|
635
|
+
msg = msg.send(color) if color && msg.respond_to?(color)
|
636
|
+
puts msg
|
637
|
+
}
|
638
|
+
|
639
|
+
debug.call "Attempting to force clear all sessions...", 0
|
640
|
+
|
641
|
+
if delete_all_sessions_with_basic_auth
|
642
|
+
debug.call "Successfully cleared sessions using Basic Auth", 0, :green
|
643
|
+
true
|
644
|
+
else
|
645
|
+
debug.call "Failed to clear sessions using Basic Auth", 0, :red
|
646
|
+
false
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
# Delete all sessions using Basic Authentication
|
651
|
+
def delete_all_sessions_with_basic_auth
|
652
|
+
debug = ->(msg, level=0, color=:light_cyan) {
|
653
|
+
verbosity = respond_to?(:verbosity) ? verbosity : 0
|
654
|
+
return unless verbosity >= level
|
655
|
+
msg = msg.send(color) if color && msg.respond_to?(color)
|
656
|
+
puts msg
|
657
|
+
}
|
658
|
+
|
659
|
+
debug.call "Attempting to delete all sessions using Basic Authentication...", 0
|
660
|
+
|
661
|
+
# First, get the list of sessions
|
662
|
+
sessions_url = session&.determine_session_endpoint || '/redfish/v1/Sessions'
|
663
|
+
|
664
|
+
begin
|
665
|
+
# Get the list of sessions
|
666
|
+
response = authenticated_request(:get, sessions_url)
|
667
|
+
|
668
|
+
if response.status != 200
|
669
|
+
debug.call "Failed to get sessions list: #{response.status} - #{response.body}", 0, :red
|
670
|
+
return false
|
671
|
+
end
|
672
|
+
|
673
|
+
# Parse the response to get session IDs
|
674
|
+
begin
|
675
|
+
sessions_data = JSON.parse(response.body)
|
676
|
+
|
677
|
+
if sessions_data['Members'] && sessions_data['Members'].any?
|
678
|
+
debug.call "Found #{sessions_data['Members'].count} active sessions", 0, :light_yellow
|
679
|
+
|
680
|
+
# Delete each session
|
681
|
+
success = true
|
682
|
+
sessions_data['Members'].each do |session|
|
683
|
+
session_url = session['@odata.id']
|
684
|
+
|
685
|
+
# Skip if no URL
|
686
|
+
next unless session_url
|
687
|
+
|
688
|
+
# Delete the session
|
689
|
+
delete_response = authenticated_request(:delete, session_url)
|
690
|
+
|
691
|
+
if delete_response.status == 200 || delete_response.status == 204
|
692
|
+
debug.call "Successfully deleted session: #{session_url}", 1, :green
|
693
|
+
else
|
694
|
+
debug.call "Failed to delete session #{session_url}: #{delete_response.status}", 0, :red
|
695
|
+
success = false
|
696
|
+
end
|
697
|
+
|
698
|
+
# Small delay between deletions
|
699
|
+
sleep(1)
|
700
|
+
end
|
701
|
+
|
702
|
+
return success
|
703
|
+
else
|
704
|
+
debug.call "No active sessions found", 0, :light_yellow
|
705
|
+
return true
|
706
|
+
end
|
707
|
+
rescue JSON::ParserError => e
|
708
|
+
debug.call "Error parsing sessions response: #{e.message}", 0, :red
|
709
|
+
return false
|
710
|
+
end
|
711
|
+
rescue => e
|
712
|
+
debug.call "Error during session deletion with Basic Auth: #{e.message}", 0, :red
|
713
|
+
return false
|
714
|
+
end
|
715
|
+
end
|
716
|
+
end
|
717
|
+
end
|