idrac 0.1.28 → 0.1.29
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 +65 -38
- data/lib/idrac/firmware.rb +253 -44
- data/lib/idrac/firmware_catalog.rb +99 -28
- data/lib/idrac/version.rb +1 -1
- data/test_firmware_update.rb +70 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c080d98bd3ca44b9cc006ba66cc443ec410dcfb6bc1b712141720cc82998884
|
4
|
+
data.tar.gz: fe0e09d1e3089c5d354abf6f786624d5afd7dfbcd7b2984d84a72be9936dcf9c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 479b4ceb21686d4803a3f780f8fe19fcd1741f17cf3b1e24cd0039bd7758688810260dd939fe60cfe07b3b829feefcfbeee4953826f315b65707d2467121fa6c
|
7
|
+
data.tar.gz: 1855a7715e1267c505cc20f9dfd9a9e0665b78ac4db5b06a6984503bb88fb849e7f41b9ebeb68b6e67a70d0b663ea3a6f9b7bbecd1e4c5e194aff198ca9b5238
|
data/bin/idrac
CHANGED
@@ -12,25 +12,9 @@ require "thor"
|
|
12
12
|
require "idrac"
|
13
13
|
|
14
14
|
module IDRAC
|
15
|
-
# Standalone catalog command that doesn't require a host
|
16
|
-
class CatalogCommand < Thor
|
17
|
-
desc "download [DIRECTORY]", "Download Dell firmware catalog"
|
18
|
-
def download(directory = nil)
|
19
|
-
# Create a FirmwareCatalog instance
|
20
|
-
catalog = IDRAC::FirmwareCatalog.new
|
21
|
-
|
22
|
-
begin
|
23
|
-
catalog_path = catalog.download(directory)
|
24
|
-
puts "Catalog downloaded to: #{catalog_path}"
|
25
|
-
rescue IDRAC::Error => e
|
26
|
-
puts "Error: #{e.message}"
|
27
|
-
exit 1
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
15
|
class CLI < Thor
|
33
|
-
|
16
|
+
# Make host not required for all commands
|
17
|
+
class_option :host, type: :string, required: false, desc: "iDRAC host address"
|
34
18
|
class_option :username, type: :string, required: false, default: "root", desc: "iDRAC username (default: root)"
|
35
19
|
class_option :password, type: :string, required: false, default: "calvin", desc: "iDRAC password (default: calvin)"
|
36
20
|
class_option :port, type: :numeric, default: 443, desc: "iDRAC port"
|
@@ -42,6 +26,9 @@ module IDRAC
|
|
42
26
|
method_option :wait, type: :boolean, default: true, desc: "Wait for the update to complete"
|
43
27
|
method_option :timeout, type: :numeric, default: 3600, desc: "Timeout in seconds when waiting"
|
44
28
|
def firmware_update(path)
|
29
|
+
# Ensure host is provided for commands that need it
|
30
|
+
ensure_host_provided
|
31
|
+
|
45
32
|
check_ssl_verification
|
46
33
|
client = create_client
|
47
34
|
firmware = IDRAC::Firmware.new(client)
|
@@ -59,13 +46,25 @@ module IDRAC
|
|
59
46
|
|
60
47
|
desc "firmware:catalog [DIRECTORY]", "Download Dell firmware catalog"
|
61
48
|
def firmware_catalog(directory = nil)
|
62
|
-
#
|
63
|
-
|
49
|
+
# This command doesn't require a host
|
50
|
+
# Create a FirmwareCatalog instance directly
|
51
|
+
catalog = IDRAC::FirmwareCatalog.new
|
52
|
+
|
53
|
+
begin
|
54
|
+
catalog_path = catalog.download(directory)
|
55
|
+
puts "Catalog downloaded to: #{catalog_path}"
|
56
|
+
rescue IDRAC::Error => e
|
57
|
+
puts "Error: #{e.message}"
|
58
|
+
exit 1
|
59
|
+
end
|
64
60
|
end
|
65
61
|
|
66
62
|
desc "firmware:status", "Show current firmware status and available updates"
|
67
63
|
method_option :catalog, type: :string, desc: "Path to existing catalog file"
|
68
64
|
def firmware_status
|
65
|
+
# Ensure host is provided for commands that need it
|
66
|
+
ensure_host_provided
|
67
|
+
|
69
68
|
check_ssl_verification
|
70
69
|
client = create_client
|
71
70
|
firmware = IDRAC::Firmware.new(client)
|
@@ -99,7 +98,7 @@ module IDRAC
|
|
99
98
|
puts "No updates available."
|
100
99
|
end
|
101
100
|
else
|
102
|
-
puts "\nTo check for updates, download the catalog first with 'idrac catalog
|
101
|
+
puts "\nTo check for updates, download the catalog first with 'idrac firmware:catalog'"
|
103
102
|
end
|
104
103
|
rescue IDRAC::Error => e
|
105
104
|
puts "Error: #{e.message}"
|
@@ -112,6 +111,9 @@ module IDRAC
|
|
112
111
|
desc "firmware:interactive", "Interactive firmware update"
|
113
112
|
method_option :catalog, type: :string, desc: "Path to existing catalog file"
|
114
113
|
def firmware_interactive
|
114
|
+
# Ensure host is provided for commands that need it
|
115
|
+
ensure_host_provided
|
116
|
+
|
115
117
|
check_ssl_verification
|
116
118
|
client = create_client
|
117
119
|
firmware = IDRAC::Firmware.new(client)
|
@@ -134,8 +136,36 @@ module IDRAC
|
|
134
136
|
catalog_path = catalog.download
|
135
137
|
end
|
136
138
|
|
137
|
-
firmware.
|
138
|
-
|
139
|
+
puts "Starting interactive firmware update. Please note:"
|
140
|
+
puts "- The iDRAC can only process one firmware update at a time"
|
141
|
+
puts "- Updates may take several minutes to complete"
|
142
|
+
puts "- For BIOS updates, a server reboot will be required to apply the update"
|
143
|
+
puts "- If you encounter errors, check the iDRAC web interface for active jobs"
|
144
|
+
puts ""
|
145
|
+
|
146
|
+
begin
|
147
|
+
firmware.interactive_update(catalog_path)
|
148
|
+
rescue ArgumentError => e
|
149
|
+
puts "Error: #{e.message}"
|
150
|
+
puts "This could be due to an issue with the interactive update process."
|
151
|
+
puts "If you're seeing a 'job ID not found' error, it might be because:"
|
152
|
+
puts "1. The firmware update job wasn't created properly"
|
153
|
+
puts "2. There's already an update in progress"
|
154
|
+
puts "3. The iDRAC needs time to process the previous request"
|
155
|
+
puts "\nTry again in a few minutes or check the iDRAC web interface for active jobs."
|
156
|
+
rescue IDRAC::Error => e
|
157
|
+
if e.message.include?("already in progress")
|
158
|
+
puts "Error: #{e.message}"
|
159
|
+
puts "\nTroubleshooting steps:"
|
160
|
+
puts "1. Check the iDRAC web interface under Maintenance > System Update for active jobs"
|
161
|
+
puts "2. Wait for any existing updates to complete (can take 15-30 minutes)"
|
162
|
+
puts "3. If no updates appear to be in progress, you may need to restart the iDRAC"
|
163
|
+
puts " (iDRAC web interface > Settings > iDRAC Settings > Reset iDRAC)"
|
164
|
+
else
|
165
|
+
puts "Error: #{e.message}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
rescue => e
|
139
169
|
puts "Error: #{e.message}"
|
140
170
|
exit 1
|
141
171
|
ensure
|
@@ -146,6 +176,9 @@ module IDRAC
|
|
146
176
|
desc "screenshot", "Take a screenshot of the current iDRAC console"
|
147
177
|
method_option :output, type: :string, desc: "Output filename (default: idrac_screenshot_timestamp.png)"
|
148
178
|
def screenshot
|
179
|
+
# Ensure host is provided for commands that need it
|
180
|
+
ensure_host_provided
|
181
|
+
|
149
182
|
check_ssl_verification
|
150
183
|
client = create_client
|
151
184
|
|
@@ -177,6 +210,13 @@ module IDRAC
|
|
177
210
|
|
178
211
|
private
|
179
212
|
|
213
|
+
def ensure_host_provided
|
214
|
+
unless options[:host]
|
215
|
+
puts "Error: No value provided for required option '--host'"
|
216
|
+
exit 1
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
180
220
|
def check_ssl_verification
|
181
221
|
# If verify_ssl is not explicitly set in the command line, show a warning
|
182
222
|
unless ARGV.include?('--verify-ssl') || ARGV.include?('--no-verify-ssl')
|
@@ -200,18 +240,5 @@ module IDRAC
|
|
200
240
|
end
|
201
241
|
end
|
202
242
|
|
203
|
-
#
|
204
|
-
|
205
|
-
class StandaloneCLI < Thor
|
206
|
-
# Register the catalog command
|
207
|
-
desc "catalog", "Download Dell firmware catalog"
|
208
|
-
subcommand "catalog", CatalogCommand
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
# Check if the first argument is 'catalog'
|
213
|
-
if ARGV[0] == 'catalog'
|
214
|
-
IDRAC::StandaloneCLI.start(ARGV)
|
215
|
-
else
|
216
|
-
IDRAC::CLI.start(ARGV)
|
217
|
-
end
|
243
|
+
# Start the CLI
|
244
|
+
IDRAC::CLI.start(ARGV)
|
data/lib/idrac/firmware.rb
CHANGED
@@ -5,6 +5,7 @@ require 'json'
|
|
5
5
|
require 'nokogiri'
|
6
6
|
require 'fileutils'
|
7
7
|
require 'securerandom'
|
8
|
+
require 'set'
|
8
9
|
require_relative 'firmware_catalog'
|
9
10
|
|
10
11
|
module IDRAC
|
@@ -129,7 +130,7 @@ module IDRAC
|
|
129
130
|
puts "Searching for updates for model: #{system_model}"
|
130
131
|
|
131
132
|
# Find system models in the catalog
|
132
|
-
models = catalog.find_system_models(system_model
|
133
|
+
models = catalog.find_system_models(system_model)
|
133
134
|
|
134
135
|
if models.empty?
|
135
136
|
puts "No matching system model found in catalog"
|
@@ -150,14 +151,34 @@ module IDRAC
|
|
150
151
|
# Print header for firmware comparison table
|
151
152
|
puts "\nFirmware Version Comparison:"
|
152
153
|
puts "=" * 100
|
153
|
-
puts "%-30s %-20s %-20s %-10s %-15s
|
154
|
+
puts "%-30s %-20s %-20s %-10s %-15s %s" % ["Component", "Current Version", "Available Version", "Updateable", "Category", "Status"]
|
154
155
|
puts "-" * 100
|
155
156
|
|
156
|
-
#
|
157
|
+
# Track components we've already displayed to avoid duplicates
|
158
|
+
displayed_components = Set.new
|
159
|
+
|
160
|
+
# First show current firmware with available updates
|
157
161
|
inventory[:firmware].each do |fw|
|
158
|
-
#
|
162
|
+
# Make sure firmware name is not nil
|
163
|
+
firmware_name = fw[:name] || ""
|
164
|
+
|
165
|
+
# Skip if we've already displayed this component
|
166
|
+
next if displayed_components.include?(firmware_name.downcase)
|
167
|
+
displayed_components.add(firmware_name.downcase)
|
168
|
+
|
169
|
+
# Extract key identifiers from the firmware name
|
170
|
+
identifiers = extract_identifiers(firmware_name)
|
171
|
+
|
172
|
+
# Try to find a matching update
|
159
173
|
matching_updates = catalog_updates.select do |update|
|
160
|
-
|
174
|
+
update_name = update[:name] || ""
|
175
|
+
|
176
|
+
# Check if any of our identifiers match the update name
|
177
|
+
identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
|
178
|
+
# Or if the update name contains the firmware name
|
179
|
+
update_name.downcase.include?(firmware_name.downcase) ||
|
180
|
+
# Or if the firmware name contains the update name
|
181
|
+
firmware_name.downcase.include?(update_name.downcase)
|
161
182
|
end
|
162
183
|
|
163
184
|
if matching_updates.any?
|
@@ -180,8 +201,8 @@ module IDRAC
|
|
180
201
|
}
|
181
202
|
|
182
203
|
# Print row with update available
|
183
|
-
puts "%-30s %-20s %-20s %-10s %-15s
|
184
|
-
fw[:name][0..29],
|
204
|
+
puts "%-30s %-20s %-20s %-10s %-15s %s" % [
|
205
|
+
fw[:name].to_s[0..29],
|
185
206
|
fw[:version],
|
186
207
|
update[:version],
|
187
208
|
fw[:updateable] ? "Yes" : "No",
|
@@ -189,20 +210,28 @@ module IDRAC
|
|
189
210
|
"UPDATE AVAILABLE"
|
190
211
|
]
|
191
212
|
else
|
192
|
-
# Print row with no update
|
193
|
-
|
194
|
-
|
213
|
+
# Print row with no update needed
|
214
|
+
status = if !needs_update
|
215
|
+
"Current"
|
216
|
+
elsif !fw[:updateable]
|
217
|
+
"Not updateable"
|
218
|
+
else
|
219
|
+
"No update needed"
|
220
|
+
end
|
221
|
+
|
222
|
+
puts "%-30s %-20s %-20s %-10s %-15s %s" % [
|
223
|
+
fw[:name].to_s[0..29],
|
195
224
|
fw[:version],
|
196
225
|
update[:version] || "N/A",
|
197
226
|
fw[:updateable] ? "Yes" : "No",
|
198
227
|
update[:category] || "N/A",
|
199
|
-
|
228
|
+
status
|
200
229
|
]
|
201
230
|
end
|
202
231
|
else
|
203
232
|
# No matching update found
|
204
|
-
puts "%-30s %-20s %-20s %-10s %-15s
|
205
|
-
fw[:name][0..29],
|
233
|
+
puts "%-30s %-20s %-20s %-10s %-15s %s" % [
|
234
|
+
fw[:name].to_s[0..29],
|
206
235
|
fw[:version],
|
207
236
|
"N/A",
|
208
237
|
fw[:updateable] ? "Yes" : "No",
|
@@ -212,11 +241,41 @@ module IDRAC
|
|
212
241
|
end
|
213
242
|
end
|
214
243
|
|
244
|
+
# Then show available updates that don't match any current firmware
|
245
|
+
catalog_updates.each do |update|
|
246
|
+
update_name = update[:name] || ""
|
247
|
+
|
248
|
+
# Skip if we've already displayed this component
|
249
|
+
next if displayed_components.include?(update_name.downcase)
|
250
|
+
displayed_components.add(update_name.downcase)
|
251
|
+
|
252
|
+
# Skip if this update was already matched to a current firmware
|
253
|
+
next if inventory[:firmware].any? do |fw|
|
254
|
+
firmware_name = fw[:name] || ""
|
255
|
+
identifiers = extract_identifiers(firmware_name)
|
256
|
+
|
257
|
+
identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
|
258
|
+
update_name.downcase.include?(firmware_name.downcase) ||
|
259
|
+
firmware_name.downcase.include?(update_name.downcase)
|
260
|
+
end
|
261
|
+
|
262
|
+
puts "%-30s %-20s %-20s %-10s %-15s %s" % [
|
263
|
+
update_name.to_s[0..29],
|
264
|
+
"Not Installed",
|
265
|
+
update[:version] || "Unknown",
|
266
|
+
"N/A",
|
267
|
+
update[:category] || "N/A",
|
268
|
+
"NEW COMPONENT"
|
269
|
+
]
|
270
|
+
end
|
271
|
+
|
272
|
+
puts "=" * 100
|
273
|
+
|
215
274
|
updates
|
216
275
|
end
|
217
276
|
|
218
|
-
def interactive_update(catalog_path = nil)
|
219
|
-
updates = check_updates(catalog_path)
|
277
|
+
def interactive_update(catalog_path = nil, selected_updates = nil)
|
278
|
+
updates = selected_updates || check_updates(catalog_path)
|
220
279
|
|
221
280
|
if updates.empty?
|
222
281
|
puts "No updates available for your system."
|
@@ -228,24 +287,30 @@ module IDRAC
|
|
228
287
|
puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}"
|
229
288
|
end
|
230
289
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
290
|
+
# If selected_updates is provided, use those directly
|
291
|
+
if selected_updates
|
292
|
+
selected = selected_updates
|
293
|
+
else
|
294
|
+
# Otherwise prompt the user for selection
|
295
|
+
puts "\nEnter the number of the update to install (or 'all' for all updates, 'q' to quit):"
|
296
|
+
choice = STDIN.gets.chomp
|
297
|
+
|
298
|
+
return if choice.downcase == 'q'
|
299
|
+
|
300
|
+
selected = if choice.downcase == 'all'
|
301
|
+
updates
|
302
|
+
else
|
303
|
+
index = choice.to_i - 1
|
304
|
+
if index >= 0 && index < updates.size
|
305
|
+
[updates[index]]
|
306
|
+
else
|
307
|
+
puts "Invalid selection."
|
308
|
+
return
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
247
312
|
|
248
|
-
|
313
|
+
selected.each do |update|
|
249
314
|
puts "Downloading #{update[:name]} version #{update[:available_version]}..."
|
250
315
|
|
251
316
|
# Create temp directory
|
@@ -274,8 +339,28 @@ module IDRAC
|
|
274
339
|
end
|
275
340
|
|
276
341
|
puts "Installing #{update[:name]} version #{update[:available_version]}..."
|
277
|
-
|
278
|
-
|
342
|
+
|
343
|
+
begin
|
344
|
+
job_id = update(update_path, wait: true)
|
345
|
+
puts "Update completed with job ID: #{job_id}"
|
346
|
+
rescue IDRAC::Error => e
|
347
|
+
if e.message.include?("already in progress")
|
348
|
+
puts "Error: #{e.message}"
|
349
|
+
puts "\nTips for resolving this issue:"
|
350
|
+
puts "1. Wait for any existing firmware updates to complete (check iDRAC web interface)"
|
351
|
+
puts "2. Restart the iDRAC if no updates appear to be in progress (Settings > iDRAC Settings > Reset iDRAC)"
|
352
|
+
puts "3. Try again after a few minutes"
|
353
|
+
elsif e.message.include?("job ID") || e.message.include?("job not found")
|
354
|
+
puts "Error: #{e.message}"
|
355
|
+
puts "\nThe job ID could not be found or monitored. This could be because:"
|
356
|
+
puts "1. The iDRAC is busy processing other requests"
|
357
|
+
puts "2. The job was created but not properly tracked"
|
358
|
+
puts "3. The iDRAC firmware may need to be updated first"
|
359
|
+
puts "\nCheck the iDRAC web interface to see if the update was actually initiated."
|
360
|
+
else
|
361
|
+
puts "Error during firmware update: #{e.message}"
|
362
|
+
end
|
363
|
+
end
|
279
364
|
|
280
365
|
ensure
|
281
366
|
# Clean up temp directory
|
@@ -323,28 +408,103 @@ module IDRAC
|
|
323
408
|
post_body << "\r\n--#{boundary}--\r\n"
|
324
409
|
|
325
410
|
# Upload the firmware
|
326
|
-
|
411
|
+
begin
|
412
|
+
response = client.authenticated_request(
|
413
|
+
:post,
|
414
|
+
http_push_uri,
|
415
|
+
{
|
416
|
+
headers: {
|
417
|
+
'Content-Type' => "multipart/form-data; boundary=#{boundary}",
|
418
|
+
'If-Match' => etag
|
419
|
+
},
|
420
|
+
body: post_body.join
|
421
|
+
}
|
422
|
+
)
|
423
|
+
rescue => e
|
424
|
+
# Check if the error is about a deployment already in progress
|
425
|
+
if e.message.include?("A deployment or update operation is already in progress")
|
426
|
+
raise Error, "A firmware update is already in progress on the iDRAC. Please wait for it to complete before starting another update. You can check the status in the iDRAC web interface under Maintenance > System Update."
|
427
|
+
else
|
428
|
+
# Re-raise the original error
|
429
|
+
raise
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
if response.status < 200 || response.status >= 300
|
434
|
+
error_message = response.body.to_s
|
435
|
+
|
436
|
+
# Check for specific error messages in the response
|
437
|
+
if error_message.include?("A deployment or update operation is already in progress")
|
438
|
+
raise Error, "A firmware update is already in progress on the iDRAC. Please wait for it to complete before starting another update. You can check the status in the iDRAC web interface under Maintenance > System Update."
|
439
|
+
elsif error_message.include?("SUP0108")
|
440
|
+
raise Error, "iDRAC Error SUP0108: A deployment or update operation is already in progress. Wait for the operation to conclude and then try again."
|
441
|
+
else
|
442
|
+
raise Error, "Firmware upload failed with status #{response.status}: #{response.body}"
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
# Extract upload ID from response
|
447
|
+
response_data = JSON.parse(response.body)
|
448
|
+
upload_id = response_data['Id'] || response_data['TaskId']
|
449
|
+
|
450
|
+
if upload_id.nil?
|
451
|
+
raise Error, "Failed to extract upload ID from firmware upload response"
|
452
|
+
end
|
453
|
+
|
454
|
+
puts "Firmware file uploaded successfully with ID: #{upload_id}"
|
455
|
+
|
456
|
+
# Step 2: Initiate the firmware update using SimpleUpdate
|
457
|
+
puts "Initiating firmware update using SimpleUpdate..."
|
458
|
+
|
459
|
+
# Construct the image URI using the uploaded file
|
460
|
+
image_uri = "#{http_push_uri}/#{upload_id}"
|
461
|
+
puts "Using ImageURI: #{image_uri}"
|
462
|
+
|
463
|
+
# Call the SimpleUpdate action
|
464
|
+
simple_update_response = client.authenticated_request(
|
327
465
|
:post,
|
328
|
-
|
466
|
+
"/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate",
|
329
467
|
{
|
330
468
|
headers: {
|
331
|
-
'Content-Type' =>
|
332
|
-
'If-Match' => etag
|
469
|
+
'Content-Type' => 'application/json'
|
333
470
|
},
|
334
|
-
body:
|
471
|
+
body: JSON.generate({
|
472
|
+
'ImageURI' => image_uri,
|
473
|
+
'@Redfish.OperationApplyTime' => 'Immediate'
|
474
|
+
})
|
335
475
|
}
|
336
476
|
)
|
337
477
|
|
338
|
-
if
|
339
|
-
raise Error, "Firmware
|
478
|
+
if simple_update_response.status < 200 || simple_update_response.status >= 300
|
479
|
+
raise Error, "Firmware update initiation failed with status #{simple_update_response.status}: #{simple_update_response.body}"
|
340
480
|
end
|
341
481
|
|
342
482
|
# Extract job ID from response
|
343
|
-
|
344
|
-
|
483
|
+
job_id = nil
|
484
|
+
|
485
|
+
# Try to get job ID from Location header
|
486
|
+
if simple_update_response.headers['location']
|
487
|
+
job_id = simple_update_response.headers['location'].split('/').last
|
488
|
+
end
|
345
489
|
|
490
|
+
# If not found in header, try to get from response body
|
491
|
+
if job_id.nil? && !simple_update_response.body.empty?
|
492
|
+
begin
|
493
|
+
update_data = JSON.parse(simple_update_response.body)
|
494
|
+
if update_data['@odata.id']
|
495
|
+
job_id = update_data['@odata.id'].split('/').last
|
496
|
+
elsif update_data['Id']
|
497
|
+
job_id = update_data['Id']
|
498
|
+
end
|
499
|
+
rescue JSON::ParserError
|
500
|
+
# Not JSON, ignore
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
# If still no job ID, use the upload ID as a fallback
|
346
505
|
if job_id.nil?
|
347
|
-
|
506
|
+
job_id = upload_id
|
507
|
+
puts "No job ID found in SimpleUpdate response, using upload ID as job ID"
|
348
508
|
end
|
349
509
|
|
350
510
|
puts "Firmware update job created with ID: #{job_id}"
|
@@ -392,5 +552,54 @@ module IDRAC
|
|
392
552
|
response_data = JSON.parse(response.body)
|
393
553
|
response_data['TaskState'] || 'Unknown'
|
394
554
|
end
|
555
|
+
|
556
|
+
# Helper method to extract identifiers from component names
|
557
|
+
def extract_identifiers(name)
|
558
|
+
return [] unless name
|
559
|
+
|
560
|
+
identifiers = []
|
561
|
+
|
562
|
+
# Extract model numbers like X520, I350, etc.
|
563
|
+
model_matches = name.scan(/[IX]\d{3,4}/)
|
564
|
+
identifiers.concat(model_matches)
|
565
|
+
|
566
|
+
# Extract PERC model like H730
|
567
|
+
perc_matches = name.scan(/[HP]\d{3,4}/)
|
568
|
+
identifiers.concat(perc_matches)
|
569
|
+
|
570
|
+
# Extract other common identifiers
|
571
|
+
if name.include?("NIC") || name.include?("Ethernet") || name.include?("Network")
|
572
|
+
identifiers << "NIC"
|
573
|
+
end
|
574
|
+
|
575
|
+
if name.include?("PERC") || name.include?("RAID")
|
576
|
+
identifiers << "PERC"
|
577
|
+
# Extract PERC model like H730
|
578
|
+
perc_match = name.match(/PERC\s+([A-Z]\d{3})/)
|
579
|
+
identifiers << perc_match[1] if perc_match
|
580
|
+
end
|
581
|
+
|
582
|
+
if name.include?("BIOS")
|
583
|
+
identifiers << "BIOS"
|
584
|
+
end
|
585
|
+
|
586
|
+
if name.include?("iDRAC") || name.include?("IDRAC") || name.include?("Remote Access Controller")
|
587
|
+
identifiers << "iDRAC"
|
588
|
+
end
|
589
|
+
|
590
|
+
if name.include?("Power Supply") || name.include?("PSU")
|
591
|
+
identifiers << "PSU"
|
592
|
+
end
|
593
|
+
|
594
|
+
if name.include?("Lifecycle Controller")
|
595
|
+
identifiers << "LC"
|
596
|
+
end
|
597
|
+
|
598
|
+
if name.include?("CPLD")
|
599
|
+
identifiers << "CPLD"
|
600
|
+
end
|
601
|
+
|
602
|
+
identifiers
|
603
|
+
end
|
395
604
|
end
|
396
605
|
end
|
@@ -62,59 +62,130 @@ module IDRAC
|
|
62
62
|
doc = parse
|
63
63
|
models = []
|
64
64
|
|
65
|
-
# Extract
|
65
|
+
# Extract model code from full model name (e.g., "PowerEdge R640" -> "R640")
|
66
66
|
model_code = nil
|
67
67
|
if model_name.include?("PowerEdge")
|
68
68
|
model_code = model_name.split.last
|
69
|
+
else
|
70
|
+
model_code = model_name
|
69
71
|
end
|
70
72
|
|
73
|
+
puts "Searching for model: #{model_name} (code: #{model_code})"
|
74
|
+
|
75
|
+
# Build a mapping of model names to system IDs
|
76
|
+
model_to_system_id = {}
|
77
|
+
|
71
78
|
doc.xpath('//SupportedSystems/Brand/Model').each do |model|
|
79
|
+
system_id = model['systemID'] || model['id']
|
72
80
|
name = model.at_xpath('Display')&.text
|
73
81
|
code = model.at_xpath('Code')&.text
|
74
82
|
|
75
|
-
|
76
|
-
|
77
|
-
models << {
|
78
|
-
name: name,
|
79
|
-
code: code,
|
80
|
-
id: model['id']
|
81
|
-
}
|
82
|
-
# Try to match by model code (e.g., R640)
|
83
|
-
elsif model_code && code && code.downcase == model_code.downcase
|
84
|
-
models << {
|
83
|
+
if name && system_id
|
84
|
+
model_to_system_id[name] = {
|
85
85
|
name: name,
|
86
86
|
code: code,
|
87
|
-
id:
|
87
|
+
id: system_id
|
88
88
|
}
|
89
|
+
|
90
|
+
# Also map just the model number (R640, etc.)
|
91
|
+
if name =~ /[RT]\d+/
|
92
|
+
model_short = name.match(/([RT]\d+\w*)/)[1]
|
93
|
+
model_to_system_id[model_short] = {
|
94
|
+
name: name,
|
95
|
+
code: code,
|
96
|
+
id: system_id
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Try exact match first
|
103
|
+
if model_to_system_id[model_name]
|
104
|
+
models << model_to_system_id[model_name]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Try model code match
|
108
|
+
if model_to_system_id[model_code]
|
109
|
+
models << model_to_system_id[model_code]
|
110
|
+
end
|
111
|
+
|
112
|
+
# If we still don't have a match, try a more flexible approach
|
113
|
+
if models.empty?
|
114
|
+
model_to_system_id.each do |name, model_info|
|
115
|
+
if name.include?(model_code) || model_code.include?(name)
|
116
|
+
models << model_info
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# If still no match, try matching by systemID directly
|
122
|
+
if models.empty?
|
123
|
+
doc.xpath('//SupportedSystems/Brand/Model').each do |model|
|
124
|
+
system_id = model['systemID'] || model['id']
|
125
|
+
name = model.at_xpath('Display')&.text
|
126
|
+
code = model.at_xpath('Code')&.text
|
127
|
+
|
128
|
+
if code && code.downcase == model_code.downcase
|
129
|
+
models << {
|
130
|
+
name: name,
|
131
|
+
code: code,
|
132
|
+
id: system_id
|
133
|
+
}
|
134
|
+
end
|
89
135
|
end
|
90
136
|
end
|
91
137
|
|
92
|
-
models
|
138
|
+
models.uniq { |m| m[:id] }
|
93
139
|
end
|
94
140
|
|
95
141
|
def find_updates_for_system(system_id)
|
96
142
|
doc = parse
|
97
143
|
updates = []
|
98
144
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
145
|
+
# Find all SoftwareComponents
|
146
|
+
doc.xpath("//SoftwareComponent").each do |component|
|
147
|
+
# Check if this component supports our system ID
|
148
|
+
supported_system_ids = component.xpath(".//SupportedSystems/Brand/Model/@systemID | .//SupportedSystems/Brand/Model/@id").map(&:value)
|
149
|
+
|
150
|
+
next unless supported_system_ids.include?(system_id)
|
151
|
+
|
152
|
+
# Get component details
|
153
|
+
name_node = component.xpath("./Name/Display[@lang='en']").first
|
154
|
+
name = name_node ? name_node.text.strip : ""
|
105
155
|
|
106
|
-
|
156
|
+
component_type_node = component.xpath("./ComponentType/Display[@lang='en']").first
|
157
|
+
component_type = component_type_node ? component_type_node.text.strip : ""
|
107
158
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
159
|
+
path = component['path'] || ""
|
160
|
+
category_node = component.xpath("./Category/Display[@lang='en']").first
|
161
|
+
category = category_node ? category_node.text.strip : ""
|
162
|
+
|
163
|
+
version = component['dellVersion'] || component['vendorVersion'] || ""
|
164
|
+
|
165
|
+
# Skip if missing essential information
|
166
|
+
next if name.empty? || path.empty? || version.empty?
|
167
|
+
|
168
|
+
# Only include firmware updates
|
169
|
+
if component_type.include?("Firmware") ||
|
170
|
+
category.include?("BIOS") ||
|
171
|
+
category.include?("Firmware") ||
|
172
|
+
category.include?("iDRAC") ||
|
173
|
+
name.include?("BIOS") ||
|
174
|
+
name.include?("Firmware") ||
|
175
|
+
name.include?("iDRAC")
|
176
|
+
|
177
|
+
updates << {
|
178
|
+
name: name,
|
179
|
+
version: version,
|
180
|
+
path: path,
|
181
|
+
component_type: component_type,
|
182
|
+
category: category,
|
183
|
+
download_url: "https://downloads.dell.com/#{path}"
|
184
|
+
}
|
185
|
+
end
|
116
186
|
end
|
117
187
|
|
188
|
+
puts "Found #{updates.size} firmware updates for system ID #{system_id}"
|
118
189
|
updates
|
119
190
|
end
|
120
191
|
|
data/lib/idrac/version.rb
CHANGED
@@ -0,0 +1,70 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative 'lib/idrac'
|
4
|
+
|
5
|
+
# Create a client
|
6
|
+
client = IDRAC::Client.new(
|
7
|
+
host: '127.0.0.1',
|
8
|
+
username: 'root',
|
9
|
+
password: 'calvin',
|
10
|
+
verify_ssl: false
|
11
|
+
)
|
12
|
+
|
13
|
+
begin
|
14
|
+
# Login to iDRAC
|
15
|
+
client.login
|
16
|
+
puts "Logged in successfully"
|
17
|
+
|
18
|
+
# Create a firmware instance
|
19
|
+
firmware = IDRAC::Firmware.new(client)
|
20
|
+
|
21
|
+
# Get system inventory
|
22
|
+
puts "Getting system inventory..."
|
23
|
+
inventory = firmware.get_system_inventory
|
24
|
+
|
25
|
+
puts "System Information:"
|
26
|
+
puts " Model: #{inventory[:system][:model]}"
|
27
|
+
puts " Manufacturer: #{inventory[:system][:manufacturer]}"
|
28
|
+
puts " Service Tag: #{inventory[:system][:service_tag]}"
|
29
|
+
puts " BIOS Version: #{inventory[:system][:bios_version]}"
|
30
|
+
|
31
|
+
puts "\nInstalled Firmware:"
|
32
|
+
inventory[:firmware].each do |fw|
|
33
|
+
puts " #{fw[:name]}: #{fw[:version]} (#{fw[:updateable] ? 'Updateable' : 'Not Updateable'})"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Check for updates
|
37
|
+
catalog_path = File.expand_path("~/.idrac/Catalog.xml")
|
38
|
+
if File.exist?(catalog_path)
|
39
|
+
puts "\nChecking for updates using catalog: #{catalog_path}"
|
40
|
+
updates = firmware.check_updates(catalog_path)
|
41
|
+
|
42
|
+
if updates.empty?
|
43
|
+
puts "No updates available."
|
44
|
+
else
|
45
|
+
puts "\nAvailable Updates:"
|
46
|
+
updates.each_with_index do |update, index|
|
47
|
+
puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# For testing, select the first update
|
51
|
+
if updates.any?
|
52
|
+
selected_update = updates.first
|
53
|
+
puts "\nSelected update: #{selected_update[:name]}: #{selected_update[:current_version]} -> #{selected_update[:available_version]}"
|
54
|
+
|
55
|
+
# Download and install the update
|
56
|
+
puts "Starting interactive update for selected component..."
|
57
|
+
firmware.interactive_update(catalog_path, [selected_update])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
else
|
61
|
+
puts "\nCatalog file not found at #{catalog_path}. Please download the catalog first."
|
62
|
+
end
|
63
|
+
|
64
|
+
rescue IDRAC::Error => e
|
65
|
+
puts "Error: #{e.message}"
|
66
|
+
ensure
|
67
|
+
# Logout
|
68
|
+
client.logout if client.instance_variable_get(:@session_id)
|
69
|
+
puts "Logged out"
|
70
|
+
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.1.
|
4
|
+
version: 0.1.29
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Siegel
|
@@ -215,6 +215,7 @@ files:
|
|
215
215
|
- lib/idrac/screenshot.rb
|
216
216
|
- lib/idrac/version.rb
|
217
217
|
- sig/idrac.rbs
|
218
|
+
- test_firmware_update.rb
|
218
219
|
homepage: http://github.com
|
219
220
|
licenses:
|
220
221
|
- MIT
|