right_aws 1.4.3 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,470 @@
1
+ #
2
+ # Copyright (c) 2008 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+
24
+ require "right_aws"
25
+
26
+ module RightAws
27
+
28
+ class SdbInterface < RightAwsBase
29
+
30
+ include RightAwsBaseInterface
31
+
32
+ SIGNATURE_VERSION = '1'
33
+ DEFAULT_HOST = 'sdb.amazonaws.com'
34
+ DEFAULT_PORT = 443
35
+ DEFAULT_PROTOCOL = 'https'
36
+ API_VERSION = '2007-11-07'
37
+
38
+ @@bench = AwsBenchmarkingBlock.new
39
+ def self.bench_xml; @@bench.xml; end
40
+ def self.bench_sdb; @@bench.service; end
41
+
42
+ # Creates new RightSdb instance.
43
+ #
44
+ # Params:
45
+ # { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default)
46
+ # :port => 443 # Amazon service port: 80 or 443(default)
47
+ # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
48
+ # :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
49
+ # :logger => Logger Object} # Logger instance: logs to STDOUT if omitted }
50
+ #
51
+ # Example:
52
+ #
53
+ # sdb = RightAws::SdbInterface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX', {:multi_thread => true, :logger => Logger.new('/tmp/x.log')}) #=> #<RightSdb:0xa6b8c27c>
54
+ #
55
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/
56
+ #
57
+ def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
58
+ init({ :name => 'S3',
59
+ :default_host => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).host : DEFAULT_HOST,
60
+ :default_port => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).port : DEFAULT_PORT,
61
+ :default_protocol => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).scheme : DEFAULT_PROTOCOL },
62
+ aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'],
63
+ aws_secret_access_key || ENV['AWS_SECRET_ACCESS_KEY'],
64
+ params)
65
+ end
66
+
67
+ #-----------------------------------------------------------------
68
+ # Requests
69
+ #-----------------------------------------------------------------
70
+ def generate_request(action, params={}) #:nodoc:
71
+ # remove empty params from request
72
+ params.delete_if {|key,value| value.blank? }
73
+ params_string = params.to_a.collect{|key,val| key + "=#{CGI::escape(val.to_s)}" }.join("&")
74
+ # prepare service data
75
+ service_hash = {"Action" => action,
76
+ "AWSAccessKeyId" => @aws_access_key_id,
77
+ "Version" => API_VERSION,
78
+ "Timestamp" => Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
79
+ "SignatureVersion" => SIGNATURE_VERSION }
80
+ # prepare string to sight
81
+ string_to_sign = service_hash.merge(params).sort{|a,b| (a[0].to_s.downcase)<=>(b[0].to_s.downcase)}.to_s
82
+ service_hash.update('Signature' => AwsUtils::sign(@aws_secret_access_key, string_to_sign))
83
+ service_string = service_hash.to_a.collect{|key,val| key + "=#{CGI::escape(val.to_s)}" }.join("&")
84
+ #
85
+ # use POST method if the length of the query string is too large
86
+ # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/MakingRESTRequests.html
87
+ if (service_string + params_string).size > 2000
88
+ request = Net::HTTP::Post.new("/?#{service_string}")
89
+ request.body = params_string
90
+ else
91
+ params_string = "&#{params_string}" unless params_string.blank?
92
+ request = Net::HTTP::Get.new("/?#{service_string}#{params_string}")
93
+ end
94
+ # prepare output hash
95
+ { :request => request,
96
+ :server => @params[:server],
97
+ :port => @params[:port],
98
+ :protocol => @params[:protocol] }
99
+ end
100
+
101
+ # Sends request to Amazon and parses the response
102
+ # Raises AwsError if any banana happened
103
+ def request_info(request, parser) #:nodoc:
104
+ thread = @params[:multi_thread] ? Thread.current : Thread.main
105
+ thread[:sdb_connection] ||= Rightscale::HttpConnection.new(:exception => AwsError, :logger => @logger)
106
+ request_info_impl(thread[:sdb_connection], @@bench, request, parser)
107
+ end
108
+
109
+ # Prepare attributes for putting.
110
+ # (used by put_attributes)
111
+ def pack_attributes(attributes, replace = false) #:nodoc:
112
+ result = {}
113
+ if attributes
114
+ idx = 0
115
+ attributes.each do |attribute, values|
116
+ # set replacement attribute
117
+ result["Attribute.#{idx}.Replace"] = 'true' if replace
118
+ # pack Name/Value
119
+ unless values.blank?
120
+ values.to_a.each do |value|
121
+ result["Attribute.#{idx}.Name"] = attribute
122
+ result["Attribute.#{idx}.Value"] = value
123
+ idx += 1
124
+ end
125
+ else
126
+ result["Attribute.#{idx}.Name"] = attribute
127
+ idx += 1
128
+ end
129
+ end
130
+ end
131
+ result
132
+ end
133
+
134
+ # Use this helper to manually escape the fields in the query expressions.
135
+ # To escape the single quotes and backslashes and to wrap the string into the single quotes.
136
+ #
137
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API.html
138
+ #
139
+ def escape(value)
140
+ %Q{'#{value.to_s.gsub(/(['\\])/){ "\\#{$1}" }}'} if value
141
+ end
142
+
143
+ # Create query expression from an array.
144
+ # (similar to ActiveRecord::Base#find using :conditions => ['query', param1, .., paramN])
145
+ #
146
+ def query_expression_from_array(params) #:nodoc:
147
+ if params
148
+ query = params.shift.to_s
149
+ query.gsub(/(\\)?(\?)/) do
150
+ if $1 # if escaped '\?' is found - replace it by '?' without backslash
151
+ "?"
152
+ else # well, if no backslash precedes '?' then replace it by next param from the list
153
+ escape(params.shift)
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ # Retrieve a list of SDB domains from Amazon.
160
+ #
161
+ # Returns a hash:
162
+ # { :domains => [domain1, ..., domainN],
163
+ # :next_token => string || nil,
164
+ # :box_usage => string,
165
+ # :request_id => string }
166
+ #
167
+ # Example:
168
+ #
169
+ # sdb = RightAws::SdbInterface.new
170
+ # sdb.list_domains #=> { :box_usage => "0.0000071759",
171
+ # :request_id => "976709f9-0111-2345-92cb-9ce90acd0982",
172
+ # :domains => ["toys", "dolls"]}
173
+ #
174
+ # If a block is given, this method yields to it. If the block returns true, list_domains will continue looping the request. If the block returns false,
175
+ # list_domains will end.
176
+ #
177
+ # sdb.list_domains(10) do |result| # list by 10 domains per iteration
178
+ # puts result.inspect
179
+ # true
180
+ # end
181
+ #
182
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_ListDomains.html
183
+ #
184
+ def list_domains(max_number_of_domains = nil, next_token = nil )
185
+ request_params = { 'MaxNumberOfDomains' => max_number_of_domains,
186
+ 'NextToken' => next_token }
187
+ link = generate_request("ListDomains", request_params)
188
+ result = request_info(link, QSdbListDomainParser.new)
189
+ # return result if no block given
190
+ return result unless block_given?
191
+ # loop if block if given
192
+ begin
193
+ # the block must return true if it wanna continue
194
+ break unless yield(result) && result[:next_token]
195
+ # make new request
196
+ request_params['NextToken'] = result[:next_token]
197
+ link = generate_request("ListDomains", request_params)
198
+ result = request_info(link, QSdbListDomainParser.new)
199
+ end while true
200
+ rescue Exception
201
+ on_exception
202
+ end
203
+
204
+ # Create new SDB domain at Amazon.
205
+ #
206
+ # Returns a hash: { :box_usage, :request_id } on success or an exception on error.
207
+ # (Amazon raises no errors if the domain already exists).
208
+ #
209
+ # Example:
210
+ #
211
+ # sdb = RightAws::SdbInterface.new
212
+ # sdb.create_domain('toys') # => { :box_usage => "0.0000071759",
213
+ # :request_id => "976709f9-0111-2345-92cb-9ce90acd0982" }
214
+ #
215
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_CreateDomain.html
216
+ def create_domain(domain_name)
217
+ link = generate_request("CreateDomain",
218
+ 'DomainName' => domain_name)
219
+ request_info(link, QSdbSimpleParser.new)
220
+ rescue Exception
221
+ on_exception
222
+ end
223
+
224
+ # Delete SDB domain at Amazon.
225
+ #
226
+ # Returns a hash: { :box_usage, :request_id } on success or an exception on error.
227
+ # (Amazon raises no errors if the domain does not exist).
228
+ #
229
+ # Example:
230
+ #
231
+ # sdb = RightAws::SdbInterface.new
232
+ # sdb.delete_domain('toys') # => { :box_usage => "0.0000071759",
233
+ # :request_id => "976709f9-0111-2345-92cb-9ce90acd0982" }
234
+ #
235
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_DeleteDomain.html
236
+ #
237
+ def delete_domain(domain_name)
238
+ link = generate_request("DeleteDomain",
239
+ 'DomainName' => domain_name)
240
+ request_info(link, QSdbSimpleParser.new)
241
+ rescue Exception
242
+ on_exception
243
+ end
244
+
245
+ # Add/Replace item attributes.
246
+ #
247
+ # Params:
248
+ # domain_name = DomainName
249
+ # item_name = ItemName
250
+ # attributes = {
251
+ # 'nameA' => [valueA1,..., valueAN],
252
+ # ...
253
+ # 'nameZ' => [valueZ1,..., valueZN]
254
+ # }
255
+ # replace = :replace | any other value to skip replacement
256
+ #
257
+ # Returns a hash: { :box_usage, :request_id } on success or an exception on error.
258
+ # (Amazon raises no errors if the attribute was not overridden, as when the :replace param is unset).
259
+ #
260
+ # Example:
261
+ #
262
+ # sdb = RightAws::SdbInterface.new
263
+ # sdb.create_domain 'family'
264
+ #
265
+ # attributes = {}
266
+ # # create attributes for Jon and Silvia
267
+ # attributes['Jon'] = %w{ car beer }
268
+ # attributes['Silvia'] = %w{ beetle rolling_pin kids }
269
+ # sdb.put_attributes 'family', 'toys', attributes #=> ok
270
+ # # now: Jon=>[car, beer], Silvia=>[beetle, rolling_pin, kids]
271
+ #
272
+ # # add attributes to Jon
273
+ # attributes.delete('Silvia')
274
+ # attributes['Jon'] = %w{ girls pub }
275
+ # sdb.put_attributes 'family', 'toys', attributes #=> ok
276
+ # # now: Jon=>[car, beer, girls, pub], Silvia=>[beetle, rolling_pin, kids]
277
+ #
278
+ # # replace attributes for Jon and add to a cat (the cat had no attributes before)
279
+ # attributes['Jon'] = %w{ vacuum_cleaner hammer spade }
280
+ # attributes['cat'] = %w{ mouse clew Jons_socks }
281
+ # sdb.put_attributes 'family', 'toys', attributes, :replace #=> ok
282
+ # # now: Jon=>[vacuum_cleaner, hammer, spade], Silvia=>[beetle, rolling_pin, kids], cat=>[mouse, clew, Jons_socks]
283
+ #
284
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_PutAttributes.html
285
+ #
286
+ def put_attributes(domain_name, item_name, attributes, replace = false)
287
+ params = { 'DomainName' => domain_name,
288
+ 'ItemName' => item_name }.merge(pack_attributes(attributes, replace))
289
+ link = generate_request("PutAttributes", params)
290
+ request_info( link, QSdbSimpleParser.new )
291
+ rescue Exception
292
+ on_exception
293
+ end
294
+
295
+ # Retrieve SDB item's attribute(s).
296
+ #
297
+ # Returns a hash:
298
+ # { :box_usage => string,
299
+ # :request_id => string,
300
+ # :attributes => { 'nameA' => [valueA1,..., valueAN],
301
+ # ... ,
302
+ # 'nameZ' => [valueZ1,..., valueZN] } }
303
+ #
304
+ # Example:
305
+ # # request all attributes
306
+ # sdb.get_attributes('family', 'toys') # => { :attributes => {"cat" => ["clew", "Jons_socks", "mouse"] },
307
+ # "Silvia" => ["beetle", "rolling_pin", "kids"],
308
+ # "Jon" => ["vacuum_cleaner", "hammer", "spade"]},
309
+ # :box_usage => "0.0000093222",
310
+ # :request_id => "81273d21-000-1111-b3f9-512d91d29ac8" }
311
+ #
312
+ # # request cat's attributes only
313
+ # sdb.get_attributes('family', 'toys', 'cat') # => { :attributes => {"cat" => ["clew", "Jons_socks", "mouse"] },
314
+ # :box_usage => "0.0000093222",
315
+ # :request_id => "81273d21-001-1111-b3f9-512d91d29ac8" }
316
+ #
317
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_GetAttributes.html
318
+ #
319
+ def get_attributes(domain_name, item_name, attribute_name=nil)
320
+ link = generate_request("GetAttributes", 'DomainName' => domain_name,
321
+ 'ItemName' => item_name,
322
+ 'AttributeName' => attribute_name )
323
+ request_info(link, QSdbGetAttributesParser.new)
324
+ rescue Exception
325
+ on_exception
326
+ end
327
+
328
+ # Delete value, attribute or item.
329
+ #
330
+ # Example:
331
+ # # delete 'vodka' and 'girls' from 'Jon' and 'mice' from 'cat'.
332
+ # sdb.delete_attributes 'family', 'toys', { 'Jon' => ['vodka', 'girls'], 'cat' => ['mice'] }
333
+ #
334
+ # # delete the all the values from attributes (i.e. delete the attributes)
335
+ # sdb.delete_attributes 'family', 'toys', { 'Jon' => [], 'cat' => [] }
336
+ # # or
337
+ # sdb.delete_attributes 'family', 'toys', [ 'Jon', 'cat' ]
338
+ #
339
+ # # delete all the attributes from item 'toys' (i.e. delete the item)
340
+ # sdb.delete_attributes 'family', 'toys'
341
+ #
342
+ # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_DeleteAttributes.html
343
+ #
344
+ def delete_attributes(domain_name, item_name, attributes = nil)
345
+ params = { 'DomainName' => domain_name,
346
+ 'ItemName' => item_name }.merge(pack_attributes(attributes))
347
+ link = generate_request("DeleteAttributes", params)
348
+ request_info( link, QSdbSimpleParser.new )
349
+ rescue Exception
350
+ on_exception
351
+ end
352
+
353
+
354
+ # QUERY:
355
+
356
+ # Perform a query on SDB.
357
+ #
358
+ # Returns a hash:
359
+ # { :box_usage => string,
360
+ # :request_id => string,
361
+ # :next_token => string,
362
+ # :items => [ItemName1,..., ItemNameN] }
363
+ #
364
+ # Example:
365
+ #
366
+ # query = "['cat' = 'clew']"
367
+ # sdb.query('family', query) #=> hash of data
368
+ # sdb.query('family', query, 10) #=> hash of data with max of 10 items
369
+ #
370
+ # If a block is given, query will iteratively yield results to it as long as the block continues to return true.
371
+ #
372
+ # # List 10 items per iteration. Don't
373
+ # # forget to escape single quotes and backslashes and wrap all the items in single quotes.
374
+ # query = "['cat'='clew'] union ['dog'='Jon\\'s boot']"
375
+ # sdb.query('family', query, 10) do |result|
376
+ # puts result.inspect
377
+ # true
378
+ # end
379
+ #
380
+ # # Same query using automatic escaping...to use the auto escape, pass the query and its params as an array:
381
+ # query = [ "['cat'=?] union ['dog'=?]", "clew", "Jon's boot" ]
382
+ # sdb.query('family', query)
383
+ #
384
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_Query.html
385
+ #
386
+ def query(domain_name, query_expression = nil, max_number_of_items = nil, next_token = nil)
387
+ query_expression = query_expression_from_array(query_expression) if query_expression.is_a?(Array)
388
+ #
389
+ request_params = { 'DomainName' => domain_name,
390
+ 'QueryExpression' => query_expression,
391
+ 'MaxNumberOfItems' => max_number_of_items,
392
+ 'NextToken' => next_token }
393
+ link = generate_request("Query", request_params)
394
+ result = request_info( link, QSdbQueryParser.new )
395
+ # return result if no block given
396
+ return result unless block_given?
397
+ # loop if block if given
398
+ begin
399
+ # the block must return true if it wanna continue
400
+ break unless yield(result) && result[:next_token]
401
+ # make new request
402
+ request_params['NextToken'] = result[:next_token]
403
+ link = generate_request("Query", request_params)
404
+ result = request_info( link, QSdbQueryParser.new )
405
+ end while true
406
+ rescue Exception
407
+ on_exception
408
+ end
409
+
410
+ #-----------------------------------------------------------------
411
+ # PARSERS:
412
+ #-----------------------------------------------------------------
413
+ class QSdbListDomainParser < RightAWSParser #:nodoc:
414
+ def reset
415
+ @result = { :domains => [] }
416
+ end
417
+ def tagend(name)
418
+ case name
419
+ when 'NextToken' : @result[:next_token] = @text
420
+ when 'DomainName' : @result[:domains] << @text
421
+ when 'BoxUsage' : @result[:box_usage] = @text
422
+ when 'RequestId' : @result[:request_id] = @text
423
+ end
424
+ end
425
+ end
426
+
427
+ class QSdbSimpleParser < RightAWSParser #:nodoc:
428
+ def reset
429
+ @result = {}
430
+ end
431
+ def tagend(name)
432
+ case name
433
+ when 'BoxUsage' : @result[:box_usage] = @text
434
+ when 'RequestId' : @result[:request_id] = @text
435
+ end
436
+ end
437
+ end
438
+
439
+ class QSdbGetAttributesParser < RightAWSParser #:nodoc:
440
+ def reset
441
+ @last_attribute_name = nil
442
+ @result = { :attributes => {} }
443
+ end
444
+ def tagend(name)
445
+ case name
446
+ when 'Name' : @last_attribute_name = @text
447
+ when 'Value' : (@result[:attributes][@last_attribute_name] ||= []) << @text
448
+ when 'BoxUsage' : @result[:box_usage] = @text
449
+ when 'RequestId' : @result[:request_id] = @text
450
+ end
451
+ end
452
+ end
453
+
454
+ class QSdbQueryParser < RightAWSParser #:nodoc:
455
+ def reset
456
+ @result = { :items => [] }
457
+ end
458
+ def tagend(name)
459
+ case name
460
+ when 'ItemName' : @result[:items] << @text
461
+ when 'BoxUsage' : @result[:box_usage] = @text
462
+ when 'RequestId' : @result[:request_id] = @text
463
+ when 'NextToken' : @result[:next_token] = @text
464
+ end
465
+ end
466
+ end
467
+
468
+ end
469
+
470
+ end