class RHC::Rest::Client

Constants

CLIENT_API_VERSIONS

Keep the list of supported API versions here The list may not necessarily be sorted; we will select the last matching one supported by the server. See api_version_negotiated

MAX_RETRIES

Attributes

auth[R]
current_api_version[RW]

Public Class Methods

new(*args) click to toggle source
# File lib/rhc/rest/client.rb, line 361
def initialize(*args)
  options = args[0].is_a?(Hash) && args[0] || {}
  @end_point, @debug, @preferred_api_versions =
    if options.empty?
      options[:user] = args.delete_at(1)
      options[:password] = args.delete_at(1)
      args
    else
      [
        options.delete(:url) ||
          (options[:server] && "https://#{options.delete(:server)}/broker/rest/api"),
        options.delete(:debug),
        options.delete(:preferred_api_versions)
      ]
    end

  @preferred_api_versions ||= CLIENT_API_VERSIONS
  @debug ||= false

  @auth = options.delete(:auth)
  @api_always_auth = options.delete(:api_always_auth)

  self.headers.merge!(options.delete(:headers)) if options[:headers]
  self.options.merge!(options)

  debug "Connecting to #{@end_point}"
end

Public Instance Methods

api() click to toggle source
# File lib/rhc/rest/client.rb, line 397
def api
  @api ||= RHC::Rest::Api.new(self, @preferred_api_versions).tap do |api|
    self.current_api_version = api.api_version_negotiated
  end
end
api_always_auth() click to toggle source
# File lib/rhc/rest/client.rb, line 393
def api_always_auth
  @api_always_auth
end
api_version_negotiated() click to toggle source
# File lib/rhc/rest/client.rb, line 403
def api_version_negotiated
  api
  current_api_version
end
attempt(retries) { |i < (retries-1), i| ... } click to toggle source
# File lib/rhc/rest/client.rb, line 408
def attempt(retries, &block)
  (0..retries).each do |i|
    yield i < (retries-1), i
  end
  raise "Too many retries, giving up."
end
request(options) { |response| ... } click to toggle source
# File lib/rhc/rest/client.rb, line 415
def request(options, &block)
  attempt(MAX_RETRIES) do |more, i|
    begin
      client, args = new_request(options.dup)
      auth = options[:auth] || self.auth
      response = nil

      debug "Request #{args[0].to_s.upcase} #{args[1]}#{"?#{args[2].map{|a| a.join('=')}.join(' ')}" if args[2] && args[0] == 'GET'}"
      time = Benchmark.realtime{ response = client.request(*(args << true)) }
      debug "   code %s %4i ms" % [response.status, (time*1000).to_i] if response

      next if more && retry_proxy(response, i, args, client)
      auth.retry_auth?(response, self) and next if more && auth
      handle_error!(response, args[1], client) unless response.ok?

      return (if block_given?
          yield response
        else
          parse_response(response.content) unless response.nil? or response.code == 204
        end)
    rescue HTTPClient::BadResponseError => e
      if e.res
        debug "Response: #{e.res.status} #{e.res.headers.inspect}\n#{e.res.content}\n-------------" if debug?

        next if more && retry_proxy(e.res, i, args, client)
        auth.retry_auth?(e.res, self) and next if more && auth
        handle_error!(e.res, args[1], client)
      end
      raise ConnectionException.new(
        "An unexpected error occurred when connecting to the server: #{e.message}")
    rescue HTTPClient::TimeoutError => e
      raise TimeoutException.new(
        "Connection to server timed out. "\
        "It is possible the operation finished without being able "\
        "to report success. Use 'rhc domain show' or 'rhc app show' "\
        "to see the status of your applications.", e)
    rescue EOFError => e
      raise ConnectionException.new(
        "Connection to server got interrupted: #{e.message}")
    rescue OpenSSL::SSL::SSLError => e
      raise SelfSignedCertificate.new(
        'self signed certificate',
        "The server is using a self-signed certificate, which means that a secure connection can't be established '#{args[1]}'.\n\n"\
        "You may disable certificate checks with the -k (or --insecure) option. Using this option means that your data is potentially visible to third parties.") if self_signed?
      raise case e.message
        when /self signed certificate/
          CertificateVerificationFailed.new(
            e.message,
            "The server is using a self-signed certificate, which means that a secure connection can't be established '#{args[1]}'.\n\n"\
            "You may disable certificate checks with the -k (or --insecure) option. Using this option means that your data is potentially visible to third parties.")
        when /certificate verify failed/
          CertificateVerificationFailed.new(
            e.message,
            "The server's certificate could not be verified, which means that a secure connection can't be established to the server '#{args[1]}'.\n\n"\
            "If your server is using a self-signed certificate, you may disable certificate checks with the -k (or --insecure) option. Using this option means that your data is potentially visible to third parties.")
        when /unable to get local issuer certificate/
          SSLConnectionFailed.new(
            e.message,
            "The server's certificate could not be verified, which means that a secure connection can't be established to the server '#{args[1]}'.\n\n"\
            "You may need to specify your system CA certificate file with --ssl-ca-file=<path_to_file>. If your server is using a self-signed certificate, you may disable certificate checks with the -k (or --insecure) option. Using this option means that your data is potentially visible to third parties.")
        when /^SSL_connect returned=1 errno=0 state=SSLv2\/v3 read server hello A/
          SSLVersionRejected.new(
            e.message,
            "The server has rejected your connection attempt with an older SSL protocol.  Pass --ssl-version=sslv3 on the command line to connect to this server.")
        when /^SSL_CTX_set_cipher_list:: no cipher match/
          SSLVersionRejected.new(
            e.message,
            "The server has rejected your connection attempt because it does not support the requested SSL protocol version.\n\n"\
            "Check with the administrator for a valid SSL version to use and pass --ssl-version=<version> on the command line to connect to this server.")
        else
          SSLConnectionFailed.new(
            e.message,
            "A secure connection could not be established to the server (#{e.message}). You may disable secure connections to your server with the -k (or --insecure) option '#{args[1]}'.\n\n"\
            "If your server is using a self-signed certificate, you may disable certificate checks with the -k (or --insecure) option. Using this option means that your data is potentially visible to third parties.")
        end
    rescue SocketError, Errno::ECONNREFUSED => e
      raise ConnectionException.new(
        "Unable to connect to the server (#{e.message})."\
        "#{client.proxy.present? ? " Check that you have correctly specified your proxy server '#{client.proxy}' as well as your OpenShift server '#{args[1]}'." : " Check that you have correctly specified your OpenShift server '#{args[1]}'."}")
    rescue Errno::ECONNRESET => e
      raise ConnectionException.new(
        "The server has closed the connection unexpectedly (#{e.message}). Your last operation may still be running on the server; please check before retrying your last request.")
    rescue RHC::Rest::Exception
      raise
    rescue => e
      debug_error(e)
      raise ConnectionException, "An unexpected error occurred: #{e.message}", e.backtrace
    end
  end
end
url() click to toggle source
# File lib/rhc/rest/client.rb, line 389
def url
  @end_point
end

Protected Instance Methods

default_verify_callback() click to toggle source
# File lib/rhc/rest/client.rb, line 554
def default_verify_callback
  lambda do |is_ok, ctx|
    @self_signed = false
    unless is_ok
      cert = ctx.current_cert
      if cert && (cert.subject.cmp(cert.issuer) == 0)
        @self_signed = true
        debug "SSL Verification failed -- Using self signed cert"
      else
        debug "SSL Verification failed -- Preverify: #{is_ok}, Error: #{ctx.error_string} (#{ctx.error})"
      end
      return false
    end
    true
  end
end
generic_error_message(url, client) click to toggle source
# File lib/rhc/rest/client.rb, line 733
def generic_error_message(url, client)
  "The server did not respond correctly. This may be an issue "\
  "with the server configuration or with your connection to the "\
  "server (such as a Web proxy or firewall)."\
  "#{client.proxy.present? ? " Please verify that your proxy server is working correctly (#{client.proxy}) and that you can access the OpenShift server #{url}" : " Please verify that you can access the OpenShift server #{url}"}"
end
handle_error!(response, url, client) click to toggle source
# File lib/rhc/rest/client.rb, line 740
def handle_error!(response, url, client)
  messages = []
  parse_error = nil
  begin
    result = RHC::Json.decode(response.content)
    messages = parse_messages(result, {})
  rescue => e
    debug "Response did not include a message from server: #{e.message}"
  end
  case response.status
  when 400
    raise_generic_error(url, client) if messages.empty?
    message, keys = messages_to_fields(messages)
    raise ValidationException.new(message || "The operation could not be completed.", keys)
  when 401
    raise UnAuthorizedException, "Not authenticated"
  when 403
    raise RequestDeniedException, messages_to_error(messages) || "You are not authorized to perform this operation."
  when 404
    if messages.length == 1
      case messages.first['exit_code']
      when 127
        raise DomainNotFoundException, messages_to_error(messages) || generic_error_message(url, client)
      when 101
        raise ApplicationNotFoundException, messages_to_error(messages) || generic_error_message(url, client)
      end
    end
    raise ResourceNotFoundException, messages_to_error(messages) || generic_error_message(url, client)
  when 409
    raise_generic_error(url, client) if messages.empty?
    message, keys = messages_to_fields(messages)
    raise ValidationException.new(message || "The operation could not be completed.", keys)
  when 422
    raise_generic_error(url, client) if messages.empty?
    message, keys = messages_to_fields(messages)
    raise ValidationException.new(message || "The operation was not valid.", keys)
  when 400
    raise ClientErrorException, messages_to_error(messages) || "The server did not accept the requested operation."
  when 500
    raise ServerErrorException, messages_to_error(messages) || generic_error_message(url, client)
  when 503
    raise ServiceUnavailableException, messages_to_error(messages) || generic_error_message(url, client)
  else
    raise ServerErrorException, messages_to_error(messages) || "Server returned an unexpected error code: #{response.status}"
  end
  raise_generic_error
end
headers() click to toggle source
# File lib/rhc/rest/client.rb, line 511
def headers
  @headers ||= {
    :accept => :json
  }
end
httpclient_for(options, auth=nil) click to toggle source
# File lib/rhc/rest/client.rb, line 526
def httpclient_for(options, auth=nil)
  user, password, token = options.delete(:user), options.delete(:password), options.delete(:token)

  if !@httpclient || @last_options != options
    @httpclient = RHC::Rest::HTTPClient.new(:agent_name => user_agent).tap do |http|
      debug "Created new httpclient"
      http.cookie_manager = nil
      http.debug_dev = $stderr if ENV['HTTP_DEBUG']

      options.select{ |sym, value| http.respond_to?("#{sym}=") }.each{ |sym, value| http.send("#{sym}=", value) }

      ssl = http.ssl_config
      options.select{ |sym, value| ssl.respond_to?("#{sym}=") }.each{ |sym, value| ssl.send("#{sym}=", value) }
      ssl.add_trust_ca(options[:ca_file]) if options[:ca_file]
      ssl.verify_callback = default_verify_callback

      @last_options = options
    end
  end
  if auth && auth.respond_to?(:to_httpclient)
    auth.to_httpclient(@httpclient, options)
  else
    @httpclient.www_auth.basic_auth.set(@end_point, user, password) if user
    @httpclient.www_auth.oauth2.set_token(@end_point, token) if token
  end
  @httpclient
end
new_request(options) click to toggle source
# File lib/rhc/rest/client.rb, line 574
def new_request(options)
  options.reverse_merge!(self.options)

  options[:connect_timeout] ||= options[:timeout] || 120
  options[:receive_timeout] ||= options[:timeout] || 0
  options[:send_timeout] ||= options[:timeout] || 0
  options[:timeout] = nil

  auth = (options[:auth] || self.auth) unless options[:no_auth]
  if auth
    auth.to_request(options, self)
  end

  headers = (self.headers.to_a + (options.delete(:headers) || []).to_a).inject({}) do |h,(k,v)|
    v = "application/#{v}" if k == :accept && v.is_a?(Symbol)
    h[k.to_s.downcase.gsub(/_/, '-')] = v
    h
  end

  modifiers = []
  version = options.delete(:api_version) || current_api_version
  modifiers << ";version=#{version}" if version

  query = options.delete(:query) || {}
  payload = options.delete(:payload)
  if options[:method].to_s.upcase == 'GET'
    query = payload
    payload = nil
  else
    headers['content-type'] ||= begin
        payload = payload.to_json unless payload.nil? || payload.is_a?(String)
        "application/json#{modifiers.join}"
      end
  end
  query = nil if query.blank?

  if headers['accept'] && modifiers.present?
    headers['accept'] << modifiers.join
  end

  # remove all unnecessary options
  options.delete(:lazy_auth)
  options.delete(:no_auth)
  options.delete(:accept)

  args = [options.delete(:method), options.delete(:url), query, payload, headers, true]
  [httpclient_for(options, auth), args]
end
options() click to toggle source
# File lib/rhc/rest/client.rb, line 521
def options
  @options ||= {
  }
end
parse_messages(result, data) click to toggle source
# File lib/rhc/rest/client.rb, line 686
def parse_messages(result, data)
  raw = (result || {})['messages'] || []
  raw.delete_if do |m|
    m.delete_if{ |k,v| k.nil? || v.blank? } if m.is_a? Hash
    m.blank?
  end
  warnings, messages, raw = Array(raw).inject([[],[],[]]) do |a, m|
    severity, field, text = m.values_at('severity', 'field', 'text')
    text = (text || "").gsub(/\A\n+/m, "").rstrip
    case severity
    when 'warning'
      a[0] << text
    when 'debug'
      a[2] << m
      a[1] << text if debug?
    when 'info'
      a[2] << m
      a[1] << text if debug? || field == 'result'
    else
      a[2] << m
      a[1] << text
    end
    a
  end

  if data.is_a?(Array)
    data.each do |d|
      d['messages'] = messages
      d['warnings'] = warnings
    end
  elsif data.is_a?(Hash)
    data['messages'] = messages
    data['warnings'] = warnings
  end

  warnings.each do |warning|
    unless (@warning_map ||= Set.new).include?(warning)
      @warning_map << warning
      warn warning
    end
  end if respond_to? :warn
  raw
end
parse_response(response) click to toggle source
# File lib/rhc/rest/client.rb, line 633
def parse_response(response)
  result = RHC::Json.decode(response)
  type = result['type']
  data = result['data'] || {}

  parse_messages result, data

  case type
  when 'domains'
    data.map{ |json| Domain.new(json, self) }
  when 'domain'
    Domain.new(data, self)
  when 'authorization'
    Authorization.new(data, self)
  when 'authorizations'
    data.map{ |json| Authorization.new(json, self) }
  when 'applications'
    data.map{ |json| Application.new(json, self) }
  when 'application'
    Application.new(data, self)
  when 'cartridges'
    data.map{ |json| Cartridge.new(json, self) }
  when 'cartridge'
    Cartridge.new(data, self)
  when 'user'
    User.new(data, self)
  when 'keys'
    data.map{ |json| Key.new(json, self) }
  when 'key'
    Key.new(data, self)
  when 'gear_groups'
    data.map{ |json| GearGroup.new(json, self) }
  when 'aliases'
    data.map{ |json| Alias.new(json, self) }
  when 'environment-variables'
    data.map{ |json| EnvironmentVariable.new(json, self) }
  when 'deployments'
    data.map{ |json| Deployment.new(json, self) }
  when 'team'
    Team.new(data, self)
  when 'teams'
    data.map{ |json| Team.new(json, self) }
  when 'member'
    RHC::Rest::Membership::Member.new(data, self)
  when 'members'
    data.map{ |json| RHC::Rest::Membership::Member.new(json, self) }
  when 'regions'
    data.map{ |json| Region.new(json, self) }
  else
    data
  end
end
raise_generic_error(url, client) click to toggle source
# File lib/rhc/rest/client.rb, line 730
def raise_generic_error(url, client)
  raise ServerErrorException.new(generic_error_message(url, client), 129)
end
retry_proxy(response, i, args, client) click to toggle source
# File lib/rhc/rest/client.rb, line 623
def retry_proxy(response, i, args, client)
  if response.status == 502
    debug "ERROR: Received bad gateway from server, will retry once if this is a GET"
    return true if i == 0 && args[0] == :get
    raise ConnectionException.new(
      "An error occurred while communicating with the server. This problem may only be temporary."\
      "#{client.proxy.present? ? " Check that you have correctly specified your proxy server '#{client.proxy}' as well as your OpenShift server '#{args[1]}'." : " Check that you have correctly specified your OpenShift server '#{args[1]}'."}")
  end
end
self_signed?() click to toggle source
# File lib/rhc/rest/client.rb, line 570
def self_signed?
  @self_signed
end
user_agent() click to toggle source
# File lib/rhc/rest/client.rb, line 517
def user_agent
  RHC::Helpers.user_agent
end

Private Instance Methods

messages_to_error(messages) click to toggle source
# File lib/rhc/rest/client.rb, line 789
def messages_to_error(messages)
  errors, remaining = messages.partition{ |m| (m['severity'] || "").upcase == 'ERROR' }
  if errors.present?
    if errors.length == 1
      errors.first['text']
    else
      "The server reported multiple errors:\n* #{errors.map{ |m| m['text'] || "An unknown server error occurred.#{ " (exit code: #{m['exit_code']}" if m['exit_code']}}" }.join("\n* ")}"
    end
  elsif remaining.present?
    "The operation did not complete successfully, but the server returned additional information:\n* #{remaining.map{ |m| m['text'] || 'No message'}.join("\n* ")}"
  end
end
messages_to_fields(messages) click to toggle source
# File lib/rhc/rest/client.rb, line 802
def messages_to_fields(messages)
  keys = messages.group_by{ |m| m['field'] }.keys.compact.sort.map(&:to_sym) rescue []
  [messages_to_error(messages), keys]
end