idrac 0.7.7 → 0.7.9

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: 133dd0ab85e38e7833aa80381ea6d22bdf134e30e48d9382b6f7e9b1671e0cad
4
- data.tar.gz: a742fa6154ebf62aeba07f0869442f486d2bed490011454eaae446d7d0b59805
3
+ metadata.gz: e60a1c39738e830b95e2e0c29ad603ab9c57966535ca40374b31cc37315f4138
4
+ data.tar.gz: 6b007a005002ff5eaf88561c3e6d717c1c6d51715aa6a994a4b72e86f1f6e2c8
5
5
  SHA512:
6
- metadata.gz: ec5a16a96be2ec689d2266f28e610c03442c36250bfc3ffb7992a99d54a08fbac582dace2111dcefdbcc601e76e06729265f3c2366337e70c23f0c1438dfe4d1
7
- data.tar.gz: 21b90d2e4f5355fc830ad511bf2821a6bae41bf4792ed6bf79cd4c24bff570fee0b932a13212cf6d08b353c2d8328fdb66a6f1e40c4e0a46b692cb3c795c8f5b
6
+ metadata.gz: 2b3014bfeb1061b6506fb9025885283015d7cd6a87868c41c832de71fdd5428dd96d6f16eb18aa2ea20ede8cb4d58d87d4f2922c2fed10958ad33ebc533469c4
7
+ data.tar.gz: f20ddd6b1be1f3105aead4387865e9b556cda333df23ba944915aa7805d83a28fef85a0acc7c33456b4d102d9f707dc0d4a70db470dd6726bfea08eaefd2ffd7
data/README.md CHANGED
@@ -210,6 +210,60 @@ client = IDRAC.new(
210
210
  )
211
211
  ```
212
212
 
213
+ ### Basic Usage (Manual Session Management)
214
+
215
+ For manual control over session lifecycle:
216
+
217
+ ```ruby
218
+ require 'idrac'
219
+
220
+ client = IDRAC::Client.new(
221
+ host: "192.168.1.100",
222
+ username: "root",
223
+ password: "calvin"
224
+ )
225
+
226
+ # Always remember to logout to clean up sessions
227
+ begin
228
+ client.login
229
+ puts client.get_power_state
230
+ puts client.system_info
231
+ ensure
232
+ client.logout
233
+ end
234
+ ```
235
+
236
+ ### Block-based Usage (Recommended)
237
+
238
+ The gem provides a block-based API that automatically handles session cleanup:
239
+
240
+ ```ruby
241
+ require 'idrac'
242
+
243
+ # Using IDRAC.connect - automatically handles login/logout
244
+ IDRAC.connect(host: "192.168.1.100", username: "root", password: "calvin") do |client|
245
+ puts client.get_power_state
246
+ puts client.system_info
247
+ # Session is automatically cleaned up when block exits
248
+ end
249
+
250
+ # Or using Client.connect
251
+ IDRAC::Client.connect(host: "192.168.1.100", username: "root", password: "calvin") do |client|
252
+ puts client.get_power_state
253
+ # Session cleanup is guaranteed
254
+ end
255
+ ```
256
+
257
+ ### Automatic Session Cleanup
258
+
259
+ The gem now includes automatic session cleanup mechanisms:
260
+
261
+ 1. **Finalizer**: Sessions are automatically cleaned up when the client object is garbage collected
262
+ 2. **Block-based API**: The `connect` method ensures sessions are cleaned up even if exceptions occur
263
+ 3. **Manual cleanup**: You can still call `client.logout` manually
264
+
265
+ This prevents the "RAC0218: The maximum number of user sessions is reached" error that occurred when sessions were not properly cleaned up.
266
+
213
267
  ## Development
214
268
 
215
269
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -218,6 +272,11 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
218
272
 
219
273
  ## Changelog
220
274
 
275
+ ### Version 0.7.8
276
+ - **Network Redirection Support**: Added optional `host_header` parameter to Client initialization
277
+ - Enables iDRAC access through network redirection scenarios where the connection IP differs from the Host header requirement
278
+ - **Configurable VNC Port**: Made VNC port configurable in `set_idrac_ip` function with `vnc_port` parameter (default: 5901)
279
+
221
280
  ### Version 0.7.7
222
281
  - **Bug Fix**: Fixed Session base_url method to use instance variables instead of client delegation
223
282
  - Resolved "undefined local variable or method 'client'" error in session.rb
data/bin/idrac CHANGED
@@ -22,7 +22,7 @@ module IDRAC
22
22
  class_option :port, type: :numeric, default: 443, desc: "iDRAC port"
23
23
  class_option :no_ssl, type: :boolean, default: false, desc: "Disable SSL"
24
24
  class_option :verify_ssl, type: :boolean, default: false, desc: "Enable SSL verification (not recommended for iDRAC's self-signed certificates)"
25
- class_option :auto_delete_sessions, type: :boolean, default: true, desc: "Automatically delete sessions when maximum sessions are reached"
25
+
26
26
  class_option :retries, type: :numeric, default: 3, desc: "Number of retries for API calls"
27
27
  class_option :retry_delay, type: :numeric, default: 1, desc: "Initial delay in seconds between retries (increases exponentially)"
28
28
  class_option :verbose, type: :boolean, default: false, aliases: '-v', desc: "Enable verbose output"
@@ -1715,7 +1715,6 @@ module IDRAC
1715
1715
  port: options[:port],
1716
1716
  use_ssl: !options[:no_ssl],
1717
1717
  verify_ssl: options[:verify_ssl],
1718
- auto_delete_sessions: options[:auto_delete_sessions],
1719
1718
  retry_count: options[:retries],
1720
1719
  retry_delay: options[:retry_delay]
1721
1720
  )
data/lib/idrac/client.rb CHANGED
@@ -9,11 +9,10 @@ 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, :session, :web
12
+ attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl, :session, :web, :host_header
13
13
  attr_accessor :direct_mode, :verbosity, :retry_count, :retry_delay
14
14
 
15
15
  include Power
16
- include SessionUtils
17
16
  include Debuggable
18
17
  include Jobs
19
18
  include Lifecycle
@@ -25,7 +24,7 @@ module IDRAC
25
24
  include SystemConfig
26
25
  include Utility
27
26
 
28
- def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: false, direct_mode: false, auto_delete_sessions: true, retry_count: 3, retry_delay: 1)
27
+ def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: false, direct_mode: false, retry_count: 3, retry_delay: 1, host_header: nil)
29
28
  @host = host
30
29
  @username = username
31
30
  @password = password
@@ -33,7 +32,7 @@ module IDRAC
33
32
  @use_ssl = use_ssl
34
33
  @verify_ssl = verify_ssl
35
34
  @direct_mode = direct_mode
36
- @auto_delete_sessions = auto_delete_sessions
35
+ @host_header = host_header
37
36
  @verbosity = 0
38
37
  @retry_count = retry_count
39
38
  @retry_delay = retry_delay
@@ -41,6 +40,34 @@ module IDRAC
41
40
  # Initialize the session and web classes
42
41
  @session = Session.new(self)
43
42
  @web = Web.new(self)
43
+
44
+ # Add finalizer to ensure sessions are cleaned up
45
+ ObjectSpace.define_finalizer(self, self.class.finalizer(@session, @web))
46
+ end
47
+
48
+ # Finalizer to clean up sessions when object is garbage collected
49
+ def self.finalizer(session, web)
50
+ proc do
51
+ begin
52
+ session.delete if session.x_auth_token
53
+ web.logout if web.session_id
54
+ rescue
55
+ # Ignore errors during cleanup
56
+ end
57
+ end
58
+ end
59
+
60
+ # Primary interface - block-based API that ensures session cleanup
61
+ def self.connect(host:, username:, password:, **options)
62
+ client = new(host: host, username: username, password: password, **options)
63
+ return client unless block_given?
64
+
65
+ begin
66
+ client.login
67
+ yield client
68
+ ensure
69
+ client.logout
70
+ end
44
71
  end
45
72
 
46
73
  def connection
@@ -86,9 +113,17 @@ module IDRAC
86
113
  end
87
114
 
88
115
  # Send an authenticated request to the iDRAC
89
- def authenticated_request(method, path, options = {})
116
+ def authenticated_request(method, path, body: nil, headers: {}, timeout: nil, open_timeout: nil, **options)
117
+ # Build options hash with all parameters
118
+ request_options = {
119
+ body: body,
120
+ headers: headers,
121
+ timeout: timeout,
122
+ open_timeout: open_timeout
123
+ }.merge(options).compact
124
+
90
125
  with_retries do
91
- _perform_authenticated_request(method, path, options)
126
+ _perform_authenticated_request(method, path, request_options)
92
127
  end
93
128
  end
94
129
 
@@ -108,144 +143,112 @@ module IDRAC
108
143
  raise Error, "Failed to authenticate after #{@retry_count} retries"
109
144
  end
110
145
 
111
- # Form the full URL
112
- full_url = "#{base_url}/redfish/v1".chomp('/') + '/' + path.sub(/^\//, '')
113
-
114
- # Log the request
115
146
  debug "Authenticated request: #{method.to_s.upcase} #{path}", 1
116
147
 
117
- # Extract options
148
+ # Extract options and prepare headers
118
149
  body = options[:body]
119
150
  headers = options[:headers] || {}
151
+ timeout = options[:timeout]
152
+ open_timeout = options[:open_timeout]
120
153
 
121
- # Add client headers
122
154
  headers['User-Agent'] ||= 'iDRAC Ruby Client'
123
155
  headers['Accept'] ||= 'application/json'
156
+ headers['Host'] = @host_header if @host_header
124
157
 
125
- # If we're in direct mode, use Basic Auth
158
+ # Determine authentication method and set headers
126
159
  if @direct_mode
127
- # Create Basic Auth header
128
- auth_header = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
129
- headers['Authorization'] = auth_header
160
+ headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
130
161
  debug "Using Basic Auth for request (direct mode)", 2
131
-
132
- begin
133
- # Make the request directly
134
- response = session.connection.run_request(
135
- method,
136
- path.sub(/^\//, ''),
137
- body,
138
- headers
139
- )
140
-
141
- debug "Response status: #{response.status}", 2
142
-
143
- # Even in direct mode, check for authentication issues
144
- if response.status == 401 || response.status == 403
145
- debug "Authentication failed in direct mode, retrying with new credentials...", 1, :light_yellow
146
- sleep(retry_count + 1) # Add some delay before retry
147
- return _perform_authenticated_request(method, path, options, retry_count + 1)
148
- end
149
-
150
- return response
151
- rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError => e
152
- # Old iDRACs (e.g. R630s) can have occasional connection issues--even SSLError is common
153
- debug "Connection error in direct mode: #{e.message}", 1, :red
154
- sleep(retry_count + 1) # Add some delay before retry
155
- return _perform_authenticated_request(method, path, options, retry_count + 1)
156
- rescue => e
157
- debugger
158
- debug "Error during direct mode request: #{e.message}", 1, :red
159
- # sleep(retry_count + 1) # Add some delay before retry
160
- # return _perform_authenticated_request(method, path, options, retry_count + 1)
161
- raise Error, "Error during authenticated request: #{e.message}"
162
- end
163
- # Use Redfish session token if available
164
162
  elsif session.x_auth_token
165
- begin
166
- headers['X-Auth-Token'] = session.x_auth_token
167
-
168
- debug "Using X-Auth-Token for authentication", 2
169
- debug "Request headers: #{headers.reject { |k,v| k =~ /auth/i }.to_json}", 3
170
- debug "Request body: #{body.to_s[0..500]}", 3 if body
171
-
172
- response = session.connection.run_request(
173
- method,
174
- path.sub(/^\//, ''),
175
- body,
176
- headers
177
- )
178
-
179
- debug "Response status: #{response.status}", 2
180
- debug "Response headers: #{response.headers.to_json}", 3
181
- debug "Response body: #{response.body.to_s[0..500]}", 3 if response.body
182
-
183
- # Handle session expiration
184
- if response.status == 401 || response.status == 403
185
- debug "Session expired or invalid, creating a new session...", 1, :light_yellow
186
-
187
- # If session.delete returns true, the session was successfully deleted
188
- if session.delete
189
- debug "Successfully cleared expired session", 1, :green
190
- end
191
-
192
- # Try to create a new session
193
- if session.create
194
- debug "Successfully created a new session after expiration, retrying request...", 1, :green
195
- return _perform_authenticated_request(method, path, options, retry_count + 1)
196
- else
197
- debug "Failed to create a new session after expiration, falling back to direct mode...", 1, :light_yellow
198
- @direct_mode = true
199
- return _perform_authenticated_request(method, path, options, retry_count + 1)
200
- end
201
- end
202
-
203
- return response
204
- rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
205
- debug "Connection error: #{e.message}", 1, :red
206
- sleep(retry_count + 1) # Add some delay before retry
207
-
208
- # If we still have the token, try to reuse it
209
- if session.x_auth_token
210
- debug "Retrying with existing token after connection error", 1, :light_yellow
211
- return _perform_authenticated_request(method, path, options, retry_count + 1)
212
- else
213
- # Otherwise try to create a new session
214
- debug "Trying to create a new session after connection error", 1, :light_yellow
215
- if session.create
216
- debug "Successfully created a new session after connection error", 1, :green
217
- return _perform_authenticated_request(method, path, options, retry_count + 1)
218
- else
219
- debug "Failed to create session after connection error, falling back to direct mode", 1, :light_yellow
220
- @direct_mode = true
221
- return _perform_authenticated_request(method, path, options, retry_count + 1)
222
- end
223
- end
224
- rescue => e
225
- debug "Error during authenticated request (token mode): #{e.message}", 1, :red
226
-
227
- # Try to create a new session
228
- if session.create
229
- debug "Successfully created a new session after error, retrying request...", 1, :green
230
- return _perform_authenticated_request(method, path, options, retry_count + 1)
231
- else
232
- debug "Failed to create a new session after error, falling back to direct mode...", 1, :light_yellow
233
- @direct_mode = true
234
- return _perform_authenticated_request(method, path, options, retry_count + 1)
235
- end
236
- end
163
+ headers['X-Auth-Token'] = session.x_auth_token
164
+ debug "Using X-Auth-Token for authentication", 2
165
+ end
166
+
167
+ # Make request with timeout handling
168
+ response = make_request_with_timeouts(method, path, body, headers, timeout, open_timeout)
169
+
170
+ # Handle authentication and connection errors
171
+ case response.status
172
+ when 401, 403
173
+ handle_auth_failure(method, path, options, retry_count)
174
+ else
175
+ debug "Response status: #{response.status}", 2
176
+ response
177
+ end
178
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError => e
179
+ handle_connection_error(e, method, path, options, retry_count)
180
+ rescue => e
181
+ handle_general_error(e, method, path, options, retry_count)
182
+ end
183
+
184
+ # Make request with timeout handling
185
+ def make_request_with_timeouts(method, path, body, headers, timeout, open_timeout)
186
+ conn = session.connection
187
+ original_timeout = conn.options.timeout
188
+ original_open_timeout = conn.options.open_timeout
189
+
190
+ begin
191
+ conn.options.timeout = timeout if timeout
192
+ conn.options.open_timeout = open_timeout if open_timeout
193
+
194
+ conn.run_request(method, path.sub(/^\//, ''), body, headers)
195
+ ensure
196
+ conn.options.timeout = original_timeout
197
+ conn.options.open_timeout = original_open_timeout
198
+ end
199
+ end
200
+
201
+ # Handle authentication failures
202
+ def handle_auth_failure(method, path, options, retry_count)
203
+ if @direct_mode
204
+ debug "Authentication failed in direct mode, retrying...", 1, :light_yellow
205
+ sleep(retry_count + 1)
206
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
237
207
  else
238
- # If we don't have a token, try to create a session
208
+ debug "Session expired, creating new session...", 1, :light_yellow
209
+ session.delete if session.x_auth_token
210
+
239
211
  if session.create
240
- debug "Successfully created a new session, making request...", 1, :green
212
+ debug "New session created, retrying request...", 1, :green
241
213
  return _perform_authenticated_request(method, path, options, retry_count + 1)
242
214
  else
243
- debug "Failed to create a session, falling back to direct mode...", 1, :light_yellow
215
+ debug "Session creation failed, falling back to direct mode...", 1, :light_yellow
244
216
  @direct_mode = true
245
217
  return _perform_authenticated_request(method, path, options, retry_count + 1)
246
218
  end
247
219
  end
248
220
  end
221
+
222
+ # Handle connection errors
223
+ def handle_connection_error(error, method, path, options, retry_count)
224
+ debug "Connection error: #{error.message}", 1, :red
225
+ sleep(retry_count + 1)
226
+
227
+ if @direct_mode || session.x_auth_token
228
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
229
+ elsif session.create
230
+ debug "Created new session after connection error", 1, :green
231
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
232
+ else
233
+ @direct_mode = true
234
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
235
+ end
236
+ end
237
+
238
+ # Handle general errors
239
+ def handle_general_error(error, method, path, options, retry_count)
240
+ debug "Error during request: #{error.message}", 1, :red
241
+
242
+ if @direct_mode
243
+ raise Error, "Error during authenticated request: #{error.message}"
244
+ elsif session.create
245
+ debug "Created new session after error, retrying...", 1, :green
246
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
247
+ else
248
+ @direct_mode = true
249
+ return _perform_authenticated_request(method, path, options, retry_count + 1)
250
+ end
251
+ end
249
252
 
250
253
  def _perform_get(path:, headers: {})
251
254
  # For screenshot functionality, we need to use the WebUI cookies
@@ -259,6 +262,7 @@ module IDRAC
259
262
  "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",
260
263
  "Accept-Encoding" => "deflate, gzip"
261
264
  }
265
+ headers_to_use["Host"] = @host_header if @host_header
262
266
 
263
267
  if web.cookies
264
268
  headers_to_use["Cookie"] = web.cookies
data/lib/idrac/session.rb CHANGED
@@ -9,7 +9,7 @@ require 'socket'
9
9
  module IDRAC
10
10
  class Session
11
11
  attr_reader :host, :username, :password, :port, :use_ssl, :verify_ssl,
12
- :x_auth_token, :session_location, :direct_mode, :auto_delete_sessions
12
+ :x_auth_token, :session_location, :direct_mode, :host_header
13
13
  attr_accessor :verbosity
14
14
 
15
15
  include Debuggable
@@ -22,11 +22,11 @@ module IDRAC
22
22
  @port = client.port
23
23
  @use_ssl = client.use_ssl
24
24
  @verify_ssl = client.verify_ssl
25
+ @host_header = client.host_header
25
26
  @x_auth_token = nil
26
27
  @session_location = nil
27
28
  @direct_mode = client.direct_mode
28
29
  @sessions_maxed = false
29
- @auto_delete_sessions = client.auto_delete_sessions
30
30
  @verbosity = client.respond_to?(:verbosity) ? client.verbosity : 0
31
31
  end
32
32
 
@@ -75,7 +75,7 @@ module IDRAC
75
75
 
76
76
  false
77
77
  end
78
-
78
+
79
79
  # Delete all sessions using Basic Authentication
80
80
  def delete_all_sessions_with_basic_auth
81
81
  debug "Attempting to delete all sessions using Basic Authentication...", 1
@@ -142,7 +142,7 @@ module IDRAC
142
142
  return try_delete_latest_sessions
143
143
  end
144
144
  end
145
-
145
+
146
146
  # Try to delete sessions by direct URL when we can't list sessions
147
147
  def try_delete_latest_sessions
148
148
  # Try to delete sessions by direct URL when we can't list sessions
@@ -208,71 +208,24 @@ module IDRAC
208
208
  begin
209
209
  debug "Deleting Redfish session...", 1
210
210
 
211
- if @session_location
212
- # Use the X-Auth-Token for authentication
213
- headers = { 'X-Auth-Token' => @x_auth_token }
214
-
215
- begin
216
- response = connection.delete(@session_location) do |req|
217
- req.headers.merge!(headers)
218
- end
219
-
220
- if response.status == 200 || response.status == 204
221
- debug "Redfish session deleted successfully", 1, :green
222
- @x_auth_token = nil
223
- @session_location = nil
224
- return true
225
- end
226
- rescue => session_e
227
- debug "Error during session deletion via location: #{session_e.message}", 1, :yellow
228
- # Continue to try basic auth method
229
- end
211
+ # Try to delete via session location first
212
+ if @session_location && delete_via_location
213
+ return true
230
214
  end
231
215
 
232
- # If deleting via session location fails or there's no session location,
233
- # try to delete by using the basic auth method
234
- if @x_auth_token
235
- # Try to determine session ID from the X-Auth-Token or session_location
236
- session_id = nil
237
-
238
- # Extract session ID from location if available
239
- if @session_location
240
- if @session_location =~ /\/([^\/]+)$/
241
- session_id = $1
242
- end
243
- end
244
-
245
- # If we have an extracted session ID
246
- if session_id
247
- debug "Trying to delete session by ID #{session_id}", 1
248
-
249
- begin
250
- endpoint = determine_session_endpoint
251
- delete_url = "#{endpoint}/#{session_id}"
252
-
253
- delete_response = request_with_basic_auth(:delete, delete_url, nil)
254
-
255
- if delete_response.status == 200 || delete_response.status == 204
256
- debug "Successfully deleted session via ID", 1, :green
257
- @x_auth_token = nil
258
- @session_location = nil
259
- return true
260
- end
261
- rescue => id_e
262
- debug "Error during session deletion via ID: #{id_e.message}", 1, :yellow
263
- end
264
- end
265
-
266
- # Last resort: clear the token variable even if we couldn't properly delete it
267
- debug "Clearing session token internally", 1, :yellow
268
- @x_auth_token = nil
269
- @session_location = nil
216
+ # Try to delete via session ID
217
+ if @x_auth_token && delete_via_session_id
218
+ return true
270
219
  end
271
220
 
221
+ # Clear token variables even if deletion failed
222
+ debug "Clearing session token internally", 1, :yellow
223
+ @x_auth_token = nil
224
+ @session_location = nil
272
225
  return false
273
226
  rescue => e
274
227
  debug "Error during Redfish session deletion: #{e.message}", 1, :red
275
- # Clear token variable anyway
228
+ # Clear token variables anyway
276
229
  @x_auth_token = nil
277
230
  @session_location = nil
278
231
  return false
@@ -348,10 +301,12 @@ module IDRAC
348
301
  end
349
302
 
350
303
  def basic_auth_headers(content_type = 'application/json')
351
- {
304
+ headers = {
352
305
  'Authorization' => "Basic #{Base64.strict_encode64("#{username}:#{password}")}",
353
306
  'Content-Type' => content_type
354
307
  }
308
+ headers['Host'] = host_header if host_header
309
+ headers
355
310
  end
356
311
 
357
312
  def request_with_basic_auth(method, url, body = nil, content_type = 'application/json')
@@ -392,6 +347,7 @@ module IDRAC
392
347
  response = connection.post(url) do |req|
393
348
  req.headers['Content-Type'] = 'application/json'
394
349
  req.headers['Accept'] = 'application/json'
350
+ req.headers['Host'] = host_header if host_header
395
351
  req.body = payload.to_json
396
352
  debug "Request headers: #{req.headers.reject { |k,v| k =~ /auth/i }.to_json}", 2
397
353
  debug "Request body: #{req.body}", 2
@@ -419,6 +375,7 @@ module IDRAC
419
375
  alt_response = connection.post(url) do |req|
420
376
  # No Content-Type header
421
377
  req.headers['Accept'] = '*/*'
378
+ req.headers['Host'] = host_header if host_header
422
379
  req.body = payload.to_json
423
380
  end
424
381
 
@@ -495,6 +452,7 @@ module IDRAC
495
452
  no_content_type_response = connection.post(url) do |req|
496
453
  req.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
497
454
  req.headers['Accept'] = '*/*'
455
+ req.headers['Host'] = host_header if host_header
498
456
  req.body = payload.to_json
499
457
  end
500
458
 
@@ -521,44 +479,39 @@ module IDRAC
521
479
  return false unless @sessions_maxed
522
480
 
523
481
  debug "Maximum sessions reached, attempting to clear sessions", 1
524
- if @auto_delete_sessions
525
- if force_clear_sessions
526
- debug "Successfully cleared sessions, trying to create a new session", 1, :green
527
-
528
- # Give the iDRAC a moment to process the session deletions
529
- sleep(3)
482
+ if force_clear_sessions
483
+ debug "Successfully cleared sessions, trying to create a new session", 1, :green
484
+
485
+ # Give the iDRAC a moment to process the session deletions
486
+ sleep(3)
487
+
488
+ # Try one more time after clearing with form-urlencoded
489
+ begin
490
+ response = connection.post(url) do |req|
491
+ req.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
492
+ req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
493
+ req.headers['Host'] = host_header if host_header
494
+ req.body = "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}"
495
+ end
530
496
 
531
- # Try one more time after clearing with form-urlencoded
532
- begin
533
- response = connection.post(url) do |req|
534
- req.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
535
- req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
536
- req.body = "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}"
537
- end
538
-
539
- if process_session_response(response)
540
- debug "Redfish session created successfully after clearing sessions", 1, :green
541
- return true
542
- else
543
- debug "Failed to create Redfish session after clearing sessions: #{response.status} - #{response.body}", 1, :red
544
- # If still failing, try direct mode
545
- debug "Falling back to direct mode", 1, :light_yellow
546
- @direct_mode = true
547
- return false
548
- end
549
- rescue => e
550
- debug "Error during session creation after clearing: #{e.class.name}: #{e.message}", 1, :red
497
+ if process_session_response(response)
498
+ debug "Redfish session created successfully after clearing sessions", 1, :green
499
+ return true
500
+ else
501
+ debug "Failed to create Redfish session after clearing sessions: #{response.status} - #{response.body}", 1, :red
502
+ # If still failing, try direct mode
551
503
  debug "Falling back to direct mode", 1, :light_yellow
552
504
  @direct_mode = true
553
505
  return false
554
506
  end
555
- else
556
- debug "Failed to clear sessions, switching to direct mode", 1, :light_yellow
507
+ rescue => e
508
+ debug "Error during session creation after clearing: #{e.class.name}: #{e.message}", 1, :red
509
+ debug "Falling back to direct mode", 1, :light_yellow
557
510
  @direct_mode = true
558
511
  return false
559
512
  end
560
513
  else
561
- debug "Auto delete sessions is disabled, switching to direct mode", 1, :light_yellow
514
+ debug "Failed to clear sessions, switching to direct mode", 1, :light_yellow
562
515
  @direct_mode = true
563
516
  return false
564
517
  end
@@ -574,6 +527,7 @@ module IDRAC
574
527
  response = connection.post(url) do |req|
575
528
  req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
576
529
  req.headers['Accept'] = '*/*'
530
+ req.headers['Host'] = host_header if host_header
577
531
  req.body = "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}"
578
532
  debug "Request headers: #{req.headers.reject { |k,v| k =~ /auth/i }.to_json}", 2
579
533
  end
@@ -603,6 +557,7 @@ module IDRAC
603
557
  req.headers['Accept'] = 'application/json'
604
558
  req.headers['X-Requested-With'] = 'XMLHttpRequest'
605
559
  req.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
560
+ req.headers['Host'] = host_header if host_header
606
561
  req.body = "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}"
607
562
  end
608
563
 
@@ -626,6 +581,61 @@ module IDRAC
626
581
  end
627
582
  end
628
583
 
584
+ # Delete session using the session location URL
585
+ def delete_via_location
586
+ headers = { 'X-Auth-Token' => @x_auth_token }
587
+
588
+ response = connection.delete(@session_location) do |req|
589
+ req.headers.merge!(headers)
590
+ end
591
+
592
+ if response.status == 200 || response.status == 204
593
+ debug "Redfish session deleted successfully via location", 1, :green
594
+ @x_auth_token = nil
595
+ @session_location = nil
596
+ return true
597
+ end
598
+
599
+ false
600
+ rescue => e
601
+ debug "Error during session deletion via location: #{e.message}", 1, :yellow
602
+ false
603
+ end
604
+
605
+ # Delete session using session ID extracted from location or token
606
+ def delete_via_session_id
607
+ session_id = extract_session_id
608
+ return false unless session_id
609
+
610
+ debug "Trying to delete session by ID #{session_id}", 1
611
+
612
+ endpoint = determine_session_endpoint
613
+ delete_url = "#{endpoint}/#{session_id}"
614
+
615
+ delete_response = request_with_basic_auth(:delete, delete_url, nil)
616
+
617
+ if delete_response.status == 200 || delete_response.status == 204
618
+ debug "Successfully deleted session via ID", 1, :green
619
+ @x_auth_token = nil
620
+ @session_location = nil
621
+ return true
622
+ end
623
+
624
+ false
625
+ rescue => e
626
+ debug "Error during session deletion via ID: #{e.message}", 1, :yellow
627
+ false
628
+ end
629
+
630
+ # Extract session ID from location URL
631
+ def extract_session_id
632
+ return nil unless @session_location
633
+
634
+ if @session_location =~ /\/([^\/]+)$/
635
+ $1
636
+ end
637
+ end
638
+
629
639
  # Determine the correct session endpoint based on Redfish version
630
640
  def determine_session_endpoint
631
641
  begin
@@ -633,6 +643,7 @@ module IDRAC
633
643
 
634
644
  response = connection.get('/redfish/v1') do |req|
635
645
  req.headers['Accept'] = 'application/json'
646
+ req.headers['Host'] = host_header if host_header
636
647
  end
637
648
 
638
649
  if response.status == 200
@@ -645,27 +656,21 @@ module IDRAC
645
656
 
646
657
  # For version 1.17.0 and below, use the /redfish/v1/Sessions endpoint
647
658
  # For newer versions, use /redfish/v1/SessionService/Sessions
648
- if Gem::Version.new(redfish_version) <= Gem::Version.new('1.17.0')
649
- endpoint = '/redfish/v1/Sessions'
650
- debug "Using endpoint #{endpoint} for Redfish version #{redfish_version}", 1
651
- return endpoint
652
- else
653
- endpoint = '/redfish/v1/SessionService/Sessions'
654
- debug "Using endpoint #{endpoint} for Redfish version #{redfish_version}", 1
655
- return endpoint
656
- end
659
+ endpoint = Gem::Version.new(redfish_version) <= Gem::Version.new('1.17.0') ?
660
+ '/redfish/v1/Sessions' :
661
+ '/redfish/v1/SessionService/Sessions'
662
+
663
+ debug "Using endpoint #{endpoint} for Redfish version #{redfish_version}", 1
664
+ return endpoint
657
665
  end
658
666
  rescue JSON::ParserError => e
659
667
  debug "Error parsing Redfish version: #{e.message}", 1, :red
660
- debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
661
668
  rescue => e
662
669
  debug "Error determining Redfish version: #{e.message}", 1, :red
663
- debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
664
670
  end
665
671
  end
666
672
  rescue => e
667
673
  debug "Error checking Redfish version: #{e.message}", 1, :red
668
- debug e.backtrace.join("\n"), 3 if e.backtrace && @verbosity >= 3
669
674
  end
670
675
 
671
676
  # Default to /redfish/v1/Sessions if we can't determine version
@@ -674,93 +679,4 @@ module IDRAC
674
679
  default_endpoint
675
680
  end
676
681
  end
677
-
678
- # Module containing extracted session methods to be included in Client
679
- module SessionUtils
680
- def force_clear_sessions
681
- debug = ->(msg, level=1, color=:light_cyan) {
682
- verbosity = respond_to?(:verbosity) ? verbosity : 0
683
- return unless verbosity >= level
684
- msg = msg.send(color) if color && msg.respond_to?(color)
685
- puts msg
686
- }
687
-
688
- debug.call "Attempting to force clear all sessions...", 1
689
-
690
- if delete_all_sessions_with_basic_auth
691
- debug.call "Successfully cleared sessions using Basic Auth", 1, :green
692
- true
693
- else
694
- debug.call "Failed to clear sessions using Basic Auth", 1, :red
695
- false
696
- end
697
- end
698
-
699
- # Delete all sessions using Basic Authentication
700
- def delete_all_sessions_with_basic_auth
701
- debug = ->(msg, level=1, color=:light_cyan) {
702
- verbosity = respond_to?(:verbosity) ? verbosity : 0
703
- return unless verbosity >= level
704
- msg = msg.send(color) if color && msg.respond_to?(color)
705
- puts msg
706
- }
707
-
708
- debug.call "Attempting to delete all sessions using Basic Authentication...", 1
709
-
710
- # First, get the list of sessions
711
- sessions_url = session&.determine_session_endpoint || '/redfish/v1/Sessions'
712
-
713
- begin
714
- # Get the list of sessions
715
- response = authenticated_request(:get, sessions_url)
716
-
717
- if response.status != 200
718
- debug.call "Failed to get sessions list: #{response.status} - #{response.body}", 1, :red
719
- return false
720
- end
721
-
722
- # Parse the response to get session IDs
723
- begin
724
- sessions_data = JSON.parse(response.body)
725
-
726
- if sessions_data['Members'] && sessions_data['Members'].any?
727
- debug.call "Found #{sessions_data['Members'].count} active sessions", 1, :light_yellow
728
-
729
- # Delete each session
730
- success = true
731
- sessions_data['Members'].each do |session|
732
- session_url = session['@odata.id']
733
-
734
- # Skip if no URL
735
- next unless session_url
736
-
737
- # Delete the session
738
- delete_response = authenticated_request(:delete, session_url)
739
-
740
- if delete_response.status == 200 || delete_response.status == 204
741
- debug.call "Successfully deleted session: #{session_url}", 1, :green
742
- else
743
- debug.call "Failed to delete session #{session_url}: #{delete_response.status}", 1, :red
744
- success = false
745
- end
746
-
747
- # Small delay between deletions
748
- sleep(1)
749
- end
750
-
751
- return success
752
- else
753
- debug.call "No active sessions found", 1, :light_yellow
754
- return true
755
- end
756
- rescue JSON::ParserError => e
757
- debug.call "Error parsing sessions response: #{e.message}", 1, :red
758
- return false
759
- end
760
- rescue => e
761
- debug.call "Error during session deletion with Basic Auth: #{e.message}", 1, :red
762
- return false
763
- end
764
- end
765
- end
766
682
  end
@@ -4,77 +4,53 @@ require 'colorize'
4
4
  module IDRAC
5
5
  module SystemConfig
6
6
  # This assigns the iDRAC IP to be a STATIC IP.
7
- def set_idrac_ip(new_ip:, new_gw:, new_nm:, vnc_password: "calvin")
8
- scp = get_system_configuration_profile(target: "iDRAC")
9
- pp scp
10
- ## We want to access the iDRAC web server even when IPs don't match (and they won't when we port forward local host):
11
- set_scp_attribute(scp, "WebServer.1#HostHeaderCheck", "Disabled")
12
- ## We want VirtualMedia to be enabled so we can mount ISOs: set_scp_attribute(scp, "VirtualMedia.1#Enable", "Enabled")
13
- set_scp_attribute(scp, "VirtualMedia.1#EncryptEnable", "Disabled")
14
- ## We want to access VNC Server on 5901 for screenshots and without SSL:
15
- set_scp_attribute(scp, "VNCServer.1#Enable", "Enabled")
16
- set_scp_attribute(scp, "VNCServer.1#Port", "5901")
17
- set_scp_attribute(scp, "VNCServer.1#SSLEncryptionBitLength", "Disabled")
18
- # And password calvin
19
- set_scp_attribute(scp, "VNCServer.1#Password", vnc_password)
20
- # Disable DHCP on management NIC
21
- set_scp_attribute(scp, "IPv4.1#DHCPEnable", "Disabled")
22
- if drac_license_version.to_i == 8
23
- # We want to use HTML for the virtual console
24
- set_scp_attribute(scp, "VirtualConsole.1#PluginType", "HTML5")
25
- # We want static IP for the iDRAC
26
- set_scp_attribute(scp, "IPv4.1#Address", new_ip)
27
- set_scp_attribute(scp, "IPv4.1#Gateway", new_gw)
28
- set_scp_attribute(scp, "IPv4.1#Netmask", new_nm)
29
- elsif drac_license_version.to_i == 9
30
- # We want static IP for the iDRAC
31
- set_scp_attribute(scp, "IPv4Static.1#Address", new_ip)
32
- set_scp_attribute(scp, "IPv4Static.1#Gateway", new_gw)
33
- set_scp_attribute(scp, "IPv4Static.1#Netmask", new_nm)
34
- # {"Name"=>"SerialCapture.1#Enable", "Value"=>"Disabled", "Set On Import"=>"True", "Comment"=>"Read and Write"},
7
+ def set_idrac_ip(new_ip:, new_gw:, new_nm:, vnc_password: "calvin", vnc_port: 5901)
8
+ # Cache license version to avoid multiple iDRAC calls
9
+ version = license_version.to_i
10
+
11
+ case version
12
+ when 8
13
+ ipv4_prefix = "IPv4"
14
+ settings = { "VirtualConsole.1#PluginType" => "HTML5" }
15
+ when 9
16
+ ipv4_prefix = "IPv4Static"
17
+ settings = {}
35
18
  else
36
- raise "Unknown iDRAC version"
37
- end
38
- while true
39
- res = self.post(path: "Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfiguration", params: {"ImportBuffer": scp.to_json, "ShareParameters": {"Target": "iDRAC"}})
40
- # A successful JOB will have a location header with a job id.
41
- # We can get a busy message instead if we've sent too many iDRAC jobs back-to-back, so we check for that here.
42
- if res[:headers]["location"].present?
43
- # We have a job id, so we're good to go.
44
- break
45
- else
46
- # Depending on iDRAC version content-length may be present or not.
47
- # res[:headers]["content-length"].blank?
48
- msg = res['body']['error']['@Message.ExtendedInfo'].first['Message']
49
- details = res['body']['error']['@Message.ExtendedInfo'].first['Resolution']
50
- # msg => "A job operation is already running. Retry the operation after the existing job is completed."
51
- # details => "Wait until the running job is completed or delete the scheduled job and retry the operation."
52
- if details =~ /Wait until the running job is completed/
53
- sleep 10
54
- else
55
- Rails.logger.warn msg+details
56
- raise "failed configuring static ip, message: #{msg}, details: #{details}"
57
- end
58
- end
19
+ raise Error, "Unsupported iDRAC version: #{version}. Supported versions: 8, 9"
59
20
  end
60
21
 
61
- # Allow some time for the iDRAC to prepare before checking the task status
62
- sleep 3
22
+ # Build base settings for all versions
23
+ settings.merge!({
24
+ "WebServer.1#HostHeaderCheck" => "Disabled",
25
+ "VirtualMedia.1#EncryptEnable" => "Disabled",
26
+ "VNCServer.1#Enable" => "Enabled",
27
+ "VNCServer.1#Port" => vnc_port.to_s,
28
+ "VNCServer.1#SSLEncryptionBitLength" => "Disabled",
29
+ "VNCServer.1#Password" => vnc_password,
30
+ "IPv4.1#DHCPEnable" => "Disabled", # only applies to iDRAC 8
31
+ "#{ipv4_prefix}.1#Address" => new_ip, # only applies to iDRAC 9
32
+ "#{ipv4_prefix}.1#Gateway" => new_gw,
33
+ "#{ipv4_prefix}.1#Netmask" => new_nm
34
+ })
63
35
 
64
- # Use handle_location to monitor task progress
65
- result = handle_location(res[:headers]["location"])
36
+ # Build SCP from scratch instead of getting full profile
37
+ scp_component = make_scp(fqdd: "iDRAC.Embedded.1", attributes: settings)
38
+ scp = { "SystemConfiguration" => { "Components" => [scp_component] } }
66
39
 
67
- # Check if the operation succeeded
68
- if result[:status] != :success
69
- # Extract error details if available
70
- message = result[:messages].first rescue "N/A"
71
- error = result[:error] || "Unknown error"
72
- raise "Failed configuring static IP: #{message} - #{error}"
40
+ # Submit configuration with job availability handling
41
+ res = wait_for_job_availability do
42
+ authenticated_request(:post,
43
+ "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfiguration",
44
+ body: {"ImportBuffer": scp.to_json, "ShareParameters": {"Target": "iDRAC"}}.to_json,
45
+ headers: {"Content-Type" => "application/json"}
46
+ )
73
47
  end
74
48
 
75
- # Finally, let's update our configuration to reflect the new port:
76
- self.idrac
77
- return true
49
+ sleep 3 # Allow iDRAC to prepare
50
+ result = handle_location_with_ip_change(res.headers["location"], new_ip)
51
+
52
+ raise "Failed configuring static IP: #{result[:messages]&.first || result[:error] || "Unknown error"}" if result[:status] != :success
53
+ true
78
54
  end
79
55
 
80
56
 
@@ -94,56 +70,7 @@ module IDRAC
94
70
  return scp
95
71
  end
96
72
 
97
- # Set an attribute in a system configuration profile
98
- def set_scp_attribute(scp, name, value)
99
- # Make a deep copy to avoid modifying the original
100
- scp_copy = JSON.parse(scp.to_json)
101
-
102
- # Clear unrelated attributes for quicker transfer
103
- scp_copy["SystemConfiguration"].delete("Comments")
104
- scp_copy["SystemConfiguration"].delete("TimeStamp")
105
- scp_copy["SystemConfiguration"].delete("ServiceTag")
106
- scp_copy["SystemConfiguration"].delete("Model")
107
73
 
108
- # Skip these attribute groups to make the transfer faster
109
- excluded_prefixes = [
110
- "User", "Telemetry", "SecurityCertificate", "AutoUpdate", "PCIe", "LDAP", "ADGroup", "ActiveDirectory",
111
- "IPMILan", "EmailAlert", "SNMP", "IPBlocking", "IPMI", "Security", "RFS", "OS-BMC", "SupportAssist",
112
- "Redfish", "RedfishEventing", "Autodiscovery", "SEKM-LKC", "Telco-EdgeServer", "8021XSecurity", "SPDM",
113
- "InventoryHash", "RSASecurID2FA", "USB", "NIC", "IPv6", "NTP", "Logging", "IOIDOpt", "SSHCrypto",
114
- "RemoteHosts", "SysLog", "Time", "SmartCard", "ACME", "ServiceModule", "Lockdown",
115
- "DefaultCredentialMitigation", "AutoOSLockGroup", "LocalSecurity", "IntegratedDatacenter",
116
- "SecureDefaultPassword.1#ForceChangePassword", "SwitchConnectionView.1#Enable", "GroupManager.1",
117
- "ASRConfig.1#Enable", "SerialCapture.1#Enable", "CertificateManagement.1",
118
- "Update", "SSH", "SysInfo", "GUI"
119
- ]
120
-
121
- # Remove excluded attribute groups
122
- if scp_copy["SystemConfiguration"]["Components"] &&
123
- scp_copy["SystemConfiguration"]["Components"][0] &&
124
- scp_copy["SystemConfiguration"]["Components"][0]["Attributes"]
125
-
126
- attrs = scp_copy["SystemConfiguration"]["Components"][0]["Attributes"]
127
-
128
- attrs.reject! do |attr|
129
- excluded_prefixes.any? { |prefix| attr["Name"] =~ /\A#{prefix}/ }
130
- end
131
-
132
- # Update or add the specified attribute
133
- if attrs.find { |a| a["Name"] == name }.nil?
134
- # Attribute doesn't exist, create it
135
- attrs << { "Name" => name, "Value" => value, "Set On Import" => "True" }
136
- else
137
- # Update existing attribute
138
- attrs.find { |a| a["Name"] == name }["Value"] = value
139
- attrs.find { |a| a["Name"] == name }["Set On Import"] = "True"
140
- end
141
-
142
- scp_copy["SystemConfiguration"]["Components"][0]["Attributes"] = attrs
143
- end
144
-
145
- return scp_copy
146
- end
147
74
 
148
75
  # Helper method to normalize enabled/disabled values
149
76
  def normalize_enabled_value(v)
@@ -266,52 +193,78 @@ module IDRAC
266
193
  end
267
194
  end
268
195
 
269
- # Merge two SCPs together
270
- def merge_scp(scp1, scp2)
271
- return scp1 || scp2 unless scp1 && scp2 # Return the one that's not nil if either is nil
272
-
273
- # Make them both arrays in case they aren't
274
- scp1_array = scp1.is_a?(Array) ? scp1 : [scp1]
275
- scp2_array = scp2.is_a?(Array) ? scp2 : [scp2]
276
-
277
- # Convert to hashes for merging
278
- hash1 = scp_to_hash(scp1_array)
279
- hash2 = scp_to_hash(scp2_array)
280
-
281
- # Perform deep merge
282
- merged = deep_merge(hash1, hash2)
283
-
284
- # Convert back to SCP array format
285
- hash_to_scp(merged)
286
- end
287
196
 
288
- private
289
197
 
290
- # Helper method for deep merging of hashes
291
- def deep_merge(hash1, hash2)
292
- result = hash1.dup
198
+ # Handle location header for IP change operations. Monitors old IP until it fails,
199
+ # then aggressively monitors new IP with tight timeouts.
200
+ def handle_location_with_ip_change(location, new_ip, timeout: 300)
201
+ return nil if location.nil? || location.empty?
293
202
 
294
- hash2.each do |key, value|
295
- if result[key].is_a?(Array) && value.is_a?(Array)
296
- # For arrays of attributes, merge by name
297
- existing_names = result[key].map { |attr| attr["Name"] }
203
+ old_ip = @host
204
+ start_time = Time.now
205
+ old_ip_failed = false
206
+
207
+ debug "Monitoring IP change: #{old_ip} → #{new_ip}", 1, :blue
208
+
209
+ while Time.now - start_time < timeout
210
+ # Try old IP until it fails, then focus on new IP
211
+ [
212
+ old_ip_failed ? nil : [self, old_ip, "Old IP failed"],
213
+ [create_temp_client(new_ip), new_ip, old_ip_failed ? "New IP not ready" : "Cannot reach new IP"]
214
+ ].compact.each do |client, ip, error_prefix|
298
215
 
299
- value.each do |attr|
300
- if existing_index = existing_names.index(attr["Name"])
301
- # Update existing attribute
302
- result[key][existing_index] = attr
216
+ begin
217
+ client.login if ip == new_ip
218
+ client.authenticated_request(:get, "/redfish/v1", timeout: 2, open_timeout: 1)
219
+
220
+ if ip == new_ip
221
+ debug "✅ IP change successful!", 1, :green
222
+ @host = new_ip
223
+ return { status: :success, ip: new_ip }
303
224
  else
304
- # Add new attribute
305
- result[key] << attr
225
+ return { status: :success, ip: old_ip }
306
226
  end
227
+ rescue => e
228
+ debug "#{error_prefix}: #{e.message}", ip == new_ip ? 2 : 1, ip == new_ip ? :gray : :yellow
229
+ old_ip_failed = true if ip == old_ip
307
230
  end
308
- else
309
- # For other values, just replace
310
- result[key] = value
311
231
  end
232
+
233
+ sleep old_ip_failed ? 2 : 5
312
234
  end
313
235
 
314
- result
236
+ { status: :timeout, error: "IP change timed out after #{timeout}s. Old IP failed: #{old_ip_failed}" }
315
237
  end
238
+
239
+ private
240
+
241
+ # Wait for job availability, retrying if busy
242
+ def wait_for_job_availability
243
+ loop do
244
+ res = yield
245
+ return res if res.headers["location"].present?
246
+
247
+ msg = res['error']['@Message.ExtendedInfo'].first['Message']
248
+ details = res['error']['@Message.ExtendedInfo'].first['Resolution']
249
+
250
+ if details =~ /Wait until the running job is completed/
251
+ sleep 10
252
+ else
253
+ Rails.logger.warn "#{msg}#{details}" if defined?(Rails)
254
+ raise "Failed configuring static IP: #{msg}, #{details}"
255
+ end
256
+ end
257
+ end
258
+
259
+ # Create temporary client for new IP monitoring
260
+ def create_temp_client(new_ip)
261
+ self.class.new(
262
+ host: new_ip, username: @username, password: @password,
263
+ port: @port, use_ssl: @use_ssl, verify_ssl: @verify_ssl,
264
+ retry_count: 1, retry_delay: 1
265
+ ).tap { |c| c.verbosity = [@verbosity - 1, 0].max }
266
+ end
267
+
268
+ private
316
269
  end
317
270
  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.7.7"
4
+ VERSION = "0.7.9"
5
5
  end
data/lib/idrac/web.rb CHANGED
@@ -60,9 +60,9 @@ module IDRAC
60
60
  elsif response.code == 400 && response.body.include?("maximum number of user sessions")
61
61
  puts "Maximum sessions reached during WebUI login".light_red
62
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
63
+ # Try to clear sessions automatically
64
+ if !@tried_clearing_sessions
65
+ puts "Attempting to clear sessions automatically".light_cyan
66
66
  @tried_clearing_sessions = true
67
67
 
68
68
  if client.session.force_clear_sessions
@@ -73,7 +73,7 @@ module IDRAC
73
73
  return false
74
74
  end
75
75
  else
76
- puts "Auto-delete sessions is disabled or already tried clearing".light_yellow
76
+ puts "Already tried clearing sessions".light_yellow
77
77
  return false
78
78
  end
79
79
  else
data/lib/idrac.rb CHANGED
@@ -36,6 +36,11 @@ module IDRAC
36
36
  def self.new(options = {})
37
37
  Client.new(options)
38
38
  end
39
+
40
+ # Block-based API that ensures session cleanup
41
+ def self.connect(**options, &block)
42
+ Client.connect(**options, &block)
43
+ end
39
44
  end
40
45
 
41
46
  require 'idrac/version'
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.7.7
4
+ version: 0.7.9
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-06-22 00:00:00.000000000 Z
11
+ date: 2025-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty