idrac 0.6.1 → 0.7.0
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/bin/idrac +28 -7
- data/lib/idrac/client.rb +1 -0
- data/lib/idrac/system.rb +121 -10
- data/lib/idrac/system_config.rb +369 -0
- data/lib/idrac/version.rb +1 -1
- data/lib/idrac.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 68c60caff773a2fd54e340a7ec1abbe02b8f9529d9f44527984923d22515ca9c
|
4
|
+
data.tar.gz: 62bf014eb0927473304cb2a4da06d09d1ca2aee67be4f4a6849d9295ef252347
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9bd33453fc8fcd7603e4ee4c34a1e63dce9da32eca413b9db297b590dae5e8787e6e46e72634dfbb0ae19cfb2b9b1fbb06702f67ff5a783ed388d8c0056bb583
|
7
|
+
data.tar.gz: b1f30af2c4ff18ae490d0a66a9fd58d6e1da2407935b0d999c2ca635fd3a7f5d184c541f6b9eee38bd6080b1db5cb4cc5feabf83e983384d63519f7973672617
|
data/bin/idrac
CHANGED
@@ -918,6 +918,27 @@ module IDRAC
|
|
918
918
|
end
|
919
919
|
end
|
920
920
|
|
921
|
+
desc "system_pci_devices", "Get PCI device information"
|
922
|
+
map "system:pci-devices" => :system_pci_devices
|
923
|
+
map "system:pci:devices" => :system_pci_devices
|
924
|
+
def system_pci_devices
|
925
|
+
with_idrac_client do |client|
|
926
|
+
pci_devices = client.pci_devices
|
927
|
+
|
928
|
+
puts "\nPCI Devices (#{pci_devices.size}):".green.bold
|
929
|
+
pci_devices.each do |device|
|
930
|
+
puts "#{device["name"]}:".bold
|
931
|
+
puts " Manufacturer: #{device["manufacturer"]}".cyan if device["manufacturer"]
|
932
|
+
puts " Device Class: #{device["device_class"]}".cyan if device["device_class"]
|
933
|
+
puts " Description: #{device["description"]}".cyan if device["description"]
|
934
|
+
puts " ID: #{device["id"]}".cyan if device["id"]
|
935
|
+
puts " Slot Type: #{device["slot_type"]}".cyan if device["slot_type"]
|
936
|
+
puts " Bus Width: #{device["bus_width"]}".cyan if device["bus_width"]
|
937
|
+
puts ""
|
938
|
+
end
|
939
|
+
end
|
940
|
+
end
|
941
|
+
|
921
942
|
desc "system_idrac_network", "Get iDRAC network configuration"
|
922
943
|
map "system:idrac_network" => :system_idrac_network
|
923
944
|
def system_idrac_network
|
@@ -1085,16 +1106,16 @@ module IDRAC
|
|
1085
1106
|
with_idrac_client do |client|
|
1086
1107
|
info = client.system_info
|
1087
1108
|
|
1088
|
-
if info
|
1109
|
+
if info["is_dell"]
|
1089
1110
|
puts "Dell iDRAC System:".green.bold
|
1090
|
-
puts " Service Tag: #{info
|
1091
|
-
puts " Model: #{info
|
1092
|
-
puts " iDRAC Version: #{info
|
1093
|
-
puts " Firmware Version: #{info
|
1111
|
+
puts " Service Tag: #{info["service_tag"]}".cyan
|
1112
|
+
puts " Model: #{info["model"]}".cyan
|
1113
|
+
puts " iDRAC Version: #{info["idrac_version"]}".cyan
|
1114
|
+
puts " Firmware Version: #{info["firmware_version"]}".cyan
|
1094
1115
|
else
|
1095
1116
|
puts "Not a Dell iDRAC system".yellow
|
1096
|
-
puts " Product: #{info
|
1097
|
-
if info
|
1117
|
+
puts " Product: #{info["product"]}".cyan
|
1118
|
+
if info["is_ancient_dell"]
|
1098
1119
|
puts " Ancient Dell System detected. Update firmware.".yellow
|
1099
1120
|
end
|
1100
1121
|
end
|
data/lib/idrac/client.rb
CHANGED
@@ -22,6 +22,7 @@ module IDRAC
|
|
22
22
|
include VirtualMedia
|
23
23
|
include Boot
|
24
24
|
include License
|
25
|
+
include SystemConfig
|
25
26
|
|
26
27
|
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
28
|
@host = host
|
data/lib/idrac/system.rb
CHANGED
@@ -235,6 +235,7 @@ module IDRAC
|
|
235
235
|
|
236
236
|
# Get PCI device information
|
237
237
|
def pci_devices
|
238
|
+
# First try the standard PCIeDevices endpoint
|
238
239
|
response = authenticated_request(:get, "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices?$expand=*($levels=1)")
|
239
240
|
|
240
241
|
if response.status == 200
|
@@ -257,17 +258,17 @@ module IDRAC
|
|
257
258
|
|
258
259
|
# Create device info with available data
|
259
260
|
device_info = {
|
260
|
-
device_class
|
261
|
-
manufacturer
|
262
|
-
name
|
263
|
-
description
|
264
|
-
id
|
265
|
-
slot_type
|
266
|
-
bus_width
|
267
|
-
nic
|
261
|
+
"device_class" => pcie_function ? pcie_function["DeviceClass"] : nil,
|
262
|
+
"manufacturer" => manufacturer,
|
263
|
+
"name" => stub["Name"],
|
264
|
+
"description" => stub["Description"],
|
265
|
+
"id" => pcie_function ? pcie_function["Id"] : stub["Id"],
|
266
|
+
"slot_type" => pcie_function ? pcie_function.dig("Oem", "Dell", "DellPCIeFunction", "SlotType") : nil,
|
267
|
+
"bus_width" => pcie_function ? pcie_function.dig("Oem", "Dell", "DellPCIeFunction", "DataBusWidth") : nil,
|
268
|
+
"nic" => pcie_function ? pcie_function.dig("Links", "EthernetInterfaces", 0, "@odata.id") : nil
|
268
269
|
}
|
269
270
|
|
270
|
-
puts "PCI Device: #{device_info[
|
271
|
+
puts "PCI Device: #{device_info["name"]} > #{device_info["manufacturer"]} > #{device_info["device_class"]} > #{device_info["description"]} > #{device_info["id"]}"
|
271
272
|
|
272
273
|
device_info
|
273
274
|
end
|
@@ -277,7 +278,117 @@ module IDRAC
|
|
277
278
|
raise Error, "Failed to parse PCI devices response: #{response.body}"
|
278
279
|
end
|
279
280
|
else
|
280
|
-
|
281
|
+
# For iDRAC 8, try Dell's recommended approach using System endpoint with PCIeDevices select option
|
282
|
+
system_pcie_response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1?$select=PCIeDevices")
|
283
|
+
|
284
|
+
if system_pcie_response.status == 200
|
285
|
+
begin
|
286
|
+
system_data = JSON.parse(system_pcie_response.body)
|
287
|
+
|
288
|
+
if system_data.key?("PCIeDevices") && !system_data["PCIeDevices"].empty?
|
289
|
+
pci_devices = []
|
290
|
+
|
291
|
+
# Process each PCIe device
|
292
|
+
system_data["PCIeDevices"].each do |device_link|
|
293
|
+
if device_link.is_a?(Hash) && device_link["@odata.id"]
|
294
|
+
device_path = device_link["@odata.id"]
|
295
|
+
device_response = authenticated_request(:get, device_path)
|
296
|
+
|
297
|
+
if device_response.status == 200
|
298
|
+
device_data = JSON.parse(device_response.body)
|
299
|
+
|
300
|
+
pci_devices << {
|
301
|
+
"device_class" => device_data["DeviceType"] || "Unknown",
|
302
|
+
"manufacturer" => device_data["Manufacturer"],
|
303
|
+
"name" => device_data["Name"] || device_data["Id"],
|
304
|
+
"description" => device_data["Description"],
|
305
|
+
"id" => device_data["Id"],
|
306
|
+
"slot_type" => device_data.dig("Oem", "Dell", "SlotType"),
|
307
|
+
"bus_width" => device_data.dig("Oem", "Dell", "BusWidth"),
|
308
|
+
"nic" => nil
|
309
|
+
}
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
return pci_devices unless pci_devices.empty?
|
315
|
+
end
|
316
|
+
rescue JSON::ParserError
|
317
|
+
# Continue to next approach
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Try NetworkAdapters as an alternative for finding PCIe devices (especially NICs and FC adapters)
|
322
|
+
nic_response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/NetworkAdapters?$expand=*($levels=1)")
|
323
|
+
|
324
|
+
if nic_response.status == 200
|
325
|
+
begin
|
326
|
+
nic_data = JSON.parse(nic_response.body)
|
327
|
+
|
328
|
+
pci_devices = []
|
329
|
+
|
330
|
+
# Extract PCI info from network adapters
|
331
|
+
if nic_data["Members"] && !nic_data["Members"].empty?
|
332
|
+
nic_data["Members"].each do |adapter|
|
333
|
+
next unless adapter["Model"] || adapter["Manufacturer"]
|
334
|
+
|
335
|
+
# Check if this is a Fiber Channel adapter by name or model
|
336
|
+
is_fc = (adapter["Name"] =~ /FC/i || adapter["Model"] =~ /FC/i ||
|
337
|
+
adapter["Id"] =~ /FC/i || adapter["Description"] =~ /Fibre/i) ? true : false
|
338
|
+
|
339
|
+
device_class = is_fc ? "FibreChannelController" : "NetworkController"
|
340
|
+
|
341
|
+
pci_devices << {
|
342
|
+
"device_class" => device_class,
|
343
|
+
"manufacturer" => adapter["Manufacturer"],
|
344
|
+
"name" => adapter["Name"] || adapter["Id"],
|
345
|
+
"description" => adapter["Description"],
|
346
|
+
"id" => adapter["Id"],
|
347
|
+
"slot_type" => adapter.dig("Oem", "Dell", "SlotType") ||
|
348
|
+
(adapter["Id"] =~ /Slot\.(\d+)/ ? "Slot #{$1}" : nil),
|
349
|
+
"bus_width" => nil,
|
350
|
+
"nic" => adapter["@odata.id"]
|
351
|
+
}
|
352
|
+
end
|
353
|
+
|
354
|
+
return pci_devices unless pci_devices.empty?
|
355
|
+
end
|
356
|
+
rescue JSON::ParserError
|
357
|
+
# Continue to fallback
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
# Last resort: check if PCIeFunctions are directly available
|
362
|
+
pcie_functions_response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/PCIeFunctions?$expand=*($levels=1)")
|
363
|
+
|
364
|
+
if pcie_functions_response.status == 200
|
365
|
+
begin
|
366
|
+
functions_data = JSON.parse(pcie_functions_response.body)
|
367
|
+
|
368
|
+
if functions_data["Members"] && !functions_data["Members"].empty?
|
369
|
+
pci_devices = functions_data["Members"].map do |function|
|
370
|
+
{
|
371
|
+
"device_class" => function["DeviceClass"] || "Unknown",
|
372
|
+
"manufacturer" => function["Manufacturer"] || "Unknown",
|
373
|
+
"name" => function["Name"] || function["Id"],
|
374
|
+
"description" => function["Description"],
|
375
|
+
"id" => function["Id"],
|
376
|
+
"slot_type" => function.dig("Oem", "Dell", "SlotType"),
|
377
|
+
"bus_width" => function.dig("Oem", "Dell", "DataBusWidth"),
|
378
|
+
"nic" => nil
|
379
|
+
}
|
380
|
+
end
|
381
|
+
|
382
|
+
return pci_devices
|
383
|
+
end
|
384
|
+
rescue JSON::ParserError
|
385
|
+
# Continue to fallback
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# Fallback for any version when all endpoints unavailable
|
390
|
+
puts "PCI device information not available through standard or alternative endpoints" if @verbose
|
391
|
+
return []
|
281
392
|
end
|
282
393
|
end
|
283
394
|
|
@@ -0,0 +1,369 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module IDRAC
|
5
|
+
module SystemConfig
|
6
|
+
# Get the system configuration profile for a given target (e.g. "RAID")
|
7
|
+
def get_system_configuration_profile(target: "RAID")
|
8
|
+
tries = 0
|
9
|
+
location = nil
|
10
|
+
started_at = Time.now
|
11
|
+
|
12
|
+
while location.nil?
|
13
|
+
debug "Exporting System Configuration try #{tries+=1}..."
|
14
|
+
|
15
|
+
response = authenticated_request(:post,
|
16
|
+
"/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ExportSystemConfiguration",
|
17
|
+
body: {"ExportFormat": "JSON", "ShareParameters":{"Target": target}}.to_json,
|
18
|
+
headers: {"Content-Type" => "application/json"}
|
19
|
+
)
|
20
|
+
|
21
|
+
if response.status == 400
|
22
|
+
debug "Failed exporting system configuration: #{response.body}", 1, :red
|
23
|
+
raise Error, "Failed exporting system configuration profile"
|
24
|
+
elsif response.status.between?(401, 599)
|
25
|
+
debug "Failed exporting system configuration: #{response.body}", 1, :red
|
26
|
+
|
27
|
+
# Parse error response
|
28
|
+
error_data = JSON.parse(response.body) rescue nil
|
29
|
+
|
30
|
+
if error_data && error_data["error"] && error_data["error"]["@Message.ExtendedInfo"]
|
31
|
+
message_info = error_data["error"]["@Message.ExtendedInfo"]
|
32
|
+
|
33
|
+
# Check for specific error conditions
|
34
|
+
if message_info.any? { |m| m["Message"] =~ /existing configuration job is already in progress/ }
|
35
|
+
debug "Existing configuration job is already in progress, retrying...", 1, :yellow
|
36
|
+
sleep 30
|
37
|
+
elsif message_info.any? { |m| m["Message"] =~ /job operation is already running/ }
|
38
|
+
debug "Existing job operation is already in progress, retrying...", 1, :yellow
|
39
|
+
sleep 60
|
40
|
+
else
|
41
|
+
# Detailed error info for debugging
|
42
|
+
debug "*" * 80, 1, :red
|
43
|
+
debug "Headers: #{response.headers.inspect}", 1, :red
|
44
|
+
debug "Body: #{response.body}", 1, :yellow
|
45
|
+
|
46
|
+
# Extract the first error message if available
|
47
|
+
error_message = message_info.first["Message"] rescue "Unknown error"
|
48
|
+
debug "Error: #{error_message}", 1, :red
|
49
|
+
|
50
|
+
raise Error, "Failed to export SCP: #{error_message}"
|
51
|
+
end
|
52
|
+
else
|
53
|
+
raise Error, "Failed to export SCP with status #{response.status}"
|
54
|
+
end
|
55
|
+
else
|
56
|
+
# Success path - extract location header
|
57
|
+
location = response.headers["location"]
|
58
|
+
|
59
|
+
if location.nil? || location.empty?
|
60
|
+
raise Error, "Empty location header in response: #{response.headers.inspect}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Progress reporting
|
65
|
+
minutes_elapsed = ((Time.now - started_at).to_f / 60).to_i
|
66
|
+
debug "Waiting for export to complete... #{minutes_elapsed} minutes", 1, :yellow
|
67
|
+
|
68
|
+
# Exponential backoff
|
69
|
+
sleep 2**[tries, 6].min # Cap at 64 seconds
|
70
|
+
|
71
|
+
if tries > 10
|
72
|
+
raise Error, "Failed exporting SCP after #{tries} tries, location: #{location}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Extract job ID from location
|
77
|
+
job_id = location.split("/").last
|
78
|
+
|
79
|
+
# Poll for job completion
|
80
|
+
job_complete = false
|
81
|
+
scp = nil
|
82
|
+
|
83
|
+
while !job_complete
|
84
|
+
job_response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks/#{job_id}")
|
85
|
+
|
86
|
+
if job_response.status == 200
|
87
|
+
job_data = JSON.parse(job_response.body)
|
88
|
+
|
89
|
+
if ["Running", "Pending", "New"].include?(job_data["TaskState"])
|
90
|
+
debug "Job status: #{job_data["TaskState"]}, waiting...", 2
|
91
|
+
sleep 3
|
92
|
+
else
|
93
|
+
job_complete = true
|
94
|
+
scp = job_data
|
95
|
+
|
96
|
+
# Verify we have the system configuration data
|
97
|
+
unless scp["SystemConfiguration"]
|
98
|
+
raise Error, "Failed exporting SCP, taskstate: #{scp["TaskState"]}, taskstatus: #{scp["TaskStatus"]}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
else
|
102
|
+
raise Error, "Failed to check job status: #{job_response.status} - #{job_response.body}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
return scp
|
107
|
+
end
|
108
|
+
|
109
|
+
# Set an attribute in a system configuration profile
|
110
|
+
def set_scp_attribute(scp, name, value)
|
111
|
+
# Make a deep copy to avoid modifying the original
|
112
|
+
scp_copy = JSON.parse(scp.to_json)
|
113
|
+
|
114
|
+
# Clear unrelated attributes for quicker transfer
|
115
|
+
scp_copy["SystemConfiguration"].delete("Comments")
|
116
|
+
scp_copy["SystemConfiguration"].delete("TimeStamp")
|
117
|
+
scp_copy["SystemConfiguration"].delete("ServiceTag")
|
118
|
+
scp_copy["SystemConfiguration"].delete("Model")
|
119
|
+
|
120
|
+
# Skip these attribute groups to make the transfer faster
|
121
|
+
excluded_prefixes = [
|
122
|
+
"User", "Telemetry", "SecurityCertificate", "AutoUpdate", "PCIe", "LDAP", "ADGroup", "ActiveDirectory",
|
123
|
+
"IPMILan", "EmailAlert", "SNMP", "IPBlocking", "IPMI", "Security", "RFS", "OS-BMC", "SupportAssist",
|
124
|
+
"Redfish", "RedfishEventing", "Autodiscovery", "SEKM-LKC", "Telco-EdgeServer", "8021XSecurity", "SPDM",
|
125
|
+
"InventoryHash", "RSASecurID2FA", "USB", "NIC", "IPv6", "NTP", "Logging", "IOIDOpt", "SSHCrypto",
|
126
|
+
"RemoteHosts", "SysLog", "Time", "SmartCard", "ACME", "ServiceModule", "Lockdown",
|
127
|
+
"DefaultCredentialMitigation", "AutoOSLockGroup", "LocalSecurity", "IntegratedDatacenter",
|
128
|
+
"SecureDefaultPassword.1#ForceChangePassword", "SwitchConnectionView.1#Enable", "GroupManager.1",
|
129
|
+
"ASRConfig.1#Enable", "SerialCapture.1#Enable", "CertificateManagement.1",
|
130
|
+
"Update", "SSH", "SysInfo", "GUI"
|
131
|
+
]
|
132
|
+
|
133
|
+
# Remove excluded attribute groups
|
134
|
+
if scp_copy["SystemConfiguration"]["Components"] &&
|
135
|
+
scp_copy["SystemConfiguration"]["Components"][0] &&
|
136
|
+
scp_copy["SystemConfiguration"]["Components"][0]["Attributes"]
|
137
|
+
|
138
|
+
attrs = scp_copy["SystemConfiguration"]["Components"][0]["Attributes"]
|
139
|
+
|
140
|
+
attrs.reject! do |attr|
|
141
|
+
excluded_prefixes.any? { |prefix| attr["Name"] =~ /\A#{prefix}/ }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Update or add the specified attribute
|
145
|
+
if attrs.find { |a| a["Name"] == name }.nil?
|
146
|
+
# Attribute doesn't exist, create it
|
147
|
+
attrs << { "Name" => name, "Value" => value, "Set On Import" => "True" }
|
148
|
+
else
|
149
|
+
# Update existing attribute
|
150
|
+
attrs.find { |a| a["Name"] == name }["Value"] = value
|
151
|
+
attrs.find { |a| a["Name"] == name }["Set On Import"] = "True"
|
152
|
+
end
|
153
|
+
|
154
|
+
scp_copy["SystemConfiguration"]["Components"][0]["Attributes"] = attrs
|
155
|
+
end
|
156
|
+
|
157
|
+
return scp_copy
|
158
|
+
end
|
159
|
+
|
160
|
+
# Helper method to normalize enabled/disabled values
|
161
|
+
def normalize_enabled_value(v)
|
162
|
+
return "Disabled" if v.nil? || v == false
|
163
|
+
return "Enabled" if v == true
|
164
|
+
|
165
|
+
raise Error, "Invalid value for normalize_enabled_value: #{v}" unless v.is_a?(String)
|
166
|
+
|
167
|
+
if v.strip.downcase == "enabled"
|
168
|
+
return "Enabled"
|
169
|
+
else
|
170
|
+
return "Disabled"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Apply a system configuration profile to the iDRAC
|
175
|
+
def set_system_configuration_profile(scp, target: "ALL", reboot: false, retry_count: 0)
|
176
|
+
# Ensure scp has the proper structure with SystemConfiguration wrapper
|
177
|
+
scp_to_apply = if scp.is_a?(Hash) && scp["SystemConfiguration"]
|
178
|
+
scp
|
179
|
+
else
|
180
|
+
# Ensure scp is an array of components
|
181
|
+
components = scp.is_a?(Array) ? scp : [scp]
|
182
|
+
{ "SystemConfiguration" => { "Components" => components } }
|
183
|
+
end
|
184
|
+
|
185
|
+
# Create the import parameters
|
186
|
+
params = {
|
187
|
+
"ImportBuffer" => JSON.pretty_generate(scp_to_apply),
|
188
|
+
"ShareParameters" => {"Target" => target},
|
189
|
+
"ShutdownType" => "Forced",
|
190
|
+
"HostPowerState" => reboot ? "On" : "Off"
|
191
|
+
}
|
192
|
+
|
193
|
+
debug "Importing System Configuration...", 1, :blue
|
194
|
+
debug "Configuration: #{JSON.pretty_generate(scp_to_apply)}", 3
|
195
|
+
|
196
|
+
# Make the API request
|
197
|
+
response = authenticated_request(
|
198
|
+
:post,
|
199
|
+
"/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfiguration",
|
200
|
+
body: params.to_json,
|
201
|
+
headers: {"Content-Type" => "application/json"}
|
202
|
+
)
|
203
|
+
|
204
|
+
# Check for immediate errors
|
205
|
+
if response.headers["content-length"].to_i > 0
|
206
|
+
debug response.inspect, 1, :red
|
207
|
+
return { status: :failed, error: "Failed importing SCP: #{response.body}" }
|
208
|
+
end
|
209
|
+
|
210
|
+
# Get the job location
|
211
|
+
job_location = response.headers["location"]
|
212
|
+
if job_location.nil? || job_location.empty?
|
213
|
+
debug response.inspect, 1, :blue
|
214
|
+
return { status: :failed, error: "Failed importing SCP... invalid iDRAC response" }
|
215
|
+
end
|
216
|
+
|
217
|
+
# Extract job ID and monitor the task
|
218
|
+
job_id = job_location.split("/").last
|
219
|
+
task = nil
|
220
|
+
|
221
|
+
begin
|
222
|
+
loop do
|
223
|
+
task_response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks/#{job_id}")
|
224
|
+
|
225
|
+
if task_response.status == 200
|
226
|
+
task = JSON.parse(task_response.body)
|
227
|
+
|
228
|
+
if task["TaskState"] != "Running"
|
229
|
+
break
|
230
|
+
end
|
231
|
+
|
232
|
+
debug "Waiting for task to complete...: #{task["TaskState"]} #{task["TaskStatus"]}", 1
|
233
|
+
sleep 5
|
234
|
+
else
|
235
|
+
return {
|
236
|
+
status: :failed,
|
237
|
+
error: "Failed to check task status: #{task_response.status} - #{task_response.body}"
|
238
|
+
}
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Check final task state
|
243
|
+
if task["TaskState"] == "Completed" && task["TaskStatus"] == "OK"
|
244
|
+
return { status: :success }
|
245
|
+
else
|
246
|
+
# For debugging purposes
|
247
|
+
debug task.inspect, 1, :yellow
|
248
|
+
|
249
|
+
# Extract any messages from the response
|
250
|
+
messages = []
|
251
|
+
if task["Messages"] && task["Messages"].is_a?(Array)
|
252
|
+
messages = task["Messages"].map { |m| m["Message"] }.compact
|
253
|
+
end
|
254
|
+
|
255
|
+
return {
|
256
|
+
status: :failed,
|
257
|
+
task_state: task["TaskState"],
|
258
|
+
task_status: task["TaskStatus"],
|
259
|
+
messages: messages,
|
260
|
+
error: messages.first || "Task failed with state: #{task["TaskState"]}"
|
261
|
+
}
|
262
|
+
end
|
263
|
+
rescue => e
|
264
|
+
return { status: :error, error: "Exception monitoring task: #{e.message}" }
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Helper method to create an SCP component with the specified FQDD and attributes
|
269
|
+
def make_scp(fqdd:, components: [], attributes: {})
|
270
|
+
com = []
|
271
|
+
att = []
|
272
|
+
|
273
|
+
# Process components
|
274
|
+
components.each do |component|
|
275
|
+
com << component
|
276
|
+
end
|
277
|
+
|
278
|
+
# Process attributes
|
279
|
+
attributes.each do |k, v|
|
280
|
+
if v.is_a?(Array)
|
281
|
+
v.each do |value|
|
282
|
+
att << { "Name" => k, "Value" => value, "Set On Import" => "True" }
|
283
|
+
end
|
284
|
+
elsif v.is_a?(Integer)
|
285
|
+
# Convert integers to strings
|
286
|
+
att << { "Name" => k, "Value" => v.to_s, "Set On Import" => "True" }
|
287
|
+
elsif v.is_a?(Hash)
|
288
|
+
# Handle nested components
|
289
|
+
v.each do |kk, vv|
|
290
|
+
com += make_scp(fqdd: kk, attributes: vv)
|
291
|
+
end
|
292
|
+
else
|
293
|
+
att << { "Name" => k, "Value" => v, "Set On Import" => "True" }
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Build the final component
|
298
|
+
bundle = { "FQDD" => fqdd }
|
299
|
+
bundle["Components"] = com if com.any?
|
300
|
+
bundle["Attributes"] = att if att.any?
|
301
|
+
|
302
|
+
return bundle
|
303
|
+
end
|
304
|
+
|
305
|
+
# Convert an SCP array to a hash for easier manipulation
|
306
|
+
def scp_to_hash(scp)
|
307
|
+
scp.inject({}) do |acc, component|
|
308
|
+
acc[component["FQDD"]] = component["Attributes"]
|
309
|
+
acc
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# Convert an SCP hash back to array format
|
314
|
+
def hash_to_scp(hash)
|
315
|
+
hash.inject([]) do |acc, (fqdd, attributes)|
|
316
|
+
acc << { "FQDD" => fqdd, "Attributes" => attributes }
|
317
|
+
acc
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Merge two SCPs together
|
322
|
+
def merge_scp(scp1, scp2)
|
323
|
+
return scp1 || scp2 unless scp1 && scp2 # Return the one that's not nil if either is nil
|
324
|
+
|
325
|
+
# Make them both arrays in case they aren't
|
326
|
+
scp1_array = scp1.is_a?(Array) ? scp1 : [scp1]
|
327
|
+
scp2_array = scp2.is_a?(Array) ? scp2 : [scp2]
|
328
|
+
|
329
|
+
# Convert to hashes for merging
|
330
|
+
hash1 = scp_to_hash(scp1_array)
|
331
|
+
hash2 = scp_to_hash(scp2_array)
|
332
|
+
|
333
|
+
# Perform deep merge
|
334
|
+
merged = deep_merge(hash1, hash2)
|
335
|
+
|
336
|
+
# Convert back to SCP array format
|
337
|
+
hash_to_scp(merged)
|
338
|
+
end
|
339
|
+
|
340
|
+
private
|
341
|
+
|
342
|
+
# Helper method for deep merging of hashes
|
343
|
+
def deep_merge(hash1, hash2)
|
344
|
+
result = hash1.dup
|
345
|
+
|
346
|
+
hash2.each do |key, value|
|
347
|
+
if result[key].is_a?(Array) && value.is_a?(Array)
|
348
|
+
# For arrays of attributes, merge by name
|
349
|
+
existing_names = result[key].map { |attr| attr["Name"] }
|
350
|
+
|
351
|
+
value.each do |attr|
|
352
|
+
if existing_index = existing_names.index(attr["Name"])
|
353
|
+
# Update existing attribute
|
354
|
+
result[key][existing_index] = attr
|
355
|
+
else
|
356
|
+
# Add new attribute
|
357
|
+
result[key] << attr
|
358
|
+
end
|
359
|
+
end
|
360
|
+
else
|
361
|
+
# For other values, just replace
|
362
|
+
result[key] = value
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
result
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
data/lib/idrac/version.rb
CHANGED
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.
|
4
|
+
version: 0.7.0
|
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-04-
|
11
|
+
date: 2025-04-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|
@@ -276,6 +276,7 @@ files:
|
|
276
276
|
- lib/idrac/session.rb
|
277
277
|
- lib/idrac/storage.rb
|
278
278
|
- lib/idrac/system.rb
|
279
|
+
- lib/idrac/system_config.rb
|
279
280
|
- lib/idrac/version.rb
|
280
281
|
- lib/idrac/virtual_media.rb
|
281
282
|
- lib/idrac/web.rb
|