idrac 0.8.5 → 0.8.7
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/lib/idrac/boot.rb +14 -11
- data/lib/idrac/client.rb +65 -1
- data/lib/idrac/error.rb +9 -0
- data/lib/idrac/license.rb +12 -2
- data/lib/idrac/network.rb +131 -24
- data/lib/idrac/session.rb +49 -4
- data/lib/idrac/storage.rb +32 -24
- data/lib/idrac/system_config.rb +119 -13
- data/lib/idrac/version.rb +1 -1
- metadata +6 -4
- data/bin/idrac-tsr +0 -90
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23b4d692e6a2623cda4369dafdc0e88755cf8e1eef198c83f85df88d670731ca
|
4
|
+
data.tar.gz: 9604b4d3dbc0277f9ffd7e1cf1029ece1517cb6a3868bc03dbc79d6abaccd74e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3646343bd2da2105448b15160f96c358e2971996daff9b1659ec4686f7f4cb259d76bf741205804c081e8b58e97b4060b0f2470bb9729bca5e480cdb37ad430e
|
7
|
+
data.tar.gz: d784e26447e27b12b87e873f9780dae75fd85ac895927b786b84c51e8b332d6215a357636b7e66b1524abfbef941192d8ef3d27a09b1fb3937113d21c7e22e23
|
data/lib/idrac/boot.rb
CHANGED
@@ -555,9 +555,12 @@ module IDRAC
|
|
555
555
|
begin
|
556
556
|
data = JSON.parse(response.body)
|
557
557
|
if data["Attributes"] && data["Attributes"].has_key?("ErrPrompt")
|
558
|
-
|
558
|
+
current_value = data["Attributes"]["ErrPrompt"]
|
559
|
+
debug "ErrPrompt current value: '#{current_value}' (checking if == 'Disabled')", 1, :cyan
|
560
|
+
return current_value == "Disabled"
|
559
561
|
else
|
560
562
|
debug "ErrPrompt attribute not found in BIOS settings", 1, :yellow
|
563
|
+
debug "Available BIOS attributes: #{data['Attributes']&.keys&.sort&.join(', ')}", 2, :yellow if data["Attributes"]
|
561
564
|
return false
|
562
565
|
end
|
563
566
|
rescue JSON::ParserError
|
@@ -640,18 +643,18 @@ module IDRAC
|
|
640
643
|
|
641
644
|
settings.each do |key, value|
|
642
645
|
attributes << {
|
643
|
-
"Name"
|
644
|
-
"Value"
|
645
|
-
"Set On Import"
|
646
|
+
"Name" => key.to_s,
|
647
|
+
"Value" => value,
|
648
|
+
"Set On Import" => "True"
|
646
649
|
}
|
647
650
|
end
|
648
651
|
|
649
652
|
scp = {
|
650
|
-
"SystemConfiguration"
|
651
|
-
"Components"
|
653
|
+
"SystemConfiguration" => {
|
654
|
+
"Components" => [
|
652
655
|
{
|
653
|
-
"FQDD"
|
654
|
-
"Attributes"
|
656
|
+
"FQDD" => "BIOS.Setup.1-1",
|
657
|
+
"Attributes" => attributes
|
655
658
|
}
|
656
659
|
]
|
657
660
|
}
|
@@ -663,9 +666,9 @@ module IDRAC
|
|
663
666
|
# Import System Configuration Profile for advanced configurations
|
664
667
|
def import_system_configuration(scp, target: "ALL", reboot: false)
|
665
668
|
params = {
|
666
|
-
"ImportBuffer"
|
667
|
-
"ShareParameters"
|
668
|
-
"Target"
|
669
|
+
"ImportBuffer" => JSON.generate(scp),
|
670
|
+
"ShareParameters" => {
|
671
|
+
"Target" => target
|
669
672
|
}
|
670
673
|
}
|
671
674
|
# Configure shutdown behavior
|
data/lib/idrac/client.rb
CHANGED
@@ -193,6 +193,12 @@ module IDRAC
|
|
193
193
|
original_timeout = conn.options.timeout
|
194
194
|
original_open_timeout = conn.options.open_timeout
|
195
195
|
|
196
|
+
# Add host header if specified (needed for SSH tunnels to iDRAC)
|
197
|
+
if @host_header
|
198
|
+
headers = headers.merge('Host' => @host_header)
|
199
|
+
debug "Added Host header to iDRAC request: #{@host_header}", 2
|
200
|
+
end
|
201
|
+
|
196
202
|
begin
|
197
203
|
conn.options.timeout = timeout if timeout
|
198
204
|
conn.options.open_timeout = open_timeout if open_timeout
|
@@ -360,6 +366,17 @@ module IDRAC
|
|
360
366
|
retries = 0
|
361
367
|
begin
|
362
368
|
yield
|
369
|
+
rescue ServiceTemporarilyUnavailableError => e
|
370
|
+
retries += 1
|
371
|
+
if retries <= max_retries
|
372
|
+
delay = e.retry_delay # Use the delay specified by iDRAC
|
373
|
+
debug "🕒 IDRAC REQUESTED RETRY: ServiceTemporarilyUnavailable - Attempt #{retries}/#{max_retries}, waiting #{delay}s as instructed by iDRAC", 1, :cyan
|
374
|
+
sleep delay
|
375
|
+
retry
|
376
|
+
else
|
377
|
+
debug "MAX RETRIES REACHED: #{e.message} after #{max_retries} attempts", 1, :red
|
378
|
+
raise e
|
379
|
+
end
|
363
380
|
rescue *error_classes => e
|
364
381
|
retries += 1
|
365
382
|
if retries <= max_retries
|
@@ -446,7 +463,54 @@ module IDRAC
|
|
446
463
|
if response.status.between?(200, 299)
|
447
464
|
return response.body
|
448
465
|
else
|
449
|
-
|
466
|
+
# Enhanced error handling with ExtendedInfo support
|
467
|
+
error_message = "Failed with status #{response.status}"
|
468
|
+
|
469
|
+
begin
|
470
|
+
error_data = JSON.parse(response.body)
|
471
|
+
|
472
|
+
# Check for standard error message
|
473
|
+
if error_data['error'] && error_data['error']['message']
|
474
|
+
error_message += ": #{error_data['error']['message']}"
|
475
|
+
end
|
476
|
+
|
477
|
+
# Check for ExtendedInfo which contains detailed error information
|
478
|
+
if error_data['error'] && error_data['error']['@Message.ExtendedInfo']
|
479
|
+
extended_info = error_data['error']['@Message.ExtendedInfo']
|
480
|
+
if extended_info.is_a?(Array) && extended_info.any?
|
481
|
+
error_message += "\nExtendedInfo:"
|
482
|
+
retry_delay = nil
|
483
|
+
extended_info.each_with_index do |info, index|
|
484
|
+
error_message += "\n #{index + 1}. #{info['Message']}" if info['Message']
|
485
|
+
error_message += " (#{info['MessageId']})" if info['MessageId']
|
486
|
+
error_message += " - Resolution: #{info['Resolution']}" if info['Resolution']
|
487
|
+
|
488
|
+
# Check for ServiceTemporarilyUnavailable with retry delay
|
489
|
+
if info['MessageId'] == 'Base.1.12.ServiceTemporarilyUnavailable'
|
490
|
+
# Extract retry delay from MessageArgs (usually first argument)
|
491
|
+
if info['MessageArgs'] && info['MessageArgs'].any?
|
492
|
+
retry_delay = info['MessageArgs'].first.to_i
|
493
|
+
debug "🕒 iDRAC ServiceTemporarilyUnavailable detected - will wait #{retry_delay} seconds as requested", 1, :yellow
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
# If we detected a ServiceTemporarilyUnavailable error, raise a special exception
|
499
|
+
if retry_delay
|
500
|
+
raise ServiceTemporarilyUnavailableError.new(error_message, retry_delay)
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
# Also add the full response body for debugging
|
506
|
+
debug "Full error response: #{response.body}", 1, :red if @verbosity && @verbosity > 0
|
507
|
+
|
508
|
+
rescue JSON::ParserError => e
|
509
|
+
error_message += " - Raw response: #{response.body}"
|
510
|
+
debug "Failed to parse JSON error response: #{e.message}", 1, :yellow if @verbosity && @verbosity > 0
|
511
|
+
end
|
512
|
+
|
513
|
+
raise Error, error_message
|
450
514
|
end
|
451
515
|
end
|
452
516
|
|
data/lib/idrac/error.rb
CHANGED
data/lib/idrac/license.rb
CHANGED
@@ -41,6 +41,18 @@ module IDRAC
|
|
41
41
|
# Extracts the iDRAC version from the license description or server header
|
42
42
|
# @return [Integer, nil] The license version (e.g. 9) or nil if not found
|
43
43
|
def license_version
|
44
|
+
# Use memoization to cache the result and avoid multiple API calls
|
45
|
+
@license_version ||= compute_license_version
|
46
|
+
end
|
47
|
+
|
48
|
+
# Clear the cached license version (useful if iDRAC state changes)
|
49
|
+
def clear_license_version_cache
|
50
|
+
@license_version = nil
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def compute_license_version
|
44
56
|
# First try to get from license info
|
45
57
|
license = license_info
|
46
58
|
if license
|
@@ -80,8 +92,6 @@ module IDRAC
|
|
80
92
|
nil
|
81
93
|
end
|
82
94
|
|
83
|
-
private
|
84
|
-
|
85
95
|
# Attempt to get license information using Dell OEM extension path (for iDRAC 8)
|
86
96
|
# @return [Hash, nil] License info or nil if not found
|
87
97
|
def try_dell_oem_license_path
|
data/lib/idrac/network.rb
CHANGED
@@ -4,6 +4,40 @@ require 'json'
|
|
4
4
|
|
5
5
|
module IDRAC
|
6
6
|
module Network
|
7
|
+
# Get iDRAC version information following Dell's approach
|
8
|
+
def get_idrac_version_info
|
9
|
+
# Get iDRAC model to determine generation (following Dell's pattern)
|
10
|
+
manager_response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1?$select=Model,FirmwareVersion")
|
11
|
+
|
12
|
+
if manager_response.status == 200
|
13
|
+
manager_data = JSON.parse(manager_response.body)
|
14
|
+
model = manager_data["Model"]
|
15
|
+
firmware_version = manager_data["FirmwareVersion"]
|
16
|
+
|
17
|
+
# Determine iDRAC generation based on model (Dell's approach)
|
18
|
+
if model.include?("12") || model.include?("13")
|
19
|
+
idrac_generation = 8
|
20
|
+
elsif model.include?("14") || model.include?("15") || model.include?("16")
|
21
|
+
idrac_generation = 9
|
22
|
+
else
|
23
|
+
idrac_generation = 10 # iDRAC9 and newer
|
24
|
+
end
|
25
|
+
|
26
|
+
# Convert firmware version to numeric for comparison (Dell's approach)
|
27
|
+
firmware_numeric = firmware_version.gsub(".", "").to_i if firmware_version
|
28
|
+
|
29
|
+
{
|
30
|
+
generation: idrac_generation,
|
31
|
+
firmware_version: firmware_version,
|
32
|
+
firmware_numeric: firmware_numeric,
|
33
|
+
model: model
|
34
|
+
}
|
35
|
+
else
|
36
|
+
# Fallback - assume newer version if we can't determine
|
37
|
+
{ generation: 9, firmware_version: "unknown", firmware_numeric: 0, model: "unknown" }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
7
41
|
def get_bmc_network
|
8
42
|
# Get the iDRAC ethernet interface
|
9
43
|
collection_response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/EthernetInterfaces")
|
@@ -45,39 +79,72 @@ module IDRAC
|
|
45
79
|
def set_bmc_network(ipv4: nil, mask: nil, gateway: nil,
|
46
80
|
dns_primary: nil, dns_secondary: nil, hostname: nil,
|
47
81
|
dhcp: false)
|
48
|
-
|
82
|
+
puts "🔧 iDRAC set_bmc_network called with: ipv4=#{ipv4}, mask=#{mask}, gateway=#{gateway}, dhcp=#{dhcp}".cyan
|
83
|
+
|
84
|
+
# Get iDRAC version information first (following Dell's approach)
|
85
|
+
begin
|
86
|
+
puts "🔍 Getting iDRAC version information...".yellow
|
87
|
+
version_info = get_idrac_version_info
|
88
|
+
puts "✅ Detected iDRAC Generation #{version_info[:generation]}, Firmware: #{version_info[:firmware_version]}".green
|
89
|
+
puts " Model: #{version_info[:model]}, Firmware Numeric: #{version_info[:firmware_numeric]}".cyan
|
90
|
+
rescue => e
|
91
|
+
puts "❌ Error getting iDRAC version info: #{e.class} - #{e.message}".red
|
92
|
+
raise e
|
93
|
+
end
|
94
|
+
|
95
|
+
# Get the interface path
|
96
|
+
puts "🔍 Getting ethernet interfaces collection...".yellow
|
49
97
|
collection_response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/EthernetInterfaces")
|
50
98
|
|
51
99
|
if collection_response.status == 200
|
100
|
+
puts "✅ Got ethernet interfaces collection successfully".green
|
52
101
|
collection = JSON.parse(collection_response.body)
|
102
|
+
puts "🔍 Collection members: #{collection["Members"]&.size || 0} interfaces found".cyan
|
53
103
|
|
54
104
|
if collection["Members"] && collection["Members"].any?
|
55
105
|
interface_path = collection["Members"][0]["@odata.id"]
|
106
|
+
puts "✅ Using interface path: #{interface_path}".green
|
56
107
|
|
57
108
|
if dhcp
|
58
109
|
puts "Setting iDRAC to DHCP mode...".yellow
|
59
110
|
body = {
|
60
111
|
"DHCPv4" => {
|
61
112
|
"DHCPEnabled" => true
|
62
|
-
}
|
63
|
-
"IPv4Addresses" => [{
|
64
|
-
"AddressOrigin" => "DHCP"
|
65
|
-
}]
|
113
|
+
}
|
66
114
|
}
|
67
115
|
else
|
68
116
|
puts "Configuring iDRAC network settings...".yellow
|
69
117
|
body = {}
|
70
118
|
|
71
|
-
#
|
72
|
-
if
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
119
|
+
# Choose API approach based on iDRAC generation and firmware version
|
120
|
+
if version_info[:generation] >= 9 && version_info[:firmware_numeric] > 0
|
121
|
+
# iDRAC9/10 - use System Configuration Profile (SCP) approach for reliable network changes
|
122
|
+
puts "Using iDRAC9+ SCP approach for network configuration".yellow
|
123
|
+
puts " Delegating to set_idrac_ip method for reliable configuration".cyan
|
124
|
+
|
125
|
+
# Use the existing set_idrac_ip method which uses SCP
|
126
|
+
return set_idrac_ip(new_ip: ipv4, new_gw: gateway, new_nm: mask)
|
127
|
+
|
128
|
+
else
|
129
|
+
# iDRAC8 or older firmware - use IPv4Addresses approach
|
130
|
+
puts "Using legacy iDRAC8 API approach (IPv4Addresses)".yellow
|
131
|
+
|
132
|
+
# Disable DHCP first for older versions
|
133
|
+
body["DHCPv4"] = {
|
134
|
+
"DHCPEnabled" => false
|
135
|
+
}
|
136
|
+
|
137
|
+
# Configure static IP using legacy API
|
138
|
+
if ipv4 && mask
|
139
|
+
body["IPv4Addresses"] = [{
|
140
|
+
"Address" => ipv4,
|
141
|
+
"SubnetMask" => mask,
|
142
|
+
"Gateway" => gateway,
|
143
|
+
"AddressOrigin" => "Static"
|
144
|
+
}]
|
145
|
+
puts " IP: #{ipv4}/#{mask}".cyan
|
146
|
+
puts " Gateway: #{gateway}".cyan if gateway
|
147
|
+
end
|
81
148
|
end
|
82
149
|
|
83
150
|
# Configure DNS if provided
|
@@ -96,24 +163,64 @@ module IDRAC
|
|
96
163
|
end
|
97
164
|
end
|
98
165
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
166
|
+
# Send the request using the version-specific approach
|
167
|
+
puts "🔍 Sending PATCH request to #{interface_path}".yellow
|
168
|
+
puts "📦 Request body: #{JSON.pretty_generate(body)}".cyan
|
169
|
+
|
170
|
+
begin
|
171
|
+
response = authenticated_request(
|
172
|
+
:patch,
|
173
|
+
interface_path,
|
174
|
+
body: body.to_json,
|
175
|
+
headers: { 'Content-Type' => 'application/json' }
|
176
|
+
)
|
177
|
+
puts "✅ Got response with status: #{response.status}".green
|
178
|
+
rescue => e
|
179
|
+
puts "❌ Error sending PATCH request: #{e.class} - #{e.message}".red
|
180
|
+
raise e
|
181
|
+
end
|
105
182
|
|
106
183
|
if response.status.between?(200, 299)
|
107
|
-
puts "iDRAC network configured successfully.".green
|
108
|
-
|
184
|
+
puts "iDRAC network configured successfully using #{version_info[:generation] >= 9 ? 'newer' : 'legacy'} API.".green
|
185
|
+
|
186
|
+
# For network configuration changes, automatically restart iDRAC to apply settings
|
187
|
+
if ipv4 && !dhcp
|
188
|
+
puts "Initiating iDRAC restart to apply network configuration...".yellow
|
189
|
+
restart_response = authenticated_request(
|
190
|
+
:post,
|
191
|
+
"/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Manager.Reset",
|
192
|
+
body: { "ResetType" => "GracefulRestart" }.to_json,
|
193
|
+
headers: { 'Content-Type' => 'application/json' }
|
194
|
+
)
|
195
|
+
|
196
|
+
if restart_response.status == 204
|
197
|
+
puts "✓ iDRAC restart initiated successfully.".green
|
198
|
+
puts "Network configuration will be applied after restart (2-3 minutes).".cyan
|
199
|
+
puts "New IP: #{ipv4}".cyan
|
200
|
+
else
|
201
|
+
puts "⚠ iDRAC restart failed (#{restart_response.status}). You may need to restart manually.".yellow
|
202
|
+
puts "Configuration is saved but may not be active until restart.".yellow
|
203
|
+
end
|
204
|
+
else
|
205
|
+
puts "WARNING: iDRAC may restart network services. Connection may be lost.".yellow
|
206
|
+
end
|
207
|
+
|
208
|
+
puts "✅ Returning true from set_bmc_network".green
|
109
209
|
true
|
110
210
|
else
|
111
|
-
|
211
|
+
# Log the error with version context for troubleshooting
|
212
|
+
puts "❌ Network configuration failed with status: #{response.status}".red
|
213
|
+
puts "🔍 Response body: #{response.body}".red
|
214
|
+
error_msg = "Failed to configure iDRAC network (Generation #{version_info[:generation]}, FW #{version_info[:firmware_version]}): #{response.status} - #{response.body}"
|
215
|
+
raise Error, error_msg
|
112
216
|
end
|
113
217
|
else
|
218
|
+
puts "❌ No ethernet interfaces found in collection".red
|
114
219
|
raise Error, "No ethernet interfaces found"
|
115
220
|
end
|
116
221
|
else
|
222
|
+
puts "❌ Failed to get ethernet interfaces collection, status: #{collection_response.status}".red
|
223
|
+
puts "🔍 Response body: #{collection_response.body}".red
|
117
224
|
raise Error, "Failed to get ethernet interfaces"
|
118
225
|
end
|
119
226
|
end
|
data/lib/idrac/session.rb
CHANGED
@@ -184,6 +184,10 @@ module IDRAC
|
|
184
184
|
# Determine the correct session endpoint based on Redfish version
|
185
185
|
session_endpoint = determine_session_endpoint
|
186
186
|
|
187
|
+
# Check current session count before creating new session
|
188
|
+
current_sessions = get_session_count
|
189
|
+
debug "Current active sessions: #{current_sessions}", 1, :cyan
|
190
|
+
|
187
191
|
payload = { "UserName" => username, "Password" => password }
|
188
192
|
|
189
193
|
debug "Attempting to create Redfish session at #{base_url}#{session_endpoint}", 1
|
@@ -191,10 +195,22 @@ module IDRAC
|
|
191
195
|
print_connection_debug_info if @verbosity >= 2
|
192
196
|
|
193
197
|
# Try creation methods in sequence
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
+
if create_session_with_content_type(session_endpoint, payload)
|
199
|
+
log_session_creation_success(current_sessions)
|
200
|
+
return true
|
201
|
+
end
|
202
|
+
if create_session_with_basic_auth(session_endpoint, payload)
|
203
|
+
log_session_creation_success(current_sessions)
|
204
|
+
return true
|
205
|
+
end
|
206
|
+
if handle_max_sessions_and_retry(session_endpoint, payload)
|
207
|
+
log_session_creation_success(current_sessions)
|
208
|
+
return true
|
209
|
+
end
|
210
|
+
if create_session_with_form_urlencoded(session_endpoint, payload)
|
211
|
+
log_session_creation_success(current_sessions)
|
212
|
+
return true
|
213
|
+
end
|
198
214
|
|
199
215
|
# If all attempts fail, switch to direct mode
|
200
216
|
@direct_mode = true
|
@@ -678,5 +694,34 @@ module IDRAC
|
|
678
694
|
debug "Defaulting to endpoint #{default_endpoint}", 1, :light_yellow
|
679
695
|
default_endpoint
|
680
696
|
end
|
697
|
+
|
698
|
+
# Get current session count
|
699
|
+
def get_session_count
|
700
|
+
begin
|
701
|
+
sessions_url = determine_session_endpoint
|
702
|
+
response = request_with_basic_auth(:get, sessions_url, nil, 'application/json')
|
703
|
+
|
704
|
+
if response.status == 200
|
705
|
+
sessions_data = JSON.parse(response.body)
|
706
|
+
count = sessions_data.dig('Members')&.size || 0
|
707
|
+
return count
|
708
|
+
end
|
709
|
+
rescue => e
|
710
|
+
debug "Error getting session count: #{e.message}", 2, :yellow
|
711
|
+
end
|
712
|
+
|
713
|
+
# Return unknown if we can't determine
|
714
|
+
"unknown"
|
715
|
+
end
|
716
|
+
|
717
|
+
# Log successful session creation with before/after count
|
718
|
+
def log_session_creation_success(before_count)
|
719
|
+
after_count = get_session_count
|
720
|
+
if before_count.is_a?(Integer) && after_count.is_a?(Integer)
|
721
|
+
debug "✅ Session created successfully! Count: #{before_count} → #{after_count} (+#{after_count - before_count})", 1, :green
|
722
|
+
else
|
723
|
+
debug "✅ Session created successfully! Previous: #{before_count}, Current: #{after_count}", 1, :green
|
724
|
+
end
|
725
|
+
end
|
681
726
|
end
|
682
727
|
end
|
data/lib/idrac/storage.rb
CHANGED
@@ -16,7 +16,8 @@ module IDRAC
|
|
16
16
|
controller_data = {
|
17
17
|
"id" => controller["Id"],
|
18
18
|
"name" => controller["Name"],
|
19
|
-
|
19
|
+
# Model is in the StorageControllers array, not at root level
|
20
|
+
"model" => controller.dig("StorageControllers", 0, "Model") || controller["Name"],
|
20
21
|
"drives_count" => controller["Drives"].size,
|
21
22
|
"status" => controller.dig("Status", "Health") || "N/A",
|
22
23
|
"firmware_version" => controller.dig("StorageControllers", 0, "FirmwareVersion"),
|
@@ -383,7 +384,23 @@ module IDRAC
|
|
383
384
|
end
|
384
385
|
|
385
386
|
# For newer firmware, use Redfish API
|
386
|
-
drive_refs = drives.map
|
387
|
+
drive_refs = drives.map do |d|
|
388
|
+
# Convert drive identifier to proper @odata.id format
|
389
|
+
if d.to_s.start_with?('/redfish/v1/')
|
390
|
+
# Already in @odata.id format
|
391
|
+
{ "@odata.id" => d.to_s }
|
392
|
+
else
|
393
|
+
# Convert FQDD to @odata.id format
|
394
|
+
drive_fqdd = d.to_s
|
395
|
+
# Build the full controller path for the drive reference
|
396
|
+
full_controller_path = if controller_id.start_with?('/redfish/v1/')
|
397
|
+
controller_id
|
398
|
+
else
|
399
|
+
"/redfish/v1/Systems/System.Embedded.1/Storage/#{controller_id}"
|
400
|
+
end
|
401
|
+
{ "@odata.id" => "#{full_controller_path}/Drives/#{drive_fqdd}" }
|
402
|
+
end
|
403
|
+
end
|
387
404
|
|
388
405
|
# [FastPath optimization for SSDs](https://www.dell.com/support/manuals/en-us/perc-h755/perc11_ug/fastpath?guid=guid-a9e90946-a41f-48ab-88f1-9ce514b4c414&lang=en-us)
|
389
406
|
payload = {
|
@@ -405,9 +422,16 @@ module IDRAC
|
|
405
422
|
|
406
423
|
payload["Encrypted"] = true if encrypt
|
407
424
|
|
425
|
+
# Ensure we have the full path to the controller
|
426
|
+
controller_path = if controller_id.start_with?('/redfish/v1/')
|
427
|
+
controller_id
|
428
|
+
else
|
429
|
+
"/redfish/v1/Systems/System.Embedded.1/Storage/#{controller_id}"
|
430
|
+
end
|
431
|
+
|
408
432
|
response = authenticated_request(
|
409
433
|
:post,
|
410
|
-
"#{
|
434
|
+
"#{controller_path}/Volumes",
|
411
435
|
body: payload.to_json,
|
412
436
|
headers: { 'Content-Type' => 'application/json' }
|
413
437
|
)
|
@@ -532,21 +556,13 @@ module IDRAC
|
|
532
556
|
|
533
557
|
return true
|
534
558
|
else
|
535
|
-
|
536
|
-
|
537
|
-
begin
|
538
|
-
error_data = JSON.parse(response.body)
|
539
|
-
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
540
|
-
rescue
|
541
|
-
# Ignore JSON parsing errors
|
542
|
-
end
|
543
|
-
|
544
|
-
raise Error, error_message
|
559
|
+
# Use generic error handler which includes ExtendedInfo parsing
|
560
|
+
handle_response(response)
|
545
561
|
end
|
546
562
|
end
|
547
563
|
|
548
564
|
# Disable Self-Encrypting Drive support on controller
|
549
|
-
def disable_local_key_management(controller_id)
|
565
|
+
def disable_local_key_management(controller_id:)
|
550
566
|
payload = { "TargetFQDD": controller_id }
|
551
567
|
|
552
568
|
response = authenticated_request(
|
@@ -567,16 +583,8 @@ module IDRAC
|
|
567
583
|
|
568
584
|
return true
|
569
585
|
else
|
570
|
-
|
571
|
-
|
572
|
-
begin
|
573
|
-
error_data = JSON.parse(response.body)
|
574
|
-
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
575
|
-
rescue
|
576
|
-
# Ignore JSON parsing errors
|
577
|
-
end
|
578
|
-
|
579
|
-
raise Error, error_message
|
586
|
+
# Use generic error handler which includes ExtendedInfo parsing
|
587
|
+
handle_response(response)
|
580
588
|
end
|
581
589
|
end
|
582
590
|
|
data/lib/idrac/system_config.rb
CHANGED
@@ -96,17 +96,24 @@ module IDRAC
|
|
96
96
|
components = scp.is_a?(Array) ? scp : [scp]
|
97
97
|
{ "SystemConfiguration" => { "Components" => components } }
|
98
98
|
end
|
99
|
+
|
100
|
+
# Validate the SCP structure before sending
|
101
|
+
unless scp_to_apply.is_a?(Hash) && scp_to_apply["SystemConfiguration"] && scp_to_apply["SystemConfiguration"]["Components"]
|
102
|
+
raise ArgumentError, "Invalid SCP structure: must contain SystemConfiguration.Components"
|
103
|
+
end
|
99
104
|
|
100
105
|
# Create the import parameters
|
106
|
+
# Use compact JSON generation to avoid formatting issues with Dell iDRAC
|
101
107
|
params = {
|
102
|
-
"ImportBuffer" => JSON.
|
108
|
+
"ImportBuffer" => JSON.generate(scp_to_apply),
|
103
109
|
"ShareParameters" => {"Target" => target},
|
104
110
|
"ShutdownType" => "Forced",
|
105
111
|
"HostPowerState" => reboot ? "On" : "Off"
|
106
112
|
}
|
107
113
|
|
108
114
|
debug "Importing System Configuration...", 1, :blue
|
109
|
-
debug "Configuration: #{JSON.pretty_generate(scp_to_apply)}",
|
115
|
+
debug "Configuration: #{JSON.pretty_generate(scp_to_apply)}", 1, :cyan
|
116
|
+
debug "ImportBuffer content: #{params['ImportBuffer']}", 1, :yellow
|
110
117
|
|
111
118
|
# Make the API request
|
112
119
|
response = authenticated_request(
|
@@ -119,7 +126,16 @@ module IDRAC
|
|
119
126
|
# Check for immediate errors
|
120
127
|
if response.headers["content-length"].to_i > 0
|
121
128
|
debug response.inspect, 1, :red
|
122
|
-
|
129
|
+
error_message = "Failed importing SCP: #{response.body}"
|
130
|
+
|
131
|
+
# Check for specific schema validation errors
|
132
|
+
if response.body.include?("invalid characters") || response.body.include?("invalid token")
|
133
|
+
error_message += "\nThis may be due to JSON formatting issues. The SCP structure might contain characters not accepted by Dell iDRAC."
|
134
|
+
elsif response.body.include?("not compliant with configuration schema")
|
135
|
+
error_message += "\nThe SCP structure does not match Dell's expected schema format."
|
136
|
+
end
|
137
|
+
|
138
|
+
return { status: :failed, error: error_message }
|
123
139
|
end
|
124
140
|
|
125
141
|
return handle_location(response.headers["location"])
|
@@ -188,7 +204,23 @@ module IDRAC
|
|
188
204
|
# Convert an SCP hash back to array format
|
189
205
|
def hash_to_scp(hash)
|
190
206
|
hash.inject([]) do |acc, (fqdd, attributes)|
|
191
|
-
|
207
|
+
# Convert hash attributes to Dell SCP format (array of Name/Value/Set On Import objects)
|
208
|
+
scp_attributes = case attributes
|
209
|
+
when Hash
|
210
|
+
attributes.map do |name, value|
|
211
|
+
{
|
212
|
+
"Name" => name.to_s,
|
213
|
+
"Value" => value.to_s,
|
214
|
+
"Set On Import" => "True"
|
215
|
+
}
|
216
|
+
end
|
217
|
+
when Array
|
218
|
+
attributes # Already in correct format
|
219
|
+
else
|
220
|
+
[]
|
221
|
+
end
|
222
|
+
|
223
|
+
acc << { "FQDD" => fqdd, "Attributes" => scp_attributes }
|
192
224
|
acc
|
193
225
|
end
|
194
226
|
end
|
@@ -198,6 +230,13 @@ module IDRAC
|
|
198
230
|
def merge_scp(*scps)
|
199
231
|
merged_components = {}
|
200
232
|
|
233
|
+
# Get iDRAC version for version-specific handling
|
234
|
+
version = begin
|
235
|
+
license_version.to_i
|
236
|
+
rescue
|
237
|
+
9 # Default to iDRAC 9 behavior if version detection fails
|
238
|
+
end
|
239
|
+
|
201
240
|
scps.compact.each do |scp|
|
202
241
|
components = extract_components(scp)
|
203
242
|
components.each do |component|
|
@@ -209,10 +248,34 @@ module IDRAC
|
|
209
248
|
|
210
249
|
# Build hash of existing attributes by name for easy lookup
|
211
250
|
attr_hash = {}
|
212
|
-
|
251
|
+
|
252
|
+
# Handle different attribute structures between iDRAC versions
|
253
|
+
existing_attrs.each do |attr|
|
254
|
+
case attr
|
255
|
+
when Hash
|
256
|
+
# iDRAC 8 style: {"Name" => "Users.3#IpmiLanPrivilege", "Value" => "Administrator"}
|
257
|
+
attr_hash[attr["Name"]] = attr if attr["Name"]
|
258
|
+
when String
|
259
|
+
# iDRAC 9 style: strings or different structure - preserve as-is
|
260
|
+
# For strings, use the string itself as both key and value
|
261
|
+
attr_hash[attr] = attr
|
262
|
+
else
|
263
|
+
# Unknown structure, preserve as-is with a generated key
|
264
|
+
attr_hash["attr_#{attr_hash.size}"] = attr
|
265
|
+
end
|
266
|
+
end
|
213
267
|
|
214
268
|
# Add/overwrite with new attributes
|
215
|
-
new_attrs.each
|
269
|
+
new_attrs.each do |attr|
|
270
|
+
case attr
|
271
|
+
when Hash
|
272
|
+
attr_hash[attr["Name"]] = attr if attr["Name"]
|
273
|
+
when String
|
274
|
+
attr_hash[attr] = attr
|
275
|
+
else
|
276
|
+
attr_hash["attr_#{attr_hash.size}"] = attr
|
277
|
+
end
|
278
|
+
end
|
216
279
|
|
217
280
|
merged_components[fqdd]["Attributes"] = attr_hash.values
|
218
281
|
else
|
@@ -225,18 +288,24 @@ module IDRAC
|
|
225
288
|
end
|
226
289
|
|
227
290
|
# Handle location header for IP change operations. Monitors old IP until it fails,
|
228
|
-
# then
|
291
|
+
# then monitors job completion at new IP with proper task polling.
|
229
292
|
def handle_location_with_ip_change(location, new_ip, timeout: 300)
|
230
293
|
return nil if location.nil? || location.empty?
|
231
294
|
|
295
|
+
# Extract job ID from location header
|
296
|
+
job_id = location.split("/").last
|
297
|
+
debug "Extracted job ID: #{job_id}", 1, :cyan
|
298
|
+
|
232
299
|
old_ip = @host
|
233
300
|
start_time = Time.now
|
234
301
|
old_ip_failed = false
|
302
|
+
task = nil
|
303
|
+
tries = 0
|
235
304
|
|
236
|
-
debug "Monitoring IP change: #{old_ip} → #{new_ip}", 1, :blue
|
305
|
+
debug "Monitoring IP change with job tracking: #{old_ip} → #{new_ip}", 1, :blue
|
237
306
|
|
238
307
|
while Time.now - start_time < timeout
|
239
|
-
# Try old IP until it fails, then focus on new IP
|
308
|
+
# Try old IP until it fails, then focus on new IP with job monitoring
|
240
309
|
[
|
241
310
|
old_ip_failed ? nil : [self, old_ip, "Old IP failed"],
|
242
311
|
[create_temp_client(new_ip), new_ip, old_ip_failed ? "New IP not ready" : "Cannot reach new IP"]
|
@@ -244,13 +313,45 @@ module IDRAC
|
|
244
313
|
|
245
314
|
begin
|
246
315
|
client.login if ip == new_ip
|
316
|
+
|
317
|
+
# Test basic connectivity first
|
247
318
|
client.authenticated_request(:get, "/redfish/v1", timeout: 2, open_timeout: 1)
|
248
319
|
|
249
320
|
if ip == new_ip
|
250
|
-
|
251
|
-
|
252
|
-
|
321
|
+
# Once we can reach the new IP, check the job status
|
322
|
+
debug "✅ New IP reachable, checking job status...", 1, :green
|
323
|
+
begin
|
324
|
+
task_response = client.authenticated_request(:get, "/redfish/v1/TaskService/Tasks/#{job_id}", timeout: 10)
|
325
|
+
task = JSON.parse(task_response.body)
|
326
|
+
|
327
|
+
debug "Job status: #{task['TaskState']} / #{task['TaskStatus']}", 1, :cyan
|
328
|
+
|
329
|
+
if task["TaskState"] == "Completed"
|
330
|
+
if task["TaskStatus"] == "OK"
|
331
|
+
debug "✅ Job completed successfully!", 1, :green
|
332
|
+
@host = new_ip
|
333
|
+
return { status: :success, ip: new_ip, job_status: task }
|
334
|
+
else
|
335
|
+
# Job completed but with error
|
336
|
+
msg = task['Messages']&.first&.dig('Message') rescue "N/A"
|
337
|
+
attr = task['Messages']&.first&.dig('Oem', 'Dell', 'Name') rescue "N/A"
|
338
|
+
error_msg = "Job failed: #{msg} : #{attr}, TaskState: #{task['TaskState']}, TaskStatus: #{task['TaskStatus']}"
|
339
|
+
debug "❌ #{error_msg}", 1, :red
|
340
|
+
return { status: :error, error: error_msg, job_status: task }
|
341
|
+
end
|
342
|
+
elsif task["TaskState"] == "Running"
|
343
|
+
debug "⏳ Job still running, continuing to wait...", 2, :yellow
|
344
|
+
# Continue monitoring
|
345
|
+
else
|
346
|
+
debug "⚠️ Unexpected job state: #{task['TaskState']}", 1, :yellow
|
347
|
+
# Continue monitoring
|
348
|
+
end
|
349
|
+
rescue => job_error
|
350
|
+
debug "Failed to check job status: #{job_error.message}", 2, :yellow
|
351
|
+
# Continue monitoring - job might not be ready yet
|
352
|
+
end
|
253
353
|
else
|
354
|
+
# Still on old IP, just test connectivity
|
254
355
|
return { status: :success, ip: old_ip }
|
255
356
|
end
|
256
357
|
rescue => e
|
@@ -259,7 +360,12 @@ module IDRAC
|
|
259
360
|
end
|
260
361
|
end
|
261
362
|
|
262
|
-
|
363
|
+
tries += 1
|
364
|
+
if tries > 20
|
365
|
+
return { status: :timeout, error: "Job monitoring exceeded maximum retries (#{tries})" }
|
366
|
+
end
|
367
|
+
|
368
|
+
sleep old_ip_failed ? 6 : 5 # Wait longer during IP change
|
263
369
|
end
|
264
370
|
|
265
371
|
{ status: :timeout, error: "IP change timed out after #{timeout}s. Old IP failed: #{old_ip_failed}" }
|
data/lib/idrac/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: idrac
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Siegel
|
8
|
+
autorequire:
|
8
9
|
bindir: bin
|
9
10
|
cert_chain: []
|
10
|
-
date:
|
11
|
+
date: 2025-09-18 00:00:00.000000000 Z
|
11
12
|
dependencies:
|
12
13
|
- !ruby/object:Gem::Dependency
|
13
14
|
name: httparty
|
@@ -254,7 +255,6 @@ files:
|
|
254
255
|
- README.md
|
255
256
|
- bin/console
|
256
257
|
- bin/idrac
|
257
|
-
- bin/idrac-tsr
|
258
258
|
- bin/setup
|
259
259
|
- idrac.gemspec
|
260
260
|
- lib/idrac.rb
|
@@ -282,6 +282,7 @@ licenses:
|
|
282
282
|
metadata:
|
283
283
|
homepage_uri: https://github.com/buildio/idrac
|
284
284
|
source_code_uri: https://github.com/buildio/idrac
|
285
|
+
post_install_message:
|
285
286
|
rdoc_options: []
|
286
287
|
require_paths:
|
287
288
|
- lib
|
@@ -296,7 +297,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
296
297
|
- !ruby/object:Gem::Version
|
297
298
|
version: '0'
|
298
299
|
requirements: []
|
299
|
-
rubygems_version: 3.
|
300
|
+
rubygems_version: 3.5.16
|
301
|
+
signing_key:
|
300
302
|
specification_version: 4
|
301
303
|
summary: API Client for Dell iDRAC
|
302
304
|
test_files: []
|
data/bin/idrac-tsr
DELETED
@@ -1,90 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require_relative '../lib/idrac'
|
4
|
-
require 'optparse'
|
5
|
-
|
6
|
-
options = {
|
7
|
-
host: ENV['IDRAC_HOST'],
|
8
|
-
username: ENV['IDRAC_USER'] || 'root',
|
9
|
-
password: ENV['IDRAC_PASSWORD'] || 'calvin',
|
10
|
-
output: nil,
|
11
|
-
data_types: [0, 1, 2], # Default: Hardware, OS, Debug
|
12
|
-
timeout: 600,
|
13
|
-
verbose: false
|
14
|
-
}
|
15
|
-
|
16
|
-
OptionParser.new do |opts|
|
17
|
-
opts.banner = "Usage: idrac-tsr [options]"
|
18
|
-
|
19
|
-
opts.on("-h", "--host HOST", "iDRAC host address") do |h|
|
20
|
-
options[:host] = h
|
21
|
-
end
|
22
|
-
|
23
|
-
opts.on("-u", "--username USER", "Username (default: root)") do |u|
|
24
|
-
options[:username] = u
|
25
|
-
end
|
26
|
-
|
27
|
-
opts.on("-p", "--password PASS", "Password (default: calvin)") do |p|
|
28
|
-
options[:password] = p
|
29
|
-
end
|
30
|
-
|
31
|
-
opts.on("-o", "--output FILE", "Output filename (default: auto-generated)") do |o|
|
32
|
-
options[:output] = o
|
33
|
-
end
|
34
|
-
|
35
|
-
opts.on("-d", "--data TYPES", "Data types to collect (comma-separated: 0=HW,1=OS,2=Debug,3=TTY,4=All)") do |d|
|
36
|
-
options[:data_types] = d.split(',').map(&:to_i)
|
37
|
-
end
|
38
|
-
|
39
|
-
opts.on("-t", "--timeout SECONDS", Integer, "Timeout in seconds (default: 600)") do |t|
|
40
|
-
options[:timeout] = t
|
41
|
-
end
|
42
|
-
|
43
|
-
opts.on("-v", "--verbose", "Verbose output") do
|
44
|
-
options[:verbose] = true
|
45
|
-
end
|
46
|
-
|
47
|
-
opts.on("--help", "Show this message") do
|
48
|
-
puts opts
|
49
|
-
exit
|
50
|
-
end
|
51
|
-
end.parse!
|
52
|
-
|
53
|
-
if options[:host].nil?
|
54
|
-
puts "Error: Host is required. Use -h HOST or set IDRAC_HOST environment variable"
|
55
|
-
exit 1
|
56
|
-
end
|
57
|
-
|
58
|
-
begin
|
59
|
-
client = IDRAC::Client.new(
|
60
|
-
host: options[:host],
|
61
|
-
username: options[:username],
|
62
|
-
password: options[:password],
|
63
|
-
verify_ssl: false
|
64
|
-
)
|
65
|
-
|
66
|
-
client.verbosity = options[:verbose] ? 1 : 0
|
67
|
-
|
68
|
-
puts "Connecting to #{options[:host]}..."
|
69
|
-
client.login
|
70
|
-
|
71
|
-
puts "Generating TSR logs (this may take several minutes)..."
|
72
|
-
result = client.generate_and_download_tsr(
|
73
|
-
output_file: options[:output],
|
74
|
-
data_selector_values: options[:data_types],
|
75
|
-
wait_timeout: options[:timeout]
|
76
|
-
)
|
77
|
-
|
78
|
-
if result
|
79
|
-
puts "✓ TSR logs saved to: #{result}"
|
80
|
-
puts " File size: #{File.size(result) / 1024 / 1024} MB" if File.exist?(result)
|
81
|
-
else
|
82
|
-
puts "✗ Failed to download TSR logs"
|
83
|
-
exit 1
|
84
|
-
end
|
85
|
-
|
86
|
-
client.logout
|
87
|
-
rescue => e
|
88
|
-
puts "Error: #{e.message}"
|
89
|
-
exit 1
|
90
|
-
end
|