class OvirtSDK4::Connection

This class is responsible for managing an HTTP connection to the engine server. It is intended as the entry point for the SDK, and it provides access to the `system` service and, from there, to the rest of the services provided by the API.

Constants

JSON_CONTENT_TYPE_RE

Regular expression used to check JSON content type.

@api private

TYPICAL_PATH

The typical URL path, used just to generate informative error messages.

@api private

XML_CONTENT_TYPE_RE

Regular expression used to check XML content type.

@api private

Public Class Methods

new(opts = {}) click to toggle source

Creates a new connection to the API server.

source,ruby

connection = OvirtSDK4::Connection.new(

url: 'https://engine.example.com/ovirt-engine/api',
username: 'admin@internal',
password: '...',
ca_file:'/etc/pki/ovirt-engine/ca.pem'

)


@param opts [Hash] The options used to create the connection.

@option opts [String] :url A string containing the base URL of the server, usually something like

`\https://server.example.com/ovirt-engine/api`.

@option opts [String] :username The name of the user, something like `admin@internal`.

@option opts [String] :password The password of the user.

@option opts [String] :token The token used to authenticate. Optionally the caller can explicitly provide

the token, instead of the user name and password. If the token isn't provided then it will be automatically
created.

@option opts [Boolean] :insecure (false) A boolean flag that indicates if the server TLS certificate and host

name should be checked.

@option opts [String] :ca_file The name of a PEM file containing the trusted CA certificates. The certificate

presented by the server will be verified using these CA certificates. If neither this nor the `ca_certs`
options are provided, then the system wide CA certificates store is used. If both options are provided,
then the certificates from both options will be trusted.

@option opts [Array<String>] :ca_certs An array of strings containing the trusted CA certificates, in PEM

format. The certificate presented by the server will be verified using these CA certificates. If neither this
nor the `ca_file` options are provided, then the system wide CA certificates store is used. If both options
are provided, then the certificates from both options will be trusted.

@option opts [Boolean] :debug (false) A boolean flag indicating if debug output should be generated. If the

values is `true` and the `log` parameter isn't `nil` then the data sent to and received from the server will be
written to the log. Be aware that user names and passwords will also be written, so handle with care.

@option opts [Logger] :log The logger where the log messages will be written.

@option opts [Boolean] :kerberos (false) A boolean flag indicating if Kerberos authentication should be used

instead of user name and password to obtain the OAuth token.

@option opts [Integer] :timeout (0) The maximun total time to wait for the response, in seconds. A value of zero

(the default) means wait for ever. If the timeout expires before the response is received a `TimeoutError`
exception will be raised.

@option opts [Integer] :connect_timeout (300) The maximun time to wait for connection establishment, in seconds.

If the timeout expires before the connection is established a `TimeoutError` exception will be raised.

@option opts [Boolean] :compress (true) A boolean flag indicating if the SDK should ask the server to send

compressed responses. Note that this is a hint for the server, and that it may return uncompressed data even
when this parameter is set to `true`. Also, compression will be automatically disabled when the `debug`
parameter is set to `true`, as otherwise the debug output will be compressed as well, and then it isn't
useful.

@option opts [String] :proxy_url A string containing the protocol, address and port number of the proxy server

to use to connect to the server. For example, in order to use the HTTP proxy `proxy.example.com` that is
listening on port `3128` the value should be `http://proxy.example.com:3128`. This is optional, and if not
given the connection will go directly to the server specified in the `url` parameter.

@option opts [String] :proxy_username The name of the user to authenticate to the proxy server.

@option opts [String] :proxy_password The password of the user to authenticate to the proxy server.

@option opts [Hash] :headers Custom HTTP headers to send with all requests. The keys of the hash can be

strings of symbols, and they will be used as the names of the headers. The values of the hash will be used
as the names of the headers. If the same header is provided here and in the `headers` parameter of a specific
method call, then the `headers` parameter of the specific method call will have precedence.

@option opts [Integer] :connections (1) The maximum number of connections to open to the host. The value must

be greater than 0

@option opts [Integer] :pipeline (0) The maximum number of request to put in an HTTP pipeline without waiting for

the response. If the value is `0` (the default) then pipelining is disabled.
# File lib/ovirtsdk4/connection.rb, line 109
def initialize(opts = {})
  # Get the values of the parameters and assign default values:
  @url = opts[:url]
  @username = opts[:username]
  @password = opts[:password]
  @token = opts[:token]
  @insecure = opts[:insecure] || false
  @ca_file = opts[:ca_file]
  @ca_certs = opts[:ca_certs]
  @debug = opts[:debug] || false
  @log = opts[:log]
  @kerberos = opts[:kerberos] || false
  @timeout = opts[:timeout] || 0
  @connect_timeout = opts[:connect_timeout] || 0
  @compress = opts[:compress] || true
  @proxy_url = opts[:proxy_url]
  @proxy_username = opts[:proxy_username]
  @proxy_password = opts[:proxy_password]
  @headers = opts[:headers]
  @connections = opts[:connections] || 1
  @pipeline = opts[:pipeline] || 0

  # Check that the URL has been provided:
  raise ArgumentError, "The 'url' option is mandatory" unless @url

  # Automatically disable compression when debug is enabled, as otherwise the debug output generated by
  # libcurl is also compressed, and that isn't useful for debugging:
  @compress = false if @debug

  # Create a temporary file to store the CA certificates, and populate it with the contents of the 'ca_file' and
  # 'ca_certs' options. The file will be removed when the connection is closed.
  @ca_store = nil
  if @ca_file || @ca_certs
    @ca_store = Tempfile.new('ca_store')
    @ca_store.write(::File.read(@ca_file)) if @ca_file
    if @ca_certs
      @ca_certs.each do |ca_cert|
        @ca_store.write(ca_cert)
      end
    end
    @ca_store.close
  end

  # Create the mutex that will be used to prevents simultaneous access to the same HTTP client by multiple threads:
  @mutex = Mutex.new

  # Create the HTTP client:
  @client = HttpClient.new(
    insecure:        @insecure,
    ca_file:         @ca_store ? @ca_store.path : nil,
    debug:           @debug,
    log:             @log,
    timeout:         @timeout,
    connect_timeout: @connect_timeout,
    compress:        @compress,
    proxy_url:       @proxy_url,
    proxy_username:  @proxy_username,
    proxy_password:  @proxy_password,
    connections:     @connections,
    pipeline:        @pipeline
  )
end

Public Instance Methods

authenticate() click to toggle source

Performs the authentication process and returns the authentication token. Usually there is no need to call this method, as authentication is performed automatically when needed. But in some situations it may be useful to perform authentication explicitly, and then use the obtained token to create other connections, using the `token` parameter of the constructor instead of the user name and password.

@return [String]

# File lib/ovirtsdk4/connection.rb, line 246
def authenticate
  # rubocop:disable Naming/MemoizedInstanceVariableName
  @token ||= create_access_token
  # rubocop:enable Naming/MemoizedInstanceVariableName
end
check_json_content_type(response) click to toggle source

Checks that the content type of the given response is JSON. If it is JSON then it does nothing. If it isn't JSON then it raises an exception.

@param response [HttpResponse] The HTTP response to check.

@api private

# File lib/ovirtsdk4/connection.rb, line 315
def check_json_content_type(response)
  check_content_type(JSON_CONTENT_TYPE_RE, 'JSON', response)
end
check_xml_content_type(response) click to toggle source

Checks that the content type of the given response is XML. If it is XML then it does nothing. If it isn't XML then it raises an exception.

@param response [HttpResponse] The HTTP response to check.

@api private

# File lib/ovirtsdk4/connection.rb, line 327
def check_xml_content_type(response)
  check_content_type(XML_CONTENT_TYPE_RE, 'XML', response)
end
close() click to toggle source

Releases the resources used by this connection, making sure that multiple threads are coordinated correctly.

# File lib/ovirtsdk4/connection.rb, line 303
def close
  @mutex.synchronize { internal_close }
end
inspect() click to toggle source

Returns a string representation of the connection.

@return [String] The string representation.

# File lib/ovirtsdk4/connection.rb, line 395
def inspect
  "#<#{self.class.name}:#{@url}>"
end
raise_error(response, detail = nil) click to toggle source

Creates and raises an error containing the details of the given HTTP response.

@param response [HttpResponse] The HTTP response where the details of the raised error will be taken from. @param detail [String, Fault] (nil) The detail of the error. It can be a string or a `Fault` object.

@api private

# File lib/ovirtsdk4/connection.rb, line 339
def raise_error(response, detail = nil)
  # Check if the detail is a fault:
  fault = detail.is_a?(Fault) ? detail : nil

  # Build the error message from the response and the fault:
  message = ''
  unless fault.nil?
    unless fault.reason.nil?
      message << ' ' unless message.empty?
      message << "Fault reason is \"#{fault.reason}\"."
    end
    unless fault.detail.nil?
      message << ' ' unless message.empty?
      message << "Fault detail is \"#{fault.detail}\"."
    end
  end
  unless response.nil?
    unless response.code.nil?
      message << ' ' unless message.empty?
      message << "HTTP response code is #{response.code}."
    end
    unless response.message.nil?
      message << ' ' unless message.empty?
      message << "HTTP response message is \"#{response.message}\"."
    end
  end

  # If the detail is a string, append it to the message:
  if detail.is_a?(String)
    message << ' ' unless message.empty?
    message << detail
    message << '.'
  end

  # Create and populate the error:
  klass = Error
  unless response.nil?
    case response.code
    when 401, 403
      klass = AuthError
    when 404
      klass = NotFoundError
    end
  end
  error = klass.new(message)
  error.code = response.code if response
  error.fault = fault

  raise error
end
send(request) click to toggle source

Sends an HTTP request, making sure that multiple threads are coordinated correctly.

@param request [HttpRequest] The request object containing the details of the HTTP request to send.

@api private

# File lib/ovirtsdk4/connection.rb, line 201
def send(request)
  @mutex.synchronize { internal_send(request) }
end
service(path) click to toggle source

Returns a reference to the service corresponding to the given path. For example, if the `path` parameter is `vms/123/diskattachments` then it will return a reference to the service that manages the disk attachments for the virtual machine with identifier `123`.

@param path [String] The path of the service, for example `vms/123/diskattachments`. @return [Service] @raise [Error] If there is no service corresponding to the given path.

# File lib/ovirtsdk4/connection.rb, line 190
def service(path)
  system_service.service(path)
end
system_service() click to toggle source

Returns a reference to the root of the services tree.

@return [SystemService]

# File lib/ovirtsdk4/connection.rb, line 177
def system_service
  @system_service ||= SystemService.new(self, '')
end
test(raise_exception = false, timeout = nil) click to toggle source

Tests the connectivity with the server. If connectivity works correctly it returns `true`. If there is any connectivity problem it will either return `false` or raise an exception if the `raise_exception` parameter is `true`.

@param raise_exception [Boolean]

@param timeout [Integer] (nil) The maximun total time to wait for the test to complete, in seconds. If the value

is `nil` (the default) then the timeout set globally for the connection will be used.

@return [Boolean]

# File lib/ovirtsdk4/connection.rb, line 229
def test(raise_exception = false, timeout = nil)
  system_service.get(timeout: timeout)
  true
rescue StandardError
  raise if raise_exception

  false
end
to_s() click to toggle source

Returns a string representation of the connection.

@return [String] The string representation.

# File lib/ovirtsdk4/connection.rb, line 404
def to_s
  inspect
end
wait(request) click to toggle source

Waits for the response to the given request, making sure that multiple threads are coordinated correctly.

@param request [HttpRequest] The request object whose corresponding response you want to wait for. @return [HttpResponse] A request object containing the details of the HTTP response received.

@api private

# File lib/ovirtsdk4/connection.rb, line 213
def wait(request)
  @mutex.synchronize { internal_wait(request) }
end

Private Instance Methods

build_sso_auth_request() click to toggle source

Builds a the URL and parameters to acquire the access token from SSO.

@return [Array] An array containing two elements, the first is the URL of the SSO service and the second is a hash

containing the parameters required to perform authentication.

@api private

# File lib/ovirtsdk4/connection.rb, line 545
def build_sso_auth_request
  # Compute the entry point and the parameters:
  parameters = {
    scope: 'ovirt-app-api'
  }
  if @kerberos
    entry_point = 'token-http-auth'
    parameters[:grant_type] = 'urn:ovirt:params:oauth:grant-type:http'
  else
    entry_point = 'token'
    parameters.merge!(
      grant_type: 'password',
      username:   @username,
      password:   @password
    )
  end

  # Compute the URL:
  url = URI(@url.to_s)
  url.path = "/ovirt-engine/sso/oauth/#{entry_point}"
  url = url.to_s

  # Return the pair containing the URL and the parameters:
  [url, parameters]
end
build_sso_revoke_request() click to toggle source

Builds a the URL and parameters to revoke the SSO access token

@return [Array] An array containing two elements, the first is the URL of the SSO service and the second is a hash

containing the parameters required to perform the revoke.

@api private

# File lib/ovirtsdk4/connection.rb, line 579
def build_sso_revoke_request
  # Compute the parameters:
  parameters = {
    scope: '',
    token: @token
  }

  # Compute the URL:
  url = URI(@url.to_s)
  url.path = '/ovirt-engine/services/sso-logout'
  url = url.to_s

  # Return the pair containing the URL and the parameters:
  [url, parameters]
end
check_content_type(expected_re, expected_name, response) click to toggle source

Checks the content type of the given HTTP response and raises an exception if it isn't the expected one.

@param expected_re [Regex] The regular expression used to check the expected content type. @param expected_name [String] The name of the expected content type. @param response [HttpResponse] The HTTP response to check.

@api private

# File lib/ovirtsdk4/connection.rb, line 446
def check_content_type(expected_re, expected_name, response)
  content_type = response.headers['content-type']
  return if expected_re =~ content_type

  detail = "The response content type '#{content_type}' isn't #{expected_name}"
  url = URI(@url)
  if url.path != TYPICAL_PATH
    detail << ". Is the path '#{url.path}' included in the 'url' parameter correct?"
    detail << " The typical one is '#{TYPICAL_PATH}'"
  end
  raise_error(response, detail)
end
create_access_token() click to toggle source

Obtains the access token from SSO to be used for bearer authentication.

@return [String] The access token.

@api private

# File lib/ovirtsdk4/connection.rb, line 466
def create_access_token
  # Build the URL and parameters required for the request:
  url, parameters = build_sso_auth_request

  # Send the request and wait for the request:
  response = get_sso_response(url, parameters)
  response = response[0] if response.is_a?(Array)

  # Check the response and raise an error if it contains an error code:
  error = get_sso_error_message(response)
  raise AuthError, "Error during SSO authentication: #{error}" if error

  response['access_token']
end
get_sso_error_message(response) click to toggle source

Extrats the error message from the given SSO response.

@param response [Hash] The result of parsing the JSON document returned by the SSO server. @return [String] The error message, or `nil` if there was no error.

# File lib/ovirtsdk4/connection.rb, line 601
def get_sso_error_message(response)
  # OAuth uses the 'error_code' attribute for the error code, and 'error' for the error description. But OpenID uses
  # 'error' for the error code and 'error_description' for the description. So we need to check if the
  # 'error_description' attribute is present, and extract the code and description accordingly.
  description = response['error_description']
  if description.nil?
    code = response['error_code']
    description = response['error']
  else
    code = response['error']
  end
  "#{code}: #{description}" if code
end
get_sso_response(url, parameters) click to toggle source

Execute a get request to the SSO server and return the response.

@param url [String] The URL of the SSO server.

@param parameters [Hash] The parameters to send to the SSO server.

@return [Hash] The JSON response.

@api private

# File lib/ovirtsdk4/connection.rb, line 510
def get_sso_response(url, parameters)
  # Create the request:
  request = HttpRequest.new
  request.method = :POST
  request.url = url
  request.headers = {
    'User-Agent'   => "RubySDK/#{VERSION}",
    'Content-Type' => 'application/x-www-form-urlencoded',
    'Accept'       => 'application/json'
  }
  request.body = URI.encode_www_form(parameters)

  # Add the global headers:
  request.headers.merge!(@headers) if @headers

  # Send the request and wait for the response:
  @client.send(request)
  response = @client.wait(request)
  raise response if response.is_a?(Exception)

  # Check the returned content type:
  check_json_content_type(response)

  # Parse and return the JSON response:
  JSON.parse(response.body)
end
internal_close() click to toggle source

Releases the resources used by this connection.

@api private

# File lib/ovirtsdk4/connection.rb, line 684
def internal_close
  # Revoke the SSO access token:
  revoke_access_token if @token

  # Close the HTTP client:
  @client.close if @client

  # Remove the temporary file that contains the trusted CA certificates:
  @ca_store.unlink if @ca_store
end
internal_send(request) click to toggle source

Sends an HTTP request.

@param request [HttpRequest] The request object containing the details of the HTTP request to send.

@api private

# File lib/ovirtsdk4/connection.rb, line 622
def internal_send(request)
  # Add the base URL to the request:
  request.url = request.url.nil? ? request.url = @url : "#{@url}/#{request.url}"

  # Set the headers common to all requests:
  request.headers.merge!(
    'User-Agent'   => "RubySDK/#{VERSION}",
    'Version'      => '4',
    'Content-Type' => 'application/xml',
    'Accept'       => 'application/xml'
  )

  # Older versions of the engine (before 4.1) required the 'all_content' as an HTTP header instead of a query
  # parameter. In order to better support those older versions of the engine we need to check if this parameter is
  # included in the request, and add the corresponding header.
  unless request.query.nil?
    all_content = request.query[:all_content]
    request.headers['All-Content'] = all_content unless all_content.nil?
  end

  # Add the global headers, but without replacing the values that may already exist:
  request.headers.merge!(@headers) { |_name, local, _global| local } if @headers

  # Set the authentication token:
  @token ||= create_access_token
  request.token = @token

  # Send the request:
  @client.send(request)
end
internal_wait(request) click to toggle source

Waits for the response to the given request.

@param request [HttpRequest] The request object whose corresponding response you want to wait for. @return [Response] A request object containing the details of the HTTP response received.

@api private

# File lib/ovirtsdk4/connection.rb, line 661
def internal_wait(request)
  # Wait for the response:
  response = @client.wait(request)
  raise response if response.is_a?(Exception)

  # If the request failed because of authentication, and it wasn't a request to the SSO service, then the
  # most likely cause is an expired SSO token. In this case we need to request a new token, and try the original
  # request again, but only once. It if fails again, we just return the failed response.
  if response.code == 401 && request.token
    @token = create_access_token
    request.token = @token
    @client.send(request)
    response = @client.wait(request)
  end

  response
end
revoke_access_token() click to toggle source

Revoke the SSO access token.

@api private

# File lib/ovirtsdk4/connection.rb, line 486
def revoke_access_token
  # Build the URL and parameters required for the request:
  url, parameters = build_sso_revoke_request

  # Send the request and wait for the response:
  response = get_sso_response(url, parameters)
  response = response[0] if response.is_a?(Array)

  # Check the response and raise an error if it contains an error code:
  error = get_sso_error_message(response)
  raise AuthError, "Error during SSO revoke: #{error}" if error
end