idrac 0.1.28 → 0.1.30

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.
@@ -5,6 +5,8 @@ require 'json'
5
5
  require 'nokogiri'
6
6
  require 'fileutils'
7
7
  require 'securerandom'
8
+ require 'set'
9
+ require 'colorize'
8
10
  require_relative 'firmware_catalog'
9
11
 
10
12
  module IDRAC
@@ -125,39 +127,59 @@ module IDRAC
125
127
  system_model = inventory[:system][:model]
126
128
  service_tag = inventory[:system][:service_tag]
127
129
 
128
- puts "Checking updates for system with service tag: #{service_tag}"
129
- puts "Searching for updates for model: #{system_model}"
130
+ puts "Checking updates for system with service tag: #{service_tag}".light_cyan
131
+ puts "Searching for updates for model: #{system_model}".light_cyan
130
132
 
131
133
  # Find system models in the catalog
132
- models = catalog.find_system_models(system_model.split.first)
134
+ models = catalog.find_system_models(system_model)
133
135
 
134
136
  if models.empty?
135
- puts "No matching system model found in catalog"
137
+ puts "No matching system model found in catalog".yellow
136
138
  return []
137
139
  end
138
140
 
139
141
  # Use the first matching model
140
142
  model = models.first
141
- puts "Found system IDs for #{model[:name]}: #{model[:id]}"
143
+ puts "Found system IDs for #{model[:name]}: #{model[:id]}".green
142
144
 
143
145
  # Find updates for this system
144
146
  catalog_updates = catalog.find_updates_for_system(model[:id])
145
- puts "Found #{catalog_updates.size} firmware updates for #{model[:name]}"
147
+ puts "Found #{catalog_updates.size} firmware updates for #{model[:name]}".green
146
148
 
147
149
  # Compare current firmware with available updates
148
150
  updates = []
149
151
 
150
152
  # Print header for firmware comparison table
151
- puts "\nFirmware Version Comparison:"
153
+ puts "\nFirmware Version Comparison:".green.bold
152
154
  puts "=" * 100
153
- puts "%-30s %-20s %-20s %-10s %-15s %-20s" % ["Component", "Current Version", "Available Version", "Updateable", "Category", "Status"]
155
+ puts "%-30s %-20s %-20s %-10s %-15s %s" % ["Component", "Current Version", "Available Version", "Updateable", "Category", "Status"]
154
156
  puts "-" * 100
155
157
 
156
- # Process each firmware component
158
+ # Track components we've already displayed to avoid duplicates
159
+ displayed_components = Set.new
160
+
161
+ # First show current firmware with available updates
157
162
  inventory[:firmware].each do |fw|
158
- # Find matching updates in catalog
163
+ # Make sure firmware name is not nil
164
+ firmware_name = fw[:name] || ""
165
+
166
+ # Skip if we've already displayed this component
167
+ next if displayed_components.include?(firmware_name.downcase)
168
+ displayed_components.add(firmware_name.downcase)
169
+
170
+ # Extract key identifiers from the firmware name
171
+ identifiers = extract_identifiers(firmware_name)
172
+
173
+ # Try to find a matching update
159
174
  matching_updates = catalog_updates.select do |update|
160
- catalog.match_component(fw[:name], update[:name])
175
+ update_name = update[:name] || ""
176
+
177
+ # Check if any of our identifiers match the update name
178
+ identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
179
+ # Or if the update name contains the firmware name
180
+ update_name.downcase.include?(firmware_name.downcase) ||
181
+ # Or if the firmware name contains the update name
182
+ firmware_name.downcase.include?(update_name.downcase)
161
183
  end
162
184
 
163
185
  if matching_updates.any?
@@ -180,73 +202,117 @@ module IDRAC
180
202
  }
181
203
 
182
204
  # Print row with update available
183
- puts "%-30s %-20s %-20s %-10s %-15s %-20s" % [
184
- fw[:name][0..29],
205
+ puts "%-30s %-20s %-20s %-10s %-15s %s" % [
206
+ fw[:name].to_s[0..29],
185
207
  fw[:version],
186
208
  update[:version],
187
- fw[:updateable] ? "Yes" : "No",
209
+ fw[:updateable] ? "Yes".light_green : "No".light_red,
188
210
  update[:category] || "N/A",
189
- "UPDATE AVAILABLE"
211
+ "UPDATE AVAILABLE".light_green.bold
190
212
  ]
191
213
  else
192
- # Print row with no update available
193
- puts "%-30s %-20s %-20s %-10s %-15s %-20s" % [
194
- fw[:name][0..29],
214
+ # Print row with no update needed
215
+ status = if !needs_update
216
+ "Current".light_blue
217
+ elsif !fw[:updateable]
218
+ "Not updateable".light_red
219
+ else
220
+ "No update needed".light_yellow
221
+ end
222
+
223
+ puts "%-30s %-20s %-20s %-10s %-15s %s" % [
224
+ fw[:name].to_s[0..29],
195
225
  fw[:version],
196
226
  update[:version] || "N/A",
197
- fw[:updateable] ? "Yes" : "No",
227
+ fw[:updateable] ? "Yes".light_green : "No".light_red,
198
228
  update[:category] || "N/A",
199
- "No update available"
229
+ status
200
230
  ]
201
231
  end
202
232
  else
203
233
  # No matching update found
204
- puts "%-30s %-20s %-20s %-10s %-15s %-20s" % [
205
- fw[:name][0..29],
234
+ puts "%-30s %-20s %-20s %-10s %-15s %s" % [
235
+ fw[:name].to_s[0..29],
206
236
  fw[:version],
207
237
  "N/A",
208
- fw[:updateable] ? "Yes" : "No",
238
+ fw[:updateable] ? "Yes".light_green : "No".light_red,
209
239
  "N/A",
210
- "No update available"
240
+ "No update available".light_yellow
211
241
  ]
212
242
  end
213
243
  end
214
244
 
245
+ # Then show available updates that don't match any current firmware
246
+ catalog_updates.each do |update|
247
+ update_name = update[:name] || ""
248
+
249
+ # Skip if we've already displayed this component
250
+ next if displayed_components.include?(update_name.downcase)
251
+ displayed_components.add(update_name.downcase)
252
+
253
+ # Skip if this update was already matched to a current firmware
254
+ next if inventory[:firmware].any? do |fw|
255
+ firmware_name = fw[:name] || ""
256
+ identifiers = extract_identifiers(firmware_name)
257
+
258
+ identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
259
+ update_name.downcase.include?(firmware_name.downcase) ||
260
+ firmware_name.downcase.include?(update_name.downcase)
261
+ end
262
+
263
+ puts "%-30s %-20s %-20s %-10s %-15s %s" % [
264
+ update_name.to_s[0..29],
265
+ "Not Installed".light_red,
266
+ update[:version] || "Unknown",
267
+ "N/A",
268
+ update[:category] || "N/A",
269
+ "NEW COMPONENT".light_blue
270
+ ]
271
+ end
272
+
273
+ puts "=" * 100
274
+
215
275
  updates
216
276
  end
217
277
 
218
- def interactive_update(catalog_path = nil)
219
- updates = check_updates(catalog_path)
278
+ def interactive_update(catalog_path = nil, selected_updates = nil)
279
+ updates = selected_updates || check_updates(catalog_path)
220
280
 
221
281
  if updates.empty?
222
- puts "No updates available for your system."
282
+ puts "No updates available for your system.".yellow
223
283
  return
224
284
  end
225
285
 
226
- puts "\nAvailable updates:"
286
+ puts "\nAvailable Updates:".green.bold
227
287
  updates.each_with_index do |update, index|
228
- puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}"
288
+ puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}".light_cyan
229
289
  end
230
290
 
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
247
-
248
- selected_updates.each do |update|
249
- puts "Downloading #{update[:name]} version #{update[:available_version]}..."
291
+ # If selected_updates is provided, use those directly
292
+ if selected_updates
293
+ selected = selected_updates
294
+ else
295
+ # Otherwise prompt the user for selection
296
+ puts "\nEnter the number of the update to install (or 'all' for all updates, 'q' to quit):".light_yellow
297
+ choice = STDIN.gets.chomp
298
+
299
+ return if choice.downcase == 'q'
300
+
301
+ selected = if choice.downcase == 'all'
302
+ updates
303
+ else
304
+ index = choice.to_i - 1
305
+ if index >= 0 && index < updates.size
306
+ [updates[index]]
307
+ else
308
+ puts "Invalid selection.".red
309
+ return
310
+ end
311
+ end
312
+ end
313
+
314
+ selected.each do |update|
315
+ puts "Downloading #{update[:name]} version #{update[:available_version]}...".light_cyan.bold
250
316
 
251
317
  # Create temp directory
252
318
  temp_dir = Dir.mktmpdir
@@ -267,15 +333,35 @@ module IDRAC
267
333
  end
268
334
  end
269
335
  else
270
- puts "Failed to download update: #{response.code} #{response.message}"
336
+ puts "Failed to download update: #{response.code} #{response.message}".red
271
337
  next
272
338
  end
273
339
  end
274
340
  end
275
341
 
276
- 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
+ puts "Installing #{update[:name]} version #{update[:available_version]}...".light_cyan.bold
343
+
344
+ begin
345
+ job_id = update(update_path, wait: true)
346
+ puts "Update completed with job ID: #{job_id}".green.bold
347
+ rescue IDRAC::Error => e
348
+ if e.message.include?("already in progress")
349
+ puts "Error: #{e.message}".red.bold
350
+ puts "\nTips for resolving this issue:".yellow.bold
351
+ puts "1. Wait for any existing firmware updates to complete (check iDRAC web interface)".light_yellow
352
+ puts "2. Restart the iDRAC if no updates appear to be in progress (Settings > iDRAC Settings > Reset iDRAC)".light_yellow
353
+ puts "3. Try again after a few minutes".light_yellow
354
+ elsif e.message.include?("job ID") || e.message.include?("job not found")
355
+ puts "Error: #{e.message}".red.bold
356
+ puts "\nThe job ID could not be found or monitored. This could be because:".yellow
357
+ puts "1. The iDRAC is busy processing other requests".light_yellow
358
+ puts "2. The job was created but not properly tracked".light_yellow
359
+ puts "3. The iDRAC firmware may need to be updated first".light_yellow
360
+ puts "\nCheck the iDRAC web interface to see if the update was actually initiated.".light_cyan
361
+ else
362
+ puts "Error during firmware update: #{e.message}".red.bold
363
+ end
364
+ end
279
365
 
280
366
  ensure
281
367
  # Clean up temp directory
@@ -323,28 +409,103 @@ module IDRAC
323
409
  post_body << "\r\n--#{boundary}--\r\n"
324
410
 
325
411
  # Upload the firmware
326
- response = client.authenticated_request(
412
+ begin
413
+ response = client.authenticated_request(
414
+ :post,
415
+ http_push_uri,
416
+ {
417
+ headers: {
418
+ 'Content-Type' => "multipart/form-data; boundary=#{boundary}",
419
+ 'If-Match' => etag
420
+ },
421
+ body: post_body.join
422
+ }
423
+ )
424
+ rescue => e
425
+ # Check if the error is about a deployment already in progress
426
+ if e.message.include?("A deployment or update operation is already in progress")
427
+ 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."
428
+ else
429
+ # Re-raise the original error
430
+ raise
431
+ end
432
+ end
433
+
434
+ if response.status < 200 || response.status >= 300
435
+ error_message = response.body.to_s
436
+
437
+ # Check for specific error messages in the response
438
+ if error_message.include?("A deployment or update operation is already in progress")
439
+ 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."
440
+ elsif error_message.include?("SUP0108")
441
+ raise Error, "iDRAC Error SUP0108: A deployment or update operation is already in progress. Wait for the operation to conclude and then try again."
442
+ else
443
+ raise Error, "Firmware upload failed with status #{response.status}: #{response.body}"
444
+ end
445
+ end
446
+
447
+ # Extract upload ID from response
448
+ response_data = JSON.parse(response.body)
449
+ upload_id = response_data['Id'] || response_data['TaskId']
450
+
451
+ if upload_id.nil?
452
+ raise Error, "Failed to extract upload ID from firmware upload response"
453
+ end
454
+
455
+ puts "Firmware file uploaded successfully with ID: #{upload_id}"
456
+
457
+ # Step 2: Initiate the firmware update using SimpleUpdate
458
+ puts "Initiating firmware update using SimpleUpdate..."
459
+
460
+ # Construct the image URI using the uploaded file
461
+ image_uri = "#{http_push_uri}/#{upload_id}"
462
+ puts "Using ImageURI: #{image_uri}"
463
+
464
+ # Call the SimpleUpdate action
465
+ simple_update_response = client.authenticated_request(
327
466
  :post,
328
- http_push_uri,
467
+ "/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate",
329
468
  {
330
469
  headers: {
331
- 'Content-Type' => "multipart/form-data; boundary=#{boundary}",
332
- 'If-Match' => etag
470
+ 'Content-Type' => 'application/json'
333
471
  },
334
- body: post_body.join
472
+ body: JSON.generate({
473
+ 'ImageURI' => image_uri,
474
+ '@Redfish.OperationApplyTime' => 'Immediate'
475
+ })
335
476
  }
336
477
  )
337
478
 
338
- if response.status < 200 || response.status >= 300
339
- raise Error, "Firmware upload failed with status #{response.status}: #{response.body}"
479
+ if simple_update_response.status < 200 || simple_update_response.status >= 300
480
+ raise Error, "Firmware update initiation failed with status #{simple_update_response.status}: #{simple_update_response.body}"
340
481
  end
341
482
 
342
483
  # Extract job ID from response
343
- response_data = JSON.parse(response.body)
344
- job_id = response_data['Id'] || response_data['TaskId']
484
+ job_id = nil
485
+
486
+ # Try to get job ID from Location header
487
+ if simple_update_response.headers['location']
488
+ job_id = simple_update_response.headers['location'].split('/').last
489
+ end
345
490
 
491
+ # If not found in header, try to get from response body
492
+ if job_id.nil? && !simple_update_response.body.empty?
493
+ begin
494
+ update_data = JSON.parse(simple_update_response.body)
495
+ if update_data['@odata.id']
496
+ job_id = update_data['@odata.id'].split('/').last
497
+ elsif update_data['Id']
498
+ job_id = update_data['Id']
499
+ end
500
+ rescue JSON::ParserError
501
+ # Not JSON, ignore
502
+ end
503
+ end
504
+
505
+ # If still no job ID, use the upload ID as a fallback
346
506
  if job_id.nil?
347
- raise Error, "Failed to extract job ID from firmware upload response"
507
+ job_id = upload_id
508
+ puts "No job ID found in SimpleUpdate response, using upload ID as job ID"
348
509
  end
349
510
 
350
511
  puts "Firmware update job created with ID: #{job_id}"
@@ -392,5 +553,54 @@ module IDRAC
392
553
  response_data = JSON.parse(response.body)
393
554
  response_data['TaskState'] || 'Unknown'
394
555
  end
556
+
557
+ # Helper method to extract identifiers from component names
558
+ def extract_identifiers(name)
559
+ return [] unless name
560
+
561
+ identifiers = []
562
+
563
+ # Extract model numbers like X520, I350, etc.
564
+ model_matches = name.scan(/[IX]\d{3,4}/)
565
+ identifiers.concat(model_matches)
566
+
567
+ # Extract PERC model like H730
568
+ perc_matches = name.scan(/[HP]\d{3,4}/)
569
+ identifiers.concat(perc_matches)
570
+
571
+ # Extract other common identifiers
572
+ if name.include?("NIC") || name.include?("Ethernet") || name.include?("Network")
573
+ identifiers << "NIC"
574
+ end
575
+
576
+ if name.include?("PERC") || name.include?("RAID")
577
+ identifiers << "PERC"
578
+ # Extract PERC model like H730
579
+ perc_match = name.match(/PERC\s+([A-Z]\d{3})/)
580
+ identifiers << perc_match[1] if perc_match
581
+ end
582
+
583
+ if name.include?("BIOS")
584
+ identifiers << "BIOS"
585
+ end
586
+
587
+ if name.include?("iDRAC") || name.include?("IDRAC") || name.include?("Remote Access Controller")
588
+ identifiers << "iDRAC"
589
+ end
590
+
591
+ if name.include?("Power Supply") || name.include?("PSU")
592
+ identifiers << "PSU"
593
+ end
594
+
595
+ if name.include?("Lifecycle Controller")
596
+ identifiers << "LC"
597
+ end
598
+
599
+ if name.include?("CPLD")
600
+ identifiers << "CPLD"
601
+ end
602
+
603
+ identifiers
604
+ end
395
605
  end
396
606
  end