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 +4 -4
- data/README.md +59 -0
- data/bin/idrac +1 -2
- data/lib/idrac/client.rb +128 -124
- data/lib/idrac/session.rb +109 -193
- data/lib/idrac/system_config.rb +102 -149
- data/lib/idrac/version.rb +1 -1
- data/lib/idrac/web.rb +4 -4
- data/lib/idrac.rb +5 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e60a1c39738e830b95e2e0c29ad603ab9c57966535ca40374b31cc37315f4138
|
4
|
+
data.tar.gz: 6b007a005002ff5eaf88561c3e6d717c1c6d51715aa6a994a4b72e86f1f6e2c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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, :
|
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,
|
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
|
-
@
|
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,
|
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,
|
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
|
-
#
|
158
|
+
# Determine authentication method and set headers
|
126
159
|
if @direct_mode
|
127
|
-
|
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
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
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
|
-
|
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 "
|
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 "
|
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, :
|
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
|
-
|
212
|
-
|
213
|
-
|
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
|
-
#
|
233
|
-
|
234
|
-
|
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
|
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
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
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
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
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
|
-
|
556
|
-
debug "
|
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 "
|
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
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
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
|
data/lib/idrac/system_config.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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 "
|
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
|
-
#
|
62
|
-
|
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
|
-
#
|
65
|
-
|
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
|
-
#
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
#
|
76
|
-
|
77
|
-
|
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
|
-
#
|
291
|
-
|
292
|
-
|
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
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
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
|
-
|
300
|
-
if
|
301
|
-
|
302
|
-
|
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
|
-
|
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
|
-
|
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
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
|
64
|
-
if
|
65
|
-
puts "
|
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 "
|
76
|
+
puts "Already tried clearing sessions".light_yellow
|
77
77
|
return false
|
78
78
|
end
|
79
79
|
else
|
data/lib/idrac.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2025-06-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|