right_aws 1.9.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. data/History.txt +164 -13
  2. data/Manifest.txt +28 -1
  3. data/README.txt +12 -10
  4. data/Rakefile +56 -29
  5. data/lib/acf/right_acf_interface.rb +343 -172
  6. data/lib/acf/right_acf_invalidations.rb +144 -0
  7. data/lib/acf/right_acf_origin_access_identities.rb +230 -0
  8. data/lib/acf/right_acf_streaming_interface.rb +229 -0
  9. data/lib/acw/right_acw_interface.rb +248 -0
  10. data/lib/as/right_as_interface.rb +698 -0
  11. data/lib/awsbase/right_awsbase.rb +755 -115
  12. data/lib/awsbase/support.rb +2 -78
  13. data/lib/awsbase/version.rb +9 -0
  14. data/lib/ec2/right_ec2.rb +274 -1294
  15. data/lib/ec2/right_ec2_ebs.rb +514 -0
  16. data/lib/ec2/right_ec2_images.rb +444 -0
  17. data/lib/ec2/right_ec2_instances.rb +797 -0
  18. data/lib/ec2/right_ec2_monitoring.rb +70 -0
  19. data/lib/ec2/right_ec2_placement_groups.rb +108 -0
  20. data/lib/ec2/right_ec2_reserved_instances.rb +243 -0
  21. data/lib/ec2/right_ec2_security_groups.rb +496 -0
  22. data/lib/ec2/right_ec2_spot_instances.rb +422 -0
  23. data/lib/ec2/right_ec2_tags.rb +139 -0
  24. data/lib/ec2/right_ec2_vpc.rb +598 -0
  25. data/lib/ec2/right_ec2_vpc2.rb +382 -0
  26. data/lib/ec2/right_ec2_windows_mobility.rb +84 -0
  27. data/lib/elb/right_elb_interface.rb +573 -0
  28. data/lib/emr/right_emr_interface.rb +728 -0
  29. data/lib/iam/right_iam_access_keys.rb +71 -0
  30. data/lib/iam/right_iam_groups.rb +195 -0
  31. data/lib/iam/right_iam_interface.rb +341 -0
  32. data/lib/iam/right_iam_mfa_devices.rb +67 -0
  33. data/lib/iam/right_iam_users.rb +251 -0
  34. data/lib/rds/right_rds_interface.rb +1657 -0
  35. data/lib/right_aws.rb +30 -13
  36. data/lib/route_53/right_route_53_interface.rb +641 -0
  37. data/lib/s3/right_s3.rb +108 -41
  38. data/lib/s3/right_s3_interface.rb +349 -118
  39. data/lib/sdb/active_sdb.rb +388 -54
  40. data/lib/sdb/right_sdb_interface.rb +323 -64
  41. data/lib/sns/right_sns_interface.rb +286 -0
  42. data/lib/sqs/right_sqs.rb +1 -2
  43. data/lib/sqs/right_sqs_gen2.rb +73 -17
  44. data/lib/sqs/right_sqs_gen2_interface.rb +146 -73
  45. data/lib/sqs/right_sqs_interface.rb +12 -22
  46. data/right_aws.gemspec +91 -0
  47. data/test/README.mdown +39 -0
  48. data/test/acf/test_right_acf.rb +11 -19
  49. data/test/awsbase/test_helper.rb +2 -0
  50. data/test/awsbase/test_right_awsbase.rb +11 -0
  51. data/test/ec2/test_right_ec2.rb +32 -1
  52. data/test/elb/test_helper.rb +2 -0
  53. data/test/elb/test_right_elb.rb +43 -0
  54. data/test/rds/test_helper.rb +2 -0
  55. data/test/rds/test_right_rds.rb +120 -0
  56. data/test/route_53/fixtures/a_record.xml +18 -0
  57. data/test/route_53/fixtures/alias_record.xml +18 -0
  58. data/test/route_53/test_helper.rb +2 -0
  59. data/test/route_53/test_right_route_53.rb +141 -0
  60. data/test/s3/test_right_s3.rb +176 -42
  61. data/test/s3/test_right_s3_stubbed.rb +6 -4
  62. data/test/sdb/test_active_sdb.rb +120 -19
  63. data/test/sdb/test_batch_put_attributes.rb +54 -0
  64. data/test/sdb/test_right_sdb.rb +71 -16
  65. data/test/sns/test_helper.rb +2 -0
  66. data/test/sns/test_right_sns.rb +153 -0
  67. data/test/sqs/test_right_sqs.rb +0 -6
  68. data/test/sqs/test_right_sqs_gen2.rb +104 -49
  69. data/test/ts_right_aws.rb +1 -0
  70. metadata +181 -22
@@ -23,13 +23,103 @@
23
23
 
24
24
  # Test
25
25
  module RightAws
26
- require 'md5'
27
- require 'pp'
26
+ require 'digest/md5'
28
27
 
29
28
  class AwsUtils #:nodoc:
30
- @@digest = OpenSSL::Digest::Digest.new("sha1")
29
+ @@digest1 = OpenSSL::Digest::Digest.new("sha1")
30
+ @@digest256 = nil
31
+ if OpenSSL::OPENSSL_VERSION_NUMBER > 0x00908000
32
+ @@digest256 = OpenSSL::Digest::Digest.new("sha256") rescue nil # Some installation may not support sha256
33
+ end
34
+
35
+ def self.utc_iso8601(time)
36
+ if time.is_a?(Fixnum) then time = Time::at(time)
37
+ elsif time.is_a?(String) then time = Time::parse(time)
38
+ end
39
+ time.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z")
40
+ end
41
+
31
42
  def self.sign(aws_secret_access_key, auth_string)
32
- Base64.encode64(OpenSSL::HMAC.digest(@@digest, aws_secret_access_key, auth_string)).strip
43
+ Base64.encode64(OpenSSL::HMAC.digest(@@digest1, aws_secret_access_key, auth_string)).strip
44
+ end
45
+
46
+ # Calculates 'Content-MD5' header value for some content
47
+ def self.content_md5(content)
48
+ Base64.encode64(Digest::MD5::new.update(content).digest).strip
49
+ end
50
+
51
+ # Escape a string accordingly Amazon rulles
52
+ # http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
53
+ def self.amz_escape(param)
54
+ param = param.flatten.join('') if param.is_a?(Array) # ruby 1.9.x Array#to_s fix
55
+ param.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do
56
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
57
+ end
58
+ end
59
+
60
+ def self.xml_escape(text) # :nodoc:
61
+ REXML::Text::normalize(text)
62
+ end
63
+
64
+ def self.xml_unescape(text) # :nodoc:
65
+ REXML::Text::unnormalize(text)
66
+ end
67
+
68
+ # Set a timestamp and a signature version
69
+ def self.fix_service_params(service_hash, signature)
70
+ service_hash["Timestamp"] ||= utc_iso8601(Time.now) unless service_hash["Expires"]
71
+ service_hash["SignatureVersion"] = signature
72
+ service_hash
73
+ end
74
+
75
+ def self.fix_headers(headers)
76
+ result = {}
77
+ headers.each do |header, value|
78
+ next if !header.is_a?(String) || value.nil?
79
+ header = header.downcase
80
+ result[header] = value if result[header].right_blank?
81
+ end
82
+ result
83
+ end
84
+
85
+ # Signature Version 0
86
+ # A deprecated guy (should work till septemper 2009)
87
+ def self.sign_request_v0(aws_secret_access_key, service_hash)
88
+ fix_service_params(service_hash, '0')
89
+ string_to_sign = "#{service_hash['Action']}#{service_hash['Timestamp'] || service_hash['Expires']}"
90
+ service_hash['Signature'] = AwsUtils::sign(aws_secret_access_key, string_to_sign)
91
+ service_hash.to_a.collect{|key,val| "#{amz_escape(key)}=#{amz_escape(val.to_s)}" }.join("&")
92
+ end
93
+
94
+ # Signature Version 1
95
+ # Another deprecated guy (should work till septemper 2009)
96
+ def self.sign_request_v1(aws_secret_access_key, service_hash)
97
+ fix_service_params(service_hash, '1')
98
+ string_to_sign = service_hash.sort{|a,b| (a[0].to_s.downcase)<=>(b[0].to_s.downcase)}.to_s
99
+ service_hash['Signature'] = AwsUtils::sign(aws_secret_access_key, string_to_sign)
100
+ service_hash.to_a.collect{|key,val| "#{amz_escape(key)}=#{amz_escape(val.to_s)}" }.join("&")
101
+ end
102
+
103
+ # Signature Version 2
104
+ # EC2, SQS and SDB requests must be signed by this guy.
105
+ # See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
106
+ # http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1928
107
+ def self.sign_request_v2(aws_secret_access_key, service_hash, http_verb, host, uri)
108
+ fix_service_params(service_hash, '2')
109
+ # select a signing method (make an old openssl working with sha1)
110
+ # make 'HmacSHA256' to be a default one
111
+ service_hash['SignatureMethod'] = 'HmacSHA256' unless ['HmacSHA256', 'HmacSHA1'].include?(service_hash['SignatureMethod'])
112
+ service_hash['SignatureMethod'] = 'HmacSHA1' unless @@digest256
113
+ # select a digest
114
+ digest = (service_hash['SignatureMethod'] == 'HmacSHA256' ? @@digest256 : @@digest1)
115
+ # form string to sign
116
+ canonical_string = service_hash.keys.sort.map do |key|
117
+ "#{amz_escape(key)}=#{amz_escape(service_hash[key])}"
118
+ end.join('&')
119
+ string_to_sign = "#{http_verb.to_s.upcase}\n#{host.downcase}\n#{uri}\n#{canonical_string}"
120
+ # sign the string
121
+ signature = amz_escape(Base64.encode64(OpenSSL::HMAC.digest(digest, aws_secret_access_key, string_to_sign)).strip)
122
+ "#{canonical_string}&Signature=#{signature}"
33
123
  end
34
124
 
35
125
  # From Amazon's SQS Dev Guide, a brief description of how to escape:
@@ -63,6 +153,25 @@ module RightAws
63
153
  $1
64
154
  end
65
155
 
156
+ def self.split_items_and_params(array)
157
+ items = Array(array).flatten.compact
158
+ params = items.last.kind_of?(Hash) ? items.pop : {}
159
+ [items, params]
160
+ end
161
+
162
+ # Generates a token in format of:
163
+ # 1. "1dd8d4e4-db6b-11df-b31d-0025b37efad0 (if UUID gem is loaded)
164
+ # 2. "1287483761-855215-zSv2z-bWGj2-31M5t-ags9m" (if UUID gem is not loaded)
165
+ TOKEN_GENERATOR_CHARSET = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
166
+ def self.generate_unique_token
167
+ time = Time.now
168
+ token = "%d-%06d" % [time.to_i, time.usec]
169
+ 4.times do
170
+ token << "-"
171
+ 5.times { token << TOKEN_GENERATOR_CHARSET[rand(TOKEN_GENERATOR_CHARSET.size)] }
172
+ end
173
+ token
174
+ end
66
175
  end
67
176
 
68
177
  class AwsBenchmarkingBlock #:nodoc:
@@ -85,15 +194,16 @@ module RightAws
85
194
  # Text, if found in an error message returned by AWS, indicates that this may be a transient
86
195
  # error. Transient errors are automatically retried with exponential back-off.
87
196
  AMAZON_PROBLEMS = [ 'internal service error',
88
- 'is currently unavailable',
89
- 'no response from',
90
- 'Please try again',
91
- 'InternalError',
92
- 'ServiceUnavailable', #from SQS docs
93
- 'Unavailable',
94
- 'This application is not currently available',
95
- 'InsufficientInstanceCapacity'
96
- ]
197
+ 'is currently unavailable',
198
+ 'no response from',
199
+ 'Please try again',
200
+ 'InternalError',
201
+ 'Internal Server Error',
202
+ 'ServiceUnavailable', #from SQS docs
203
+ 'Unavailable',
204
+ 'This application is not currently available',
205
+ 'InsufficientInstanceCapacity'
206
+ ]
97
207
  @@amazon_problems = AMAZON_PROBLEMS
98
208
  # Returns a list of Amazon service responses which are known to be transient problems.
99
209
  # We have to re-request if we get any of them, because the problem will probably disappear.
@@ -107,11 +217,35 @@ module RightAws
107
217
  def self.amazon_problems=(problems_list)
108
218
  @@amazon_problems = problems_list
109
219
  end
110
-
220
+
221
+ # Raise an exception if a timeout occures while an API call is in progress.
222
+ # This helps to avoid a duplicate resources creation when Amazon hangs for some time and
223
+ # RightHttpConnection is forced to use retries to get a response from it.
224
+ #
225
+ # If an API call action is in the list then no attempts to retry are performed.
226
+ #
227
+ RAISE_ON_TIMEOUT_ON_ACTIONS = %w{
228
+ AllocateAddress
229
+ CreateSnapshot
230
+ CreateVolume
231
+ PurchaseReservedInstancesOffering
232
+ RequestSpotInstances
233
+ RunInstances
234
+ }
235
+ @@raise_on_timeout_on_actions = RAISE_ON_TIMEOUT_ON_ACTIONS.dup
236
+
237
+ def self.raise_on_timeout_on_actions
238
+ @@raise_on_timeout_on_actions
239
+ end
240
+
241
+ def self.raise_on_timeout_on_actions=(actions_list)
242
+ @@raise_on_timeout_on_actions = actions_list
243
+ end
244
+
111
245
  end
112
246
 
113
247
  module RightAwsBaseInterface
114
- DEFAULT_SIGNATURE_VERSION = '1'
248
+ DEFAULT_SIGNATURE_VERSION = '2'
115
249
 
116
250
  @@caching = false
117
251
  def self.caching
@@ -123,6 +257,8 @@ module RightAws
123
257
 
124
258
  # Current aws_access_key_id
125
259
  attr_reader :aws_access_key_id
260
+ # Current aws_secret_access_key
261
+ attr_reader :aws_secret_access_key
126
262
  # Last HTTP request object
127
263
  attr_reader :last_request
128
264
  # Last HTTP response object
@@ -144,24 +280,61 @@ module RightAws
144
280
 
145
281
  def init(service_info, aws_access_key_id, aws_secret_access_key, params={}) #:nodoc:
146
282
  @params = params
283
+ # If one defines EC2_URL he may forget to use a single slash as an "empty service" path.
284
+ # Amazon does not like this therefore add this bad boy if he is missing...
285
+ service_info[:default_service] = '/' if service_info[:default_service].right_blank?
147
286
  raise AwsError.new("AWS access keys are required to operate on #{service_info[:name]}") \
148
- if aws_access_key_id.blank? || aws_secret_access_key.blank?
287
+ if aws_access_key_id.right_blank? || aws_secret_access_key.right_blank?
149
288
  @aws_access_key_id = aws_access_key_id
150
289
  @aws_secret_access_key = aws_secret_access_key
151
- @params[:server] ||= service_info[:default_host]
152
- @params[:port] ||= service_info[:default_port]
153
- @params[:service] ||= service_info[:default_service]
154
- @params[:protocol] ||= service_info[:default_protocol]
155
- @params[:multi_thread] ||= defined?(AWS_DAEMON)
290
+ # if the endpoint was explicitly defined - then use it
291
+ if @params[:endpoint_url]
292
+ uri = URI.parse(@params[:endpoint_url])
293
+ @params[:server] = uri.host
294
+ @params[:port] = uri.port
295
+ @params[:service] = uri.path
296
+ @params[:protocol] = uri.scheme
297
+ # make sure the 'service' path is not empty
298
+ @params[:service] = service_info[:default_service] if @params[:service].right_blank?
299
+ @params[:region] = nil
300
+ default_port = uri.default_port
301
+ else
302
+ @params[:server] ||= service_info[:default_host]
303
+ @params[:server] = "#{@params[:region]}.#{@params[:server]}" if @params[:region]
304
+ @params[:port] ||= service_info[:default_port]
305
+ @params[:service] ||= service_info[:default_service]
306
+ @params[:protocol] ||= service_info[:default_protocol]
307
+ default_port = @params[:protocol] == 'https' ? 443 : 80
308
+ end
309
+ # build a host name to sign
310
+ @params[:host_to_sign] = @params[:server].dup
311
+ @params[:host_to_sign] << ":#{@params[:port]}" unless default_port == @params[:port].to_i
312
+ # a set of options to be passed to RightHttpConnection object
313
+ @params[:connection_options] = {} unless @params[:connection_options].is_a?(Hash)
314
+ @with_connection_options = {}
315
+ @params[:connections] ||= :shared # || :dedicated
316
+ @params[:max_connections] ||= 10
317
+ @params[:connection_lifetime] ||= 20*60
318
+ @params[:api_version] ||= service_info[:default_api_version]
156
319
  @logger = @params[:logger]
320
+ @logger = ::Rails.logger if !@logger && defined?(::Rails) && ::Rails.respond_to?(:logger)
157
321
  @logger = RAILS_DEFAULT_LOGGER if !@logger && defined?(RAILS_DEFAULT_LOGGER)
158
322
  @logger = Logger.new(STDOUT) if !@logger
159
- @logger.info "New #{self.class.name} using #{@params[:multi_thread] ? 'multi' : 'single'}-threaded mode"
323
+ @logger.info "New #{self.class.name} using #{@params[:connections]} connections mode"
160
324
  @error_handler = nil
161
325
  @cache = {}
162
326
  @signature_version = (params[:signature_version] || DEFAULT_SIGNATURE_VERSION).to_s
163
327
  end
164
328
 
329
+ def signed_service_params(aws_secret_access_key, service_hash, http_verb=nil, host=nil, service=nil )
330
+ case signature_version.to_s
331
+ when '0' then AwsUtils::sign_request_v0(aws_secret_access_key, service_hash)
332
+ when '1' then AwsUtils::sign_request_v1(aws_secret_access_key, service_hash)
333
+ when '2' then AwsUtils::sign_request_v2(aws_secret_access_key, service_hash, http_verb, host, service)
334
+ else raise AwsError.new("Unknown signature version (#{signature_version.to_s}) requested")
335
+ end
336
+ end
337
+
165
338
  # Returns +true+ if the describe_xxx responses are being cached
166
339
  def caching?
167
340
  @params.key?(:cache) ? @params[:cache] : @@caching
@@ -177,8 +350,10 @@ module RightAws
177
350
  if caching?
178
351
  function = function.to_sym
179
352
  # get rid of requestId (this bad boy was added for API 2008-08-08+ and it is uniq for every response)
180
- response = response.sub(%r{<requestId>.+?</requestId>}, '')
181
- response_md5 = MD5.md5(response).to_s
353
+ # feb 04, 2009 (load balancer uses 'RequestId' hence use 'i' modifier to hit it also)
354
+ response = response.sub(%r{<requestId>.+?</requestId>}i, '')
355
+ # this should work for both ruby 1.8.x and 1.9.x
356
+ response_md5 = Digest::MD5::new.update(response).to_s
182
357
  # check for changes
183
358
  unless @cache[function] && @cache[function][:response_md5] == response_md5
184
359
  # well, the response is new, reset cache data
@@ -209,17 +384,116 @@ module RightAws
209
384
  raise if $!.is_a?(AwsNoChange)
210
385
  AwsError::on_aws_exception(self, options)
211
386
  end
212
-
213
- # Return +true+ if this instance works in multi_thread mode and +false+ otherwise.
214
- def multi_thread
215
- @params[:multi_thread]
387
+
388
+ #----------------------------
389
+ # HTTP Connections handling
390
+ #----------------------------
391
+
392
+ def get_server_url(request) # :nodoc:
393
+ "#{request[:protocol]}://#{request[:server]}:#{request[:port]}"
394
+ end
395
+
396
+ def get_connections_storage(aws_service) # :nodoc:
397
+ case @params[:connections].to_s
398
+ when 'dedicated' then @connections_storage ||= {}
399
+ else Thread.current[aws_service] ||= {}
400
+ end
401
+ end
402
+
403
+ def destroy_connection(request, reason) # :nodoc:
404
+ connections = get_connections_storage(request[:aws_service])
405
+ server_url = get_server_url(request)
406
+ if connections[server_url]
407
+ connections[server_url][:connection].finish(reason)
408
+ connections.delete(server_url)
409
+ end
410
+ end
411
+
412
+ # Expire the connection if it has expired.
413
+ def get_connection(request) # :nodoc:
414
+ server_url = get_server_url(request)
415
+ connection_storage = get_connections_storage(request[:aws_service])
416
+ life_time_scratch = Time.now-@params[:connection_lifetime]
417
+ # Delete out-of-dated connections
418
+ connections_in_list = 0
419
+ connection_storage.to_a.sort{|conn1, conn2| conn2[1][:last_used_at] <=> conn1[1][:last_used_at]}.each do |serv_url, conn_opts|
420
+ if @params[:max_connections] <= connections_in_list
421
+ conn_opts[:connection].finish('out-of-limit')
422
+ connection_storage.delete(server_url)
423
+ elsif conn_opts[:last_used_at] < life_time_scratch
424
+ conn_opts[:connection].finish('out-of-date')
425
+ connection_storage.delete(server_url)
426
+ else
427
+ connections_in_list += 1
428
+ end
429
+ end
430
+ connection = (connection_storage[server_url] ||= {})
431
+ connection[:last_used_at] = Time.now
432
+ connection[:connection] ||= Rightscale::HttpConnection.new(:exception => RightAws::AwsError, :logger => @logger)
216
433
  end
217
434
 
218
- def request_info_impl(connection, benchblock, request, parser, &block) #:nodoc:
219
- @connection = connection
435
+ #----------------------------
436
+ # HTTP Requests handling
437
+ #----------------------------
438
+
439
+ # ACF, AMS, EC2, LBS and SDB uses this guy
440
+ # SQS and S3 use their own methods
441
+ def generate_request_impl(verb, action, options={}, custom_options={}) #:nodoc:
442
+ # Form a valid http verb: 'GET' or 'POST' (all the other are not supported now)
443
+ http_verb = verb.to_s.upcase
444
+ # remove empty keys from request options
445
+ options.delete_if { |key, value| value.nil? }
446
+ # prepare service data
447
+ service_hash = {"Action" => action,
448
+ "AWSAccessKeyId" => @aws_access_key_id,
449
+ "Version" => custom_options[:api_version] || @params[:api_version] }
450
+ service_hash.merge!(options)
451
+ service_hash["SecurityToken"] = @params[:token] if @params[:token]
452
+ # Sign request options
453
+ service_params = signed_service_params(@aws_secret_access_key, service_hash, http_verb, @params[:host_to_sign], @params[:service])
454
+ # Use POST if the length of the query string is too large
455
+ # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/MakingRESTRequests.html
456
+ if http_verb != 'POST' && service_params.size > 2000
457
+ http_verb = 'POST'
458
+ if signature_version == '2'
459
+ service_params = signed_service_params(@aws_secret_access_key, service_hash, http_verb, @params[:host_to_sign], @params[:service])
460
+ end
461
+ end
462
+ # create a request
463
+ case http_verb
464
+ when 'GET'
465
+ request = Net::HTTP::Get.new("#{@params[:service]}?#{service_params}")
466
+ when 'POST'
467
+ request = Net::HTTP::Post.new(@params[:service])
468
+ request.body = service_params
469
+ request['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
470
+ else
471
+ raise "Unsupported HTTP verb #{verb.inspect}!"
472
+ end
473
+ # prepare output hash
474
+ request_hash = { :request => request,
475
+ :server => @params[:server],
476
+ :port => @params[:port],
477
+ :protocol => @params[:protocol] }
478
+ request_hash.merge!(@params[:connection_options])
479
+ request_hash.merge!(@with_connection_options)
480
+
481
+ # If an action is marked as "non-retryable" and there was no :raise_on_timeout option set
482
+ # explicitly then do set that option
483
+ if Array(RightAwsBase::raise_on_timeout_on_actions).include?(action) && !request_hash.has_key?(:raise_on_timeout)
484
+ request_hash.merge!(:raise_on_timeout => true)
485
+ end
486
+
487
+ request_hash
488
+ end
489
+
490
+ # All services uses this guy.
491
+ def request_info_impl(aws_service, benchblock, request, parser, &block) #:nodoc:
492
+ request[:aws_service] = aws_service
493
+ @connection = get_connection(request)
220
494
  @last_request = request[:request]
221
495
  @last_response = nil
222
- response=nil
496
+ response = nil
223
497
  blockexception = nil
224
498
 
225
499
  if(block != nil)
@@ -231,25 +505,31 @@ module RightAws
231
505
  # Exceptions can originate from code directly in the block, or from user
232
506
  # code called in the other block which is passed to response.read_body.
233
507
  benchblock.service.add! do
234
- responsehdr = @connection.request(request) do |response|
235
- #########
236
- begin
237
- @last_response = response
238
- if response.is_a?(Net::HTTPSuccess)
239
- @error_handler = nil
240
- response.read_body(&block)
241
- else
242
- @error_handler = AWSErrorHandler.new(self, parser, :errors_list => self.class.amazon_problems) unless @error_handler
243
- check_result = @error_handler.check(request)
244
- if check_result
508
+ begin
509
+ responsehdr = @connection.request(request) do |response|
510
+ #########
511
+ begin
512
+ @last_response = response
513
+ if response.is_a?(Net::HTTPSuccess)
245
514
  @error_handler = nil
246
- return check_result
515
+ response.read_body(&block)
516
+ else
517
+ @error_handler = AWSErrorHandler.new(self, parser, :errors_list => self.class.amazon_problems) unless @error_handler
518
+ check_result = @error_handler.check(request)
519
+ if check_result
520
+ @error_handler = nil
521
+ return check_result
522
+ end
523
+ raise AwsError.new(@last_errors, @last_response.code, @last_request_id)
247
524
  end
248
- raise AwsError.new(@last_errors, @last_response.code, @last_request_id)
525
+ rescue Exception => e
526
+ blockexception = e
249
527
  end
250
- rescue Exception => e
251
- blockexception = e
252
528
  end
529
+ rescue Exception => e
530
+ # Kill a connection if we run into a low level connection error
531
+ destroy_connection(request, "error: #{e.message}")
532
+ raise e
253
533
  end
254
534
  #########
255
535
 
@@ -263,7 +543,15 @@ module RightAws
263
543
  return parser.result
264
544
  end
265
545
  else
266
- benchblock.service.add!{ response = @connection.request(request) }
546
+ benchblock.service.add! do
547
+ begin
548
+ response = @connection.request(request)
549
+ rescue Exception => e
550
+ # Kill a connection if we run into a low level connection error
551
+ destroy_connection(request, "error: #{e.message}")
552
+ raise e
553
+ end
554
+ end
267
555
  # check response for errors...
268
556
  @last_response = response
269
557
  if response.is_a?(Net::HTTPSuccess)
@@ -285,7 +573,7 @@ module RightAws
285
573
  raise
286
574
  end
287
575
 
288
- def request_cache_or_info(method, link, parser_class, benchblock, use_cache=true) #:nodoc:
576
+ def request_cache_or_info(method, link, parser_class, benchblock, use_cache=true, &block) #:nodoc:
289
577
  # We do not want to break the logic of parsing hence will use a dummy parser to process all the standard
290
578
  # steps (errors checking etc). The dummy parser does nothig - just returns back the params it received.
291
579
  # If the caching is enabled and hit then throw AwsNoChange.
@@ -295,7 +583,7 @@ module RightAws
295
583
  cache_hits?(method.to_sym, response.body) if use_cache
296
584
  parser = parser_class.new(:logger => @logger)
297
585
  benchblock.xml.add!{ parser.parse(response, params) }
298
- result = block_given? ? yield(parser) : parser.result
586
+ result = block ? block.call(parser) : parser.result
299
587
  # update parsed data
300
588
  update_cache(method.to_sym, :parsed => result) if use_cache
301
589
  result
@@ -303,9 +591,316 @@ module RightAws
303
591
 
304
592
  # Returns Amazons request ID for the latest request
305
593
  def last_request_id
306
- @last_response && @last_response.body.to_s[%r{<requestId>(.+?)</requestId>}] && $1
594
+ @last_response && @last_response.body.to_s[%r{<requestId>(.+?)</requestId>}i] && $1
307
595
  end
308
596
 
597
+ # Incrementally lists something.
598
+ def incrementally_list_items(action, parser_class, params={}, &block) # :nodoc:
599
+ params = params.dup
600
+ params['MaxItems'] = params.delete(:max_items) if params[:max_items]
601
+ params['Marker'] = params.delete(:marker) if params[:marker]
602
+ last_response = nil
603
+ loop do
604
+ last_response = request_info( generate_request(action, params), parser_class.new(:logger => @logger))
605
+ params['Marker'] = last_response[:marker]
606
+ break unless block && block.call(last_response) && !last_response[:marker].right_blank?
607
+ end
608
+ last_response
609
+ end
610
+
611
+ # Format array of items into Amazons handy hash ('?' is a place holder):
612
+ # Options:
613
+ # :default => "something" : Set a value to "something" when it is nil
614
+ # :default => :skip_nils : Skip nil values
615
+ #
616
+ # amazonize_list('Item', ['a', 'b', 'c']) =>
617
+ # { 'Item.1' => 'a', 'Item.2' => 'b', 'Item.3' => 'c' }
618
+ #
619
+ # amazonize_list('Item.?.instance', ['a', 'c']) #=>
620
+ # { 'Item.1.instance' => 'a', 'Item.2.instance' => 'c' }
621
+ #
622
+ # amazonize_list(['Item.?.Name', 'Item.?.Value'], {'A' => 'a', 'B' => 'b'}) #=>
623
+ # { 'Item.1.Name' => 'A', 'Item.1.Value' => 'a',
624
+ # 'Item.2.Name' => 'B', 'Item.2.Value' => 'b' }
625
+ #
626
+ # amazonize_list(['Item.?.Name', 'Item.?.Value'], [['A','a'], ['B','b']]) #=>
627
+ # { 'Item.1.Name' => 'A', 'Item.1.Value' => 'a',
628
+ # 'Item.2.Name' => 'B', 'Item.2.Value' => 'b' }
629
+ #
630
+ # amazonize_list(['Filter.?.Key', 'Filter.?.Value.?'], {'A' => ['aa','ab'], 'B' => ['ba','bb']}) #=>
631
+ # amazonize_list(['Filter.?.Key', 'Filter.?.Value.?'], [['A',['aa','ab']], ['B',['ba','bb']]]) #=>
632
+ # {"Filter.1.Key"=>"A",
633
+ # "Filter.1.Value.1"=>"aa",
634
+ # "Filter.1.Value.2"=>"ab",
635
+ # "Filter.2.Key"=>"B",
636
+ # "Filter.2.Value.1"=>"ba",
637
+ # "Filter.2.Value.2"=>"bb"}
638
+ def amazonize_list(masks, list, options={}) #:nodoc:
639
+ groups = {}
640
+ list_idx = options[:index] || 1
641
+ Array(list).each do |list_item|
642
+ Array(masks).each_with_index do |mask, mask_idx|
643
+ key = mask[/\?/] ? mask.dup : mask.dup + '.?'
644
+ key.sub!('?', list_idx.to_s)
645
+ value = Array(list_item)[mask_idx]
646
+ if value.is_a?(Array)
647
+ groups.merge!(amazonize_list(key, value, options))
648
+ else
649
+ if value.nil?
650
+ next if options[:default] == :skip_nils
651
+ value = options[:default]
652
+ end
653
+ # Hack to avoid having unhandled '?' in keys : do replace them all with '1':
654
+ # bad: ec2.amazonize_list(['Filter.?.Key', 'Filter.?.Value.?'], { a: => :b }) => {"Filter.1.Key"=>:a, "Filter.1.Value.?"=>1}
655
+ # good: ec2.amazonize_list(['Filter.?.Key', 'Filter.?.Value.?'], { a: => :b }) => {"Filter.1.Key"=>:a, "Filter.1.Value.1"=>1}
656
+ key.gsub!('?', '1')
657
+ groups[key] = value
658
+ end
659
+ end
660
+ list_idx += 1
661
+ end
662
+ groups
663
+ end
664
+
665
+ BLOCK_DEVICE_KEY_MAPPING = { # :nodoc:
666
+ :device_name => 'DeviceName',
667
+ :virtual_name => 'VirtualName',
668
+ :no_device => 'NoDevice',
669
+ :ebs_snapshot_id => 'Ebs.SnapshotId',
670
+ :ebs_volume_size => 'Ebs.VolumeSize',
671
+ :ebs_delete_on_termination => 'Ebs.DeleteOnTermination' }
672
+
673
+ def amazonize_block_device_mappings(block_device_mappings, key = 'BlockDeviceMapping') # :nodoc:
674
+ result = {}
675
+ unless block_device_mappings.right_blank?
676
+ block_device_mappings = [block_device_mappings] unless block_device_mappings.is_a?(Array)
677
+ block_device_mappings.each_with_index do |b, idx|
678
+ BLOCK_DEVICE_KEY_MAPPING.each do |local_name, remote_name|
679
+ value = b[local_name]
680
+ case local_name
681
+ when :no_device then value = value ? '' : nil # allow to pass :no_device as boolean
682
+ end
683
+ result["#{key}.#{idx+1}.#{remote_name}"] = value unless value.nil?
684
+ end
685
+ end
686
+ end
687
+ result
688
+ end
689
+
690
+ # Build API request keys set.
691
+ #
692
+ # Options is a hash, expectations is a set of keys [and rules] how to represent options.
693
+ # Mappings is an Array (may include hashes) or a Hash.
694
+ #
695
+ # Example:
696
+ #
697
+ # options = { :valid_from => Time.now - 10,
698
+ # :instance_count => 3,
699
+ # :image_id => 'ami-08f41161',
700
+ # :spot_price => 0.059,
701
+ # :instance_type => 'c1.medium',
702
+ # :instance_count => 1,
703
+ # :key_name => 'tim',
704
+ # :availability_zone => 'us-east-1a',
705
+ # :monitoring_enabled => true,
706
+ # :launch_group => 'lg1',
707
+ # :availability_zone_group => 'azg1',
708
+ # :groups => ['a', 'b', 'c'],
709
+ # :group_ids => 'sg-1',
710
+ # :user_data => 'konstantin',
711
+ # :block_device_mappings => [ { :device_name => '/dev/sdk',
712
+ # :ebs_snapshot_id => 'snap-145cbc7d',
713
+ # :ebs_delete_on_termination => true,
714
+ # :ebs_volume_size => 3,
715
+ # :virtual_name => 'ephemeral2' }]}
716
+ # mappings = { :spot_price,
717
+ # :availability_zone_group,
718
+ # :launch_group,
719
+ # :type,
720
+ # :instance_count,
721
+ # :image_id => 'LaunchSpecification.ImageId',
722
+ # :instance_type => 'LaunchSpecification.InstanceType',
723
+ # :key_name => 'LaunchSpecification.KeyName',
724
+ # :addressing_type => 'LaunchSpecification.AddressingType',
725
+ # :kernel_id => 'LaunchSpecification.KernelId',
726
+ # :ramdisk_id => 'LaunchSpecification.RamdiskId',
727
+ # :subnet_id => 'LaunchSpecification.SubnetId',
728
+ # :availability_zone => 'LaunchSpecification.Placement.AvailabilityZone',
729
+ # :monitoring_enabled => 'LaunchSpecification.Monitoring.Enabled',
730
+ # :valid_from => { :value => Proc.new { !options[:valid_from].right_blank? && AwsUtils::utc_iso8601(options[:valid_from]) }},
731
+ # :valid_until => { :value => Proc.new { !options[:valid_until].right_blank? && AwsUtils::utc_iso8601(options[:valid_until]) }},
732
+ # :user_data => { :name => 'LaunchSpecification.UserData',
733
+ # :value => Proc.new { !options[:user_data].right_blank? && Base64.encode64(options[:user_data]).delete("\n") }},
734
+ # :groups => { :amazonize_list => 'LaunchSpecification.SecurityGroup'},
735
+ # :group_ids => { :amazonize_list => 'LaunchSpecification.SecurityGroupId'},
736
+ # :block_device_mappings => { :amazonize_bdm => 'LaunchSpecification.BlockDeviceMapping'})
737
+ #
738
+ # map_api_keys_and_values( options, mappings) #=>
739
+ # {"LaunchSpecification.BlockDeviceMapping.1.Ebs.DeleteOnTermination" => true,
740
+ # "LaunchSpecification.BlockDeviceMapping.1.VirtualName" => "ephemeral2",
741
+ # "LaunchSpecification.BlockDeviceMapping.1.Ebs.VolumeSize" => 3,
742
+ # "LaunchSpecification.BlockDeviceMapping.1.Ebs.SnapshotId" => "snap-145cbc7d",
743
+ # "LaunchSpecification.BlockDeviceMapping.1.DeviceName" => "/dev/sdk",
744
+ # "LaunchSpecification.SecurityGroupId.1" => "sg-1",
745
+ # "LaunchSpecification.InstanceType" => "c1.medium",
746
+ # "LaunchSpecification.KeyName" => "tim",
747
+ # "LaunchSpecification.ImageId" => "ami-08f41161",
748
+ # "LaunchSpecification.SecurityGroup.1" => "a",
749
+ # "LaunchSpecification.SecurityGroup.2" => "b",
750
+ # "LaunchSpecification.SecurityGroup.3" => "c",
751
+ # "LaunchSpecification.Placement.AvailabilityZone" => "us-east-1a",
752
+ # "LaunchSpecification.Monitoring.Enabled" => true,
753
+ # "LaunchGroup" => "lg1",
754
+ # "InstanceCount" => 1,
755
+ # "SpotPrice" => 0.059,
756
+ # "AvailabilityZoneGroup" => "azg1",
757
+ # "ValidFrom" => "2011-06-30T08:06:30.000Z",
758
+ # "LaunchSpecification.UserData" => "a29uc3RhbnRpbg=="}
759
+ #
760
+ def map_api_keys_and_values(options, *mappings) # :nodoc:
761
+ result = {}
762
+ vars = {}
763
+ # Fix inputs and make them all to be hashes
764
+ mappings.flatten.each do |mapping|
765
+ unless mapping.is_a?(Hash)
766
+ # mapping is just a :key_name
767
+ mapping = { mapping => { :name => mapping.to_s.right_camelize, :value => options[mapping] }}
768
+ else
769
+ mapping.each do |local_key, api_opts|
770
+ unless api_opts.is_a?(Hash)
771
+ # mapping is a { :key_name => 'ApiKeyName' }
772
+ mapping[local_key] = { :name => api_opts.to_s, :value => options[local_key]}
773
+ else
774
+ # mapping is a { :key_name => { :name => 'ApiKeyName', :value => 'Value', ... etc} }
775
+ api_opts[:name] = local_key.to_s.right_camelize if (api_opts.keys & [:name, :amazonize_list, :amazonize_bdm]).right_blank?
776
+ api_opts[:value] = options[local_key] unless api_opts.has_key?(:value)
777
+ end
778
+ end
779
+ end
780
+ vars.merge! mapping
781
+ end
782
+ # Build API keys set
783
+ # vars now is a Hash:
784
+ # { :key1 => { :name => 'ApiKey1', :value => 'BlahBlah'},
785
+ # :key2 => { :amazonize_list => 'ApiKey2.?', :value => [1, ...] },
786
+ # :key3 => { :amazonize_bdm => 'BDM', :value => [{..}, ...] }, ... }
787
+ #
788
+ vars.each do |local_key, api_opts|
789
+ if api_opts[:amazonize_list]
790
+ result.merge!(amazonize_list( api_opts[:amazonize_list], api_opts[:value] )) unless api_opts[:value].right_blank?
791
+ elsif api_opts[:amazonize_bdm]
792
+ result.merge!(amazonize_block_device_mappings( api_opts[:value], api_opts[:amazonize_bdm] )) unless api_opts[:value].right_blank?
793
+ else
794
+ api_key = api_opts[:name]
795
+ value = api_opts[:value]
796
+ value = value.call if value.is_a?(Proc)
797
+ next if value.right_blank?
798
+ result[api_key] = value
799
+ end
800
+ end
801
+ #
802
+ result
803
+ end
804
+
805
+ # Transform a hash of parameters into a hash suitable for sending
806
+ # to Amazon using a key mapping.
807
+ #
808
+ # amazonize_hash_with_key_mapping('Group.Filter',
809
+ # {:some_param => 'SomeParam'},
810
+ # {:some_param => 'value'}) #=> {'Group.Filter.SomeParam' => 'value'}
811
+ #
812
+ def amazonize_hash_with_key_mapping(key, mapping, hash, options={})
813
+ result = {}
814
+ unless hash.right_blank?
815
+ mapping.each do |local_name, remote_name|
816
+ value = hash[local_name]
817
+ next if value.nil?
818
+ result["#{key}.#{remote_name}"] = value
819
+ end
820
+ end
821
+ result
822
+ end
823
+
824
+ # Transform a list of hashes of parameters into a hash suitable for sending
825
+ # to Amazon using a key mapping.
826
+ #
827
+ # amazonize_list_with_key_mapping('Group.Filter',
828
+ # [{:some_param => 'SomeParam'}, {:some_param => 'SomeParam'}],
829
+ # {:some_param => 'value'}) #=>
830
+ # {'Group.Filter.1.SomeParam' => 'value',
831
+ # 'Group.Filter.2.SomeParam' => 'value'}
832
+ #
833
+ def amazonize_list_with_key_mapping(key, mapping, list, options={})
834
+ result = {}
835
+ unless list.right_blank?
836
+ list.each_with_index do |item, index|
837
+ mapping.each do |local_name, remote_name|
838
+ value = item[local_name]
839
+ next if value.nil?
840
+ result["#{key}.#{index+1}.#{remote_name}"] = value
841
+ end
842
+ end
843
+ end
844
+ end
845
+
846
+ # Execute a block of code with custom set of settings for right_http_connection.
847
+ # Accepts next options (see Rightscale::HttpConnection for explanation):
848
+ # :raise_on_timeout
849
+ # :http_connection_retry_count
850
+ # :http_connection_open_timeout
851
+ # :http_connection_read_timeout
852
+ # :http_connection_retry_delay
853
+ # :user_agent
854
+ # :exception
855
+ #
856
+ # Example #1:
857
+ #
858
+ # # Try to create a snapshot but stop with exception if timeout is received
859
+ # # to avoid having a duplicate API calls that create duplicate snapshots.
860
+ # ec2 = Rightscale::Ec2::new(aws_access_key_id, aws_secret_access_key)
861
+ # ec2.with_connection_options(:raise_on_timeout => true) do
862
+ # ec2.create_snapshot('vol-898a6fe0', 'KD: WooHoo!!')
863
+ # end
864
+ #
865
+ # Example #2:
866
+ #
867
+ # # Opposite case when the setting is global:
868
+ # @ec2 = Rightscale::Ec2::new(aws_access_key_id, aws_secret_access_key,
869
+ # :connection_options => { :raise_on_timeout => true })
870
+ # # Create an SSHKey but do tries on timeout
871
+ # ec2.with_connection_options(:raise_on_timeout => false) do
872
+ # new_key = ec2.create_key_pair('my_test_key')
873
+ # end
874
+ #
875
+ # Example #3:
876
+ #
877
+ # # Global settings (HttpConnection level):
878
+ # Rightscale::HttpConnection::params[:http_connection_open_timeout] = 5
879
+ # Rightscale::HttpConnection::params[:http_connection_read_timeout] = 250
880
+ # Rightscale::HttpConnection::params[:http_connection_retry_count] = 2
881
+ #
882
+ # # Local setings (RightAws level)
883
+ # ec2 = Rightscale::Ec2::new(AWS_ID, AWS_KEY,
884
+ # :region => 'us-east-1',
885
+ # :connection_options => {
886
+ # :http_connection_read_timeout => 2,
887
+ # :http_connection_retry_count => 5,
888
+ # :user_agent => 'Mozilla 4.0'
889
+ # })
890
+ #
891
+ # # Custom settings (API call level)
892
+ # ec2.with_connection_options(:raise_on_timeout => true,
893
+ # :http_connection_read_timeout => 10,
894
+ # :user_agent => '') do
895
+ # pp ec2.describe_images
896
+ # end
897
+ #
898
+ def with_connection_options(options, &block)
899
+ @with_connection_options = options
900
+ block.call self
901
+ ensure
902
+ @with_connection_options = {}
903
+ end
309
904
  end
310
905
 
311
906
 
@@ -426,7 +1021,7 @@ module RightAws
426
1021
  @reiteration_delay = @@reiteration_start_delay
427
1022
  @retries = 0
428
1023
  # close current HTTP(S) connection on 5xx, errors from list and 4xx errors
429
- @close_on_error = params[:close_on_error].nil? ? @@close_on_error : params[:close_on_error]
1024
+ @close_on_error = params[:close_on_error].nil? ? @@close_on_error : params[:close_on_error]
430
1025
  @close_on_4xx_probability = params[:close_on_4xx_probability] || @@close_on_4xx_probability
431
1026
  end
432
1027
 
@@ -439,7 +1034,7 @@ module RightAws
439
1034
  last_errors_text = ''
440
1035
  response = @aws.last_response
441
1036
  # log error
442
- request_text_data = "#{request[:server]}:#{request[:port]}#{request[:request].path}"
1037
+ request_text_data = "#{request[:protocol]}://#{request[:server]}:#{request[:port]}#{request[:request].path}"
443
1038
  # is this a redirect?
444
1039
  # yes!
445
1040
  if response.is_a?(Net::HTTPRedirection)
@@ -449,33 +1044,43 @@ module RightAws
449
1044
  @aws.logger.warn("##### #{@aws.class.name} returned an error: #{response.code} #{response.message}\n#{response.body} #####")
450
1045
  @aws.logger.warn("##### #{@aws.class.name} request: #{request_text_data} ####")
451
1046
  end
452
- # Check response body: if it is an Amazon XML document or not:
453
- if redirect_detected || (response.body && response.body[/<\?xml/]) # ... it is a xml document
1047
+
1048
+ # Extract error/redirection message from the response body
1049
+ # Amazon claims that a redirection must have a body but somethimes it is nil....
1050
+ if response.body && response.body[/^(<\?xml|<ErrorResponse)/]
1051
+ error_parser = RightErrorResponseParser.new
454
1052
  @aws.class.bench_xml.add! do
455
- error_parser = RightErrorResponseParser.new
456
- error_parser.parse(response)
457
- @aws.last_errors = error_parser.errors
458
- @aws.last_request_id = error_parser.requestID
459
- last_errors_text = @aws.last_errors.flatten.join("\n")
460
- # on redirect :
461
- if redirect_detected
462
- location = response['location']
463
- # ... log information and ...
464
- @aws.logger.info("##### #{@aws.class.name} redirect requested: #{response.code} #{response.message} #####")
465
- @aws.logger.info("##### New location: #{location} #####")
466
- # ... fix the connection data
467
- request[:server] = URI.parse(location).host
468
- request[:protocol] = URI.parse(location).scheme
469
- request[:port] = URI.parse(location).port
470
- end
1053
+ error_parser.parse(response.body)
471
1054
  end
472
- else # ... it is not a xml document(probably just a html page?)
1055
+ @aws.last_errors = error_parser.errors
1056
+ @aws.last_request_id = error_parser.requestID
1057
+ last_errors_text = @aws.last_errors.flatten.join("\n")
1058
+ else
473
1059
  @aws.last_errors = [[response.code, "#{response.message} (#{request_text_data})"]]
474
1060
  @aws.last_request_id = '-undefined-'
475
1061
  last_errors_text = response.message
476
1062
  end
477
- # now - check the error
478
- unless redirect_detected
1063
+
1064
+ # Ok, it is a redirect, find the new destination location
1065
+ if redirect_detected
1066
+ location = response['location']
1067
+ # As for 301 ( Moved Permanently) Amazon does not return a 'Location' header but
1068
+ # it is possible to extract a new endpoint from the response body
1069
+ if location.right_blank? && response.code=='301' && response.body
1070
+ new_endpoint = response.body[/<Endpoint>(.*?)<\/Endpoint>/] && $1
1071
+ location = "#{request[:protocol]}://#{new_endpoint}:#{request[:port]}#{request[:request].path}"
1072
+ end
1073
+ # ... log information and ...
1074
+ @aws.logger.info("##### #{@aws.class.name} redirect requested: #{response.code} #{response.message} #####")
1075
+ @aws.logger.info(" Old location: #{request_text_data}")
1076
+ @aws.logger.info(" New location: #{location}")
1077
+ @aws.logger.info(" Request Verb: #{request[:request].class.name}")
1078
+ # ... fix the connection data
1079
+ request[:server] = URI.parse(location).host
1080
+ request[:protocol] = URI.parse(location).scheme
1081
+ request[:port] = URI.parse(location).port
1082
+ else
1083
+ # Not a redirect but an error: try to find the error in our list
479
1084
  @errors_list.each do |error_to_find|
480
1085
  if last_errors_text[/#{error_to_find}/i]
481
1086
  error_found = true
@@ -485,13 +1090,14 @@ module RightAws
485
1090
  end
486
1091
  end
487
1092
  end
1093
+
488
1094
  # check the time has gone from the first error come
489
1095
  if redirect_detected || error_found
490
1096
  # Close the connection to the server and recreate a new one.
491
1097
  # It may have a chance that one server is a semi-down and reconnection
492
1098
  # will help us to connect to the other server
493
1099
  if !redirect_detected && @close_on_error
494
- @aws.connection.finish "#{self.class.name}: error match to pattern '#{error_match}'"
1100
+ @aws.destroy_connection(request, "#{self.class.name}: error match to pattern '#{error_match}'")
495
1101
  end
496
1102
 
497
1103
  if (Time.now < @stop_at)
@@ -501,32 +1107,34 @@ module RightAws
501
1107
  sleep @reiteration_delay
502
1108
  @reiteration_delay *= 2
503
1109
 
504
- # Always make sure that the fp is set to point to the beginning(?)
505
- # of the File/IO. TODO: it assumes that offset is 0, which is bad.
506
- if(request[:request].body_stream && request[:request].body_stream.respond_to?(:pos))
507
- begin
508
- request[:request].body_stream.pos = 0
509
- rescue Exception => e
510
- @logger.warn("Retry may fail due to unable to reset the file pointer" +
511
- " -- #{self.class.name} : #{e.inspect}")
512
- end
513
- end
514
1110
  else
515
1111
  @aws.logger.info("##### Retry ##{@retries} is being performed due to a redirect. ####")
516
1112
  end
1113
+
1114
+ # Always make sure that the fp is set to point to the beginning(?)
1115
+ # of the File/IO. TODO: it assumes that offset is 0, which is bad.
1116
+ if(request[:request].body_stream && request[:request].body_stream.respond_to?(:pos))
1117
+ begin
1118
+ request[:request].body_stream.pos = 0
1119
+ rescue Exception => e
1120
+ @logger.warn("Retry may fail due to unable to reset the file pointer" +
1121
+ " -- #{self.class.name} : #{e.inspect}")
1122
+ end
1123
+ end
1124
+
517
1125
  result = @aws.request_info(request, @parser)
518
1126
  else
519
1127
  @aws.logger.warn("##### Ooops, time is over... ####")
520
1128
  end
521
1129
  # aha, this is unhandled error:
522
1130
  elsif @close_on_error
523
- # Is this a 5xx error ?
524
- if @aws.last_response.code.to_s[/^5\d\d$/]
525
- @aws.connection.finish "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}'"
1131
+ # On 5xx(Server errors), 403(RequestTimeTooSkewed) and 408(Request Timeout) a conection has to be closed
1132
+ if @aws.last_response.code.to_s[/^(5\d\d|403|408)$/]
1133
+ @aws.destroy_connection(request, "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}'")
526
1134
  # Is this a 4xx error ?
527
1135
  elsif @aws.last_response.code.to_s[/^4\d\d$/] && @close_on_4xx_probability > rand(100)
528
- @aws.connection.finish "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}', " +
529
- "probability: #{@close_on_4xx_probability}%"
1136
+ @aws.destroy_connection(request, "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}', " +
1137
+ "probability: #{@close_on_4xx_probability}%")
530
1138
  end
531
1139
  end
532
1140
  result
@@ -537,21 +1145,12 @@ module RightAws
537
1145
 
538
1146
  #-----------------------------------------------------------------
539
1147
 
540
- class RightSaxParserCallback #:nodoc:
541
- def self.include_callback
542
- include XML::SaxParser::Callbacks
543
- end
1148
+ class RightSaxParserCallbackTemplate #:nodoc:
544
1149
  def initialize(right_aws_parser)
545
1150
  @right_aws_parser = right_aws_parser
546
1151
  end
547
- def on_start_element(name, attr_hash)
548
- @right_aws_parser.tag_start(name, attr_hash)
549
- end
550
1152
  def on_characters(chars)
551
- @right_aws_parser.text(chars)
552
- end
553
- def on_end_element(name)
554
- @right_aws_parser.tag_end(name)
1153
+ @right_aws_parser.text(chars)
555
1154
  end
556
1155
  def on_start_document; end
557
1156
  def on_comment(msg); end
@@ -559,7 +1158,28 @@ module RightAws
559
1158
  def on_cdata_block(cdata); end
560
1159
  def on_end_document; end
561
1160
  end
562
-
1161
+
1162
+ class RightSaxParserCallback < RightSaxParserCallbackTemplate
1163
+ def self.include_callback
1164
+ include XML::SaxParser::Callbacks
1165
+ end
1166
+ def on_start_element(name, attr_hash)
1167
+ @right_aws_parser.tag_start(name, attr_hash)
1168
+ end
1169
+ def on_end_element(name)
1170
+ @right_aws_parser.tag_end(name)
1171
+ end
1172
+ end
1173
+
1174
+ class RightSaxParserCallbackNs < RightSaxParserCallbackTemplate
1175
+ def on_start_element_ns(name, attr_hash, prefix, uri, namespaces)
1176
+ @right_aws_parser.tag_start(name, attr_hash)
1177
+ end
1178
+ def on_end_element_ns(name, prefix, uri)
1179
+ @right_aws_parser.tag_end(name)
1180
+ end
1181
+ end
1182
+
563
1183
  class RightAWSParser #:nodoc:
564
1184
  # default parsing library
565
1185
  DEFAULT_XML_LIBRARY = 'rexml'
@@ -577,24 +1197,30 @@ module RightAws
577
1197
  attr_accessor :result
578
1198
  attr_reader :xmlpath
579
1199
  attr_accessor :xml_lib
1200
+ attr_reader :full_tag_name
1201
+ attr_reader :tag
580
1202
 
581
1203
  def initialize(params={})
582
1204
  @xmlpath = ''
1205
+ @full_tag_name = ''
583
1206
  @result = false
584
1207
  @text = ''
1208
+ @tag = ''
585
1209
  @xml_lib = params[:xml_lib] || @@xml_lib
586
1210
  @logger = params[:logger]
587
1211
  reset
588
1212
  end
589
1213
  def tag_start(name, attributes)
590
1214
  @text = ''
1215
+ @tag = name
1216
+ @full_tag_name += @full_tag_name.empty? ? name : "/#{name}"
591
1217
  tagstart(name, attributes)
592
- @xmlpath += @xmlpath.empty? ? name : "/#{name}"
1218
+ @xmlpath = @full_tag_name
593
1219
  end
594
1220
  def tag_end(name)
595
- @xmlpath[/^(.*?)\/?#{name}$/]
596
- @xmlpath = $1
1221
+ @xmlpath = @full_tag_name[/^(.*?)\/?#{name}$/] && $1
597
1222
  tagend(name)
1223
+ @full_tag_name = @xmlpath
598
1224
  end
599
1225
  def text(text)
600
1226
  @text += text
@@ -614,31 +1240,39 @@ module RightAws
614
1240
  if @xml_lib=='libxml' && !defined?(XML::SaxParser)
615
1241
  begin
616
1242
  require 'xml/libxml'
617
- # is it new ? - Setup SaxParserCallback
618
- if XML::Parser::VERSION >= '0.5.1.0'
619
- RightSaxParserCallback.include_callback
1243
+ # Setup SaxParserCallback
1244
+ if XML::Parser::VERSION >= '0.5.1' &&
1245
+ XML::Parser::VERSION < '0.9.7'
1246
+ RightSaxParserCallback.include_callback
620
1247
  end
621
1248
  rescue LoadError => e
622
- @@supported_xml_libs.delete(@xml_lib)
623
- @xml_lib = DEFAULT_XML_LIBRARY
1249
+ @@supported_xml_libs.delete(@xml_lib)
1250
+ @xml_lib = DEFAULT_XML_LIBRARY
624
1251
  if @logger
625
1252
  @logger.error e.inspect
626
1253
  @logger.error e.backtrace
627
- @logger.info "Can not load 'libxml' library. '#{DEFAULT_XML_LIBRARY}' is used for parsing."
1254
+ @logger.info "Can not load 'libxml' library. '#{DEFAULT_XML_LIBRARY}' is used for parsing."
628
1255
  end
629
1256
  end
630
1257
  end
631
1258
  # Parse the xml text
632
1259
  case @xml_lib
633
- when 'libxml'
634
- xml = XML::SaxParser.new
635
- xml.string = xml_text
1260
+ when 'libxml'
1261
+ if XML::Parser::VERSION >= '0.9.9'
1262
+ # avoid warning on every usage
1263
+ xml = XML::SaxParser.string(xml_text)
1264
+ else
1265
+ xml = XML::SaxParser.new
1266
+ xml.string = xml_text
1267
+ end
636
1268
  # check libxml-ruby version
637
- if XML::Parser::VERSION >= '0.5.1.0'
1269
+ if XML::Parser::VERSION >= '0.9.7'
1270
+ xml.callbacks = RightSaxParserCallbackNs.new(self)
1271
+ elsif XML::Parser::VERSION >= '0.5.1'
638
1272
  xml.callbacks = RightSaxParserCallback.new(self)
639
1273
  else
640
1274
  xml.on_start_element{|name, attr_hash| self.tag_start(name, attr_hash)}
641
- xml.on_characters{ |text| self.text(text)}
1275
+ xml.on_characters{ |text| self.text(text)}
642
1276
  xml.on_end_element{ |name| self.tag_end(name)}
643
1277
  end
644
1278
  xml.parse
@@ -713,5 +1347,11 @@ module RightAws
713
1347
  end
714
1348
  end
715
1349
 
1350
+ class RightBoolResponseParser < RightAWSParser #:nodoc:
1351
+ def tagend(name)
1352
+ @result = (@text=='true') if name == 'return'
1353
+ end
1354
+ end
1355
+
716
1356
  end
717
1357