idrac 0.10.3 → 0.10.5

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: 67737e81c936f047dc3264470e8cf3d913e10d0b300af332f4bd16aca03f4078
4
- data.tar.gz: 62467838651fb0d17ab870948d1b3a82e02f5de31297a2e6d7d7ed260dea044a
3
+ metadata.gz: 9c4504dd010f1458b9705e73489d4d68ec18214ef13a5a5187dde3ed52812d1a
4
+ data.tar.gz: eb9ab621e77b1ad17ade9579b4286a7696f7f0bb9dc0761e8862446ee8f39963
5
5
  SHA512:
6
- metadata.gz: 4164655c54761c104a7b43f5f489972d74238c7f086b3bd865988e875eb756edaa751b6bd3241b0655c154fae57f575849d09796bbc9c0feca015192def2cbe3
7
- data.tar.gz: 5aff6a5a4cbd3362e6dab402f493759ae7b0db72f75adc40c20a9cc84da2e1edea8990d27cd3b0dd4f10ee21ed0775404d101a2dd09752a8a55883dcc2312303
6
+ metadata.gz: af0824a2ceb96d5c89a2552277d24bdf754eb748a7644d756deb3864620130b005d3f5cefa1f0921d6617fd03aab79c31fd8d9de8b0716a2e3124a8916f1bde4
7
+ data.tar.gz: 3c85876b2e93b31bed8f8dd4a8ea2ab4ae851048a2767c18e319151b74748c797502971c0932b9e534150c95ce66c3bb9ff93e5194a0d6e37108714724363396
data/lib/idrac/boot.rb CHANGED
@@ -323,22 +323,7 @@ module IDRAC
323
323
  raise Error, "Failed to get BIOS information. Status code: #{response.status}"
324
324
  end
325
325
  end
326
- =begin
327
- # Servers can boot in BIOS mode or in UEFI (modern, extensible BIOS replacement) mode.
328
- # We use UEFI mode.
329
- # self.get(path: "Systems/System.Embedded.1/Bios/Settings?$select=BootMode")
330
- res = self.get(path: "Systems/System.Embedded.1/Bios")
331
- if res["body"]["Attributes"]["BootMode"] == "Uefi"
332
- return { status: :success }
333
- else
334
- res = self.set_system_configuration_profile(scp_boot_mode_uefi, reboot: true)
335
- # Then must power cycle the server
336
- self.power_on!(wait: true)
337
- self.power_off!(wait: true)
338
- return res
339
- end
340
326
 
341
- =end
342
327
  def scp_boot_mode_uefi(idrac_license_version: 9)
343
328
  opts = { "BootMode" => 'Uefi' }
344
329
  # If we're iDRAC 9, we need enable a placeholder, otherwise we can't order the
@@ -90,12 +90,29 @@ module IDRAC
90
90
  # Id ("Installed-<componentID>-<version>"). Used to match DUPs by
91
91
  # componentID rather than by ambiguous display name.
92
92
  fw_id = component_data['Id'].to_s
93
- component_id = component_data.dig('Oem', 'Dell', 'DellSoftwareInventory', 'ComponentID')
93
+ oem = component_data.dig('Oem', 'Dell', 'DellSoftwareInventory') || {}
94
+ component_id = oem['ComponentID']
94
95
  component_id ||= fw_id[/\A(?:Installed|Current|Previous|Available)-(\d+)-/, 1]
96
+ # The same Oem block carries the device's PCI IDs, and it carries
97
+ # them exactly where the componentID is missing. The iDRAC reports
98
+ # componentID "0" for firmware it did not install itself, so
99
+ # factory-flashed hardware is unmatchable by componentID: on an
100
+ # R6525, 13 of 23 entries report "0" (both LOMs, both ConnectX-6,
101
+ # the PERC, all 8 NVMe drives) and each publishes all four PCI
102
+ # IDs, while the 10 with a real componentID publish none. An
103
+ # entry gains its componentID once a DUP is applied — a LOM went
104
+ # from "Installed-0-22.91.5" to "Installed-108255-23.61.3" — so
105
+ # the unmatchable set is first-touch hardware, not an edge case.
106
+ # No PCIeDevices/PCIeFunctions lookup is needed to identify what
107
+ # the componentID cannot; see #updates_for_component.
95
108
  firmware_inventory << {
96
109
  name: component_data['Name'],
97
110
  id: component_data['Id'],
98
111
  component_id: component_id,
112
+ vendor_id: oem['VendorID'],
113
+ device_id: oem['DeviceID'],
114
+ sub_vendor_id: oem['SubVendorID'],
115
+ sub_device_id: oem['SubDeviceID'],
99
116
  version: component_data['Version'],
100
117
  updateable: component_data['Updateable'] || false,
101
118
  status: component_data['Status'] ? component_data['Status']['State'] : 'Unknown'
@@ -172,6 +172,16 @@ module IDRAC
172
172
  # picks wrong DUPs and iDRAC rejects them (RED097 / "not compatible").
173
173
  component_ids = component.xpath(".//SupportedDevices/Device/@componentID").map(&:value).uniq
174
174
 
175
+ # The PCI IDs this DUP declares it can flash. The PCIe/storage half of
176
+ # the iDRAC inventory reports componentID "0" (measured on an R6525:
177
+ # every NIC, the PERC and all 8 NVMe drives), so componentID alone
178
+ # cannot match them and the old name heuristic guessed — it offered a
179
+ # Broadcom LOM an Intel package. PCI IDs are the identity both sides
180
+ # publish, so a cross-vendor match becomes impossible.
181
+ pci_ids = component.xpath(".//SupportedDevices/Device/PCIInfo").map { |p|
182
+ pci_key(p["vendorID"], p["deviceID"], p["subVendorID"], p["subDeviceID"])
183
+ }.compact.uniq
184
+
175
185
  # Skip if missing essential information
176
186
  next if name.empty? || path.empty? || version.empty?
177
187
 
@@ -191,6 +201,7 @@ module IDRAC
191
201
  component_type: component_type,
192
202
  category: category,
193
203
  component_ids: component_ids,
204
+ pci_ids: pci_ids,
194
205
  download_url: "https://downloads.dell.com/#{path}"
195
206
  }
196
207
  end
@@ -200,24 +211,66 @@ module IDRAC
200
211
  updates
201
212
  end
202
213
 
214
+ # Normalize one PCI ID quad to a comparable key. Dell publishes the same
215
+ # four IDs on both sides but spells them differently: the catalog writes
216
+ # bare uppercase hex (<PCIInfo vendorID="14E4" deviceID="165F"/>) while
217
+ # Redfish writes "0x790e". Returns nil unless all four are present, so a
218
+ # caller can tell "no PCI identity" from "PCI identity that matched
219
+ # nothing".
220
+ def pci_key(vendor_id, device_id, sub_vendor_id, sub_device_id)
221
+ parts = [vendor_id, device_id, sub_vendor_id, sub_device_id].map do |id|
222
+ id.to_s.strip.sub(/\A0x/i, "").upcase
223
+ end
224
+ return nil if parts.any?(&:empty?)
225
+
226
+ parts.map { |p| p.rjust(4, "0") }.join("/")
227
+ end
228
+
203
229
  # Select the catalog DUPs that apply to one installed component (`fw` from
204
- # the iDRAC inventory). Matches on Dell componentID — the reliable key
205
- # and only falls back to the legacy name heuristic when the inventory
206
- # didn't expose a componentID (e.g. it reports "0").
230
+ # the iDRAC inventory), most specific key first.
231
+ #
232
+ # 1. Dell componentID, when the inventory gives a real one. Exact and
233
+ # unambiguous; unchanged.
234
+ # 2. PCI IDs, for the entries that report componentID "0" — on an R6525
235
+ # that is every NIC, the PERC and every drive. The full quad first, then
236
+ # vendor+device if the catalog lists no package for that exact subsystem
237
+ # ID. Both tiers pin the vendor, which is the whole point: name matching
238
+ # reduced any NIC to the token "NIC" and any RAID device to "PERC", then
239
+ # substring-matched the DUP's package name, so a "Broadcom NetXtreme
240
+ # Gigabit Ethernet" LOM was offered "Intel NIC Family Version 25.0.0
241
+ # Firmware for Intel I350 and X550 Adapters" — wrong vendor — and a
242
+ # "PERC H755N Front" was offered the H355 package. It was also unstable:
243
+ # the same tool offered two identical PERCs different packages, because
244
+ # the winner depended on catalog order.
245
+ # 3. The name heuristic, only when the inventory gave neither key. When PCI
246
+ # IDs are present they are the answer: if nothing matches them, the
247
+ # catalog has no package for this device, and guessing by name is how
248
+ # the wrong-vendor DUPs got offered in the first place.
207
249
  def updates_for_component(catalog_updates, fw)
208
250
  component_id = fw[:component_id]
209
251
  if component_id && component_id != "0"
210
- catalog_updates.select { |u| Array(u[:component_ids]).include?(component_id) }
211
- else
212
- name = fw[:name] || ""
213
- ids = extract_identifiers(name)
214
- catalog_updates.select do |u|
215
- un = u[:name] || ""
216
- ids.any? { |id| un.downcase.include?(id.downcase) } ||
217
- un.downcase.include?(name.downcase) ||
218
- name.downcase.include?(un.downcase)
252
+ return catalog_updates.select { |u| Array(u[:component_ids]).include?(component_id) }
253
+ end
254
+
255
+ key = pci_key(fw[:vendor_id], fw[:device_id], fw[:sub_vendor_id], fw[:sub_device_id])
256
+ if key
257
+ exact = catalog_updates.select { |u| Array(u[:pci_ids]).include?(key) }
258
+ return exact if exact.any?
259
+
260
+ chip = key.split("/").first(2).join("/")
261
+ return catalog_updates.select do |u|
262
+ Array(u[:pci_ids]).any? { |k| k.start_with?("#{chip}/") }
219
263
  end
220
264
  end
265
+
266
+ name = fw[:name] || ""
267
+ ids = extract_identifiers(name)
268
+ catalog_updates.select do |u|
269
+ un = u[:name] || ""
270
+ ids.any? { |id| un.downcase.include?(id.downcase) } ||
271
+ un.downcase.include?(name.downcase) ||
272
+ name.downcase.include?(un.downcase)
273
+ end
221
274
  end
222
275
 
223
276
  # Sortable key for a Dell version string (e.g. "25.5.9.0001", "22.00.6").
data/lib/idrac/power.rb CHANGED
@@ -58,7 +58,15 @@ module IDRAC
58
58
  return true
59
59
  end
60
60
 
61
+ # Reset types that restart the host rather than leave it down. A restart never settles on
62
+ # "Off", so it must never be waited on — or escalated to — as if it were a shutdown.
63
+ RESTART_TYPES = %w[ForceRestart GracefulRestart PowerCycle].freeze
64
+
61
65
  def power_off(wait: true, kind: "ForceOff")
66
+ # A restart is not a shutdown. Sent down the power-off path it fails the wait for "Off" and
67
+ # escalates to ForceOff, which leaves the host powered down. Route it to the restart instead.
68
+ return reboot(kind: kind, wait: wait) if RESTART_TYPES.include?(kind)
69
+
62
70
  ensure_authenticated!
63
71
 
64
72
  puts "Powering off server...".light_cyan
@@ -93,37 +101,39 @@ module IDRAC
93
101
  return true
94
102
  end
95
103
 
96
- def reboot
104
+ def reboot(kind: "ForceRestart", wait: false)
97
105
  ensure_authenticated!
98
106
 
99
- puts "Rebooting server...".light_cyan
107
+ puts "Rebooting server (#{kind})...".light_cyan
100
108
 
101
109
  # Check current power state first
102
110
  current_state = get_power_state rescue "Unknown"
103
111
  if current_state == "Off"
104
112
  puts "Server is currently off, powering on instead of rebooting".yellow
105
- return power_on
113
+ return power_on(wait: wait)
106
114
  end
107
115
 
108
- # Send reboot command (Reset with ResetType=ForceRestart)
109
116
  path = "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset"
110
- payload = { "ResetType" => "ForceRestart" }
111
-
112
- response = authenticated_request(:post, path, body: payload.to_json)
117
+ response = authenticated_request(:post, path, body: { "ResetType" => kind }.to_json)
113
118
 
114
119
  if response.status >= 200 && response.status < 300
115
120
  puts "Server reboot command sent successfully".green
116
- return true
117
- elsif response.status == 409
121
+ elsif response.status == 409 && kind != "GracefulRestart"
118
122
  error_data = JSON.parse(response.body) rescue nil
119
123
  puts "Received conflict (409) error from iDRAC: #{error_data.inspect}"
120
124
  # Try gracefulRestart as an alternative
121
125
  puts "Trying GracefulRestart instead...".yellow
122
- payload = { "ResetType" => "GracefulRestart" }
123
- authenticated_request(:post, path, body: payload.to_json)
126
+ response = authenticated_request(:post, path, body: { "ResetType" => "GracefulRestart" }.to_json)
127
+ raise Error, "Failed to reboot server. Status code: #{response.status}" unless response.status.between?(200, 299)
124
128
  else
125
129
  raise Error, "Failed to reboot server. Status code: #{response.status}"
126
130
  end
131
+
132
+ # "On" is the only terminal state a restart reaches. Never wait for "Off" here: the host may
133
+ # never report it, and treating that as failure is what turned a reboot into a shutdown.
134
+ wait_for_power_state(target_state: "On", tries: 12) if wait
135
+
136
+ return true
127
137
  end
128
138
 
129
139
  def get_power_state
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.10.3"
4
+ VERSION = "0.10.5"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idrac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.3
4
+ version: 0.10.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Siegel