From d9c2cc00364bb31450ab901307f2708ac2178903 Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Mon, 27 Feb 2012 23:14:11 -0800 Subject: [PATCH] cleanups + refactorings + better error reporting per joyent cloudapi spec --- lib/fog/joyent/compute.rb | 78 +++++++++++++++++++++++---------- lib/fog/joyent/errors.rb | 90 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 22 deletions(-) create mode 100644 lib/fog/joyent/errors.rb diff --git a/lib/fog/joyent/compute.rb b/lib/fog/joyent/compute.rb index f236c87b6..0614470a2 100644 --- a/lib/fog/joyent/compute.rb +++ b/lib/fog/joyent/compute.rb @@ -1,4 +1,5 @@ -require File.expand_path(File.join(File.dirname(__FILE__), '..', 'brightbox')) +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'joyent')) +require File.expand_path(File.join(File.dirname(__FILE__), 'errors')) require 'fog/compute' require 'multi_json' @@ -71,6 +72,8 @@ module Fog request :delete_machine_tag request :delete_all_machine_tags + + class Mock def self.data @data ||= Hash.new do |hash, key| @@ -113,14 +116,15 @@ module Fog @rsa = OpenSSL::PKey::RSA.new(@joyent_key) - @header_method = method(:header_for_signature) + @header_method = method(:header_for_signature_auth) else raise ArgumentError, "options[:joyent_keyfile] provided does not exist." end + elsif options[:joyent_password] @joyent_password = options[:joyent_password] - @header_method = method(:header_for_basic) + @header_method = method(:header_for_basic_auth) else raise ArgumentError, "Must provide either a joyent_password or joyent_keyname and joyent_keyfile pair" end @@ -132,36 +136,42 @@ module Fog ) end - def request(request_options = {}) - (request_options[:headers] ||= {}).merge!({ + def request(request = {}) + request[:headers] = { "X-Api-Version" => @joyent_version, "Content-Type" => "application/json", "Accept" => "application/json" - }).merge!(@header_method.call) + }.merge(request[:headers] || {}).merge(@header_method.call) - if request_options[:body] - request_options[:body] = MultiJson.encode(request_options[:body]) + if request[:body] + request[:body] = MultiJson.encode(request[:body]) end - response = @connection.request(request_options) + response = @connection.request(request) if response.headers["Content-Type"] == "application/json" - response.body = MultiJson.decode(response.body) - response.body = decode_time_props(response.body) + response.body = json_decode(response.body) end + raise_if_error!(request, response) + response end private - def header_for_basic + def json_decode(body) + parsed = MultiJson.decode(body) + decode_time_attrs(parsed) + end + + def header_for_basic_auth { "Authorization" => "Basic #{Base64.encode64("#{@joyent_username}:#{@joyent_password}").delete("\r\n")}" } end - def header_for_signature + def header_for_signature_auth date = Time.now.utc.httpdate signature = Base64.encode64(@rsa.sign("sha256", date)).delete("\r\n") key_id = "/#{@joyent_username}/keys/#{@joyent_keyname}" @@ -172,22 +182,46 @@ module Fog } end - def decode_time_props(obj) + def decode_time_attrs(obj) if obj.kind_of?(Hash) - if obj["created"] - obj["created"] = Time.parse(obj["created"]) - end - - if obj["updated"] - obj["updated"] = Time.parse(obj["updated"]) - end + obj["created"] = Time.parse(obj["created"]) if obj["created"] + obj["updated"] = Time.parse(obj["updated"]) if obj["updated"] elsif obj.kind_of?(Array) obj.map do |o| - decode_time_props(o) + decode_time_attrs(o) end end + obj end + + def raise_if_error!(request, response) + case response.status + when 401 then + raise Errors::Unauthorized.new('Invalid credentials were used', request, response) + when 403 then + raise Errors::Forbidden.new('No permissions to the specified resource', request, response) + when 404 then + raise Errors::NotFound.new('Requested resource was not found', request, response) + when 405 then + raise Errors::MethodNotAllowed.new('Method not supported for the given resource', request, response) + when 406 then + raise Errors::NotAcceptable.new('Try sending a different Accept header', request, response) + when 409 then + raise Errors::Conflict.new('Most likely invalid or missing parameters', request, response) + when 414 then + raise Errors::RequestEntityTooLarge.new('You sent too much data', request, response) + when 415 then + raise Errors::UnsupportedMediaType.new('You encoded your request in a format we don\'t understand', request, response) + when 420 then + raise Errors::PolicyNotForfilled.new('You are sending too many requests', request, response) + when 449 then + raise Errors::RetryWith.new('Invalid API Version requested; try with a different API Version', request, response) + when 503 then + raise Errors::ServiceUnavailable.new('Either there\'s no capacity in this datacenter, or we\'re in a maintenance window', request, response) + end + end + end # Real end end diff --git a/lib/fog/joyent/errors.rb b/lib/fog/joyent/errors.rb new file mode 100644 index 000000000..aa55dd260 --- /dev/null +++ b/lib/fog/joyent/errors.rb @@ -0,0 +1,90 @@ +module Fog + module Compute + class Joyent < Fog::Service + + class Errors + module MessageParserMixin + def message + if response.body["code"] && response.body["message"] + "[ERROR #{response.body['code']}] : #{response.body['message']}" + else + '' + end + end + + def to_s + message + end + end + + # https://us-west-1.api.joyentcloud.com/docs#cloudapi-http-responses + # + # HTTP Status Codes + # + # Your client should check for each of the following status codes from any API request: + # + # Response Code Description + + # 400 Bad Request Invalid HTTP Request + class BadRequest < Excon::Errors::BadRequest + include MessageParserMixin + end + + # 401 Unauthorized Either no Authorization header was sent, or invalid credentials were used + class Unauthorized < Excon::Errors::Unauthorized + include MessageParserMixin + end + + # 403 Forbidden No permissions to the specified resource + class Forbidden < Excon::Errors::Forbidden + include MessageParserMixin + end + + # 404 Not Found Something you requested was not found + class NotFound < Excon::Errors::NotFound + include MessageParserMixin + end + + # 405 Method Not Allowed Method not supported for the given resource + class MethodNotAllowed < Excon::Errors::MethodNotAllowed + include MessageParserMixin + end + + # 406 Not Acceptable Try sending a different Accept header + class NotAcceptable < Excon::Errors::NotAcceptable + include MessageParserMixin + end + + # 409 Conflict Most likely invalid or missing parameters + class Conflict < Excon::Errors::Conflict + include MessageParserMixin + end + + # 413 Request Entity Too Large You sent too much data + class RequestEntityTooLarge < Excon::Errors::RequestEntityTooLarge + include MessageParserMixin + end + + # 415 Unsupported Media Type You encoded your request in a format we don't understand + class UnsupportedMediaType < Excon::Errors::UnsupportedMediaType + include MessageParserMixin + end + + # 420 Slow Down You're sending too many requests + class PolicyNotForfilled < Excon::Errors::HTTPStatusError + include MessageParserMixin + end + + # 449 Retry With Invalid Version header; try with a different X-Api-Version string + class RetryWith < Excon::Errors::HTTPStatusError + include MessageParserMixin + end + + # 503 Service Unavailable Either there's no capacity in this datacenter, or we're in a maintenance window + class ServiceUnavailable < Excon::Errors::ServiceUnavailable + include MessageParserMixin + end + end + end + end +end