right_aws 1.4.3 → 1.5.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.
@@ -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