kerryb-right_aws 1.7.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,475 @@
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
+ DEFAULT_HOST = 'sdb.amazonaws.com'
33
+ DEFAULT_PORT = 443
34
+ DEFAULT_PROTOCOL = 'https'
35
+ API_VERSION = '2007-11-07'
36
+
37
+ @@bench = AwsBenchmarkingBlock.new
38
+ def self.bench_xml; @@bench.xml; end
39
+ def self.bench_sdb; @@bench.service; end
40
+
41
+ # Creates new RightSdb instance.
42
+ #
43
+ # Params:
44
+ # { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default)
45
+ # :port => 443 # Amazon service port: 80 or 443(default)
46
+ # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
47
+ # :signature_version => '0' # The signature version : '0' or '1'(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 => 'SDB',
59
+ :default_host => ENV['SDB_URL'] ? URI.parse(ENV['SDB_URL']).host : DEFAULT_HOST,
60
+ :default_port => ENV['SDB_URL'] ? URI.parse(ENV['SDB_URL']).port : DEFAULT_PORT,
61
+ :default_protocol => ENV['SDB_URL'] ? URI.parse(ENV['SDB_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
+ service_hash.update(params)
81
+ # prepare string to sight
82
+ string_to_sign = case signature_version
83
+ when '0' : service_hash["Action"] + service_hash["Timestamp"]
84
+ when '1' : service_hash.sort{|a,b| (a[0].to_s.downcase)<=>(b[0].to_s.downcase)}.to_s
85
+ end
86
+ service_hash.update('Signature' => AwsUtils::sign(@aws_secret_access_key, string_to_sign))
87
+ service_string = service_hash.to_a.collect{|key,val| key + "=#{CGI::escape(val.to_s)}" }.join("&")
88
+ #
89
+ # use POST method if the length of the query string is too large
90
+ # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/MakingRESTRequests.html
91
+ if (service_string + params_string).size > 2000
92
+ request = Net::HTTP::Post.new("/?#{service_string}")
93
+ request.body = params_string
94
+ else
95
+ params_string = "&#{params_string}" unless params_string.blank?
96
+ request = Net::HTTP::Get.new("/?#{service_string}#{params_string}")
97
+ end
98
+ # prepare output hash
99
+ { :request => request,
100
+ :server => @params[:server],
101
+ :port => @params[:port],
102
+ :protocol => @params[:protocol],
103
+ :proxy => @params[:proxy] }
104
+ end
105
+
106
+ # Sends request to Amazon and parses the response
107
+ # Raises AwsError if any banana happened
108
+ def request_info(request, parser) #:nodoc:
109
+ thread = @params[:multi_thread] ? Thread.current : Thread.main
110
+ thread[:sdb_connection] ||= Rightscale::HttpConnection.new(:exception => AwsError, :logger => @logger)
111
+ request_info_impl(thread[:sdb_connection], @@bench, request, parser)
112
+ end
113
+
114
+ # Prepare attributes for putting.
115
+ # (used by put_attributes)
116
+ def pack_attributes(attributes, replace = false) #:nodoc:
117
+ result = {}
118
+ if attributes
119
+ idx = 0
120
+ attributes.each do |attribute, values|
121
+ # set replacement attribute
122
+ result["Attribute.#{idx}.Replace"] = 'true' if replace
123
+ # pack Name/Value
124
+ unless values.blank?
125
+ values.to_a.each do |value|
126
+ result["Attribute.#{idx}.Name"] = attribute
127
+ result["Attribute.#{idx}.Value"] = value
128
+ idx += 1
129
+ end
130
+ else
131
+ result["Attribute.#{idx}.Name"] = attribute
132
+ idx += 1
133
+ end
134
+ end
135
+ end
136
+ result
137
+ end
138
+
139
+ # Use this helper to manually escape the fields in the query expressions.
140
+ # To escape the single quotes and backslashes and to wrap the string into the single quotes.
141
+ #
142
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API.html
143
+ #
144
+ def escape(value)
145
+ %Q{'#{value.to_s.gsub(/(['\\])/){ "\\#{$1}" }}'} if value
146
+ end
147
+
148
+ # Create query expression from an array.
149
+ # (similar to ActiveRecord::Base#find using :conditions => ['query', param1, .., paramN])
150
+ #
151
+ def query_expression_from_array(params) #:nodoc:
152
+ unless params.blank?
153
+ query = params.shift.to_s
154
+ query.gsub(/(\\)?(\?)/) do
155
+ if $1 # if escaped '\?' is found - replace it by '?' without backslash
156
+ "?"
157
+ else # well, if no backslash precedes '?' then replace it by next param from the list
158
+ escape(params.shift)
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ # Retrieve a list of SDB domains from Amazon.
165
+ #
166
+ # Returns a hash:
167
+ # { :domains => [domain1, ..., domainN],
168
+ # :next_token => string || nil,
169
+ # :box_usage => string,
170
+ # :request_id => string }
171
+ #
172
+ # Example:
173
+ #
174
+ # sdb = RightAws::SdbInterface.new
175
+ # sdb.list_domains #=> { :box_usage => "0.0000071759",
176
+ # :request_id => "976709f9-0111-2345-92cb-9ce90acd0982",
177
+ # :domains => ["toys", "dolls"]}
178
+ #
179
+ # 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,
180
+ # list_domains will end.
181
+ #
182
+ # sdb.list_domains(10) do |result| # list by 10 domains per iteration
183
+ # puts result.inspect
184
+ # true
185
+ # end
186
+ #
187
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_ListDomains.html
188
+ #
189
+ def list_domains(max_number_of_domains = nil, next_token = nil )
190
+ request_params = { 'MaxNumberOfDomains' => max_number_of_domains,
191
+ 'NextToken' => next_token }
192
+ link = generate_request("ListDomains", request_params)
193
+ result = request_info(link, QSdbListDomainParser.new)
194
+ # return result if no block given
195
+ return result unless block_given?
196
+ # loop if block if given
197
+ begin
198
+ # the block must return true if it wanna continue
199
+ break unless yield(result) && result[:next_token]
200
+ # make new request
201
+ request_params['NextToken'] = result[:next_token]
202
+ link = generate_request("ListDomains", request_params)
203
+ result = request_info(link, QSdbListDomainParser.new)
204
+ end while true
205
+ rescue Exception
206
+ on_exception
207
+ end
208
+
209
+ # Create new SDB domain at Amazon.
210
+ #
211
+ # Returns a hash: { :box_usage, :request_id } on success or an exception on error.
212
+ # (Amazon raises no errors if the domain already exists).
213
+ #
214
+ # Example:
215
+ #
216
+ # sdb = RightAws::SdbInterface.new
217
+ # sdb.create_domain('toys') # => { :box_usage => "0.0000071759",
218
+ # :request_id => "976709f9-0111-2345-92cb-9ce90acd0982" }
219
+ #
220
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_CreateDomain.html
221
+ def create_domain(domain_name)
222
+ link = generate_request("CreateDomain",
223
+ 'DomainName' => domain_name)
224
+ request_info(link, QSdbSimpleParser.new)
225
+ rescue Exception
226
+ on_exception
227
+ end
228
+
229
+ # Delete SDB domain at Amazon.
230
+ #
231
+ # Returns a hash: { :box_usage, :request_id } on success or an exception on error.
232
+ # (Amazon raises no errors if the domain does not exist).
233
+ #
234
+ # Example:
235
+ #
236
+ # sdb = RightAws::SdbInterface.new
237
+ # sdb.delete_domain('toys') # => { :box_usage => "0.0000071759",
238
+ # :request_id => "976709f9-0111-2345-92cb-9ce90acd0982" }
239
+ #
240
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_DeleteDomain.html
241
+ #
242
+ def delete_domain(domain_name)
243
+ link = generate_request("DeleteDomain",
244
+ 'DomainName' => domain_name)
245
+ request_info(link, QSdbSimpleParser.new)
246
+ rescue Exception
247
+ on_exception
248
+ end
249
+
250
+ # Add/Replace item attributes.
251
+ #
252
+ # Params:
253
+ # domain_name = DomainName
254
+ # item_name = ItemName
255
+ # attributes = {
256
+ # 'nameA' => [valueA1,..., valueAN],
257
+ # ...
258
+ # 'nameZ' => [valueZ1,..., valueZN]
259
+ # }
260
+ # replace = :replace | any other value to skip replacement
261
+ #
262
+ # Returns a hash: { :box_usage, :request_id } on success or an exception on error.
263
+ # (Amazon raises no errors if the attribute was not overridden, as when the :replace param is unset).
264
+ #
265
+ # Example:
266
+ #
267
+ # sdb = RightAws::SdbInterface.new
268
+ # sdb.create_domain 'family'
269
+ #
270
+ # attributes = {}
271
+ # # create attributes for Jon and Silvia
272
+ # attributes['Jon'] = %w{ car beer }
273
+ # attributes['Silvia'] = %w{ beetle rolling_pin kids }
274
+ # sdb.put_attributes 'family', 'toys', attributes #=> ok
275
+ # # now: Jon=>[car, beer], Silvia=>[beetle, rolling_pin, kids]
276
+ #
277
+ # # add attributes to Jon
278
+ # attributes.delete('Silvia')
279
+ # attributes['Jon'] = %w{ girls pub }
280
+ # sdb.put_attributes 'family', 'toys', attributes #=> ok
281
+ # # now: Jon=>[car, beer, girls, pub], Silvia=>[beetle, rolling_pin, kids]
282
+ #
283
+ # # replace attributes for Jon and add to a cat (the cat had no attributes before)
284
+ # attributes['Jon'] = %w{ vacuum_cleaner hammer spade }
285
+ # attributes['cat'] = %w{ mouse clew Jons_socks }
286
+ # sdb.put_attributes 'family', 'toys', attributes, :replace #=> ok
287
+ # # now: Jon=>[vacuum_cleaner, hammer, spade], Silvia=>[beetle, rolling_pin, kids], cat=>[mouse, clew, Jons_socks]
288
+ #
289
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_PutAttributes.html
290
+ #
291
+ def put_attributes(domain_name, item_name, attributes, replace = false)
292
+ params = { 'DomainName' => domain_name,
293
+ 'ItemName' => item_name }.merge(pack_attributes(attributes, replace))
294
+ link = generate_request("PutAttributes", params)
295
+ request_info( link, QSdbSimpleParser.new )
296
+ rescue Exception
297
+ on_exception
298
+ end
299
+
300
+ # Retrieve SDB item's attribute(s).
301
+ #
302
+ # Returns a hash:
303
+ # { :box_usage => string,
304
+ # :request_id => string,
305
+ # :attributes => { 'nameA' => [valueA1,..., valueAN],
306
+ # ... ,
307
+ # 'nameZ' => [valueZ1,..., valueZN] } }
308
+ #
309
+ # Example:
310
+ # # request all attributes
311
+ # sdb.get_attributes('family', 'toys') # => { :attributes => {"cat" => ["clew", "Jons_socks", "mouse"] },
312
+ # "Silvia" => ["beetle", "rolling_pin", "kids"],
313
+ # "Jon" => ["vacuum_cleaner", "hammer", "spade"]},
314
+ # :box_usage => "0.0000093222",
315
+ # :request_id => "81273d21-000-1111-b3f9-512d91d29ac8" }
316
+ #
317
+ # # request cat's attributes only
318
+ # sdb.get_attributes('family', 'toys', 'cat') # => { :attributes => {"cat" => ["clew", "Jons_socks", "mouse"] },
319
+ # :box_usage => "0.0000093222",
320
+ # :request_id => "81273d21-001-1111-b3f9-512d91d29ac8" }
321
+ #
322
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_GetAttributes.html
323
+ #
324
+ def get_attributes(domain_name, item_name, attribute_name=nil)
325
+ link = generate_request("GetAttributes", 'DomainName' => domain_name,
326
+ 'ItemName' => item_name,
327
+ 'AttributeName' => attribute_name )
328
+ request_info(link, QSdbGetAttributesParser.new)
329
+ rescue Exception
330
+ on_exception
331
+ end
332
+
333
+ # Delete value, attribute or item.
334
+ #
335
+ # Example:
336
+ # # delete 'vodka' and 'girls' from 'Jon' and 'mice' from 'cat'.
337
+ # sdb.delete_attributes 'family', 'toys', { 'Jon' => ['vodka', 'girls'], 'cat' => ['mice'] }
338
+ #
339
+ # # delete the all the values from attributes (i.e. delete the attributes)
340
+ # sdb.delete_attributes 'family', 'toys', { 'Jon' => [], 'cat' => [] }
341
+ # # or
342
+ # sdb.delete_attributes 'family', 'toys', [ 'Jon', 'cat' ]
343
+ #
344
+ # # delete all the attributes from item 'toys' (i.e. delete the item)
345
+ # sdb.delete_attributes 'family', 'toys'
346
+ #
347
+ # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_DeleteAttributes.html
348
+ #
349
+ def delete_attributes(domain_name, item_name, attributes = nil)
350
+ params = { 'DomainName' => domain_name,
351
+ 'ItemName' => item_name }.merge(pack_attributes(attributes))
352
+ link = generate_request("DeleteAttributes", params)
353
+ request_info( link, QSdbSimpleParser.new )
354
+ rescue Exception
355
+ on_exception
356
+ end
357
+
358
+
359
+ # QUERY:
360
+
361
+ # Perform a query on SDB.
362
+ #
363
+ # Returns a hash:
364
+ # { :box_usage => string,
365
+ # :request_id => string,
366
+ # :next_token => string,
367
+ # :items => [ItemName1,..., ItemNameN] }
368
+ #
369
+ # Example:
370
+ #
371
+ # query = "['cat' = 'clew']"
372
+ # sdb.query('family', query) #=> hash of data
373
+ # sdb.query('family', query, 10) #=> hash of data with max of 10 items
374
+ #
375
+ # If a block is given, query will iteratively yield results to it as long as the block continues to return true.
376
+ #
377
+ # # List 10 items per iteration. Don't
378
+ # # forget to escape single quotes and backslashes and wrap all the items in single quotes.
379
+ # query = "['cat'='clew'] union ['dog'='Jon\\'s boot']"
380
+ # sdb.query('family', query, 10) do |result|
381
+ # puts result.inspect
382
+ # true
383
+ # end
384
+ #
385
+ # # Same query using automatic escaping...to use the auto escape, pass the query and its params as an array:
386
+ # query = [ "['cat'=?] union ['dog'=?]", "clew", "Jon's boot" ]
387
+ # sdb.query('family', query)
388
+ #
389
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_Query.html
390
+ #
391
+ def query(domain_name, query_expression = nil, max_number_of_items = nil, next_token = nil)
392
+ query_expression = query_expression_from_array(query_expression) if query_expression.is_a?(Array)
393
+ #
394
+ request_params = { 'DomainName' => domain_name,
395
+ 'QueryExpression' => query_expression,
396
+ 'MaxNumberOfItems' => max_number_of_items,
397
+ 'NextToken' => next_token }
398
+ link = generate_request("Query", request_params)
399
+ result = request_info( link, QSdbQueryParser.new )
400
+ # return result if no block given
401
+ return result unless block_given?
402
+ # loop if block if given
403
+ begin
404
+ # the block must return true if it wanna continue
405
+ break unless yield(result) && result[:next_token]
406
+ # make new request
407
+ request_params['NextToken'] = result[:next_token]
408
+ link = generate_request("Query", request_params)
409
+ result = request_info( link, QSdbQueryParser.new )
410
+ end while true
411
+ rescue Exception
412
+ on_exception
413
+ end
414
+
415
+ #-----------------------------------------------------------------
416
+ # PARSERS:
417
+ #-----------------------------------------------------------------
418
+ class QSdbListDomainParser < RightAWSParser #:nodoc:
419
+ def reset
420
+ @result = { :domains => [] }
421
+ end
422
+ def tagend(name)
423
+ case name
424
+ when 'NextToken' : @result[:next_token] = @text
425
+ when 'DomainName' : @result[:domains] << @text
426
+ when 'BoxUsage' : @result[:box_usage] = @text
427
+ when 'RequestId' : @result[:request_id] = @text
428
+ end
429
+ end
430
+ end
431
+
432
+ class QSdbSimpleParser < RightAWSParser #:nodoc:
433
+ def reset
434
+ @result = {}
435
+ end
436
+ def tagend(name)
437
+ case name
438
+ when 'BoxUsage' : @result[:box_usage] = @text
439
+ when 'RequestId' : @result[:request_id] = @text
440
+ end
441
+ end
442
+ end
443
+
444
+ class QSdbGetAttributesParser < RightAWSParser #:nodoc:
445
+ def reset
446
+ @last_attribute_name = nil
447
+ @result = { :attributes => {} }
448
+ end
449
+ def tagend(name)
450
+ case name
451
+ when 'Name' : @last_attribute_name = @text
452
+ when 'Value' : (@result[:attributes][@last_attribute_name] ||= []) << @text
453
+ when 'BoxUsage' : @result[:box_usage] = @text
454
+ when 'RequestId' : @result[:request_id] = @text
455
+ end
456
+ end
457
+ end
458
+
459
+ class QSdbQueryParser < RightAWSParser #:nodoc:
460
+ def reset
461
+ @result = { :items => [] }
462
+ end
463
+ def tagend(name)
464
+ case name
465
+ when 'ItemName' : @result[:items] << @text
466
+ when 'BoxUsage' : @result[:box_usage] = @text
467
+ when 'RequestId' : @result[:request_id] = @text
468
+ when 'NextToken' : @result[:next_token] = @text
469
+ end
470
+ end
471
+ end
472
+
473
+ end
474
+
475
+ end