Class: IDRAC::Firmware

Inherits:
Object
  • Object
show all
Defined in:
lib/idrac/firmware.rb

Constant Summary collapse

CATALOG_URL =
"https://downloads.dell.com/catalog/Catalog.xml.gz"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client) ⇒ Firmware

Returns a new instance of Firmware.



20
21
22
# File 'lib/idrac/firmware.rb', line 20

def initialize(client)
  @client = client
end

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



16
17
18
# File 'lib/idrac/firmware.rb', line 16

def client
  @client
end

Instance Method Details

#check_updates(catalog_path = nil) ⇒ Object

Raises:



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/idrac/firmware.rb', line 115

def check_updates(catalog_path = nil)
  # Ensure we have a client for system inventory
  raise Error, "Client is required for checking updates" unless client
  
  # Download catalog if not provided
  catalog_path ||= download_catalog
  
  # Get system inventory
  inventory = get_system_inventory
  
  # Create a FirmwareCatalog instance
  catalog = FirmwareCatalog.new(catalog_path)
  
  # Extract system information
  system_model = inventory[:system][:model]
  service_tag = inventory[:system][:service_tag]
  
  puts "Checking updates for system with service tag: #{service_tag}".light_cyan
  puts "Searching for updates for model: #{system_model}".light_cyan
  
  # Find system models in the catalog
  models = catalog.find_system_models(system_model)
  
  if models.empty?
    puts "No matching system model found in catalog".yellow
    return []
  end
  
  # Use the first matching model
  model = models.first
  puts "Found system IDs for #{model[:name]}: #{model[:id]}".green
  
  # Find updates for this system
  catalog_updates = catalog.find_updates_for_system(model[:id])
  puts "Found #{catalog_updates.size} firmware updates for #{model[:name]}".green
  
  # Compare current firmware with available updates
  updates = []
  
  # Print header for firmware comparison table
  puts "\nFirmware Version Comparison:".green.bold
  puts "=" * 100
  puts "%-30s %-20s %-20s %-10s %-15s %s" % ["Component", "Current Version", "Available Version", "Updateable", "Category", "Status"]
  puts "-" * 100
  
  # Track components we've already displayed to avoid duplicates
  displayed_components = Set.new
  
  # First show current firmware with available updates
  inventory[:firmware].each do |fw|
    # Make sure firmware name is not nil
    firmware_name = fw[:name] || ""
    
    # Skip if we've already displayed this component
    next if displayed_components.include?(firmware_name.downcase)
    displayed_components.add(firmware_name.downcase)
    
    # Extract key identifiers from the firmware name
    identifiers = extract_identifiers(firmware_name)
    
    # Try to find a matching update
    matching_updates = catalog_updates.select do |update|
      update_name = update[:name] || ""
      
      # Check if any of our identifiers match the update name
      identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
      # Or if the update name contains the firmware name
      update_name.downcase.include?(firmware_name.downcase) ||
      # Or if the firmware name contains the update name
      firmware_name.downcase.include?(update_name.downcase)
    end
    
    if matching_updates.any?
      # Use the first matching update
      update = matching_updates.first
      
      # Check if version is newer
      needs_update = catalog.compare_versions(fw[:version], update[:version])
      
      # Add to updates list if needed
      if needs_update && fw[:updateable]
        updates << {
          name: fw[:name],
          current_version: fw[:version],
          available_version: update[:version],
          path: update[:path],
          component_type: update[:component_type],
          category: update[:category],
          download_url: update[:download_url]
        }
        
        # Print row with update available
        puts "%-30s %-20s %-20s %-10s %-15s %s" % [
          fw[:name].to_s[0..29],
          fw[:version],
          update[:version],
          fw[:updateable] ? "Yes".light_green : "No".light_red,
          update[:category] || "N/A",
          "UPDATE AVAILABLE".light_green.bold
        ]
      else
        # Print row with no update needed
        status = if !needs_update
                   "Current".light_blue
                 elsif !fw[:updateable]
                   "Not updateable".light_red
                 else
                   "No update needed".light_yellow
                 end
        
        puts "%-30s %-20s %-20s %-10s %-15s %s" % [
          fw[:name].to_s[0..29],
          fw[:version],
          update[:version] || "N/A",
          fw[:updateable] ? "Yes".light_green : "No".light_red,
          update[:category] || "N/A",
          status
        ]
      end
    else
      # No matching update found
      puts "%-30s %-20s %-20s %-10s %-15s %s" % [
        fw[:name].to_s[0..29],
        fw[:version],
        "N/A",
        fw[:updateable] ? "Yes".light_green : "No".light_red,
        "N/A",
        "No update available".light_yellow
      ]
    end
  end
  
  # Then show available updates that don't match any current firmware
  catalog_updates.each do |update|
    update_name = update[:name] || ""
    
    # Skip if we've already displayed this component
    next if displayed_components.include?(update_name.downcase)
    displayed_components.add(update_name.downcase)
    
    # Skip if this update was already matched to a current firmware
    next if inventory[:firmware].any? do |fw|
      firmware_name = fw[:name] || ""
      identifiers = extract_identifiers(firmware_name)
      
      identifiers.any? { |id| update_name.downcase.include?(id.downcase) } ||
      update_name.downcase.include?(firmware_name.downcase) ||
      firmware_name.downcase.include?(update_name.downcase)
    end
    
    puts "%-30s %-20s %-20s %-10s %-15s %s" % [
      update_name.to_s[0..29],
      "Not Installed".light_red,
      update[:version] || "Unknown",
      "N/A",
      update[:category] || "N/A",
      "NEW COMPONENT".light_blue
    ]
  end
  
  puts "=" * 100
  
  updates
end

#download_catalog(output_dir = nil) ⇒ Object



47
48
49
50
51
# File 'lib/idrac/firmware.rb', line 47

def download_catalog(output_dir = nil)
  # Use the new FirmwareCatalog class
  catalog = FirmwareCatalog.new
  catalog.download(output_dir)
end

#download_firmware(update) ⇒ Object



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/idrac/firmware.rb', line 390

def download_firmware(update)
  return false unless update && update[:download_url]
  
  begin
    # Create a temporary directory for the download
    temp_dir = Dir.mktmpdir
    
    # Extract the filename from the path
    filename = File.basename(update[:path])
    local_path = File.join(temp_dir, filename)
    
    puts "Downloading firmware from #{update[:download_url]}".light_cyan
    puts "Saving to #{local_path}".light_cyan
    
    # Download the file
    uri = URI.parse(update[:download_url])
    
    Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
      request = Net::HTTP::Get.new(uri)
      
      http.request(request) do |response|
        if response.code == "200"
          File.open(local_path, 'wb') do |file|
            response.read_body do |chunk|
              file.write(chunk)
            end
          end
          
          puts "Download completed successfully".green
          return local_path
        else
          puts "Failed to download firmware: #{response.code} #{response.message}".red
          return false
        end
      end
    end
  rescue => e
    puts "Error downloading firmware: #{e.message}".red.bold
    return false
  end
end

#get_power_stateObject

Raises:



432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/idrac/firmware.rb', line 432

def get_power_state
  # Ensure we have a client
  raise Error, "Client is required for power management" unless client
  
  # Login to iDRAC if needed
  client. unless client.instance_variable_get(:@session_id)
  
  # Get system information
  response = client.authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
  
  if response.status == 200
    system_data = JSON.parse(response.body)
    return system_data["PowerState"]
  else
    raise Error, "Failed to get power state. Status code: #{response.status}"
  end
end

#get_system_inventoryObject

Raises:



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/idrac/firmware.rb', line 53

def get_system_inventory
  # Ensure we have a client
  raise Error, "Client is required for system inventory" unless client
  
  puts "Retrieving system inventory..."
  
  # Get basic system information
  system_uri = URI.parse("#{client.base_url}/redfish/v1/Systems/System.Embedded.1")
  system_response = client.authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
  
  if system_response.status != 200
    raise Error, "Failed to get system information: #{system_response.status}"
  end
  
  system_data = JSON.parse(system_response.body)
  
  # Get firmware inventory
  firmware_uri = URI.parse("#{client.base_url}/redfish/v1/UpdateService/FirmwareInventory")
  firmware_response = client.authenticated_request(:get, "/redfish/v1/UpdateService/FirmwareInventory")
  
  if firmware_response.status != 200
    raise Error, "Failed to get firmware inventory: #{firmware_response.status}"
  end
  
  firmware_data = JSON.parse(firmware_response.body)
  
  # Get detailed firmware information for each component
  firmware_inventory = []
  
  if firmware_data['Members'] && firmware_data['Members'].is_a?(Array)
    firmware_data['Members'].each do |member|
      if member['@odata.id']
        component_uri = member['@odata.id']
        component_response = client.authenticated_request(:get, component_uri)
        
        if component_response.status == 200
          component_data = JSON.parse(component_response.body)
          firmware_inventory << {
            name: component_data['Name'],
            id: component_data['Id'],
            version: component_data['Version'],
            updateable: component_data['Updateable'] || false,
            status: component_data['Status'] ? component_data['Status']['State'] : 'Unknown'
          }
        end
      end
    end
  end
  
  {
    system: {
      model: system_data['Model'],
      manufacturer: system_data['Manufacturer'],
      serial_number: system_data['SerialNumber'],
      part_number: system_data['PartNumber'],
      bios_version: system_data['BiosVersion'],
      service_tag: system_data['SKU']
    },
    firmware: firmware_inventory
  }
end

#interactive_update(catalog_path = nil, selected_updates = nil) ⇒ Object



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/idrac/firmware.rb', line 280

def interactive_update(catalog_path = nil, selected_updates = nil)
  # Check if updates are available
  updates = selected_updates || check_updates(catalog_path)
  
  if updates.empty?
    puts "No updates available for your system.".yellow
    return
  end
  
  # Display available updates
  puts "\nAvailable Updates:".green.bold
  updates.each_with_index do |update, index|
    puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}".light_cyan
  end
  
  # If no specific updates were selected, ask the user which ones to install
  if selected_updates.nil?
    puts "\nEnter the number of the update to install (or 'all' for all updates, 'q' to quit):".light_yellow
    input = STDIN.gets.chomp
    
    if input.downcase == 'q'
      puts "Update cancelled.".yellow
      return
    elsif input.downcase == 'all'
      selected_updates = updates
    else
      begin
        index = input.to_i - 1
        if index >= 0 && index < updates.length
          selected_updates = [updates[index]]
        else
          puts "Invalid selection. Please enter a number between 1 and #{updates.length}.".red
          return
        end
      rescue
        puts "Invalid input. Please enter a number, 'all', or 'q'.".red
        return
      end
    end
  end
  
  # Process each selected update
  selected_updates.each do |update|
    puts "\nDownloading #{update[:name]} version #{update[:available_version]}...".light_cyan
    
    begin
      # Download the firmware
      firmware_file = download_firmware(update)
      
      if firmware_file
        puts "Installing #{update[:name]} version #{update[:available_version]}...".light_cyan
        
        begin
          # Upload and install the firmware
          job_id = upload_firmware(firmware_file)
          
          if job_id
            puts "Firmware update job created with ID: #{job_id}".green
            
            # Wait for the job to complete
            success = wait_for_job_completion(job_id, 1800) # 30 minutes timeout
            
            if success
              puts "Successfully updated #{update[:name]} to version #{update[:available_version]}".green.bold
            else
              puts "Failed to update #{update[:name]}. Check the iDRAC web interface for more details.".red
              puts "You may need to wait for any existing jobs to complete before trying again.".yellow
            end
          else
            puts "Failed to create update job for #{update[:name]}".red
          end
        rescue IDRAC::Error => e
          if e.message.include?("already in progress")
            puts "Error: A firmware update is already in progress.".red.bold
            puts "Please wait for the current update to complete before starting another.".yellow
            puts "You can check the status in the iDRAC web interface under Maintenance > System Update.".light_cyan
          elsif e.message.include?("job ID not found") || e.message.include?("Failed to get job status")
            puts "Error: Could not monitor the update job.".red.bold
            puts "The update may still be in progress. Check the iDRAC web interface for status.".yellow
            puts "This can happen if the iDRAC is busy processing the update request.".light_cyan
          else
            puts "Error during firmware update: #{e.message}".red.bold
          end
          
          # If we encounter an error with one update, ask if the user wants to continue with others
          if selected_updates.length > 1 && update != selected_updates.last
            puts "\nDo you want to continue with the remaining updates? (y/n)".light_yellow
            continue = STDIN.gets.chomp.downcase
            break unless continue == 'y'
          end
        end
      else
        puts "Failed to download firmware for #{update[:name]}".red
      end
    rescue => e
      puts "Error processing update for #{update[:name]}: #{e.message}".red.bold
      
      # If we encounter an error with one update, ask if the user wants to continue with others
      if selected_updates.length > 1 && update != selected_updates.last
        puts "\nDo you want to continue with the remaining updates? (y/n)".light_yellow
        continue = STDIN.gets.chomp.downcase
        break unless continue == 'y'
      end
    ensure
      # Clean up temporary files
      FileUtils.rm_f(firmware_file) if firmware_file && File.exist?(firmware_file)
    end
  end
end

#update(firmware_path, options = {}) ⇒ Object

Raises:



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/idrac/firmware.rb', line 24

def update(firmware_path, options = {})
  # Validate firmware file exists
  unless File.exist?(firmware_path)
    raise Error, "Firmware file not found: #{firmware_path}"
  end

  # Ensure we have a client
  raise Error, "Client is required for firmware update" unless client

  # Login to iDRAC
  client. unless client.instance_variable_get(:@session_id)

  # Upload firmware file
  job_id = upload_firmware(firmware_path)
  
  # Check if we should wait for the update to complete
  if options[:wait]
    wait_for_job_completion(job_id, options[:timeout] || 3600)
  end

  job_id
end