right_aws 3.0.4 → 3.0.5
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 +10 -0
- data/lib/awsbase/right_awsbase.rb +7 -1
- data/lib/awsbase/version.rb +1 -1
- data/lib/ec2/right_ec2.rb +14 -2
- data/lib/ec2/right_ec2_ebs.rb +35 -2
- data/lib/ec2/right_ec2_instances.rb +21 -12
- data/lib/emr/right_emr_interface.rb +1 -0
- data/lib/s3/right_s3.rb +28 -5
- data/lib/s3/right_s3_interface.rb +184 -4
- data/right_aws.gemspec +1 -0
- data/test/s3/test_right_s3.rb +79 -5
- metadata +8 -17
data/History.txt
CHANGED
@@ -346,3 +346,13 @@ the source key.
|
|
346
346
|
- Fixed:
|
347
347
|
- #125 - fixes redirect bug in file PUT requests (Cary)
|
348
348
|
- some other minor fixes
|
349
|
+
|
350
|
+
=== 3.0.5
|
351
|
+
Release Notes:
|
352
|
+
- Added: API '2012-06-15' support for CreateVolume and DescribeVolumes API calls (to support IOPS)
|
353
|
+
- Fixed:
|
354
|
+
- Single-threaded multipart upload support (https://github.com/rightscale/right_aws/pull/116)
|
355
|
+
- S3 multi object delete (https://github.com/rightscale/right_aws/pull/106)
|
356
|
+
- Support for "ami_version" added in emr interface (https://github.com/rightscale/right_aws/pull/129)
|
357
|
+
- S3: Added block references to several methods (https://github.com/rightscale/right_aws/pull/130)
|
358
|
+
- Some other minor changes
|
@@ -43,7 +43,12 @@ module RightAws
|
|
43
43
|
Base64.encode64(OpenSSL::HMAC.digest(@@digest1, aws_secret_access_key, auth_string)).strip
|
44
44
|
end
|
45
45
|
|
46
|
-
#
|
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
|
47
52
|
# http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
|
48
53
|
def self.amz_escape(param)
|
49
54
|
param = param.flatten.join('') if param.is_a?(Array) # ruby 1.9.x Array#to_s fix
|
@@ -443,6 +448,7 @@ module RightAws
|
|
443
448
|
"AWSAccessKeyId" => @aws_access_key_id,
|
444
449
|
"Version" => custom_options[:api_version] || @params[:api_version] }
|
445
450
|
service_hash.merge!(options)
|
451
|
+
service_hash["SecurityToken"] = @params[:token] if @params[:token]
|
446
452
|
# Sign request options
|
447
453
|
service_params = signed_service_params(@aws_secret_access_key, service_hash, http_verb, @params[:host_to_sign], @params[:service])
|
448
454
|
# Use POST if the length of the query string is too large
|
data/lib/awsbase/version.rb
CHANGED
data/lib/ec2/right_ec2.rb
CHANGED
@@ -91,9 +91,12 @@ module RightAws
|
|
91
91
|
'm2.xlarge' ,
|
92
92
|
'm2.2xlarge',
|
93
93
|
'm2.4xlarge',
|
94
|
+
'm3.xlarge' ,
|
95
|
+
'm3.2xlarge',
|
94
96
|
'cc1.4xlarge',
|
95
97
|
'cg1.4xlarge',
|
96
|
-
'cc2.8xlarge'
|
98
|
+
'cc2.8xlarge',
|
99
|
+
'hi1.4xlarge' ]
|
97
100
|
|
98
101
|
@@bench = AwsBenchmarkingBlock.new
|
99
102
|
def self.bench_xml
|
@@ -120,6 +123,7 @@ module RightAws
|
|
120
123
|
# * <tt>:logger</tt>: for log messages, default: RAILS_DEFAULT_LOGGER else STDOUT
|
121
124
|
# * <tt>:signature_version</tt>: The signature version : '0','1' or '2'(default)
|
122
125
|
# * <tt>:cache</tt>: true/false: caching for: ec2_describe_images, describe_instances,
|
126
|
+
# * <tt>:token</tt>: Option SecurityToken for temporary credentials
|
123
127
|
# describe_images_by_owner, describe_images_by_executable_by, describe_availability_zones,
|
124
128
|
# describe_security_groups, describe_key_pairs, describe_addresses,
|
125
129
|
# describe_volumes, describe_snapshots methods, default: false.
|
@@ -154,12 +158,15 @@ module RightAws
|
|
154
158
|
# 'RemoteFunctionName' -> :remote_funtion_name
|
155
159
|
cache_name = remote_function_name.right_underscore.to_sym
|
156
160
|
list, options = AwsUtils::split_items_and_params(list_and_options)
|
161
|
+
custom_options = {}
|
157
162
|
# Resource IDs to fetch
|
158
163
|
request_hash = amazonize_list(remote_item_name, list)
|
159
164
|
# Other custom options
|
160
165
|
options.each do |key, values|
|
161
166
|
next if values.right_blank?
|
162
167
|
case key
|
168
|
+
when :options
|
169
|
+
custom_options = values
|
163
170
|
when :filters then
|
164
171
|
request_hash.merge!(amazonize_list(['Filter.?.Name', 'Filter.?.Value.?'], values))
|
165
172
|
else
|
@@ -167,12 +174,17 @@ module RightAws
|
|
167
174
|
end
|
168
175
|
end
|
169
176
|
cache_for = (list.right_blank? && options.right_blank?) ? cache_name : nil
|
170
|
-
link = generate_request(remote_function_name, request_hash)
|
177
|
+
link = generate_request(remote_function_name, request_hash, custom_options)
|
171
178
|
request_cache_or_info(cache_for, link, parser_class, @@bench, cache_for, &block)
|
172
179
|
rescue Exception
|
173
180
|
on_exception
|
174
181
|
end
|
175
182
|
|
183
|
+
def merge_new_options_into_list_and_options(list_and_options, new_options)
|
184
|
+
list, options = AwsUtils::split_items_and_params(list_and_options)
|
185
|
+
list << options.merge(new_options)
|
186
|
+
end
|
187
|
+
|
176
188
|
#-----------------------------------------------------------------
|
177
189
|
# Keys
|
178
190
|
#-----------------------------------------------------------------
|
data/lib/ec2/right_ec2_ebs.rb
CHANGED
@@ -29,6 +29,9 @@ module RightAws
|
|
29
29
|
# EBS: Volumes
|
30
30
|
#-----------------------------------------------------------------
|
31
31
|
|
32
|
+
VOLUME_API_VERSION = (API_VERSION > '2012-06-15') ? API_VERSION : '2012-06-15'
|
33
|
+
VOLUME_TYPES = ['standard', 'io1']
|
34
|
+
|
32
35
|
# Describe EBS volumes.
|
33
36
|
#
|
34
37
|
# Accepts a list of volumes and/or a set of filters as the last parameter.
|
@@ -47,6 +50,14 @@ module RightAws
|
|
47
50
|
# :aws_id => "vol-60957009",
|
48
51
|
# :aws_created_at => "2008-06-18T08:19:20.000Z",
|
49
52
|
# :aws_instance_id => "i-c014c0a9"},
|
53
|
+
# {:aws_id => "vol-71de8b1f",
|
54
|
+
# :aws_size => 5,
|
55
|
+
# :snapshot_id => nil,
|
56
|
+
# :zone => "us-east-1a",
|
57
|
+
# :aws_status => "available",
|
58
|
+
# :aws_created_at => "2012-06-21T18:47:34.000Z",
|
59
|
+
# :volume_type => "io1",
|
60
|
+
# :iop => "5"},#
|
50
61
|
# {:aws_size => 1,
|
51
62
|
# :zone => "merlot",
|
52
63
|
# :snapshot_id => nil,
|
@@ -59,6 +70,7 @@ module RightAws
|
|
59
70
|
# P.S. filters: http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeVolumes.html
|
60
71
|
#
|
61
72
|
def describe_volumes(*list_and_options)
|
73
|
+
list_and_options = merge_new_options_into_list_and_options(list_and_options, :options => {:api_version => VOLUME_API_VERSION})
|
62
74
|
describe_resources_with_list_and_options('DescribeVolumes', 'VolumeId', QEc2DescribeVolumesParser, list_and_options)
|
63
75
|
end
|
64
76
|
|
@@ -73,12 +85,29 @@ module RightAws
|
|
73
85
|
# :aws_created_at => "2008-06-24T18:13:32.000Z",
|
74
86
|
# :aws_size => 94}
|
75
87
|
#
|
76
|
-
|
88
|
+
# ec2.create_volume(nil, 5, 'us-east-1a', :iops => '5', :volume_type => 'io1') #=>
|
89
|
+
# {:aws_id=>"vol-71de8b1f",
|
90
|
+
# :aws_size=>5,
|
91
|
+
# :snapshot_id=>nil,
|
92
|
+
# :zone=>"us-east-1a",
|
93
|
+
# :aws_status=>"creating",
|
94
|
+
# :aws_created_at=>"2012-06-21T18:47:34.000Z",
|
95
|
+
# :volume_type=>"io1",
|
96
|
+
# :iops=>"5"}
|
97
|
+
#
|
98
|
+
def create_volume(snapshot_id, size, zone, options={})
|
77
99
|
hash = { "Size" => size.to_s,
|
78
100
|
"AvailabilityZone" => zone.to_s }
|
79
101
|
# Get rig of empty snapshot: e8s guys do not like it
|
80
102
|
hash["SnapshotId"] = snapshot_id.to_s unless snapshot_id.right_blank?
|
81
|
-
|
103
|
+
# Add IOPS support (default behavior) but skip it when an old API version call is requested
|
104
|
+
options[:options] ||= {}
|
105
|
+
options[:options][:api_version] ||= VOLUME_API_VERSION
|
106
|
+
if options[:options][:api_version] >= VOLUME_API_VERSION
|
107
|
+
hash["VolumeType"] = options[:volume_type] unless options[:volume_type].right_blank?
|
108
|
+
hash["Iops"] = options[:iops] unless options[:iops].right_blank?
|
109
|
+
end
|
110
|
+
link = generate_request("CreateVolume", hash, options[:options])
|
82
111
|
request_info(link, QEc2CreateVolumeParser.new(:logger => @logger))
|
83
112
|
rescue Exception
|
84
113
|
on_exception
|
@@ -364,6 +393,8 @@ module RightAws
|
|
364
393
|
when 'size' then @result[:aws_size] = @text.to_i ###
|
365
394
|
when 'snapshotId' then @result[:snapshot_id] = @text.right_blank? ? nil : @text ###
|
366
395
|
when 'availabilityZone' then @result[:zone] = @text ###
|
396
|
+
when 'volumeType' then @result[:volume_type] = @text
|
397
|
+
when 'iops' then @result[:iops] = @text
|
367
398
|
end
|
368
399
|
end
|
369
400
|
def reset
|
@@ -403,6 +434,8 @@ module RightAws
|
|
403
434
|
when 'snapshotId' then @item[:snapshot_id] = @text.right_blank? ? nil : @text
|
404
435
|
when 'availabilityZone' then @item[:zone] = @text
|
405
436
|
when 'deleteOnTermination' then @item[:delete_on_termination] = (@text == 'true')
|
437
|
+
when 'volumeType' then @item[:volume_type] = @text
|
438
|
+
when 'iops' then @item[:iops] = @text
|
406
439
|
else
|
407
440
|
case full_tag_name
|
408
441
|
when %r{/volumeSet/item/volumeId$} then @item[:aws_id] = @text
|
@@ -25,6 +25,8 @@ module RightAws
|
|
25
25
|
|
26
26
|
class Ec2
|
27
27
|
|
28
|
+
INSTANCE_API_VERSION = (API_VERSION > '2012-07-20') ? API_VERSION : '2012-07-20'
|
29
|
+
|
28
30
|
#-----------------------------------------------------------------
|
29
31
|
# Instances
|
30
32
|
#-----------------------------------------------------------------
|
@@ -108,6 +110,7 @@ module RightAws
|
|
108
110
|
# P.S. filters: http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeInstances.html
|
109
111
|
#
|
110
112
|
def describe_instances(*list_and_options)
|
113
|
+
list_and_options = merge_new_options_into_list_and_options(list_and_options, :options => {:api_version => INSTANCE_API_VERSION})
|
111
114
|
describe_resources_with_list_and_options('DescribeInstances', 'InstanceId', QEc2DescribeInstancesParser, list_and_options) do |parser|
|
112
115
|
get_desc_instances(parser.result)
|
113
116
|
end
|
@@ -158,7 +161,6 @@ module RightAws
|
|
158
161
|
:group_names => group_names,
|
159
162
|
:key_name => key_name,
|
160
163
|
:instance_type => instance_type,
|
161
|
-
:addressing_type => addressing_type,
|
162
164
|
:kernel_id => kernel_id,
|
163
165
|
:ramdisk_id => ramdisk_id,
|
164
166
|
:availability_zone => availability_zone,
|
@@ -174,9 +176,10 @@ module RightAws
|
|
174
176
|
|
175
177
|
# Launch new EC2 instances.
|
176
178
|
#
|
177
|
-
# Options: :image_id, :
|
179
|
+
# Options: :image_id, :min_count, max_count, :key_name, :kernel_id, :ramdisk_id,
|
178
180
|
# :availability_zone, :monitoring_enabled, :subnet_id, :disable_api_termination, :instance_initiated_shutdown_behavior,
|
179
|
-
# :block_device_mappings, :placement_group_name, :license_pool, :group_ids, :group_names, :private_ip_address
|
181
|
+
# :block_device_mappings, :placement_group_name, :license_pool, :group_ids, :group_names, :private_ip_address,
|
182
|
+
# :ebs_optimized
|
180
183
|
#
|
181
184
|
# Returns a list of launched instances or an exception.
|
182
185
|
#
|
@@ -223,9 +226,9 @@ module RightAws
|
|
223
226
|
def launch_instances(image_id, options={})
|
224
227
|
options[:user_data] = options[:user_data].to_s
|
225
228
|
params = map_api_keys_and_values( options,
|
226
|
-
:key_name, :
|
229
|
+
:key_name, :kernel_id,
|
227
230
|
:ramdisk_id, :subnet_id, :instance_initiated_shutdown_behavior,
|
228
|
-
:private_ip_address, :additional_info, :license_pool,
|
231
|
+
:private_ip_address, :additional_info, :license_pool, :ebs_optimized,
|
229
232
|
:image_id => { :value => image_id },
|
230
233
|
:min_count => { :value => options[:min_count] || 1 },
|
231
234
|
:max_count => { :value => options[:max_count] || options[:min_count] || 1 },
|
@@ -243,7 +246,12 @@ module RightAws
|
|
243
246
|
:value => Proc.new{ options[:monitoring_enabled] && options[:monitoring_enabled].to_s }})
|
244
247
|
# Log debug information
|
245
248
|
@logger.info("Launching instance of image #{image_id}. Options: #{params.inspect}")
|
246
|
-
|
249
|
+
# Add IOPS support (default behavior) but skip it when an old API version call is requested
|
250
|
+
options[:options] ||= {}
|
251
|
+
options[:options][:api_version] ||= INSTANCE_API_VERSION
|
252
|
+
params.delete("EbsOptimized") if options[:options][:api_version] < INSTANCE_API_VERSION
|
253
|
+
#
|
254
|
+
link = generate_request("RunInstances", params, options[:options])
|
247
255
|
instances = request_info(link, QEc2DescribeInstancesParser.new(:logger => @logger))
|
248
256
|
get_desc_instances(instances)
|
249
257
|
rescue Exception
|
@@ -383,7 +391,7 @@ module RightAws
|
|
383
391
|
when 'GroupId' then request_hash.merge!(amazonize_list('GroupId', value))
|
384
392
|
else request_hash["#{attribute}.Value"] = value
|
385
393
|
end
|
386
|
-
link = generate_request('ModifyInstanceAttribute', request_hash)
|
394
|
+
link = generate_request('ModifyInstanceAttribute', request_hash, :api_version => INSTANCE_API_VERSION)
|
387
395
|
request_info(link, RightBoolResponseParser.new(:logger => @logger))
|
388
396
|
rescue Exception
|
389
397
|
on_exception
|
@@ -588,9 +596,10 @@ module RightAws
|
|
588
596
|
when 'requesterId' then @item[:requester_id] = @text
|
589
597
|
when 'virtualizationType' then @item[:virtualization_type] = @text
|
590
598
|
when 'clientToken' then @item[:client_token] = @text
|
591
|
-
when 'sourceDestCheck' then @item[:source_dest_check] = @text == 'true'
|
599
|
+
when 'sourceDestCheck' then @item[:source_dest_check] = @text == 'true'
|
592
600
|
when 'tenancy' then @item[:placement_tenancy] = @text
|
593
601
|
when 'hypervisor' then @item[:hypervisor] = @text
|
602
|
+
when 'ebsOptimized' then @item[:ebs_optimized] = @text == 'true'
|
594
603
|
else
|
595
604
|
case full_tag_name
|
596
605
|
# EC2 Groups
|
@@ -618,7 +627,7 @@ module RightAws
|
|
618
627
|
when 'volumeId' then @block_device_mapping[:ebs_volume_id] = @text
|
619
628
|
when 'status' then @block_device_mapping[:ebs_status] = @text
|
620
629
|
when 'attachTime' then @block_device_mapping[:ebs_attach_time] = @text
|
621
|
-
when 'deleteOnTermination' then @block_device_mapping[:ebs_delete_on_termination] = @text == 'true'
|
630
|
+
when 'deleteOnTermination' then @block_device_mapping[:ebs_delete_on_termination] = @text == 'true'
|
622
631
|
when 'item' then @item[:block_device_mappings] << @block_device_mapping
|
623
632
|
end
|
624
633
|
when %r{/instancesSet/item$} then @reservation[:instances_set] << @item
|
@@ -675,9 +684,9 @@ module RightAws
|
|
675
684
|
when %r{/ramdisk/value$} then @result = @text
|
676
685
|
when %r{/userData/value$} then @result = @text
|
677
686
|
when %r{/rootDeviceName/value$} then @result = @text
|
678
|
-
when %r{/disableApiTermination/value} then @result = @text == 'true'
|
687
|
+
when %r{/disableApiTermination/value} then @result = @text == 'true'
|
679
688
|
when %r{/instanceInitiatedShutdownBehavior/value$} then @result = @text
|
680
|
-
when %r{/sourceDestCheck/value$} then @result = @text == 'true'
|
689
|
+
when %r{/sourceDestCheck/value$} then @result = @text == 'true'
|
681
690
|
when %r{/groupSet/item} # no trailing $
|
682
691
|
case name
|
683
692
|
when 'groupId' then @group[:group_id] = @text
|
@@ -692,7 +701,7 @@ module RightAws
|
|
692
701
|
when 'volumeId' then @block_device_mapping[:ebs_volume_id] = @text
|
693
702
|
when 'status' then @block_device_mapping[:ebs_status] = @text
|
694
703
|
when 'attachTime' then @block_device_mapping[:ebs_attach_time] = @text
|
695
|
-
when 'deleteOnTermination' then @block_device_mapping[:ebs_delete_on_termination] = @text == 'true'
|
704
|
+
when 'deleteOnTermination' then @block_device_mapping[:ebs_delete_on_termination] = @text == 'true'
|
696
705
|
when 'item' then @result << @block_device_mapping
|
697
706
|
end
|
698
707
|
end
|
@@ -124,6 +124,7 @@ module RightAws
|
|
124
124
|
:additional_info => 'AdditionalInfo',
|
125
125
|
:log_uri => 'LogUri',
|
126
126
|
:name => 'Name',
|
127
|
+
:ami_version => 'AmiVersion',
|
127
128
|
# JobFlowInstancesConfig
|
128
129
|
:ec2_key_name => 'Instances.Ec2KeyName',
|
129
130
|
:hadoop_version => 'Instances.HadoopVersion',
|
data/lib/s3/right_s3.rb
CHANGED
@@ -259,11 +259,12 @@ module RightAws
|
|
259
259
|
# key = RightAws::S3::Key.create(bucket, 'logs/today/1.log')
|
260
260
|
# key.head
|
261
261
|
#
|
262
|
-
def key(key_name, head=false)
|
262
|
+
def key(key_name, head=false, &blck)
|
263
263
|
raise 'Key name can not be empty.' if key_name.right_blank?
|
264
264
|
key_instance = nil
|
265
265
|
# if this key exists - find it ....
|
266
266
|
keys({'prefix'=>key_name}, head).each do |key|
|
267
|
+
blck.call if block_given?
|
267
268
|
if key.name == key_name.to_s
|
268
269
|
key_instance = key
|
269
270
|
break
|
@@ -282,9 +283,9 @@ module RightAws
|
|
282
283
|
#
|
283
284
|
# bucket.put('logs/today/1.log', 'Olala!') #=> true
|
284
285
|
#
|
285
|
-
def put(key, data=nil, meta_headers={}, perms=nil, headers={})
|
286
|
+
def put(key, data=nil, meta_headers={}, perms=nil, headers={}, &blck)
|
286
287
|
key = Key.create(self, key.to_s, data, meta_headers) unless key.is_a?(Key)
|
287
|
-
key.put(data, perms, headers)
|
288
|
+
key.put(data, perms, headers, &blck)
|
288
289
|
end
|
289
290
|
|
290
291
|
# Retrieve data object from Amazon.
|
@@ -518,11 +519,33 @@ module RightAws
|
|
518
519
|
# ...
|
519
520
|
# key.put('Olala!') #=> true
|
520
521
|
#
|
521
|
-
def put(data=nil, perms=nil, headers={})
|
522
|
+
def put(data=nil, perms=nil, headers={}, &blck)
|
522
523
|
headers['x-amz-acl'] = perms if perms
|
523
524
|
@data = data || @data
|
524
525
|
meta = self.class.add_meta_prefix(@meta_headers)
|
525
|
-
@bucket.s3.interface.put(@bucket.name, @name, @data, meta.merge(headers))
|
526
|
+
@bucket.s3.interface.put(@bucket.name, @name, @data, meta.merge(headers), &blck)
|
527
|
+
end
|
528
|
+
|
529
|
+
# Store object data on S3 using the Multipart Upload API. This is useful if you do not know the file size
|
530
|
+
# upfront (for example reading from pipe or socket) or if you are transmitting data over an unreliable network.
|
531
|
+
#
|
532
|
+
# Parameter +data+ is an object which responds to :read or an object which can be converted to a String prior to upload.
|
533
|
+
# Parameter +part_size+ determines the size of each part sent (must be > 5MB per Amazon's API requirements)
|
534
|
+
#
|
535
|
+
# If data is a stream the caller is responsible for calling close() on the stream after this methods returns
|
536
|
+
#
|
537
|
+
# Returns +true+.
|
538
|
+
#
|
539
|
+
# upload_data = StringIO.new('My sample data')
|
540
|
+
# key = RightAws::S3::Key.create(bucket, 'logs/today/1.log')
|
541
|
+
# key.data = upload_data
|
542
|
+
# key.put_multipart(:part_size => 5*1024*1024) #=> true
|
543
|
+
#
|
544
|
+
def put_multipart(data=nil, perms=nil, headers={}, part_size=nil)
|
545
|
+
headers['x-amz-acl'] = perms if perms
|
546
|
+
@data = data || @data
|
547
|
+
meta = self.class.add_meta_prefix(@meta_headers)
|
548
|
+
@bucket.s3.interface.store_object_multipart({:bucket => @bucket.name, :key => @name, :data => @data, :headers => meta.merge(headers), :part_size => part_size})
|
526
549
|
end
|
527
550
|
|
528
551
|
# Rename an object. Returns new object name.
|
@@ -26,6 +26,8 @@ module RightAws
|
|
26
26
|
class S3Interface < RightAwsBase
|
27
27
|
|
28
28
|
USE_100_CONTINUE_PUT_SIZE = 1_000_000
|
29
|
+
MINIMUM_PART_SIZE = 5 * 1024 * 1024
|
30
|
+
DEFAULT_RETRY_COUNT = 5
|
29
31
|
|
30
32
|
include RightAwsBaseInterface
|
31
33
|
|
@@ -41,15 +43,20 @@ module RightAws
|
|
41
43
|
S3_REQUEST_PARAMETERS = [ 'acl',
|
42
44
|
'location',
|
43
45
|
'logging', # this one is beta, no support for now
|
46
|
+
'partNumber',
|
44
47
|
'response-content-type',
|
45
48
|
'response-content-language',
|
46
49
|
'response-expires',
|
47
50
|
'response-cache-control',
|
48
51
|
'response-content-disposition',
|
49
52
|
'response-content-encoding',
|
50
|
-
'torrent'
|
51
|
-
|
53
|
+
'torrent',
|
54
|
+
'uploadId',
|
55
|
+
'uploads',
|
56
|
+
'delete'].sort
|
57
|
+
MULTI_OBJECT_DELETE_MAX_KEYS = 1000
|
52
58
|
|
59
|
+
|
53
60
|
@@bench = AwsBenchmarkingBlock.new
|
54
61
|
def self.bench_xml
|
55
62
|
@@bench.xml
|
@@ -428,7 +435,7 @@ module RightAws
|
|
428
435
|
# mode.
|
429
436
|
#
|
430
437
|
|
431
|
-
def put(bucket, key, data=nil, headers={})
|
438
|
+
def put(bucket, key, data=nil, headers={}, &blck)
|
432
439
|
# On Windows, if someone opens a file in text mode, we must reset it so
|
433
440
|
# to binary mode for streaming to work properly
|
434
441
|
if(data.respond_to?(:binmode))
|
@@ -439,7 +446,7 @@ module RightAws
|
|
439
446
|
headers['expect'] = '100-continue'
|
440
447
|
end
|
441
448
|
req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}", :data=>data))
|
442
|
-
request_info(req_hash, RightHttp2xxParser.new)
|
449
|
+
request_info(req_hash, RightHttp2xxParser.new, &blck)
|
443
450
|
rescue
|
444
451
|
on_exception
|
445
452
|
end
|
@@ -524,6 +531,134 @@ module RightAws
|
|
524
531
|
r[:verified_md5] ? (return r) : (raise AwsError.new("Uploaded object failed MD5 checksum verification: #{r.inspect}"))
|
525
532
|
end
|
526
533
|
|
534
|
+
# New experimental API for uploading objects using the multipart upload API.
|
535
|
+
# store_object_multipart is similar in function to the store_object method, but breaks the input into parts and transmits each
|
536
|
+
# part separately. The multipart upload API has the benefit of being be able to retransmit a part in isolation without needing to
|
537
|
+
# restart the entire upload. This makes it ideal for uploading large files over unreliable networks. It also does not
|
538
|
+
# require the file size to be known before starting the upload, making it useful for stream data as it is created (say via reading a pipe or socket).
|
539
|
+
# The hash of the response headers contains useful information like the location (the URI for the newly created object), bucket, key, and etag).
|
540
|
+
#
|
541
|
+
# The optional argument of :headers allows the caller to specify arbitrary request header values.
|
542
|
+
#
|
543
|
+
# s3.store_object_multipart(:bucket => "foobucket", :key => "foo", :data => "polemonium" )
|
544
|
+
# => {:location=>"https://s3.amazonaws.com/right_s3_awesome_test_bucket_000B1_officedrop/test%2Flarge_multipart_file",
|
545
|
+
# :e_tag=>"\"72b81ac08aed4d4d1055c11f56c2a258-1\"",
|
546
|
+
# :key=>"test/large_multipart_file",
|
547
|
+
# :bucket=>"right_s3_awesome_test_bucket_000B1_officedrop"}
|
548
|
+
#
|
549
|
+
# f = File.new("some_file", "r")
|
550
|
+
# s3.store_object_multipart(:bucket => "foobucket", :key => "foo", :data => f )
|
551
|
+
# => {:location=>"https://s3.amazonaws.com/right_s3_awesome_test_bucket_000B1_officedrop/test%2Flarge_multipart_file",
|
552
|
+
# :e_tag=>"\"72b81ac08aed4d4d1055c11f56c2a258-1\"",
|
553
|
+
# :key=>"test/large_multipart_file",
|
554
|
+
# :bucket=>"right_s3_awesome_test_bucket_000B1_officedrop"}
|
555
|
+
def store_object_multipart(params)
|
556
|
+
AwsUtils.allow_only([:bucket, :key, :data, :headers, :part_size, :retry_count], params)
|
557
|
+
AwsUtils.mandatory_arguments([:bucket, :key, :data], params)
|
558
|
+
params[:headers] = {} unless params[:headers]
|
559
|
+
|
560
|
+
params[:data].binmode if(params[:data].respond_to?(:binmode)) # On Windows, if someone opens a file in text mode, we must reset it to binary mode for streaming to work properly
|
561
|
+
|
562
|
+
# detect whether we are using straight read or converting to string first
|
563
|
+
unless(params[:data].respond_to?(:read))
|
564
|
+
params[:data] = StringIO.new(params[:data].to_s)
|
565
|
+
end
|
566
|
+
|
567
|
+
# make sure part size is > 5 MB minimum
|
568
|
+
params[:part_size] ||= MINIMUM_PART_SIZE
|
569
|
+
if params[:part_size] < MINIMUM_PART_SIZE
|
570
|
+
raise AwsError.new("Part size for a multipart upload must be greater than or equal to #{5 * 1024 * 1024} bytes. #{params[:part_size]} bytes was provided.")
|
571
|
+
end
|
572
|
+
|
573
|
+
# make sure retry_count is positive
|
574
|
+
params[:retry_count] ||= DEFAULT_RETRY_COUNT
|
575
|
+
if params[:retry_count] < 0
|
576
|
+
raise AwsError.new("Retry count must be positive. #{params[:retry_count]} bytes was provided.")
|
577
|
+
end
|
578
|
+
|
579
|
+
# Set 100-continue for large part sizes
|
580
|
+
if (params[:part_size] >= USE_100_CONTINUE_PUT_SIZE)
|
581
|
+
params[:headers]['expect'] = '100-continue'
|
582
|
+
end
|
583
|
+
|
584
|
+
# initiate upload
|
585
|
+
initiate_hash = generate_rest_request('POST', params[:headers].merge(:url=>"#{params[:bucket]}/#{CGI::escape params[:key]}?uploads"))
|
586
|
+
initiate_resp = request_info(initiate_hash, S3MultipartUploadInitiateResponseParser.new)
|
587
|
+
upload_id = initiate_resp[:upload_id]
|
588
|
+
|
589
|
+
# split into parts and upload each one, re-trying if necessary
|
590
|
+
# upload occurs serially at this time.
|
591
|
+
part_etags = []
|
592
|
+
part_data = ""
|
593
|
+
index = 1
|
594
|
+
until params[:data].eof?
|
595
|
+
part_data = params[:data].read(params[:part_size])
|
596
|
+
unless part_data.size == 0
|
597
|
+
retry_attempts = 1
|
598
|
+
while true
|
599
|
+
begin
|
600
|
+
send_part_hash = generate_rest_request('PUT', params[:headers].merge({ :url=>"#{params[:bucket]}/#{CGI::escape params[:key]}?partNumber=#{index}&uploadId=#{upload_id}", :data=>part_data } ))
|
601
|
+
send_part_resp = request_info(send_part_hash, S3HttpResponseHeadParser.new)
|
602
|
+
part_etags << {:part_num => index, :etag => send_part_resp['etag']}
|
603
|
+
index += 1
|
604
|
+
break # successful, can move to next part
|
605
|
+
rescue AwsError => e
|
606
|
+
if retry_attempts >= params[:retry_count]
|
607
|
+
raise e
|
608
|
+
else
|
609
|
+
#Hit an error attempting to transmit part, retry until retry_attemts have been exhausted
|
610
|
+
retry_attempts += 1
|
611
|
+
end
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
# assemble complete upload message
|
618
|
+
complete_body = "<CompleteMultipartUpload>"
|
619
|
+
part_etags.each do |part_hash|
|
620
|
+
complete_body << "<Part><PartNumber>#{part_hash[:part_num]}</PartNumber><ETag>#{part_hash[:etag]}</ETag></Part>"
|
621
|
+
end
|
622
|
+
complete_body << "</CompleteMultipartUpload>"
|
623
|
+
complete_req_hash = generate_rest_request('POST', {:url=>"#{params[:bucket]}/#{CGI::escape params[:key]}?uploadId=#{upload_id}", :data => complete_body})
|
624
|
+
return request_info(complete_req_hash, S3CompleteMultipartParser.new)
|
625
|
+
rescue
|
626
|
+
on_exception
|
627
|
+
end
|
628
|
+
|
629
|
+
class S3MultipartUploadInitiateResponseParser < RightAWSParser
|
630
|
+
def reset
|
631
|
+
@result = {}
|
632
|
+
end
|
633
|
+
def headers_to_string(headers)
|
634
|
+
result = {}
|
635
|
+
headers.each do |key, value|
|
636
|
+
value = value.first if value.is_a?(Array) && value.size<2
|
637
|
+
result[key] = value
|
638
|
+
end
|
639
|
+
result
|
640
|
+
end
|
641
|
+
def tagend(name)
|
642
|
+
case name
|
643
|
+
when 'UploadId' then @result[:upload_id] = @text
|
644
|
+
end
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
class S3CompleteMultipartParser < RightAWSParser # :nodoc:
|
649
|
+
def reset
|
650
|
+
@result = {}
|
651
|
+
end
|
652
|
+
def tagend(name)
|
653
|
+
case name
|
654
|
+
when 'Location' then @result[:location] = @text
|
655
|
+
when 'Bucket' then @result[:bucket] = @text
|
656
|
+
when 'Key' then @result[:key] = @text
|
657
|
+
when 'ETag' then @result[:e_tag] = @text
|
658
|
+
end
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
527
662
|
# Retrieves object data from Amazon. Returns a +hash+ or an exception.
|
528
663
|
#
|
529
664
|
# s3.get('my_awesome_bucket', 'log/curent/1.log') #=>
|
@@ -655,6 +790,34 @@ module RightAws
|
|
655
790
|
on_exception
|
656
791
|
end
|
657
792
|
|
793
|
+
# Deletes multiple keys. Returns an array with errors, if any.
|
794
|
+
#
|
795
|
+
# s3.delete_multiple('my_awesome_bucket', ['key1', 'key2', ...)
|
796
|
+
# #=> [ { :key => 'key2', :code => 'AccessDenied', :message => "Access Denied" } ]
|
797
|
+
#
|
798
|
+
def delete_multiple(bucket, keys=[], headers={})
|
799
|
+
errors = []
|
800
|
+
keys = Array.new(keys)
|
801
|
+
while keys.length > 0
|
802
|
+
data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
803
|
+
data += "<Delete>\n<Quiet>true</Quiet>\n"
|
804
|
+
keys.take(MULTI_OBJECT_DELETE_MAX_KEYS).each do |key|
|
805
|
+
data += "<Object><Key>#{AwsUtils::xml_escape(key)}</Key></Object>\n"
|
806
|
+
end
|
807
|
+
data += "</Delete>"
|
808
|
+
req_hash = generate_rest_request('POST', headers.merge(
|
809
|
+
:url => "#{bucket}?delete",
|
810
|
+
:data => data,
|
811
|
+
'content-md5' => AwsUtils::content_md5(data)
|
812
|
+
))
|
813
|
+
errors += request_info(req_hash, S3DeleteMultipleParser.new)
|
814
|
+
keys = keys.drop(MULTI_OBJECT_DELETE_MAX_KEYS)
|
815
|
+
end
|
816
|
+
errors
|
817
|
+
rescue
|
818
|
+
on_exception
|
819
|
+
end
|
820
|
+
|
658
821
|
# Copy an object.
|
659
822
|
# directive: :copy - copy meta-headers from source (default value)
|
660
823
|
# :replace - replace meta-headers by passed ones
|
@@ -1021,6 +1184,23 @@ module RightAws
|
|
1021
1184
|
on_exception
|
1022
1185
|
end
|
1023
1186
|
|
1187
|
+
class S3DeleteMultipleParser < RightAWSParser # :nodoc:
|
1188
|
+
def reset
|
1189
|
+
@result = []
|
1190
|
+
end
|
1191
|
+
def tagstart(name, attributes)
|
1192
|
+
@error = {} if name == 'Error'
|
1193
|
+
end
|
1194
|
+
def tagend(name)
|
1195
|
+
case name
|
1196
|
+
when 'Key' then @error[:key] = @text
|
1197
|
+
when 'Code' then @error[:code] = @text
|
1198
|
+
when 'Message' then @error[:message] = @text
|
1199
|
+
when 'Error' then @result << @error
|
1200
|
+
end
|
1201
|
+
end
|
1202
|
+
end
|
1203
|
+
|
1024
1204
|
#-----------------------------------------------------------------
|
1025
1205
|
# PARSERS:
|
1026
1206
|
#-----------------------------------------------------------------
|
data/right_aws.gemspec
CHANGED
@@ -43,6 +43,7 @@ Gem::Specification.new do |spec|
|
|
43
43
|
spec.add_development_dependency('rake')
|
44
44
|
spec.add_development_dependency('rcov')
|
45
45
|
|
46
|
+
spec.summary = 'The RightScale AWS gems have been designed to provide a robust, fast, and secure interface to Amazon EC2, EBS, S3, SQS, SDB, and CloudFront.'
|
46
47
|
spec.description = <<-EOF
|
47
48
|
== DESCRIPTION:
|
48
49
|
|
data/test/s3/test_right_s3.rb
CHANGED
@@ -13,6 +13,8 @@ class TestS3 < Test::Unit::TestCase
|
|
13
13
|
@key1 = 'test/woohoo1/'
|
14
14
|
@key2 = 'test1/key/woohoo2'
|
15
15
|
@key3 = 'test2/A%B@C_D&E?F+G=H"I'
|
16
|
+
@key4 = 'test/large_multipart_file_string'
|
17
|
+
@key5 = 'test/large_multipart_file_stream'
|
16
18
|
@key1_copy = 'test/woohoo1_2'
|
17
19
|
@key1_new_name = 'test/woohoo1_3'
|
18
20
|
@key2_new_name = 'test1/key/woohoo2_new'
|
@@ -41,6 +43,26 @@ class TestS3 < Test::Unit::TestCase
|
|
41
43
|
assert @s3.put(@bucket, @key3, RIGHT_OBJECT_TEXT, 'x-amz-meta-family'=>'Woohoo3!'), 'Put bucket fail'
|
42
44
|
end
|
43
45
|
|
46
|
+
def test_04_put_multipart_string
|
47
|
+
test_text = ""
|
48
|
+
for i in 1..100000
|
49
|
+
test_text << "Testing test text #{i}\n"
|
50
|
+
end
|
51
|
+
assert @s3.store_object_multipart({:bucket => @bucket, :key => @key4, :data => StringIO.new(test_text)}), 'Put bucket multipart fail'
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_04b_store_object_multipart_stream
|
55
|
+
rd, wr = IO.pipe
|
56
|
+
producer = Thread.new(wr) do |out|
|
57
|
+
for i in 1..100000
|
58
|
+
out.write("Testing stream text #{i}\n")
|
59
|
+
end
|
60
|
+
out.close
|
61
|
+
end
|
62
|
+
assert @s3.store_object_multipart({:bucket => @bucket, :key => @key5, :data => rd , :part_size => (20*1024*1024)}), 'Put bucket multipart fail'
|
63
|
+
rd.close
|
64
|
+
end
|
65
|
+
|
44
66
|
def test_05_get_and_get_object
|
45
67
|
assert_raise(Rightscale::AwsError) { @s3.get(@bucket, 'undefined/key') }
|
46
68
|
data1 = @s3.get(@bucket, @key1)
|
@@ -74,10 +96,12 @@ class TestS3 < Test::Unit::TestCase
|
|
74
96
|
|
75
97
|
def test_08_keys
|
76
98
|
keys = @s3.list_bucket(@bucket).map{|b| b[:key]}
|
77
|
-
assert_equal keys.size,
|
99
|
+
assert_equal keys.size, 5, "There should be 5 keys"
|
78
100
|
assert(keys.include?(@key1))
|
79
101
|
assert(keys.include?(@key2))
|
80
102
|
assert(keys.include?(@key3))
|
103
|
+
assert(keys.include?(@key4))
|
104
|
+
assert(keys.include?(@key5))
|
81
105
|
end
|
82
106
|
|
83
107
|
def test_09_copy_key
|
@@ -144,6 +168,7 @@ class TestS3 < Test::Unit::TestCase
|
|
144
168
|
|
145
169
|
|
146
170
|
|
171
|
+
|
147
172
|
#---------------------------
|
148
173
|
# Rightscale::S3 classes
|
149
174
|
#---------------------------
|
@@ -408,7 +433,7 @@ class TestS3 < Test::Unit::TestCase
|
|
408
433
|
sleep 10
|
409
434
|
|
410
435
|
assert_equal({:enabled => true, :targetbucket => @bucket2, :targetprefix => "loggylogs/"}, bucket.logging_info)
|
411
|
-
|
436
|
+
|
412
437
|
assert bucket.disable_logging
|
413
438
|
|
414
439
|
# check 'Drop' method
|
@@ -457,14 +482,63 @@ class TestS3 < Test::Unit::TestCase
|
|
457
482
|
end
|
458
483
|
end
|
459
484
|
|
485
|
+
def test_44_delete_multiple
|
486
|
+
bucket = RightAws::S3::Bucket.create(@s, @bucket, true)
|
487
|
+
|
488
|
+
key1 = Rightscale::S3::Key.create(bucket, @key1)
|
489
|
+
key2 = Rightscale::S3::Key.create(bucket, @key2)
|
490
|
+
key3 = Rightscale::S3::Key.create(bucket, @key3)
|
491
|
+
|
492
|
+
assert @s3.put(@bucket, @key1, RIGHT_OBJECT_TEXT), 'Put bucket fail'
|
493
|
+
assert @s3.put(@bucket, @key2, RIGHT_OBJECT_TEXT), 'Put bucket fail'
|
494
|
+
assert @s3.put(@bucket, @key3, RIGHT_OBJECT_TEXT), 'Put bucket fail'
|
495
|
+
|
496
|
+
key1.refresh
|
497
|
+
key2.refresh
|
498
|
+
key3.refresh
|
499
|
+
|
500
|
+
assert key1.exists?
|
501
|
+
assert key2.exists?
|
502
|
+
assert key3.exists?
|
503
|
+
|
504
|
+
result = @s3.delete_multiple(@bucket, [@key1, @key2, @key3])
|
505
|
+
assert result.empty?
|
506
|
+
|
507
|
+
key1.refresh
|
508
|
+
key2.refresh
|
509
|
+
key3.refresh
|
510
|
+
|
511
|
+
assert !key1.exists?
|
512
|
+
assert !key2.exists?
|
513
|
+
assert !key3.exists?
|
514
|
+
end
|
515
|
+
|
516
|
+
def test_45_delete_multiple_more_than_1000_objects
|
517
|
+
n = 1200
|
518
|
+
keys = (1..n).map { |i| "key-#{i}"}
|
519
|
+
|
520
|
+
keys.each do |key|
|
521
|
+
assert @s3.put(@bucket, key, RIGHT_OBJECT_TEXT), 'Put bucket fail'
|
522
|
+
end
|
523
|
+
|
524
|
+
result = @s3.delete_multiple(@bucket, keys)
|
525
|
+
assert result.empty?
|
526
|
+
|
527
|
+
keys_after = @s3.list_bucket(@bucket).map { |obj| obj[:key] }
|
528
|
+
|
529
|
+
keys.each do |key|
|
530
|
+
assert !keys_after.include?(key)
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
460
534
|
private
|
461
535
|
|
462
536
|
def request( uri )
|
463
537
|
url = URI.parse( uri )
|
464
538
|
|
465
|
-
http = Net::HTTP.new(url.host,
|
466
|
-
http.use_ssl = true
|
467
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
539
|
+
http = Net::HTTP.new(url.host, 80)
|
540
|
+
# http.use_ssl = true
|
541
|
+
# http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
468
542
|
http.request(Net::HTTP::Get.new( url.request_uri ))
|
469
543
|
end
|
470
544
|
|
metadata
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: right_aws
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
prerelease:
|
4
|
+
prerelease: false
|
6
5
|
segments:
|
7
6
|
- 3
|
8
7
|
- 0
|
9
|
-
-
|
10
|
-
version: 3.0.
|
8
|
+
- 5
|
9
|
+
version: 3.0.5
|
11
10
|
platform: ruby
|
12
11
|
authors:
|
13
12
|
- RightScale, Inc.
|
@@ -15,17 +14,16 @@ autorequire:
|
|
15
14
|
bindir: bin
|
16
15
|
cert_chain: []
|
17
16
|
|
18
|
-
date:
|
17
|
+
date: 2013-03-05 00:00:00 -08:00
|
18
|
+
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: right_http_connection
|
22
22
|
prerelease: false
|
23
23
|
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
-
none: false
|
25
24
|
requirements:
|
26
25
|
- - ">="
|
27
26
|
- !ruby/object:Gem::Version
|
28
|
-
hash: 21
|
29
27
|
segments:
|
30
28
|
- 1
|
31
29
|
- 2
|
@@ -37,11 +35,9 @@ dependencies:
|
|
37
35
|
name: rake
|
38
36
|
prerelease: false
|
39
37
|
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
-
none: false
|
41
38
|
requirements:
|
42
39
|
- - ">="
|
43
40
|
- !ruby/object:Gem::Version
|
44
|
-
hash: 3
|
45
41
|
segments:
|
46
42
|
- 0
|
47
43
|
version: "0"
|
@@ -51,11 +47,9 @@ dependencies:
|
|
51
47
|
name: rcov
|
52
48
|
prerelease: false
|
53
49
|
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
-
none: false
|
55
50
|
requirements:
|
56
51
|
- - ">="
|
57
52
|
- !ruby/object:Gem::Version
|
58
|
-
hash: 3
|
59
53
|
segments:
|
60
54
|
- 0
|
61
55
|
version: "0"
|
@@ -186,6 +180,7 @@ files:
|
|
186
180
|
- test/sqs/test_right_sqs_gen2.rb
|
187
181
|
- test/test_credentials.rb
|
188
182
|
- test/ts_right_aws.rb
|
183
|
+
has_rdoc: true
|
189
184
|
homepage:
|
190
185
|
licenses: []
|
191
186
|
|
@@ -198,32 +193,28 @@ rdoc_options:
|
|
198
193
|
require_paths:
|
199
194
|
- lib
|
200
195
|
required_ruby_version: !ruby/object:Gem::Requirement
|
201
|
-
none: false
|
202
196
|
requirements:
|
203
197
|
- - ">="
|
204
198
|
- !ruby/object:Gem::Version
|
205
|
-
hash: 57
|
206
199
|
segments:
|
207
200
|
- 1
|
208
201
|
- 8
|
209
202
|
- 7
|
210
203
|
version: 1.8.7
|
211
204
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
212
|
-
none: false
|
213
205
|
requirements:
|
214
206
|
- - ">="
|
215
207
|
- !ruby/object:Gem::Version
|
216
|
-
hash: 3
|
217
208
|
segments:
|
218
209
|
- 0
|
219
210
|
version: "0"
|
220
211
|
requirements:
|
221
212
|
- libxml-ruby >= 0.5.2.0 is encouraged
|
222
213
|
rubyforge_project: rightaws
|
223
|
-
rubygems_version: 1.
|
214
|
+
rubygems_version: 1.3.6
|
224
215
|
signing_key:
|
225
216
|
specification_version: 3
|
226
|
-
summary:
|
217
|
+
summary: The RightScale AWS gems have been designed to provide a robust, fast, and secure interface to Amazon EC2, EBS, S3, SQS, SDB, and CloudFront.
|
227
218
|
test_files:
|
228
219
|
- test/acf/test_helper.rb
|
229
220
|
- test/acf/test_right_acf.rb
|