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.
- data/History.txt +164 -13
- data/Manifest.txt +28 -1
- data/README.txt +12 -10
- data/Rakefile +56 -29
- data/lib/acf/right_acf_interface.rb +343 -172
- data/lib/acf/right_acf_invalidations.rb +144 -0
- data/lib/acf/right_acf_origin_access_identities.rb +230 -0
- data/lib/acf/right_acf_streaming_interface.rb +229 -0
- data/lib/acw/right_acw_interface.rb +248 -0
- data/lib/as/right_as_interface.rb +698 -0
- data/lib/awsbase/right_awsbase.rb +755 -115
- data/lib/awsbase/support.rb +2 -78
- data/lib/awsbase/version.rb +9 -0
- data/lib/ec2/right_ec2.rb +274 -1294
- data/lib/ec2/right_ec2_ebs.rb +514 -0
- data/lib/ec2/right_ec2_images.rb +444 -0
- data/lib/ec2/right_ec2_instances.rb +797 -0
- data/lib/ec2/right_ec2_monitoring.rb +70 -0
- data/lib/ec2/right_ec2_placement_groups.rb +108 -0
- data/lib/ec2/right_ec2_reserved_instances.rb +243 -0
- data/lib/ec2/right_ec2_security_groups.rb +496 -0
- data/lib/ec2/right_ec2_spot_instances.rb +422 -0
- data/lib/ec2/right_ec2_tags.rb +139 -0
- data/lib/ec2/right_ec2_vpc.rb +598 -0
- data/lib/ec2/right_ec2_vpc2.rb +382 -0
- data/lib/ec2/right_ec2_windows_mobility.rb +84 -0
- data/lib/elb/right_elb_interface.rb +573 -0
- data/lib/emr/right_emr_interface.rb +728 -0
- data/lib/iam/right_iam_access_keys.rb +71 -0
- data/lib/iam/right_iam_groups.rb +195 -0
- data/lib/iam/right_iam_interface.rb +341 -0
- data/lib/iam/right_iam_mfa_devices.rb +67 -0
- data/lib/iam/right_iam_users.rb +251 -0
- data/lib/rds/right_rds_interface.rb +1657 -0
- data/lib/right_aws.rb +30 -13
- data/lib/route_53/right_route_53_interface.rb +641 -0
- data/lib/s3/right_s3.rb +108 -41
- data/lib/s3/right_s3_interface.rb +349 -118
- data/lib/sdb/active_sdb.rb +388 -54
- data/lib/sdb/right_sdb_interface.rb +323 -64
- data/lib/sns/right_sns_interface.rb +286 -0
- data/lib/sqs/right_sqs.rb +1 -2
- data/lib/sqs/right_sqs_gen2.rb +73 -17
- data/lib/sqs/right_sqs_gen2_interface.rb +146 -73
- data/lib/sqs/right_sqs_interface.rb +12 -22
- data/right_aws.gemspec +91 -0
- data/test/README.mdown +39 -0
- data/test/acf/test_right_acf.rb +11 -19
- data/test/awsbase/test_helper.rb +2 -0
- data/test/awsbase/test_right_awsbase.rb +11 -0
- data/test/ec2/test_right_ec2.rb +32 -1
- data/test/elb/test_helper.rb +2 -0
- data/test/elb/test_right_elb.rb +43 -0
- data/test/rds/test_helper.rb +2 -0
- data/test/rds/test_right_rds.rb +120 -0
- data/test/route_53/fixtures/a_record.xml +18 -0
- data/test/route_53/fixtures/alias_record.xml +18 -0
- data/test/route_53/test_helper.rb +2 -0
- data/test/route_53/test_right_route_53.rb +141 -0
- data/test/s3/test_right_s3.rb +176 -42
- data/test/s3/test_right_s3_stubbed.rb +6 -4
- data/test/sdb/test_active_sdb.rb +120 -19
- data/test/sdb/test_batch_put_attributes.rb +54 -0
- data/test/sdb/test_right_sdb.rb +71 -16
- data/test/sns/test_helper.rb +2 -0
- data/test/sns/test_right_sns.rb +153 -0
- data/test/sqs/test_right_sqs.rb +0 -6
- data/test/sqs/test_right_sqs_gen2.rb +104 -49
- data/test/ts_right_aws.rb +1 -0
- 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
|
-
@@
|
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(@@
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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 = '
|
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.
|
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
|
-
|
152
|
-
@params[:
|
153
|
-
|
154
|
-
|
155
|
-
|
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[:
|
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
|
-
|
181
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
219
|
-
|
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
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
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
|
-
|
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
|
-
|
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!
|
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 =
|
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
|
-
|
453
|
-
|
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
|
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
|
-
|
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
|
-
|
478
|
-
|
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.
|
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
|
-
#
|
524
|
-
if @aws.last_response.code.to_s[/^5\d\d$/]
|
525
|
-
@aws.
|
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.
|
529
|
-
|
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
|
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
|
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
|
-
#
|
618
|
-
if XML::Parser::VERSION >= '0.5.1
|
619
|
-
|
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
|
-
|
635
|
-
|
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
|
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
|
|