lib/awsbase/right_awsbase.rb in icehouse-right_aws-1.11.0 vs lib/awsbase/right_awsbase.rb in icehouse-right_aws-2.2.0

- old
+ new

@@ -22,18 +22,24 @@ # # Test module RightAws require 'digest/md5' - require 'pp' class AwsUtils #:nodoc: @@digest1 = OpenSSL::Digest::Digest.new("sha1") @@digest256 = nil if OpenSSL::OPENSSL_VERSION_NUMBER > 0x00908000 @@digest256 = OpenSSL::Digest::Digest.new("sha256") rescue nil # Some installation may not support sha256 end + + def self.utc_iso8601(time) + if time.is_a?(Fixnum) then time = Time::at(time) + elsif time.is_a?(String) then time = Time::parse(time) + end + time.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z") + end def self.sign(aws_secret_access_key, auth_string) Base64.encode64(OpenSSL::HMAC.digest(@@digest1, aws_secret_access_key, auth_string)).strip end @@ -43,13 +49,21 @@ param.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do '%' + $1.unpack('H2' * $1.size).join('%').upcase end end + def self.xml_escape(text) # :nodoc: + REXML::Text::normalize(text) + end + + def self.xml_unescape(text) # :nodoc: + REXML::Text::unnormalize(text) + end + # Set a timestamp and a signature version def self.fix_service_params(service_hash, signature) - service_hash["Timestamp"] ||= Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z") unless service_hash["Expires"] + service_hash["Timestamp"] ||= utc_iso8601(Time.now) unless service_hash["Expires"] service_hash["SignatureVersion"] = signature service_hash end # Signature Version 0 @@ -122,14 +136,28 @@ caller[1]=~/`(.*?)'/ $1 end def self.split_items_and_params(array) - items = array.to_a.flatten.compact + items = Array(array).flatten.compact params = items.last.kind_of?(Hash) ? items.pop : {} [items, params] end + + # Generates a token in format of: + # 1. "1dd8d4e4-db6b-11df-b31d-0025b37efad0 (if UUID gem is loaded) + # 2. "1287483761-855215-zSv2z-bWGj2-31M5t-ags9m" (if UUID gem is not loaded) + TOKEN_GENERATOR_CHARSET = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + def self.generate_unique_token + time = Time.now + token = "%d-%06d" % [time.to_i, time.usec] + 4.times do + token << "-" + 5.times { token << TOKEN_GENERATOR_CHARSET[rand(TOKEN_GENERATOR_CHARSET.size)] } + end + token + end end class AwsBenchmarkingBlock #:nodoc: attr_accessor :xml, :service def initialize @@ -171,11 +199,35 @@ # Sets the list of Amazon side problems. Use in conjunction with the # getter to append problems. def self.amazon_problems=(problems_list) @@amazon_problems = problems_list end - + + # Raise an exception if a timeout occures while an API call is in progress. + # This helps to avoid a duplicate resources creation when Amazon hangs for some time and + # RightHttpConnection is forced to use retries to get a response from it. + # + # If an API call action is in the list then no attempts to retry are performed. + # + RAISE_ON_TIMEOUT_ON_ACTIONS = %w{ + AllocateAddress + CreateSnapshot + CreateVolume + PurchaseReservedInstancesOffering + RequestSpotInstances + RunInstances + } + @@raise_on_timeout_on_actions = RAISE_ON_TIMEOUT_ON_ACTIONS.dup + + def self.raise_on_timeout_on_actions + @@raise_on_timeout_on_actions + end + + def self.raise_on_timeout_on_actions=(actions_list) + @@raise_on_timeout_on_actions = actions_list + end + end module RightAwsBaseInterface DEFAULT_SIGNATURE_VERSION = '2' @@ -212,37 +264,46 @@ def init(service_info, aws_access_key_id, aws_secret_access_key, params={}) #:nodoc: @params = params # If one defines EC2_URL he may forget to use a single slash as an "empty service" path. # Amazon does not like this therefore add this bad boy if he is missing... - service_info[:default_service] = '/' if service_info[:default_service].blank? + service_info[:default_service] = '/' if service_info[:default_service].right_blank? raise AwsError.new("AWS access keys are required to operate on #{service_info[:name]}") \ - if aws_access_key_id.blank? || aws_secret_access_key.blank? + if aws_access_key_id.right_blank? || aws_secret_access_key.right_blank? @aws_access_key_id = aws_access_key_id @aws_secret_access_key = aws_secret_access_key # if the endpoint was explicitly defined - then use it if @params[:endpoint_url] - @params[:server] = URI.parse(@params[:endpoint_url]).host - @params[:port] = URI.parse(@params[:endpoint_url]).port - @params[:service] = URI.parse(@params[:endpoint_url]).path + uri = URI.parse(@params[:endpoint_url]) + @params[:server] = uri.host + @params[:port] = uri.port + @params[:service] = uri.path + @params[:protocol] = uri.scheme # make sure the 'service' path is not empty - @params[:service] = service_info[:default_service] if @params[:service].blank? - @params[:protocol] = URI.parse(@params[:endpoint_url]).scheme + @params[:service] = service_info[:default_service] if @params[:service].right_blank? @params[:region] = nil + default_port = uri.default_port else @params[:server] ||= service_info[:default_host] @params[:server] = "#{@params[:region]}.#{@params[:server]}" if @params[:region] @params[:port] ||= service_info[:default_port] @params[:service] ||= service_info[:default_service] @params[:protocol] ||= service_info[:default_protocol] + default_port = @params[:protocol] == 'https' ? 443 : 80 end -# @params[:multi_thread] ||= defined?(AWS_DAEMON) + # build a host name to sign + @params[:host_to_sign] = @params[:server].dup + @params[:host_to_sign] << ":#{@params[:port]}" unless default_port == @params[:port].to_i + # a set of options to be passed to RightHttpConnection object + @params[:connection_options] = {} unless @params[:connection_options].is_a?(Hash) + @with_connection_options = {} @params[:connections] ||= :shared # || :dedicated @params[:max_connections] ||= 10 @params[:connection_lifetime] ||= 20*60 @params[:api_version] ||= service_info[:default_api_version] @logger = @params[:logger] + @logger = ::Rails.logger if !@logger && defined?(::Rails) && ::Rails.respond_to?(:logger) @logger = RAILS_DEFAULT_LOGGER if !@logger && defined?(RAILS_DEFAULT_LOGGER) @logger = Logger.new(STDOUT) if !@logger @logger.info "New #{self.class.name} using #{@params[:connections]} connections mode" @error_handler = nil @cache = {} @@ -273,11 +334,12 @@ if caching? function = function.to_sym # get rid of requestId (this bad boy was added for API 2008-08-08+ and it is uniq for every response) # feb 04, 2009 (load balancer uses 'RequestId' hence use 'i' modifier to hit it also) response = response.sub(%r{<requestId>.+?</requestId>}i, '') - response_md5 = MD5.md5(response).to_s + # this should work for both ruby 1.8.x and 1.9.x + response_md5 = Digest::MD5::new.update(response).to_s # check for changes unless @cache[function] && @cache[function][:response_md5] == response_md5 # well, the response is new, reset cache data update_cache(function, {:response_md5 => response_md5, :timestamp => Time.now, @@ -304,16 +366,62 @@ def on_exception(options={:raise=>true, :log=>true}) # :nodoc: raise if $!.is_a?(AwsNoChange) AwsError::on_aws_exception(self, options) end - -# # Return +true+ if this instance works in multi_thread mode and +false+ otherwise. -# def multi_thread -# @params[:multi_thread] -# end + #---------------------------- + # HTTP Connections handling + #---------------------------- + + def get_server_url(request) # :nodoc: + "#{request[:protocol]}://#{request[:server]}:#{request[:port]}" + end + + def get_connections_storage(aws_service) # :nodoc: + case @params[:connections].to_s + when 'dedicated' then @connections_storage ||= {} + else Thread.current[aws_service] ||= {} + end + end + + def destroy_connection(request, reason) # :nodoc: + connections = get_connections_storage(request[:aws_service]) + server_url = get_server_url(request) + if connections[server_url] + connections[server_url][:connection].finish(reason) + connections.delete(server_url) + end + end + + # Expire the connection if it has expired. + def get_connection(request) # :nodoc: + server_url = get_server_url(request) + connection_storage = get_connections_storage(request[:aws_service]) + life_time_scratch = Time.now-@params[:connection_lifetime] + # Delete out-of-dated connections + connections_in_list = 0 + connection_storage.to_a.sort{|conn1, conn2| conn2[1][:last_used_at] <=> conn1[1][:last_used_at]}.each do |serv_url, conn_opts| + if @params[:max_connections] <= connections_in_list + conn_opts[:connection].finish('out-of-limit') + connection_storage.delete(server_url) + elsif conn_opts[:last_used_at] < life_time_scratch + conn_opts[:connection].finish('out-of-date') + connection_storage.delete(server_url) + else + connections_in_list += 1 + end + end + connection = (connection_storage[server_url] ||= {}) + connection[:last_used_at] = Time.now + connection[:connection] ||= Rightscale::HttpConnection.new(:exception => RightAws::AwsError, :logger => @logger) + end + + #---------------------------- + # HTTP Requests handling + #---------------------------- + # ACF, AMS, EC2, LBS and SDB uses this guy # SQS and S3 use their own methods def generate_request_impl(verb, action, options={}) #:nodoc: # Form a valid http verb: 'GET' or 'POST' (all the other are not supported now) http_verb = verb.to_s.upcase @@ -323,68 +431,51 @@ service_hash = {"Action" => action, "AWSAccessKeyId" => @aws_access_key_id, "Version" => @params[:api_version] } service_hash.merge!(options) # Sign request options - service_params = signed_service_params(@aws_secret_access_key, service_hash, http_verb, @params[:server], @params[:service]) + service_params = signed_service_params(@aws_secret_access_key, service_hash, http_verb, @params[:host_to_sign], @params[:service]) # Use POST if the length of the query string is too large # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/MakingRESTRequests.html if http_verb != 'POST' && service_params.size > 2000 http_verb = 'POST' if signature_version == '2' - service_params = signed_service_params(@aws_secret_access_key, service_hash, http_verb, @params[:server], @params[:service]) + service_params = signed_service_params(@aws_secret_access_key, service_hash, http_verb, @params[:host_to_sign], @params[:service]) end end # create a request case http_verb when 'GET' request = Net::HTTP::Get.new("#{@params[:service]}?#{service_params}") when 'POST' request = Net::HTTP::Post.new(@params[:service]) request.body = service_params - request['Content-Type'] = 'application/x-www-form-urlencoded' + request['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8' else raise "Unsupported HTTP verb #{verb.inspect}!" end # prepare output hash - { :request => request, - :server => @params[:server], - :port => @params[:port], - :protocol => @params[:protocol] } - end - - def get_connection(aws_service, request) #:nodoc - server_url = "#{request[:protocol]}://#{request[:server]}:#{request[:port]}}" - # - case @params[:connections].to_s - when 'dedicated' - @connections_storage ||= {} - else # 'dedicated' - @connections_storage = (Thread.current[aws_service] ||= {}) + request_hash = { :request => request, + :server => @params[:server], + :port => @params[:port], + :protocol => @params[:protocol] } + request_hash.merge!(@params[:connection_options]) + request_hash.merge!(@with_connection_options) + + # If an action is marked as "non-retryable" and there was no :raise_on_timeout option set + # explicitly then do set that option + if Array(RightAwsBase::raise_on_timeout_on_actions).include?(action) && !request_hash.has_key?(:raise_on_timeout) + request_hash.merge!(:raise_on_timeout => true) end - # - @connections_storage[server_url] ||= {} - @connections_storage[server_url][:last_used_at] = Time.now - @connections_storage[server_url][:connection] ||= Rightscale::HttpConnection.new(:exception => RightAws::AwsError, :logger => @logger) - # keep X most recent connections (but were used not far than Y minutes ago) - connections = 0 - @connections_storage.to_a.sort{|i1, i2| i2[1][:last_used_at] <=> i1[1][:last_used_at]}.to_a.each do |i| - if i[0] != server_url && (@params[:max_connections] <= connections || i[1][:last_used_at] < Time.now - @params[:connection_lifetime]) - # delete the connection from the list - @connections_storage.delete(i[0]) - # then finish it - i[1][:connection].finish((@params[:max_connections] <= connections) ? "out-of-limit" : "out-of-date") rescue nil - else - connections += 1 - end - end - @connections_storage[server_url][:connection] + + request_hash end # All services uses this guy. def request_info_impl(aws_service, benchblock, request, parser, &block) #:nodoc: - @connection = get_connection(aws_service, request) + request[:aws_service] = aws_service + @connection = get_connection(request) @last_request = request[:request] @last_response = nil response = nil blockexception = nil @@ -395,29 +486,35 @@ # low-level code of HttpConnection. The solution is not to let any # exception escape the block that we pass to HttpConnection::request. # Exceptions can originate from code directly in the block, or from user # code called in the other block which is passed to response.read_body. benchblock.service.add! do - responsehdr = @connection.request(request) do |response| - ######### - begin - @last_response = response - if response.is_a?(Net::HTTPSuccess) - @error_handler = nil - response.read_body(&block) - else - @error_handler = AWSErrorHandler.new(self, parser, :errors_list => self.class.amazon_problems) unless @error_handler - check_result = @error_handler.check(request) - if check_result + begin + responsehdr = @connection.request(request) do |response| + ######### + begin + @last_response = response + if response.is_a?(Net::HTTPSuccess) @error_handler = nil - return check_result + response.read_body(&block) + else + @error_handler = AWSErrorHandler.new(self, parser, :errors_list => self.class.amazon_problems) unless @error_handler + check_result = @error_handler.check(request) + if check_result + @error_handler = nil + return check_result + end + raise AwsError.new(@last_errors, @last_response.code, @last_request_id) end - raise AwsError.new(@last_errors, @last_response.code, @last_request_id) + rescue Exception => e + blockexception = e end - rescue Exception => e - blockexception = e end + rescue Exception => e + # Kill a connection if we run into a low level connection error + destroy_connection(request, "error: #{e.message}") + raise e end ######### #OK, now we are out of the block passed to the lower level if(blockexception) @@ -427,11 +524,19 @@ parser.parse(responsehdr) end return parser.result end else - benchblock.service.add!{ response = @connection.request(request) } + benchblock.service.add! do + begin + response = @connection.request(request) + rescue Exception => e + # Kill a connection if we run into a low level connection error + destroy_connection(request, "error: #{e.message}") + raise e + end + end # check response for errors... @last_response = response if response.is_a?(Net::HTTPSuccess) @error_handler = nil benchblock.xml.add! { parser.parse(response) } @@ -449,32 +554,49 @@ rescue @error_handler = nil raise end - def request_cache_or_info(method, link, parser_class, benchblock, use_cache=true) #:nodoc: + def request_cache_or_info(method, link, parser_class, benchblock, use_cache=true, &block) #:nodoc: # We do not want to break the logic of parsing hence will use a dummy parser to process all the standard # steps (errors checking etc). The dummy parser does nothig - just returns back the params it received. # If the caching is enabled and hit then throw AwsNoChange. # P.S. caching works for the whole images list only! (when the list param is blank) # check cache response, params = request_info(link, RightDummyParser.new) cache_hits?(method.to_sym, response.body) if use_cache parser = parser_class.new(:logger => @logger) benchblock.xml.add!{ parser.parse(response, params) } - result = block_given? ? yield(parser) : parser.result + result = block ? block.call(parser) : parser.result # update parsed data update_cache(method.to_sym, :parsed => result) if use_cache result end # Returns Amazons request ID for the latest request def last_request_id @last_response && @last_response.body.to_s[%r{<requestId>(.+?)</requestId>}i] && $1 end + # Incrementally lists something. + def incrementally_list_items(action, parser_class, params={}, &block) # :nodoc: + params = params.dup + params['MaxItems'] = params.delete(:max_items) if params[:max_items] + params['Marker'] = params.delete(:marker) if params[:marker] + last_response = nil + loop do + last_response = request_info( generate_request(action, params), parser_class.new(:logger => @logger)) + params['Marker'] = last_response[:marker] + break unless block && block.call(last_response) && !last_response[:marker].right_blank? + end + last_response + end + # Format array of items into Amazons handy hash ('?' is a place holder): + # Options: + # :default => "something" : Set a value to "something" when it is nil + # :default => :skip_nils : Skip nil values # # amazonize_list('Item', ['a', 'b', 'c']) => # { 'Item.1' => 'a', 'Item.2' => 'b', 'Item.3' => 'c' } # # amazonize_list('Item.?.instance', ['a', 'c']) #=> @@ -494,26 +616,118 @@ # "Filter.1.Value.1"=>"aa", # "Filter.1.Value.2"=>"ab", # "Filter.2.Key"=>"B", # "Filter.2.Value.1"=>"ba", # "Filter.2.Value.2"=>"bb"} - def amazonize_list(masks, list) #:nodoc: + def amazonize_list(masks, list, options={}) #:nodoc: groups = {} - list.to_a.each_with_index do |list_item, i| - masks.to_a.each_with_index do |mask, mask_idx| + Array(list).each_with_index do |list_item, i| + Array(masks).each_with_index do |mask, mask_idx| key = mask[/\?/] ? mask.dup : mask.dup + '.?' key.sub!('?', (i+1).to_s) - value = list_item.to_a[mask_idx] + value = Array(list_item)[mask_idx] if value.is_a?(Array) - groups.merge!(amazonize_list(key, value)) + groups.merge!(amazonize_list(key, value, options)) else + if value.nil? + next if options[:default] == :skip_nils + value = options[:default] + end + # Hack to avoid having unhandled '?' in keys : do replace them all with '1': + # bad: ec2.amazonize_list(['Filter.?.Key', 'Filter.?.Value.?'], { a: => :b }) => {"Filter.1.Key"=>:a, "Filter.1.Value.?"=>1} + # good: ec2.amazonize_list(['Filter.?.Key', 'Filter.?.Value.?'], { a: => :b }) => {"Filter.1.Key"=>:a, "Filter.1.Value.1"=>1} + key.gsub!('?', '1') groups[key] = value end end end groups end + + BLOCK_DEVICE_KEY_MAPPING = { # :nodoc: + :device_name => 'DeviceName', + :virtual_name => 'VirtualName', + :no_device => 'NoDevice', + :ebs_snapshot_id => 'Ebs.SnapshotId', + :ebs_volume_size => 'Ebs.VolumeSize', + :ebs_delete_on_termination => 'Ebs.DeleteOnTermination' } + + def amazonize_block_device_mappings(block_device_mappings, key = 'BlockDeviceMapping') # :nodoc: + result = {} + unless block_device_mappings.right_blank? + block_device_mappings = [block_device_mappings] unless block_device_mappings.is_a?(Array) + block_device_mappings.each_with_index do |b, idx| + BLOCK_DEVICE_KEY_MAPPING.each do |local_name, remote_name| + value = b[local_name] + case local_name + when :no_device then value = value ? '' : nil # allow to pass :no_device as boolean + end + result["#{key}.#{idx+1}.#{remote_name}"] = value unless value.nil? + end + end + end + result + end + + # Execute a block of code with custom set of settings for right_http_connection. + # Accepts next options (see Rightscale::HttpConnection for explanation): + # :raise_on_timeout + # :http_connection_retry_count + # :http_connection_open_timeout + # :http_connection_read_timeout + # :http_connection_retry_delay + # :user_agent + # :exception + # + # Example #1: + # + # # Try to create a snapshot but stop with exception if timeout is received + # # to avoid having a duplicate API calls that create duplicate snapshots. + # ec2 = Rightscale::Ec2::new(aws_access_key_id, aws_secret_access_key) + # ec2.with_connection_options(:raise_on_timeout => true) do + # ec2.create_snapshot('vol-898a6fe0', 'KD: WooHoo!!') + # end + # + # Example #2: + # + # # Opposite case when the setting is global: + # @ec2 = Rightscale::Ec2::new(aws_access_key_id, aws_secret_access_key, + # :connection_options => { :raise_on_timeout => true }) + # # Create an SSHKey but do tries on timeout + # ec2.with_connection_options(:raise_on_timeout => false) do + # new_key = ec2.create_key_pair('my_test_key') + # end + # + # Example #3: + # + # # Global settings (HttpConnection level): + # Rightscale::HttpConnection::params[:http_connection_open_timeout] = 5 + # Rightscale::HttpConnection::params[:http_connection_read_timeout] = 250 + # Rightscale::HttpConnection::params[:http_connection_retry_count] = 2 + # + # # Local setings (RightAws level) + # ec2 = Rightscale::Ec2::new(AWS_ID, AWS_KEY, + # :region => 'us-east-1', + # :connection_options => { + # :http_connection_read_timeout => 2, + # :http_connection_retry_count => 5, + # :user_agent => 'Mozilla 4.0' + # }) + # + # # Custom settings (API call level) + # ec2.with_connection_options(:raise_on_timeout => true, + # :http_connection_read_timeout => 10, + # :user_agent => '') do + # pp ec2.describe_images + # end + # + def with_connection_options(options, &block) + @with_connection_options = options + block.call self + ensure + @with_connection_options = {} + end end # Exception class to signal any Amazon errors. All errors occuring during calls to Amazon's # web services raise this type of error. @@ -630,11 +844,11 @@ @stop_at = @started_at + (params[:reiteration_time] || @@reiteration_time) @errors_list = params[:errors_list] || [] @reiteration_delay = @@reiteration_start_delay @retries = 0 # close current HTTP(S) connection on 5xx, errors from list and 4xx errors - @close_on_error = params[:close_on_error].nil? ? @@close_on_error : params[:close_on_error] + @close_on_error = params[:close_on_error].nil? ? @@close_on_error : params[:close_on_error] @close_on_4xx_probability = params[:close_on_4xx_probability] || @@close_on_4xx_probability end # Returns false if def check(request) #:nodoc: @@ -673,14 +887,21 @@ end # Ok, it is a redirect, find the new destination location if redirect_detected location = response['location'] + # As for 301 ( Moved Permanently) Amazon does not return a 'Location' header but + # it is possible to extract a new endpoint from the response body + if location.right_blank? && response.code=='301' && response.body + new_endpoint = response.body[/<Endpoint>(.*?)<\/Endpoint>/] && $1 + location = "#{request[:protocol]}://#{new_endpoint}:#{request[:port]}#{request[:request].path}" + end # ... log information and ... @aws.logger.info("##### #{@aws.class.name} redirect requested: #{response.code} #{response.message} #####") @aws.logger.info(" Old location: #{request_text_data}") @aws.logger.info(" New location: #{location}") + @aws.logger.info(" Request Verb: #{request[:request].class.name}") # ... fix the connection data request[:server] = URI.parse(location).host request[:protocol] = URI.parse(location).scheme request[:port] = URI.parse(location).port else @@ -699,11 +920,11 @@ if redirect_detected || error_found # Close the connection to the server and recreate a new one. # It may have a chance that one server is a semi-down and reconnection # will help us to connect to the other server if !redirect_detected && @close_on_error - @aws.connection.finish "#{self.class.name}: error match to pattern '#{error_match}'" + @aws.destroy_connection(request, "#{self.class.name}: error match to pattern '#{error_match}'") end if (Time.now < @stop_at) @retries += 1 unless redirect_detected @@ -728,50 +949,62 @@ else @aws.logger.warn("##### Ooops, time is over... ####") end # aha, this is unhandled error: elsif @close_on_error - # Is this a 5xx error ? - if @aws.last_response.code.to_s[/^5\d\d$/] - @aws.connection.finish "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}'" + # On 5xx(Server errors), 403(RequestTimeTooSkewed) and 408(Request Timeout) a conection has to be closed + if @aws.last_response.code.to_s[/^(5\d\d|403|408)$/] + @aws.destroy_connection(request, "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}'") # Is this a 4xx error ? elsif @aws.last_response.code.to_s[/^4\d\d$/] && @close_on_4xx_probability > rand(100) - @aws.connection.finish "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}', " + - "probability: #{@close_on_4xx_probability}%" + @aws.destroy_connection(request, "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}', " + + "probability: #{@close_on_4xx_probability}%") end end result end end #----------------------------------------------------------------- - class RightSaxParserCallback #:nodoc: - def self.include_callback - include XML::SaxParser::Callbacks - end + class RightSaxParserCallbackTemplate #:nodoc: def initialize(right_aws_parser) @right_aws_parser = right_aws_parser end - def on_start_element(name, attr_hash) - @right_aws_parser.tag_start(name, attr_hash) - end def on_characters(chars) @right_aws_parser.text(chars) end - def on_end_element(name) - @right_aws_parser.tag_end(name) - end def on_start_document; end def on_comment(msg); end def on_processing_instruction(target, data); end def on_cdata_block(cdata); end def on_end_document; end end - + + class RightSaxParserCallback < RightSaxParserCallbackTemplate + def self.include_callback + include XML::SaxParser::Callbacks + end + def on_start_element(name, attr_hash) + @right_aws_parser.tag_start(name, attr_hash) + end + def on_end_element(name) + @right_aws_parser.tag_end(name) + end + end + + class RightSaxParserCallbackNs < RightSaxParserCallbackTemplate + def on_start_element_ns(name, attr_hash, prefix, uri, namespaces) + @right_aws_parser.tag_start(name, attr_hash) + end + def on_end_element_ns(name, prefix, uri) + @right_aws_parser.tag_end(name) + end + end + class RightAWSParser #:nodoc: # default parsing library DEFAULT_XML_LIBRARY = 'rexml' # a list of supported parsers @@supported_xml_libs = [DEFAULT_XML_LIBRARY, 'libxml'] @@ -828,31 +1061,39 @@ @xml_lib = DEFAULT_XML_LIBRARY unless @@supported_xml_libs.include?(@xml_lib) # load xml library if @xml_lib=='libxml' && !defined?(XML::SaxParser) begin require 'xml/libxml' - # is it new ? - Setup SaxParserCallback - if XML::Parser::VERSION >= '0.5.1.0' - RightSaxParserCallback.include_callback + # Setup SaxParserCallback + if XML::Parser::VERSION >= '0.5.1' && + XML::Parser::VERSION < '0.9.7' + RightSaxParserCallback.include_callback end rescue LoadError => e - @@supported_xml_libs.delete(@xml_lib) - @xml_lib = DEFAULT_XML_LIBRARY + @@supported_xml_libs.delete(@xml_lib) + @xml_lib = DEFAULT_XML_LIBRARY if @logger @logger.error e.inspect @logger.error e.backtrace - @logger.info "Can not load 'libxml' library. '#{DEFAULT_XML_LIBRARY}' is used for parsing." + @logger.info "Can not load 'libxml' library. '#{DEFAULT_XML_LIBRARY}' is used for parsing." end end end # Parse the xml text case @xml_lib - when 'libxml' - xml = XML::SaxParser.new - xml.string = xml_text + when 'libxml' + if XML::Parser::VERSION >= '0.9.9' + # avoid warning on every usage + xml = XML::SaxParser.string(xml_text) + else + xml = XML::SaxParser.new + xml.string = xml_text + end # check libxml-ruby version - if XML::Parser::VERSION >= '0.5.1.0' + if XML::Parser::VERSION >= '0.9.7' + xml.callbacks = RightSaxParserCallbackNs.new(self) + elsif XML::Parser::VERSION >= '0.5.1' xml.callbacks = RightSaxParserCallback.new(self) else xml.on_start_element{|name, attr_hash| self.tag_start(name, attr_hash)} xml.on_characters{ |text| self.text(text)} xml.on_end_element{ |name| self.tag_end(name)} @@ -924,9 +1165,15 @@ end class RightHttp2xxParser < RightAWSParser # :nodoc: def parse(response) @result = response.is_a?(Net::HTTPSuccess) + end + end + + class RightBoolResponseParser < RightAWSParser #:nodoc: + def tagend(name) + @result = (@text=='true') if name == 'return' end end end