Class: IDRAC::FirmwareCatalog

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

Constant Summary collapse

DELL_CATALOG_BASE =
'https://downloads.dell.com'
DELL_CATALOG_URL =
"#{DELL_CATALOG_BASE}/catalog/Catalog.xml.gz"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(catalog_path = nil) ⇒ FirmwareCatalog

Returns a new instance of FirmwareCatalog.



15
16
17
# File 'lib/idrac/firmware_catalog.rb', line 15

def initialize(catalog_path = nil)
  @catalog_path = catalog_path
end

Instance Attribute Details

#catalog_pathObject (readonly)

Returns the value of attribute catalog_path.



13
14
15
# File 'lib/idrac/firmware_catalog.rb', line 13

def catalog_path
  @catalog_path
end

Instance Method Details

#compare_versions(current_version, available_version) ⇒ Object



268
269
270
271
272
273
274
275
276
277
278
279
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
# File 'lib/idrac/firmware_catalog.rb', line 268

def compare_versions(current_version, available_version)
  # If versions are identical, no update needed
  return false if current_version == available_version
  
  # If either version is N/A, no update available
  return false if current_version == "N/A" || available_version == "N/A"
  
  # Try to handle Dell's version format (e.g., A00, A01, etc.)
  if available_version.match?(/^[A-Z]\d+$/)
    # If current version doesn't match Dell's format, assume update is needed
    return true unless current_version.match?(/^[A-Z]\d+$/)
    
    # Compare Dell version format (A00 < A01 < A02 < ... < B00 < B01 ...)
    available_letter = available_version[0]
    available_number = available_version[1..-1].to_i
    
    current_letter = current_version[0]
    current_number = current_version[1..-1].to_i
    
    return true if current_letter < available_letter
    return true if current_letter == available_letter && current_number < available_number
    return false
  end
  
  # For numeric versions, try to compare them
  if current_version.match?(/^[\d\.]+$/) && available_version.match?(/^[\d\.]+$/)
    current_parts = current_version.split('.').map(&:to_i)
    available_parts = available_version.split('.').map(&:to_i)
    
    # Compare each part of the version
    max_length = [current_parts.length, available_parts.length].max
    max_length.times do |i|
      current_part = current_parts[i] || 0
      available_part = available_parts[i] || 0
      
      return true if current_part < available_part
      return false if current_part > available_part
    end
    
    # If we get here, versions are equal
    return false
  end
  
  # If we can't determine, assume update is needed
  true
end

#download(output_dir = nil) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/idrac/firmware_catalog.rb', line 19

def download(output_dir = nil)
  # Default to ~/.idrac directory
  output_dir ||= File.expand_path('~/.idrac')
  FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
  
  catalog_gz = File.join(output_dir, 'Catalog.xml.gz')
  catalog_xml = File.join(output_dir, 'Catalog.xml')
  
  puts "Downloading Dell catalog from #{DELL_CATALOG_URL}...".light_cyan
  
  begin
    # Download the catalog
    URI.open(DELL_CATALOG_URL) do |remote_file|
      File.open(catalog_gz, 'wb') do |local_file|
        local_file.write(remote_file.read)
      end
    end
    
    puts "Extracting catalog...".light_cyan
    
    # Extract the catalog
    system("gunzip -f #{catalog_gz}")
    
    if File.exist?(catalog_xml)
      puts "Catalog downloaded and extracted to #{catalog_xml}".green
      @catalog_path = catalog_xml
      return catalog_xml
    else
      raise Error, "Failed to extract catalog"
    end
  rescue => e
    puts "Error downloading catalog: #{e.message}".red.bold
    raise Error, "Failed to download Dell catalog: #{e.message}"
  end
end

#extract_identifiers(name) ⇒ Object



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
# File 'lib/idrac/firmware_catalog.rb', line 193

def extract_identifiers(name)
  return [] unless name
  
  identifiers = []
  
  # Extract model numbers like X520, I350, etc.
  model_matches = name.scan(/[IX]\d{3,4}/)
  identifiers.concat(model_matches)
  
  # Extract PERC model like H730
  perc_matches = name.scan(/[HP]\d{3,4}/)
  identifiers.concat(perc_matches)
  
  # Extract other common identifiers
  if name.include?("NIC") || name.include?("Ethernet") || name.include?("Network")
    identifiers << "NIC"
  end
  
  if name.include?("PERC") || name.include?("RAID")
    identifiers << "PERC"
    # Extract PERC model like H730
    perc_match = name.match(/PERC\s+([A-Z]\d{3})/)
    identifiers << perc_match[1] if perc_match
  end
  
  if name.include?("BIOS")
    identifiers << "BIOS"
  end
  
  if name.include?("iDRAC") || name.include?("IDRAC") || name.include?("Remote Access Controller")
    identifiers << "iDRAC"
  end
  
  if name.include?("Power Supply") || name.include?("PSU")
    identifiers << "PSU"
  end
  
  if name.include?("Lifecycle Controller")
    identifiers << "LC"
  end
  
  if name.include?("CPLD")
    identifiers << "CPLD"
  end
  
  identifiers
end

#find_system_models(model_name) ⇒ Object



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
114
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
# File 'lib/idrac/firmware_catalog.rb', line 62

def find_system_models(model_name)
  doc = parse
  models = []
  
  # Extract model code from full model name (e.g., "PowerEdge R640" -> "R640")
  model_code = nil
  if model_name.include?("PowerEdge")
    model_code = model_name.split.last
  else
    model_code = model_name
  end
  
  puts "Searching for model: #{model_name} (code: #{model_code})"
  
  # Build a mapping of model names to system IDs
  model_to_system_id = {}
  
  doc.xpath('//SupportedSystems/Brand/Model').each do |model|
    system_id = model['systemID'] || model['id']
    name = model.at_xpath('Display')&.text
    code = model.at_xpath('Code')&.text
    
    if name && system_id
      model_to_system_id[name] = {
        name: name,
        code: code,
        id: system_id
      }
      
      # Also map just the model number (R640, etc.)
      if name =~ /[RT]\d+/
        model_short = name.match(/([RT]\d+\w*)/)[1]
        model_to_system_id[model_short] = {
          name: name,
          code: code,
          id: system_id
        }
      end
    end
  end
  
  # Try exact match first
  if model_to_system_id[model_name]
    models << model_to_system_id[model_name]
  end
  
  # Try model code match
  if model_to_system_id[model_code]
    models << model_to_system_id[model_code]
  end
  
  # If we still don't have a match, try a more flexible approach
  if models.empty?
    model_to_system_id.each do |name, model_info|
      if name.include?(model_code) || model_code.include?(name)
        models << model_info
      end
    end
  end
  
  # If still no match, try matching by systemID directly
  if models.empty?
    doc.xpath('//SupportedSystems/Brand/Model').each do |model|
      system_id = model['systemID'] || model['id']
      name = model.at_xpath('Display')&.text
      code = model.at_xpath('Code')&.text
      
      if code && code.downcase == model_code.downcase
        models << {
          name: name,
          code: code,
          id: system_id
        }
      end
    end
  end
  
  models.uniq { |m| m[:id] }
end

#find_updates_for_system(system_id) ⇒ Object



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
# File 'lib/idrac/firmware_catalog.rb', line 142

def find_updates_for_system(system_id)
  doc = parse
  updates = []
  
  # Find all SoftwareComponents
  doc.xpath("//SoftwareComponent").each do |component|
    # Check if this component supports our system ID
    supported_system_ids = component.xpath(".//SupportedSystems/Brand/Model/@systemID | .//SupportedSystems/Brand/Model/@id").map(&:value)
    
    next unless supported_system_ids.include?(system_id)
    
    # Get component details
    name_node = component.xpath("./Name/Display[@lang='en']").first
    name = name_node ? name_node.text.strip : ""
    
    component_type_node = component.xpath("./ComponentType/Display[@lang='en']").first
    component_type = component_type_node ? component_type_node.text.strip : ""
    
    path = component['path'] || ""
    category_node = component.xpath("./Category/Display[@lang='en']").first
    category = category_node ? category_node.text.strip : ""
    
    version = component['dellVersion'] || component['vendorVersion'] || ""
    
    # Skip if missing essential information
    next if name.empty? || path.empty? || version.empty?
    
    # Only include firmware updates
    if component_type.include?("Firmware") ||
       category.include?("BIOS") ||
       category.include?("Firmware") ||
       category.include?("iDRAC") ||
       name.include?("BIOS") ||
       name.include?("Firmware") ||
       name.include?("iDRAC")
      
      updates << {
        name: name,
        version: version,
        path: path,
        component_type: component_type,
        category: category,
        download_url: "https://downloads.dell.com/#{path}"
      }
    end
  end
  
  puts "Found #{updates.size} firmware updates for system ID #{system_id}"
  updates
end

#match_component(firmware_name, catalog_name) ⇒ Object



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
# File 'lib/idrac/firmware_catalog.rb', line 241

def match_component(firmware_name, catalog_name)
  # Normalize names for comparison
  catalog_name_lower = catalog_name.downcase.strip
  firmware_name_lower = firmware_name.downcase.strip
  
  # 1. Direct substring match
  return true if catalog_name_lower.include?(firmware_name_lower) || firmware_name_lower.include?(catalog_name_lower)
  
  # 2. Special case for BIOS
  return true if catalog_name_lower.include?("bios") && firmware_name_lower.include?("bios")
  
  # 3. Check identifiers
  firmware_identifiers = extract_identifiers(firmware_name)
  catalog_identifiers = extract_identifiers(catalog_name)
  
  return true if (firmware_identifiers & catalog_identifiers).any?
  
  # 4. Special case for network adapters
  if (firmware_name_lower.include?("ethernet") || firmware_name_lower.include?("network")) &&
     (catalog_name_lower.include?("ethernet") || catalog_name_lower.include?("network"))
    return true
  end
  
  # No match found
  false
end

#parseObject

Raises:



55
56
57
58
59
60
# File 'lib/idrac/firmware_catalog.rb', line 55

def parse
  raise Error, "No catalog path specified" unless @catalog_path
  raise Error, "Catalog file not found: #{@catalog_path}" unless File.exist?(@catalog_path)
  
  File.open(@catalog_path) { |f| Nokogiri::XML(f) }
end