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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 180bec6c43d2aced138c9479bbbfe23cec04c6149d2cd66343b5a738c0ce479f
4
- data.tar.gz: 10fae4fc88a7d1ba456e3f7dc7d2efbfb8c7500b7839e7b59a2e27d5dae53c7c
3
+ metadata.gz: 6c080d98bd3ca44b9cc006ba66cc443ec410dcfb6bc1b712141720cc82998884
4
+ data.tar.gz: fe0e09d1e3089c5d354abf6f786624d5afd7dfbcd7b2984d84a72be9936dcf9c
5
5
  SHA512:
6
- metadata.gz: '0990d968c95f92c2b410b8338c88e8bff464089cc828f9bd49c2fa8bb8111aff7f059fc68d8ff958ccf9c2036af6aebcd2614b96b4ef757bbf93d59f47eecba5'
7
- data.tar.gz: bfea9fe661ac565bc5ee652204a927a4c19977abef08bdd51ddf0c79027cc9f40c24d3b21522a9247d8933c22499f99b2bea73be02e0e647f81b42bf7ef2ec53
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
- class_option :host, type: :string, required: true, desc: "iDRAC host address"
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
- # Forward to the catalog command
63
- CatalogCommand.new.download(directory)
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 download'"
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.interactive_update(catalog_path)
138
- rescue IDRAC::Error => e
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
- # Create a separate CLI class for commands that don't require a host
204
- module IDRAC
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)
@@ -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.split.first)
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 %-20s" % ["Component", "Current Version", "Available Version", "Updateable", "Category", "Status"]
154
+ puts "%-30s %-20s %-20s %-10s %-15s %s" % ["Component", "Current Version", "Available Version", "Updateable", "Category", "Status"]
154
155
  puts "-" * 100
155
156
 
156
- # Process each firmware component
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
- # Find matching updates in catalog
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
- catalog.match_component(fw[:name], update[:name])
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 %-20s" % [
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 available
193
- puts "%-30s %-20s %-20s %-10s %-15s %-20s" % [
194
- fw[:name][0..29],
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
- "No update available"
228
+ status
200
229
  ]
201
230
  end
202
231
  else
203
232
  # No matching update found
204
- puts "%-30s %-20s %-20s %-10s %-15s %-20s" % [
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
- puts "\nEnter the number of the update to install (or 'all' for all updates, 'q' to quit):"
232
- choice = STDIN.gets.chomp
233
-
234
- return if choice.downcase == 'q'
235
-
236
- selected_updates = if choice.downcase == 'all'
237
- updates
238
- else
239
- index = choice.to_i - 1
240
- if index >= 0 && index < updates.size
241
- [updates[index]]
242
- else
243
- puts "Invalid selection."
244
- return
245
- end
246
- end
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
- selected_updates.each do |update|
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
- job_id = update(update_path, wait: true)
278
- puts "Update completed with job ID: #{job_id}"
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
- response = client.authenticated_request(
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
- http_push_uri,
466
+ "/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate",
329
467
  {
330
468
  headers: {
331
- 'Content-Type' => "multipart/form-data; boundary=#{boundary}",
332
- 'If-Match' => etag
469
+ 'Content-Type' => 'application/json'
333
470
  },
334
- body: post_body.join
471
+ body: JSON.generate({
472
+ 'ImageURI' => image_uri,
473
+ '@Redfish.OperationApplyTime' => 'Immediate'
474
+ })
335
475
  }
336
476
  )
337
477
 
338
- if response.status < 200 || response.status >= 300
339
- raise Error, "Firmware upload failed with status #{response.status}: #{response.body}"
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
- response_data = JSON.parse(response.body)
344
- job_id = response_data['Id'] || response_data['TaskId']
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
- raise Error, "Failed to extract job ID from firmware upload response"
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 just the model number for PowerEdge servers (e.g., R640 from PowerEdge R640)
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
- # Try to match by full name
76
- if name && name.downcase.include?(model_name.downcase)
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: model['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
- doc.xpath("//SoftwareComponent[SupportedSystems/Brand/Model[@id='#{system_id}']]").each do |component|
100
- name = component.at_xpath('Name')&.text
101
- version = component.at_xpath('Version')&.text
102
- path = component.at_xpath('Path')&.text
103
- component_type = component.at_xpath('ComponentType')&.text
104
- category = component.at_xpath('Category')&.text
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
- next unless name && version && path
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
- updates << {
109
- name: name,
110
- version: version,
111
- path: path,
112
- component_type: component_type,
113
- category: category,
114
- download_url: "https://downloads.dell.com/#{path}"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IDRAC
4
- VERSION = "0.1.28"
4
+ VERSION = "0.1.29"
5
5
  end
@@ -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.28
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