idrac 0.1.31 → 0.1.38

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.
data/updater.rb DELETED
@@ -1,729 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'net/http'
4
- require 'uri'
5
- require 'json'
6
- require 'nokogiri'
7
- require 'open-uri'
8
- require 'fileutils'
9
- require 'zip'
10
- require 'set'
11
- require 'securerandom'
12
-
13
- class DellFirmwareUpdater
14
- DEFAULT_USERNAME = 'root'
15
- DEFAULT_PASSWORD = 'calvin'
16
- DOWNLOAD_DIR = 'dell_firmware_downloads'
17
-
18
- # Dell catalog URLs
19
- DELL_CATALOG_BASE = 'https://downloads.dell.com'
20
- DELL_CATALOG_URL = "#{DELL_CATALOG_BASE}/catalog/Catalog.xml.gz"
21
- DELL_CATALOG_INDEX_URL = "#{DELL_CATALOG_BASE}/catalog/CatalogIndex.xml"
22
-
23
- def initialize(idrac_ip, username = DEFAULT_USERNAME, password = DEFAULT_PASSWORD)
24
- @idrac_ip = idrac_ip
25
- @username = username
26
- @password = password
27
- @service_tag = nil
28
- @model = nil
29
- @redfish_base_url = "https://#{@idrac_ip}/redfish/v1"
30
-
31
- FileUtils.mkdir_p(DOWNLOAD_DIR) unless Dir.exist?(DOWNLOAD_DIR)
32
- end
33
-
34
- def get_system_info
35
- uri = URI.parse("#{@redfish_base_url}/Systems/System.Embedded.1")
36
-
37
- begin
38
- response = make_redfish_request(uri)
39
- data = JSON.parse(response.body)
40
- @service_tag = data['SKU']
41
- @model = data['Model']
42
-
43
- # Try to get additional system identifiers that might match Dell's catalog
44
- @system_id = data['SystemType'] || ''
45
- @manufacturer = data['Manufacturer'] || ''
46
-
47
- puts "Service Tag: #{@service_tag}"
48
- puts "Model: #{@model}"
49
- puts "System ID: #{@system_id}" if @system_id
50
- puts "Manufacturer: #{@manufacturer}" if @manufacturer
51
-
52
- return {
53
- service_tag: @service_tag,
54
- model: @model,
55
- system_id: @system_id,
56
- manufacturer: @manufacturer
57
- }
58
- rescue => e
59
- puts "Error retrieving system information: #{e.message}"
60
- nil
61
- end
62
- end
63
-
64
- def get_current_firmware_versions
65
- uri = URI.parse("#{@redfish_base_url}/UpdateService/FirmwareInventory")
66
-
67
- begin
68
- response = make_redfish_request(uri)
69
- data = JSON.parse(response.body)
70
-
71
- firmware_list = []
72
-
73
- if data['Members'] && !data['Members'].empty?
74
- data['Members'].each do |member|
75
- member_uri = member['@odata.id']
76
- firmware_uri = URI.parse("https://#{@idrac_ip}#{member_uri}")
77
- firmware_response = make_redfish_request(firmware_uri)
78
- firmware_data = JSON.parse(firmware_response.body)
79
-
80
- firmware_list << {
81
- name: firmware_data['Name'],
82
- id: firmware_data['Id'],
83
- version: firmware_data['Version'],
84
- updateable: firmware_data['Updateable'] || false
85
- }
86
- end
87
- end
88
-
89
- firmware_list
90
- rescue => e
91
- puts "Error retrieving current firmware versions: #{e.message}"
92
- []
93
- end
94
- end
95
-
96
- def download_dell_catalog
97
- catalog_file = File.join(DOWNLOAD_DIR, 'Catalog.xml.gz')
98
- catalog_xml = File.join(DOWNLOAD_DIR, 'Catalog.xml')
99
- etag_file = File.join(DOWNLOAD_DIR, 'Catalog.etag')
100
-
101
- # Check if we already have the catalog and if it's current
102
- if File.exist?(catalog_xml) && File.exist?(etag_file)
103
- current_etag = File.read(etag_file).strip
104
-
105
- # Check if the catalog has changed using HEAD request
106
- uri = URI.parse(DELL_CATALOG_URL)
107
- http = Net::HTTP.new(uri.host, uri.port)
108
- http.use_ssl = true
109
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
110
-
111
- request = Net::HTTP::Head.new(uri.request_uri)
112
- response = http.request(request)
113
-
114
- server_etag = response['etag']&.strip
115
-
116
- if server_etag && server_etag == current_etag
117
- puts "Catalog is already up to date (ETag: #{current_etag})"
118
- return catalog_xml
119
- else
120
- puts "Catalog has changed. Current ETag: #{current_etag}, Server ETag: #{server_etag}"
121
- end
122
- end
123
-
124
- puts "Downloading Dell catalog..."
125
-
126
- begin
127
- # Download the compressed catalog
128
- uri = URI.parse(DELL_CATALOG_URL)
129
- http = Net::HTTP.new(uri.host, uri.port)
130
- http.use_ssl = true
131
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
132
-
133
- request = Net::HTTP::Get.new(uri.request_uri)
134
- response = http.request(request)
135
-
136
- # Save the ETag for future checks
137
- if response['etag']
138
- File.write(etag_file, response['etag'].strip)
139
- end
140
-
141
- # Save the catalog
142
- File.open(catalog_file, 'wb') do |file|
143
- file.write(response.body)
144
- end
145
-
146
- # Decompress the catalog
147
- system("gunzip -f #{catalog_file}")
148
-
149
- puts "Catalog downloaded and extracted to #{catalog_xml}"
150
- return catalog_xml
151
- rescue => e
152
- puts "Error downloading Dell catalog: #{e.message}"
153
- # If download fails but we have an existing catalog, use that
154
- if File.exist?(catalog_xml)
155
- puts "Using existing catalog file"
156
- return catalog_xml
157
- end
158
- return nil
159
- end
160
- end
161
-
162
- def get_available_updates_from_catalog(catalog_path)
163
- return [] unless @model && File.exist?(catalog_path)
164
-
165
- begin
166
- doc = Nokogiri::XML(File.open(catalog_path))
167
-
168
- # Extract model code from full model name (e.g., "PowerEdge R730" -> "R730")
169
- model_code = @model.split(/\s+/).last
170
-
171
- puts "Searching for updates for model: #{@model} (code: #{model_code})"
172
-
173
- # First, build a mapping of model names to system IDs
174
- model_to_system_id = {}
175
-
176
- doc.xpath("//SupportedSystems/Brand/Model").each do |model_node|
177
- system_id = model_node['systemID']
178
- display_node = model_node.xpath("./Display[@lang='en']").first
179
-
180
- if display_node && system_id
181
- model_name = display_node.text.strip
182
- model_to_system_id[model_name] = system_id
183
-
184
- # Also map just the model number (R730, etc.)
185
- if model_name =~ /[RT]\d+/
186
- model_short = model_name.match(/([RT]\d+\w*)/)[1]
187
- model_to_system_id[model_short] = system_id
188
- end
189
- end
190
- end
191
-
192
- # Find the system ID for our model
193
- target_system_ids = []
194
-
195
- # Try exact match first
196
- if model_to_system_id[@model]
197
- target_system_ids << model_to_system_id[@model]
198
- end
199
-
200
- # Try model code match
201
- if model_to_system_id[model_code]
202
- target_system_ids << model_to_system_id[model_code]
203
- end
204
-
205
- # If we still don't have a match, try a more flexible approach
206
- if target_system_ids.empty?
207
- model_to_system_id.each do |name, id|
208
- if name.include?(model_code) || model_code.include?(name)
209
- target_system_ids << id
210
- end
211
- end
212
- end
213
-
214
- # Print what we found
215
- if target_system_ids.empty?
216
- puts "Could not find system ID for #{@model} or #{model_code}"
217
- puts "Available models in catalog: #{model_to_system_id.keys.take(10).join(', ')}..."
218
- else
219
- puts "Found system IDs for #{@model}: #{target_system_ids.join(', ')}"
220
- end
221
-
222
- # Find all components for this model
223
- updates = []
224
-
225
- # Find SoftwareComponents for this model
226
- doc.xpath("//SoftwareComponent").each do |component|
227
- # Check if this component supports any of our target system IDs
228
- supported_system_ids = component.xpath(".//SupportedSystems/Brand/Model/@systemID").map(&:value)
229
-
230
- match_found = false
231
- matching_id = nil
232
-
233
- # Check if any of our target system IDs are supported
234
- target_system_ids.each do |target_id|
235
- if supported_system_ids.include?(target_id)
236
- match_found = true
237
- matching_id = target_id
238
- break
239
- end
240
- end
241
-
242
- # If no direct match, try to match by model name in CDATA
243
- if !match_found
244
- cdata_content = component.xpath(".//SupportedSystems/Brand/Model/Display").map(&:text).join(" ")
245
- if cdata_content.include?(model_code)
246
- match_found = true
247
- matching_id = "CDATA_match"
248
- end
249
- end
250
-
251
- next unless match_found
252
-
253
- # Get component details
254
- name_node = component.xpath("./Name/Display[@lang='en']").first
255
- name = name_node ? name_node.text.strip : ""
256
-
257
- component_type_node = component.xpath("./ComponentType/Display[@lang='en']").first
258
- component_type = component_type_node ? component_type_node.text.strip : ""
259
-
260
- path = component['path'] || ""
261
- category_node = component.xpath("./Category/Display[@lang='en']").first
262
- category = category_node ? category_node.text.strip : ""
263
-
264
- release_date = component['releaseDate'] || ""
265
- version = component['dellVersion'] || component['vendorVersion'] || ""
266
-
267
- # Only include firmware updates
268
- if component_type.include?("Firmware") ||
269
- category.include?("BIOS") ||
270
- category.include?("Firmware") ||
271
- category.include?("iDRAC") ||
272
- name.include?("BIOS") ||
273
- name.include?("Firmware") ||
274
- name.include?("iDRAC")
275
-
276
- download_url = "#{DELL_CATALOG_BASE}/#{path}"
277
-
278
- updates << {
279
- name: name,
280
- version: version,
281
- category: category,
282
- release_date: release_date,
283
- download_url: download_url,
284
- file_name: File.basename(path),
285
- matching_system_id: matching_id
286
- }
287
- end
288
- end
289
-
290
- puts "Found #{updates.length} firmware updates for #{@model}"
291
- updates
292
- rescue => e
293
- puts "Error parsing Dell catalog: #{e.message}"
294
- puts e.backtrace.join("\n")
295
- []
296
- end
297
- end
298
-
299
- def download_firmware(update)
300
- return false unless update && update[:download_url]
301
-
302
- begin
303
- file_path = File.join(DOWNLOAD_DIR, update[:file_name])
304
-
305
- puts "Downloading #{update[:name]} v#{update[:version]} (#{update[:file_name]})..."
306
- File.open(file_path, 'wb') do |file|
307
- file.write(URI.open(update[:download_url]).read)
308
- end
309
-
310
- puts "Downloaded to #{file_path}"
311
-
312
- # If it's a zip file, extract it to find the DUP or EFI file
313
- if file_path.end_with?('.zip')
314
- extract_dir = File.join(DOWNLOAD_DIR, File.basename(file_path, '.zip'))
315
- FileUtils.mkdir_p(extract_dir)
316
-
317
- Zip::File.open(file_path) do |zip_file|
318
- zip_file.each do |entry|
319
- # Look for .EFI files which are suitable for Redfish update
320
- if entry.name.end_with?('.EFI', '.efi')
321
- entry_path = File.join(extract_dir, entry.name)
322
- entry.extract(entry_path)
323
- puts "Extracted #{entry.name} to #{entry_path}"
324
- return entry_path
325
- end
326
- end
327
- end
328
- end
329
-
330
- return file_path
331
- rescue => e
332
- puts "Error downloading firmware: #{e.message}"
333
- false
334
- end
335
- end
336
-
337
- def upload_firmware(file_path)
338
- begin
339
- # Step 1: Get the HttpPushUri from UpdateService
340
- puts "Getting HttpPushUri from UpdateService..."
341
- update_service_uri = URI.parse("#{@redfish_base_url}/UpdateService")
342
- update_service_response = make_redfish_request(update_service_uri)
343
- update_service_data = JSON.parse(update_service_response.body)
344
-
345
- http_push_uri = update_service_data['HttpPushUri']
346
- if http_push_uri.nil?
347
- http_push_uri = "/redfish/v1/UpdateService/FirmwareInventory"
348
- puts "HttpPushUri not found, using default: #{http_push_uri}"
349
- else
350
- puts "Found HttpPushUri: #{http_push_uri}"
351
- end
352
-
353
- # Step 2: Get the ETag for the HttpPushUri
354
- uri = URI.parse("https://#{@idrac_ip}#{http_push_uri}")
355
- request = Net::HTTP::Get.new(uri)
356
- request.basic_auth(@username, @password)
357
-
358
- http = Net::HTTP.new(uri.host, uri.port)
359
- http.use_ssl = true
360
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
361
-
362
- response = http.request(request)
363
- etag = response['ETag']
364
-
365
- puts "Got ETag: #{etag}"
366
-
367
- # Step 3: Upload the firmware file using multipart/form-data
368
- puts "Uploading firmware file to iDRAC..."
369
-
370
- # Create a boundary for multipart/form-data
371
- boundary = "----WebKitFormBoundary#{SecureRandom.hex(16)}"
372
-
373
- request = Net::HTTP::Post.new(uri)
374
- request.basic_auth(@username, @password)
375
- request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
376
- request['If-Match'] = etag
377
-
378
- # Read the file content
379
- file_content = File.binread(file_path)
380
- filename = File.basename(file_path)
381
-
382
- # Create the multipart body
383
- post_body = []
384
- post_body << "--#{boundary}\r\n"
385
- post_body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n"
386
- post_body << "Content-Type: application/octet-stream\r\n\r\n"
387
- post_body << file_content
388
- post_body << "\r\n--#{boundary}--\r\n"
389
-
390
- request.body = post_body.join
391
-
392
- response = http.request(request)
393
-
394
- if response.code.to_i == 201
395
- puts "Firmware file uploaded successfully"
396
- upload_data = JSON.parse(response.body)
397
- available_entry = upload_data['Id']
398
- puts "Created entry ID: #{available_entry}"
399
-
400
- # Step 4: Initiate the firmware update using SimpleUpdate
401
- puts "Initiating firmware update..."
402
- uri = URI.parse("#{@redfish_base_url}/UpdateService/Actions/UpdateService.SimpleUpdate")
403
-
404
- request = Net::HTTP::Post.new(uri)
405
- request.basic_auth(@username, @password)
406
- request.content_type = 'application/json'
407
-
408
- # Use the URI of the uploaded file
409
- image_uri = "#{http_push_uri}/#{available_entry}"
410
- puts "Using ImageURI: #{image_uri}"
411
-
412
- request.body = JSON.generate({
413
- 'ImageURI' => image_uri,
414
- '@Redfish.OperationApplyTime' => 'Immediate'
415
- })
416
-
417
- response = http.request(request)
418
-
419
- if response.code.to_i >= 200 && response.code.to_i < 300
420
- puts "Firmware update initiated successfully"
421
-
422
- # Extract job ID from response
423
- job_id = nil
424
- if response['Location']
425
- job_id = response['Location'].split('/').last
426
- elsif response.body && !response.body.empty?
427
- begin
428
- response_data = JSON.parse(response.body)
429
- if response_data['@odata.id']
430
- job_id = response_data['@odata.id'].split('/').last
431
- elsif response_data['Id']
432
- job_id = response_data['Id']
433
- end
434
- rescue JSON::ParserError
435
- # Not JSON, ignore
436
- end
437
- end
438
-
439
- # If we have a job ID, monitor the job
440
- if job_id
441
- puts "Monitoring job #{job_id}..."
442
- monitor_update_job(job_id)
443
- else
444
- puts "No job ID found. Cannot monitor progress."
445
- end
446
-
447
- return true
448
- else
449
- puts "Firmware update failed: #{response.code} - #{response.body}"
450
- return false
451
- end
452
- else
453
- puts "Firmware file upload failed: #{response.code} - #{response.body}"
454
- return false
455
- end
456
- rescue => e
457
- puts "Error uploading firmware: #{e.message}"
458
- puts e.backtrace.join("\n")
459
- false
460
- end
461
- end
462
-
463
- def monitor_update_job(job_id)
464
- uri = URI.parse("#{@redfish_base_url}/TaskService/Tasks/#{job_id}")
465
-
466
- puts "Update job started. This may take several minutes."
467
- puts "Progress: [" + " " * 50 + "] 0%"
468
-
469
- completed = false
470
- last_message = ""
471
- start_time = Time.now
472
-
473
- while !completed && (Time.now - start_time) < 3600 # 1 hour timeout
474
- begin
475
- response = make_redfish_request(uri)
476
-
477
- if response.code.to_i >= 200 && response.code.to_i < 300
478
- job_data = JSON.parse(response.body)
479
-
480
- state = job_data['TaskState'] || ""
481
- percent = job_data['PercentComplete'] || 0
482
- status = job_data['TaskStatus'] || ""
483
- message = job_data['Messages'].first['Message'] rescue ""
484
-
485
- # Only update if something changed
486
- if last_message != "#{state} - #{percent}% - #{status} - #{message}"
487
- # Clear the current line
488
- print "\r" + " " * 80 + "\r"
489
-
490
- # Print progress bar
491
- progress = (percent / 2).to_i
492
- progress_bar = "[" + "#" * progress + " " * (50 - progress) + "] #{percent}%"
493
- puts "Status: #{state} - #{status}"
494
- puts "Progress: #{progress_bar}"
495
- puts "Message: #{message}" if !message.empty?
496
-
497
- last_message = "#{state} - #{percent}% - #{status} - #{message}"
498
- end
499
-
500
- # Check if job is completed
501
- if ['Completed', 'Exception', 'Killed', 'Cancelled'].include?(state)
502
- completed = true
503
-
504
- if state == 'Completed' && status == 'OK'
505
- puts "\nFirmware update completed successfully!"
506
- else
507
- puts "\nFirmware update failed: #{state} - #{status}"
508
- puts "Error message: #{message}" if !message.empty?
509
-
510
- # Try to get more detailed error information
511
- if job_data['Messages'] && !job_data['Messages'].empty?
512
- job_data['Messages'].each do |msg|
513
- puts "- #{msg['Message']}" if msg['Message']
514
- end
515
- end
516
- end
517
- end
518
- else
519
- puts "Error checking job status: #{response.code}"
520
- end
521
- rescue => e
522
- puts "Error monitoring job: #{e.message}"
523
- end
524
-
525
- # Don't hammer the API
526
- sleep 10 unless completed
527
- end
528
-
529
- if !completed
530
- puts "Timeout waiting for job to complete. Please check the iDRAC web interface."
531
- end
532
- end
533
-
534
- def compare_firmware_versions(current_firmware, available_updates)
535
- puts "\nFirmware Version Comparison:"
536
- puts "=" * 100
537
- puts "%-30s %-20s %-20s %-10s %-15s %s" % ["Component", "Current Version", "Available Version", "Updateable", "Category", "Status"]
538
- puts "-" * 100
539
-
540
- # Track components we've already displayed to avoid duplicates
541
- displayed_components = Set.new
542
-
543
- # First show current firmware with available updates
544
- current_firmware.each do |firmware|
545
- # Make sure firmware name is not nil
546
- firmware_name = firmware[:name] || ""
547
-
548
- # Skip if we've already displayed this component
549
- next if displayed_components.include?(firmware_name.downcase)
550
- displayed_components.add(firmware_name.downcase)
551
-
552
- # Extract key identifiers from the firmware name
553
- # This helps match components like "Intel(R) Ethernet 10G 4P X520" to "Intel NIC Family for X520"
554
- identifiers = extract_identifiers(firmware_name)
555
-
556
- # Try to find a matching update
557
- matching_update = available_updates.find do |update|
558
- update_name = update[:name] || ""
559
-
560
- # Check if any of our identifiers match the update name
561
- identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
562
- # Or if the update name contains the firmware name
563
- update_name.downcase.include?(firmware_name.downcase) ||
564
- # Or if the firmware name contains the update name
565
- firmware_name.downcase.include?(update_name.downcase)
566
- end
567
-
568
- current_version = firmware[:version] || "Unknown"
569
- available_version = matching_update ? matching_update[:version] : "N/A"
570
- category = matching_update ? matching_update[:category] : "N/A"
571
-
572
- # Determine update status
573
- status = if available_version == "N/A"
574
- "No update available"
575
- elsif available_version == current_version
576
- "Current"
577
- else
578
- "UPDATE AVAILABLE"
579
- end
580
-
581
- puts "%-30s %-20s %-20s %-10s %-15s %s" % [
582
- firmware_name.to_s[0..29],
583
- current_version,
584
- available_version,
585
- firmware[:updateable] ? "Yes" : "No",
586
- category,
587
- status
588
- ]
589
- end
590
-
591
- # Then show available updates that don't match any current firmware
592
- available_updates.each do |update|
593
- update_name = update[:name] || ""
594
-
595
- # Skip if we've already displayed this component
596
- next if displayed_components.include?(update_name.downcase)
597
- displayed_components.add(update_name.downcase)
598
-
599
- # Skip if this update was already matched to a current firmware
600
- next if current_firmware.any? do |firmware|
601
- firmware_name = firmware[:name] || ""
602
- identifiers = extract_identifiers(firmware_name)
603
-
604
- identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
605
- update_name.downcase.include?(firmware_name.downcase) ||
606
- firmware_name.downcase.include?(update_name.downcase)
607
- end
608
-
609
- puts "%-30s %-20s %-20s %-10s %-15s %s" % [
610
- update_name.to_s[0..29],
611
- "Not Installed",
612
- update[:version] || "Unknown",
613
- "N/A",
614
- update[:category] || "N/A",
615
- "NEW COMPONENT"
616
- ]
617
- end
618
-
619
- puts "=" * 100
620
- end
621
-
622
- # Helper method to extract identifiers from component names
623
- def extract_identifiers(name)
624
- return [] unless name
625
-
626
- identifiers = []
627
-
628
- # Extract model numbers like X520, I350, etc.
629
- model_matches = name.scan(/[IX]\d{3,4}/)
630
- identifiers.concat(model_matches)
631
-
632
- # Extract other common identifiers
633
- if name.include?("NIC") || name.include?("Ethernet") || name.include?("Network")
634
- identifiers << "NIC"
635
- end
636
-
637
- if name.include?("PERC") || name.include?("RAID")
638
- identifiers << "PERC"
639
- # Extract PERC model like H730
640
- perc_match = name.match(/PERC\s+([A-Z]\d{3})/)
641
- identifiers << perc_match[1] if perc_match
642
- end
643
-
644
- if name.include?("BIOS")
645
- identifiers << "BIOS"
646
- end
647
-
648
- if name.include?("iDRAC") || name.include?("IDRAC")
649
- identifiers << "iDRAC"
650
- end
651
-
652
- if name.include?("Power Supply") || name.include?("PSU")
653
- identifiers << "PSU"
654
- end
655
-
656
- identifiers
657
- end
658
-
659
- private
660
-
661
- def make_redfish_request(uri)
662
- request = Net::HTTP::Get.new(uri)
663
- request.basic_auth(@username, @password)
664
-
665
- http = Net::HTTP.new(uri.host, uri.port)
666
- http.use_ssl = true
667
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
668
-
669
- http.request(request)
670
- end
671
- end
672
-
673
- # Example usage
674
- if __FILE__ == $0
675
- if ARGV.length < 1
676
- puts "Usage: #{$0} <idrac_ip> [username] [password]"
677
- exit 1
678
- end
679
-
680
- idrac_ip = ARGV[0]
681
- username = ARGV[1] || DellFirmwareUpdater::DEFAULT_USERNAME
682
- password = ARGV[2] || DellFirmwareUpdater::DEFAULT_PASSWORD
683
-
684
- updater = DellFirmwareUpdater.new(idrac_ip, username, password)
685
-
686
- # Get system information
687
- system_info = updater.get_system_info
688
- if system_info
689
- # Get current firmware versions
690
- current_firmware = updater.get_current_firmware_versions
691
-
692
- # Download Dell catalog
693
- catalog_path = updater.download_dell_catalog
694
-
695
- if catalog_path
696
- # Get available updates from catalog
697
- available_updates = updater.get_available_updates_from_catalog(catalog_path)
698
-
699
- if available_updates.any?
700
- # Compare current vs available versions
701
- updater.compare_firmware_versions(current_firmware, available_updates)
702
-
703
- # Prompt for which update to download
704
- puts "\nAvailable updates:"
705
- available_updates.each_with_index do |update, index|
706
- puts "#{index + 1}. #{update[:name]} - #{update[:version]} (#{update[:category]})"
707
- end
708
-
709
- print "\nEnter the number of the update to download (or 0 to exit): "
710
- selection = STDIN.gets.chomp.to_i
711
-
712
- if selection > 0 && selection <= available_updates.length
713
- update = available_updates[selection - 1]
714
- firmware_path = updater.download_firmware(update)
715
-
716
- if firmware_path
717
- print "Do you want to upload and install this firmware? (y/n): "
718
- if STDIN.gets.chomp.downcase == 'y'
719
- updater.upload_firmware(firmware_path)
720
- end
721
- end
722
- end
723
- else
724
- puts "No updates found for #{system_info[:model]}"
725
- end
726
- end
727
- end
728
- end
729
-