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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e29d49fd29e12b9b169c281408e19002beb792c6ad957595190547b8d40ea04
4
- data.tar.gz: 0b52402b5396fd59801c2be34ce764d2e27e90049d6f14dbf501bbb02308339a
3
+ metadata.gz: 68c60caff773a2fd54e340a7ec1abbe02b8f9529d9f44527984923d22515ca9c
4
+ data.tar.gz: 62bf014eb0927473304cb2a4da06d09d1ca2aee67be4f4a6849d9295ef252347
5
5
  SHA512:
6
- metadata.gz: 1aba6ce8154a8a3d91eebf28a65ce2c7522207e37fd3777ad5b34dd7e8f61d198d9fa726cdd6f4f1b5d7176c4d2b9a6a274451df01c09a177e7024c7c6102f7f
7
- data.tar.gz: 502b02ee0ab18757af3dfe345011ef0311b2432c8d1c7895a6a774fdf8dd099ae2cadbde4c89f7b5939f36505ce9a6fcab7540586472d03e719224be97cffce9
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.is_dell
1109
+ if info["is_dell"]
1089
1110
  puts "Dell iDRAC System:".green.bold
1090
- puts " Service Tag: #{info.service_tag}".cyan
1091
- puts " Model: #{info.model}".cyan
1092
- puts " iDRAC Version: #{info.idrac_version}".cyan
1093
- puts " Firmware Version: #{info.firmware_version}".cyan
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.product}".cyan
1097
- if info.is_ancient_dell
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: pcie_function ? pcie_function["DeviceClass"] : nil,
261
- manufacturer: manufacturer,
262
- name: stub["Name"],
263
- description: stub["Description"],
264
- id: pcie_function ? pcie_function["Id"] : stub["Id"],
265
- slot_type: pcie_function ? pcie_function.dig("Oem", "Dell", "DellPCIeFunction", "SlotType") : nil,
266
- bus_width: pcie_function ? pcie_function.dig("Oem", "Dell", "DellPCIeFunction", "DataBusWidth") : nil,
267
- nic: pcie_function ? pcie_function.dig("Links", "EthernetInterfaces", 0, "@odata.id") : nil
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[:name]} > #{device_info[:manufacturer]} > #{device_info[:device_class]} > #{device_info[:description]} > #{device_info[:id]}"
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
- raise Error, "Failed to get PCI devices. Status code: #{response.status}"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IDRAC
4
- VERSION = "0.6.1"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/idrac.rb CHANGED
@@ -53,3 +53,4 @@ require 'idrac/virtual_media'
53
53
  require 'idrac/boot'
54
54
  require 'idrac/license'
55
55
  require 'idrac/client'
56
+ require 'idrac/system_config'
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.6.1
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-24 00:00:00.000000000 Z
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