class RedfishClient::Connector

Connector serves as a low-level wrapper around HTTP calls that are used to retrieve data from the service API. It abstracts away implementation details such as sending the proper headers in request, which do not change between resource fetches.

Library users should treat this class as an implementation detail and use higer-level {RedfishClient::Resource} instead.

Constants

BASIC_AUTH_HEADER

Basic and token authentication header names

DEFAULT_HEADERS

Default headers, as required by Redfish spec redfish.dmtf.org/schemas/DSP0266_1.4.0.html#request-headers

LOCATION_HEADER
TOKEN_AUTH_HEADER

Public Class Methods

new(url, verify: true, cache: nil, use_session: true) click to toggle source

Create new connector.

By default, connector performs no caching. If caching is desired, Hash should be used as a cache implementation.

It is also possible to pass in custom caching class. Instances of that class should respond to the following four methods:

1. `[](key)`         - Used to access cached content and should return
                       `nil` if the key has no associated value.
2. `[]=(key, value)` - Cache `value` under the `key`
3. `clear`           - Clear the complete cache.
4. `delete(key)`     - Invalidate cache entry associated with `key`.

@param url [String] base url of the Redfish service @param verify [Boolean] verify SSL certificate of the service @param use_session [Boolean] Use a session for authentication @param cache [Object] cache backend

# File lib/redfish_client/connector.rb, line 53
def initialize(url, verify: true, cache: nil, use_session: true)
  @url = url
  @headers = DEFAULT_HEADERS.dup
  middlewares = Excon.defaults[:middlewares] +
    [Excon::Middleware::RedirectFollower]
  @connection = Excon.new(@url,
                          ssl_verify_peer: verify,
                          middlewares: middlewares)
  @cache = cache || NilHash.new
  @use_session = use_session
end

Public Instance Methods

add_headers(headers) click to toggle source

Add HTTP headers to the requests made by the connector.

@param headers [Hash<String, String>] headers to be added

# File lib/redfish_client/connector.rb, line 68
def add_headers(headers)
  @headers.merge!(headers)
end
delete(path) click to toggle source

Issue DELETE requests to the service.

@param path [String] path to the resource, relative to the base @return [Response] response object

# File lib/redfish_client/connector.rb, line 131
def delete(path)
  request(:delete, path)
end
get(path) click to toggle source

Issue GET request to service.

This method will first try to return cached response if available. If cache does not contain entry for this request, data will be fetched from remote and then cached, but only if the response has an OK (200) status.

@param path [String] path to the resource, relative to the base url @return [Response] response object

# File lib/redfish_client/connector.rb, line 104
def get(path)
  request(:get, path)
end
login() click to toggle source

Authenticate against the service.

Calling this method will try to authenticate against API using credentials provided by #{set_auth_info} call. If authentication fails, # {AuthError} will be raised.

@raise [AuthError] if credentials are invalid

# File lib/redfish_client/connector.rb, line 174
def login
  @session_path ? session_login : basic_login
end
logout() click to toggle source

Sign out of the service.

# File lib/redfish_client/connector.rb, line 179
def logout
  # We bypass request here because we do not want any retries on 401
  # when doing logout.
  if @session_oid
    params = prepare_request_params(:delete, @session_oid)
    @connection.request(params)
    @session_oid = nil
  end
  remove_headers([BASIC_AUTH_HEADER, TOKEN_AUTH_HEADER])
end
patch(path, data = nil, **options) click to toggle source

Issue PATCH requests to the service with optional ETag support.

@param path [String] path to the resource, relative to the base @param data [Hash] data to be sent over the socket @param options [Hash] optional parameters including :etag @return [Response] response object

# File lib/redfish_client/connector.rb, line 123
def patch(path, data = nil, **options)
  etag_handler(path, data, options[:etag])
end
post(path, data = nil) click to toggle source

Issue POST requests to the service.

@param path [String] path to the resource, relative to the base @param data [Hash] data to be sent over the socket, JSON encoded @return [Response] response object

# File lib/redfish_client/connector.rb, line 113
def post(path, data = nil)
  request(:post, path, data)
end
remove_headers(headers) click to toggle source

Remove HTTP headers from requests made by the connector.

Headers that are not currently set are silently ignored and no error is raised.

@param headers [List<String>] headers to remove

# File lib/redfish_client/connector.rb, line 78
def remove_headers(headers)
  headers.each { |h| @headers.delete(h) }
end
request(method, path, data = nil) click to toggle source

Issue requests to the service.

@param mathod [Symbol] HTTP method (:get, :post, :patch or :delete) @param path [String] path to the resource, relative to the base @param data [Hash] data to be sent over the socket @return [Response] response object

# File lib/redfish_client/connector.rb, line 88
def request(method, path, data = nil)
  return @cache[path] if method == :get && @cache[path]

  do_request(method, path, data).tap do |r|
    @cache[path] = r if method == :get && r.status == 200
  end
end
reset(path = nil) click to toggle source

Clear the cached responses.

If path is passed as a parameter, only one cache entry gets invalidated, else complete cache gets invalidated.

Next GET request will repopulate the cache.

@param path [String] path to invalidate

# File lib/redfish_client/connector.rb, line 143
def reset(path = nil)
  path.nil? ? @cache.clear : @cache.delete(path)
end
set_auth_info(username, password, auth_test_path, session_path = nil) click to toggle source

Set authentication-related variables.

Last parameter controls the kind of login connector will perform. If session_path is `nil`, basic authentication will be used, otherwise connector will use session-based authentication.

Note that actual login is done lazily. If you need to check for credential validity, call #{login} method.

@param username [String] API username @param password [String] API password @param auth_test_path [String] API path to test credential's validity @param session_path [String, nil] API session path

# File lib/redfish_client/connector.rb, line 160
def set_auth_info(username, password, auth_test_path, session_path = nil)
  @username = username
  @password = password
  @auth_test_path = auth_test_path
  @session_path = @use_session ? session_path : nil
end

Private Instance Methods

auth_valid?() click to toggle source
# File lib/redfish_client/connector.rb, line 339
def auth_valid?
  # We bypass request here because we do not want any retries on 401
  # when checking authentication headers.
  reset(@auth_test_path) # Do not want to see cached response
  params = prepare_request_params(:get, @auth_test_path)
  @connection.request(params).status == 200
end
basic_login() click to toggle source
# File lib/redfish_client/connector.rb, line 326
def basic_login
  payload = Base64.encode64("#{@username}:#{@password}").strip
  add_headers(BASIC_AUTH_HEADER => "Basic #{payload}")
  return if auth_valid?

  remove_headers([BASIC_AUTH_HEADER])
  raise_invalid_auth_error
end
do_request(method, path, data) click to toggle source
# File lib/redfish_client/connector.rb, line 275
def do_request(method, path, data)
  params = prepare_request_params(method, path, data)
  r = @connection.request(params)
  if r.status == 401
    login
    r = @connection.request(params)
  end
  Response.new(r.status, downcase_headers(r.data[:headers]), r.data[:body])
end
downcase_headers(headers) click to toggle source
# File lib/redfish_client/connector.rb, line 285
def downcase_headers(headers)
  headers.each_with_object({}) { |(k, v), obj| obj[k.downcase] = v }
end
etag_handler(path, data, etag) click to toggle source

ETag handler containing workarounds for PATCH requests with ETags. Based on sushy's _etag_handler implementation.

@param path [String] path to the resource @param data [Hash] data to be sent @param etag [String, nil] ETag value @return [Response] response object

# File lib/redfish_client/connector.rb, line 199
def etag_handler(path, data, etag)
  # Guard clause: if no ETag provided, perform regular PATCH and return
  return request(:patch, path, data) if etag.nil? || etag.empty?

  logger = Logger.new($stdout)
  logger.level = Logger::WARN

  # Prepare headers with If-Match
  headers_to_add = { "If-Match" => etag }
  add_headers(headers_to_add)

  begin
    # First attempt with the provided ETag
    response = request(:patch, path, data)

    # Handle 412 Precondition Failed with retry logic.
    # Some hardware vendors have non-standard Redfish implementations
    # that incorrectly handle ETags (e.g., rejecting weak ETags or
    # requiring ETags to be omitted even when provided correctly).
    # To work around these vendor-specific issues, we retry with:
    # 1. Converting weak ETag (W/"...") to strong ETag ("...")
    # 2. Removing the If-Match header entirely
    # This approach is based on similar workarounds implemented in
    # the Sushy library:
    # https://github.com/openstack/sushy
    # Other statuses (success or errors like 400, 500) should be
    # returned as-is for the caller to handle appropriately.
    unless response.status == 412
      return response
    end

    logger.warn("Initial request with eTag failed: HTTP 412")

    # Check for weak ETag (W/"...")
    weak_etag_pattern = /^(W\/)(".+")$/
    match = weak_etag_pattern.match(etag)

    if match
      logger.info("Weak eTag provided with original request to #{path}. " \
                 "Attempting conversion to strong eTag and re-trying.")

      # Try with strong ETag (remove W/ prefix)
      strong_etag = match[2]
      remove_headers(["If-Match"])
      add_headers("If-Match" => strong_etag)

      response = request(:patch, path, data)

      unless response.status == 412
        return response
      end

      logger.warn("Request to #{path} with weak eTag converted to " \
                 "strong eTag also failed. Making the final attempt " \
                 "with no eTag specified.")
    else
      # ETag is strong, retry without it
      logger.warn("Strong eTag provided - retrying request to #{path} " \
                 "with eTag removed.")
    end

    # Final attempt without If-Match header
    remove_headers(["If-Match"])
    response = request(:patch, path, data)

    if response.status == 412
      logger.error("Final re-try with eTag removed has also failed with HTTP 412")
    end

    response
  ensure
    # Clean up headers
    remove_headers(headers_to_add.keys) unless headers_to_add.empty?
  end
end
prepare_request_params(method, path, data = nil) click to toggle source
# File lib/redfish_client/connector.rb, line 289
def prepare_request_params(method, path, data = nil)
  params = { method: method, path: path }
  if data
    params[:body] = data.to_json
    params[:headers] = @headers.merge("Content-Type" => "application/json")
  else
    params[:headers] = @headers
  end
  params
end
raise_invalid_auth_error() click to toggle source
# File lib/redfish_client/connector.rb, line 335
def raise_invalid_auth_error
  raise AuthError, "Invalid credentials"
end
save_session_oid!(body, headers) click to toggle source
# File lib/redfish_client/connector.rb, line 316
def save_session_oid!(body, headers)
  @session_oid = body["@odata.id"] if body.key?("@odata.id")
  return if @session_oid

  return unless headers.key?(LOCATION_HEADER)

  location = URI.parse(headers[LOCATION_HEADER])
  @session_oid = [location.path, location.query].compact.join("?")
end
session_login() click to toggle source
# File lib/redfish_client/connector.rb, line 300
def session_login
  # We bypass request here because we do not want any retries on 401
  # when doing login.
  params = prepare_request_params(:post, @session_path,
                                  "UserName" => @username,
                                  "Password" => @password)
  r = @connection.request(params)
  raise_invalid_auth_error unless r.status == 201

  body    = JSON.parse(r.data[:body])
  headers = r.data[:headers]

  add_headers(TOKEN_AUTH_HEADER => headers[TOKEN_AUTH_HEADER])
  save_session_oid!(body, headers)
end