kerryb-right_aws 1.7.3

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,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