class IDRAC::Firmware
def check_updates(catalog_path = nil)
def check_updates(catalog_path = nil) # Download catalog if not provided catalog_path ||= download_catalog # Get system inventory inventory = get_system_inventory # Parse catalog catalog_doc = File.open(catalog_path) { |f| Nokogiri::XML(f) } # Extract service tag service_tag = inventory[:system][:service_tag] puts "Checking updates for system with service tag: #{service_tag}" # Find applicable updates updates = [] # Get current firmware versions current_versions = {} inventory[:firmware].each do |fw| current_versions[fw[:name]] = fw[:version] end # Find matching components in catalog catalog_doc.xpath('//SoftwareComponent').each do |component| name = component.at_xpath('Name')&.text version = component.at_xpath('Version')&.text path = component.at_xpath('Path')&.text component_type = component.at_xpath('ComponentType')&.text # Check if this component matches any of our firmware inventory[:firmware].each do |fw| if fw[:name].include?(name) || name.include?(fw[:name]) current_version = fw[:version] # Simple version comparison (this could be improved) if version != current_version updates << { name: name, current_version: current_version, available_version: version, path: path, component_type: component_type, download_url: "https://downloads.dell.com/#{path}" } end end end end updates end
def download_catalog(output_dir = nil)
def download_catalog(output_dir = nil) output_dir ||= Dir.pwd FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir) catalog_gz_path = File.join(output_dir, "Catalog.xml.gz") catalog_path = File.join(output_dir, "Catalog.xml") puts "Downloading Dell catalog from #{CATALOG_URL}..." uri = URI.parse(CATALOG_URL) Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| request = Net::HTTP::Get.new(uri) http.request(request) do |response| if response.code == "200" File.open(catalog_gz_path, 'wb') do |file| response.read_body do |chunk| file.write(chunk) end end else raise Error, "Failed to download catalog: #{response.code} #{response.message}" end end end puts "Extracting catalog..." system("gunzip -f #{catalog_gz_path}") if File.exist?(catalog_path) puts "Catalog downloaded and extracted to #{catalog_path}" return catalog_path else raise Error, "Failed to extract catalog" end end
def get_job_status(job_id)
def get_job_status(job_id) response = client.authenticated_request( :get, "/redfish/v1/TaskService/Tasks/#{job_id}" ) if response.status != 200 raise Error, "Failed to get job status with status #{response.status}: #{response.body}" end response_data = JSON.parse(response.body) response_data['TaskState'] || 'Unknown' end
def get_system_inventory
def get_system_inventory 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
def initialize(client)
def initialize(client) @client = client end
def interactive_update(catalog_path = nil)
def interactive_update(catalog_path = nil) updates = check_updates(catalog_path) if updates.empty? puts "No updates available for your system." return end puts "\nAvailable updates:" updates.each_with_index do |update, index| puts "#{index + 1}. #{update[:name]}: #{update[:current_version]} -> #{update[:available_version]}" end puts "\nEnter the number of the update to install (or 'all' for all updates, 'q' to quit):" choice = STDIN.gets.chomp return if choice.downcase == 'q' selected_updates = if choice.downcase == 'all' updates else index = choice.to_i - 1 if index >= 0 && index < updates.size [updates[index]] else puts "Invalid selection." return end end selected_updates.each do |update| puts "Downloading #{update[:name]} version #{update[:available_version]}..." # Create temp directory temp_dir = Dir.mktmpdir begin # Download the update update_filename = File.basename(update[:path]) update_path = File.join(temp_dir, update_filename) uri = URI.parse(update[:download_url]) Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| request = Net::HTTP::Get.new(uri) http.request(request) do |response| if response.code == "200" File.open(update_path, 'wb') do |file| response.read_body do |chunk| file.write(chunk) end end else puts "Failed to download update: #{response.code} #{response.message}" next end end end puts "Installing #{update[:name]} version #{update[:available_version]}..." job_id = update(update_path, wait: true) puts "Update completed with job ID: #{job_id}" ensure # Clean up temp directory FileUtils.remove_entry(temp_dir) end end end
def update(firmware_path, options = {})
def update(firmware_path, options = {}) # Validate firmware file exists unless File.exist?(firmware_path) raise Error, "Firmware file not found: #{firmware_path}" end # Login to iDRAC client.login 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
def upload_firmware(firmware_path)
def upload_firmware(firmware_path) puts "Uploading firmware file: #{firmware_path}" # Get the HttpPushUri from UpdateService update_service_response = client.authenticated_request(:get, "/redfish/v1/UpdateService") update_service_data = JSON.parse(update_service_response.body) http_push_uri = update_service_data['HttpPushUri'] if http_push_uri.nil? http_push_uri = "/redfish/v1/UpdateService/FirmwareInventory" puts "HttpPushUri not found, using default: #{http_push_uri}" else puts "Found HttpPushUri: #{http_push_uri}" end # Get the ETag for the HttpPushUri etag_response = client.authenticated_request(:get, http_push_uri) etag = etag_response.headers['etag'] puts "Got ETag: #{etag}" # Create a boundary for multipart/form-data boundary = "----WebKitFormBoundary#{SecureRandom.hex(16)}" # Read the file content file_content = File.binread(firmware_path) filename = File.basename(firmware_path) # Create the multipart body post_body = [] post_body << "--#{boundary}\r\n" post_body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n" post_body << "Content-Type: application/octet-stream\r\n\r\n" post_body << file_content post_body << "\r\n--#{boundary}--\r\n" # Upload the firmware response = client.authenticated_request( :post, http_push_uri, { headers: { 'Content-Type' => "multipart/form-data; boundary=#{boundary}", 'If-Match' => etag }, body: post_body.join } ) if response.status < 200 || response.status >= 300 raise Error, "Firmware upload failed with status #{response.status}: #{response.body}" end # Extract job ID from response response_data = JSON.parse(response.body) job_id = response_data['Id'] || response_data['TaskId'] if job_id.nil? raise Error, "Failed to extract job ID from firmware upload response" end puts "Firmware update job created with ID: #{job_id}" job_id end
def wait_for_job_completion(job_id, timeout)
def wait_for_job_completion(job_id, timeout) puts "Waiting for firmware update job #{job_id} to complete..." start_time = Time.now loop do status = get_job_status(job_id) case status when 'Completed' puts "Firmware update completed successfully" return true when 'Failed' raise Error, "Firmware update job failed" when 'Scheduled', 'Running', 'Downloading', 'Pending' # Job still in progress else puts "Unknown job status: #{status}" end if Time.now - start_time > timeout raise Error, "Firmware update timed out after #{timeout} seconds" end # Wait before checking again sleep 10 end end