class IDRAC::Client
def authenticated_request(method, path, options = {}, retry_count = 0)
def authenticated_request(method, path, options = {}, retry_count = 0) # Limit retries to prevent infinite loops if retry_count >= 3 puts "Maximum retry count reached for authenticated request".red.bold raise Error, "Maximum retry count reached for authenticated request" end # If we're in direct mode, use Basic Auth if @direct_mode # Create Basic Auth header auth_header = "Basic #{Base64.strict_encode64("#{username}:#{password}")}" # Add the Authorization header to the request options[:headers] ||= {} options[:headers]['Authorization'] = auth_header # Make the request begin response = connection.send(method, path) do |req| req.headers.merge!(options[:headers]) req.body = options[:body] if options[:body] end return response rescue => e puts "Error during authenticated request (direct mode): #{e.message}".red.bold raise Error, "Error during authenticated request: #{e.message}" end else # Use X-Auth-Token if available if @x_auth_token # Add the X-Auth-Token header to the request options[:headers] ||= {} options[:headers]['X-Auth-Token'] = @x_auth_token # Make the request begin response = connection.send(method, path) do |req| req.headers.merge!(options[:headers]) req.body = options[:body] if options[:body] end # Check if the session is still valid if response.status == 401 || response.status == 403 puts "Session expired or invalid, attempting to create a new session...".light_yellow # Try to create a new session if create_redfish_session puts "Successfully created a new session, retrying request...".green return authenticated_request(method, path, options, retry_count + 1) else puts "Failed to create a new session, falling back to direct mode...".light_yellow @direct_mode = true return authenticated_request(method, path, options, retry_count + 1) end end return response rescue => e puts "Error during authenticated request (token mode): #{e.message}".red.bold # Try to create a new session if create_redfish_session puts "Successfully created a new session after error, retrying request...".green return authenticated_request(method, path, options, retry_count + 1) else puts "Failed to create a new session after error, falling back to direct mode...".light_yellow @direct_mode = true return authenticated_request(method, path, options, retry_count + 1) end end else # If we don't have a token, try to create a session if create_redfish_session puts "Successfully created a new session, making request...".green return authenticated_request(method, path, options, retry_count + 1) else puts "Failed to create a session, falling back to direct mode...".light_yellow @direct_mode = true return authenticated_request(method, path, options, retry_count + 1) end end end end
def base_url
def base_url protocol = use_ssl ? 'https' : 'http' "#{protocol}://#{host}:#{port}" end
def connection
def connection @connection ||= Faraday.new(url: base_url, ssl: { verify: verify_ssl }) do |faraday| faraday.request :multipart faraday.request :url_encoded faraday.adapter Faraday.default_adapter end end
def create_redfish_session
def create_redfish_session # Skip if we're in direct mode if @direct_mode puts "Skipping Redfish session creation (direct mode)".light_yellow return false end url = '/redfish/v1/SessionService/Sessions' payload = { "UserName" => username, "Password" => password } # Try first with just Content-Type header (no Basic Auth) begin response = connection.post(url) do |req| req.headers['Content-Type'] = 'application/json' req.body = payload.to_json end if response.status == 201 || response.status == 200 # Extract X-Auth-Token from response headers @x_auth_token = response.headers['X-Auth-Token'] # Extract session location from response headers @session_location = response.headers['Location'] puts "Redfish session created successfully".green @sessions_maxed = false return true end rescue => e puts "First session creation attempt failed: #{e.message}".light_red end # If that fails, try with Basic Auth begin # Use Basic Auth for the session creation basic_auth_headers = { 'Authorization' => "Basic #{Base64.strict_encode64("#{username}:#{password}")}", 'Content-Type' => 'application/json' } response = connection.post(url) do |req| req.headers.merge!(basic_auth_headers) req.body = payload.to_json end if response.status == 201 || response.status == 200 # Extract X-Auth-Token from response headers @x_auth_token = response.headers['X-Auth-Token'] # Extract session location from response headers @session_location = response.headers['Location'] puts "Redfish session created successfully with Basic Auth".green @sessions_maxed = false return true elsif response.status == 400 && response.body.include?("maximum number of user sessions") puts "Maximum sessions reached during Redfish session creation".light_red @sessions_maxed = true # Try to clear sessions if auto_delete_sessions is enabled if @auto_delete_sessions puts "Auto-delete sessions is enabled, attempting to clear sessions".light_cyan if force_clear_sessions puts "Successfully cleared sessions, trying to create a new session".green # Try one more time after clearing response = connection.post(url) do |req| req.headers.merge!(basic_auth_headers) req.body = payload.to_json end if response.status == 201 || response.status == 200 @x_auth_token = response.headers['X-Auth-Token'] @session_location = response.headers['Location'] puts "Redfish session created successfully after clearing sessions".green @sessions_maxed = false return true else puts "Failed to create Redfish session after clearing: #{response.status} - #{response.body}".red # If we still can't create a session, switch to direct mode @direct_mode = true return false end else puts "Failed to clear sessions, switching to direct mode".light_yellow @direct_mode = true return false end else puts "Auto-delete sessions is disabled, switching to direct mode".light_yellow @direct_mode = true return false end else puts "Failed to create Redfish session: #{response.status} - #{response.body}".red # If we get a 415 error, try with form-urlencoded if response.status == 415 puts "Trying with form-urlencoded content type".light_cyan response = connection.post(url) do |req| req.headers['Content-Type'] = 'application/x-www-form-urlencoded' req.headers['Authorization'] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}" req.body = "UserName=#{URI.encode_www_form_component(username)}&Password=#{URI.encode_www_form_component(password)}" end if response.status == 201 || response.status == 200 @x_auth_token = response.headers['X-Auth-Token'] @session_location = response.headers['Location'] puts "Redfish session created successfully with form-urlencoded".green @sessions_maxed = false return true else puts "Failed with form-urlencoded too: #{response.status} - #{response.body}".red @direct_mode = true return false end else @direct_mode = true return false end end rescue => e puts "Error during Redfish session creation: #{e.message}".red.bold @direct_mode = true return false end end
def delete_all_sessions_with_basic_auth
def delete_all_sessions_with_basic_auth puts "Attempting to delete all sessions using Basic Authentication...".light_cyan # First, get the list of sessions sessions_url = '/redfish/v1/SessionService/Sessions' # Create a connection with Basic Auth basic_auth_headers = { 'Authorization' => "Basic #{Base64.strict_encode64("#{username}:#{password}")}", 'Content-Type' => 'application/json' } begin # Get the list of sessions response = connection.get(sessions_url) do |req| req.headers.merge!(basic_auth_headers) end if response.status != 200 puts "Failed to get sessions list: #{response.status} - #{response.body}".red return false end # Parse the response to get session IDs begin sessions_data = JSON.parse(response.body) if sessions_data['Members'] && sessions_data['Members'].any? puts "Found #{sessions_data['Members'].count} active sessions".light_yellow # Delete each session success = true sessions_data['Members'].each do |session| session_url = session['@odata.id'] # Skip if no URL next unless session_url # Delete the session delete_response = connection.delete(session_url) do |req| req.headers.merge!(basic_auth_headers) end if delete_response.status == 200 || delete_response.status == 204 puts "Successfully deleted session: #{session_url}".green else puts "Failed to delete session #{session_url}: #{delete_response.status}".red success = false end # Small delay between deletions sleep(1) end return success else puts "No active sessions found".light_yellow return true end rescue JSON::ParserError => e puts "Error parsing sessions response: #{e.message}".red.bold return false end rescue => e puts "Error during session deletion with Basic Auth: #{e.message}".red.bold return false end end
def delete_redfish_session
def delete_redfish_session return unless @x_auth_token && @session_location begin puts "Deleting Redfish session...".light_cyan # Use the X-Auth-Token for authentication headers = { 'X-Auth-Token' => @x_auth_token } response = connection.delete(@session_location) do |req| req.headers.merge!(headers) end if response.status == 200 || response.status == 204 puts "Redfish session deleted successfully".green @x_auth_token = nil @session_location = nil return true else puts "Failed to delete Redfish session: #{response.status} - #{response.body}".red return false end rescue => e puts "Error during Redfish session deletion: #{e.message}".red.bold return false end end
def force_clear_sessions
def force_clear_sessions puts "Attempting to force clear all sessions...".light_cyan # Try to delete sessions directly using Basic Auth if delete_all_sessions_with_basic_auth puts "Successfully cleared sessions using Basic Auth".green return true else puts "Failed to clear sessions using Basic Auth".red return false end end
def get(path:, headers: {})
def get(path:, headers: {}) # For screenshot functionality, we need to use the WebUI cookies if @cookies.nil? && path.include?('screen/screen.jpg') webui_login unless @session_id end headers_to_use = { "User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36", "Accept-Encoding" => "deflate, gzip" } if @cookies headers_to_use["Cookie"] = @cookies elsif @direct_mode # In direct mode, use Basic Auth headers_to_use["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}" elsif @x_auth_token headers_to_use["X-Auth-Token"] = @x_auth_token end response = HTTParty.get( "#{base_url}/#{path}", headers: headers_to_use.merge(headers), verify: false ) end
def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: true, direct_mode: false, auto_delete_sessions: true)
def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: true, direct_mode: false, auto_delete_sessions: true) @host = host @username = username @password = password @port = port @use_ssl = use_ssl @verify_ssl = verify_ssl @session_id = nil @cookies = nil @x_auth_token = nil @direct_mode = direct_mode @sessions_maxed = false @tried_clearing_sessions = false @auto_delete_sessions = auto_delete_sessions end
def login
def login # If we're in direct mode, skip login attempts if @direct_mode puts "Using direct mode (Basic Auth) for all requests".light_yellow return true end # Try to create a Redfish session if create_redfish_session puts "Successfully logged in to iDRAC using Redfish session".green return true else if @sessions_maxed puts "Maximum sessions reached and could not clear sessions".light_red else puts "Failed to create Redfish session, falling back to direct mode".light_yellow end @direct_mode = true return true end end
def logout
def logout if @x_auth_token delete_redfish_session end if @session_id webui_logout end puts "Logged out from iDRAC".green return true end
def screenshot
def screenshot # Create a Screenshot instance and capture a screenshot screenshot_instance = Screenshot.new(self) screenshot_instance.capture end
def webui_login(retry_count = 0)
def webui_login(retry_count = 0) # Limit retries to prevent infinite loops if retry_count >= 3 puts "Maximum retry count reached for WebUI login".red return false end # Skip if we already have a session ID return true if @session_id begin puts "Logging in to WebUI...".light_cyan # Create the login URL login_url = "#{base_url}/data/login" # Create the login payload payload = { 'user' => username, 'password' => password } # Make the login request response = HTTParty.post( login_url, body: payload, verify: verify_ssl, headers: { 'Content-Type' => 'application/x-www-form-urlencoded' } ) # Check if the login was successful if response.code == 200 # Extract the session ID from the response if response.body.include?('ST2') @session_id = response.body.match(/ST2=([^;]+)/)[1] @cookies = response.headers['set-cookie'] puts "WebUI login successful".green return true else puts "WebUI login failed: No session ID found in response".red return false end elsif response.code == 400 && response.body.include?("maximum number of user sessions") puts "Maximum sessions reached during WebUI login".light_red # Try to clear sessions if auto_delete_sessions is enabled if @auto_delete_sessions && !@tried_clearing_sessions puts "Auto-delete sessions is enabled, attempting to clear sessions".light_cyan @tried_clearing_sessions = true if force_clear_sessions puts "Successfully cleared sessions, trying WebUI login again".green return webui_login(retry_count + 1) else puts "Failed to clear sessions for WebUI login".red return false end else puts "Auto-delete sessions is disabled or already tried clearing".light_yellow return false end else puts "WebUI login failed: #{response.code} - #{response.body}".red return false end rescue => e puts "Error during WebUI login: #{e.message}".red.bold return false end end
def webui_logout
def webui_logout return unless @session_id begin puts "Logging out from WebUI...".light_cyan # Create the logout URL logout_url = "#{base_url}/data/logout" # Make the logout request response = HTTParty.get( logout_url, verify: verify_ssl, headers: { 'Cookie' => @cookies } ) # Check if the logout was successful if response.code == 200 puts "WebUI logout successful".green @session_id = nil @cookies = nil return true else puts "WebUI logout failed: #{response.code} - #{response.body}".red return false end rescue => e puts "Error during WebUI logout: #{e.message}".red.bold return false end end