class IDRAC::FirmwareCatalog
def compare_versions(current_version, available_version)
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
def download(output_dir = nil)
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
def extract_identifiers(name)
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
def find_system_models(model_name)
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
def find_updates_for_system(system_id)
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
def initialize(catalog_path = nil)
def initialize(catalog_path = nil) @catalog_path = catalog_path end
def match_component(firmware_name, catalog_name)
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
def parse
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