fog-aws 3.6.3 → 3.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +12 -0
- data/CHANGELOG.md +74 -2
- data/Gemfile +1 -1
- data/README.md +20 -0
- data/fog-aws.gemspec +1 -1
- data/lib/fog/aws.rb +1 -1
- data/lib/fog/aws/credential_fetcher.rb +59 -7
- data/lib/fog/aws/models/compute/flavors.rb +472 -32
- data/lib/fog/aws/models/compute/server.rb +4 -2
- data/lib/fog/aws/models/compute/servers.rb +2 -0
- data/lib/fog/aws/models/compute/vpc.rb +1 -1
- data/lib/fog/aws/models/storage/file.rb +124 -3
- data/lib/fog/aws/parsers/storage/upload_part_copy_object.rb +18 -0
- data/lib/fog/aws/requests/compute/request_spot_instances.rb +1 -1
- data/lib/fog/aws/requests/compute/run_instances.rb +20 -0
- data/lib/fog/aws/requests/compute/stop_instances.rb +11 -3
- data/lib/fog/aws/requests/storage/upload_part_copy.rb +92 -0
- data/lib/fog/aws/storage.rb +2 -1
- data/lib/fog/aws/version.rb +1 -1
- data/tests/credentials_tests.rb +50 -0
- data/tests/requests/storage/multipart_copy_tests.rb +80 -0
- metadata +10 -9
@@ -14,6 +14,7 @@ module Fog
|
|
14
14
|
attribute :associate_public_ip, :aliases => 'associatePublicIP'
|
15
15
|
attribute :availability_zone, :aliases => 'availabilityZone'
|
16
16
|
attribute :block_device_mapping, :aliases => 'blockDeviceMapping'
|
17
|
+
attribute :hibernation_options, :aliases => 'hibernationOptions'
|
17
18
|
attribute :network_interfaces, :aliases => 'networkInterfaces'
|
18
19
|
attribute :client_token, :aliases => 'clientToken'
|
19
20
|
attribute :disable_api_termination, :aliases => 'disableApiTermination'
|
@@ -144,6 +145,7 @@ module Fog
|
|
144
145
|
|
145
146
|
options = {
|
146
147
|
'BlockDeviceMapping' => block_device_mapping,
|
148
|
+
'HibernationOptions' => hibernation_options,
|
147
149
|
'NetworkInterfaces' => network_interfaces,
|
148
150
|
'ClientToken' => client_token,
|
149
151
|
'DisableApiTermination' => disable_api_termination,
|
@@ -227,9 +229,9 @@ module Fog
|
|
227
229
|
true
|
228
230
|
end
|
229
231
|
|
230
|
-
def stop(
|
232
|
+
def stop(options = {})
|
231
233
|
requires :id
|
232
|
-
service.stop_instances(id,
|
234
|
+
service.stop_instances(id, options)
|
233
235
|
true
|
234
236
|
end
|
235
237
|
|
@@ -22,6 +22,7 @@ module Fog
|
|
22
22
|
# ami_launch_index=nil,
|
23
23
|
# availability_zone=nil,
|
24
24
|
# block_device_mapping=nil,
|
25
|
+
# hibernation_options=nil,
|
25
26
|
# network_interfaces=nil,
|
26
27
|
# client_token=nil,
|
27
28
|
# dns_name=nil,
|
@@ -119,6 +120,7 @@ module Fog
|
|
119
120
|
# ami_launch_index=0,
|
120
121
|
# availability_zone="us-east-1b",
|
121
122
|
# block_device_mapping=[],
|
123
|
+
# hibernation_options=[],
|
122
124
|
# client_token=nil,
|
123
125
|
# dns_name="ec2-25-2-474-44.compute-1.amazonaws.com",
|
124
126
|
# groups=["default"],
|
@@ -4,6 +4,10 @@ module Fog
|
|
4
4
|
module AWS
|
5
5
|
class Storage
|
6
6
|
class File < Fog::Model
|
7
|
+
MIN_MULTIPART_CHUNK_SIZE = 5242880
|
8
|
+
MAX_SINGLE_PUT_SIZE = 5368709120
|
9
|
+
MULTIPART_COPY_THRESHOLD = 15728640
|
10
|
+
|
7
11
|
# @see AWS Object docs http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectOps.html
|
8
12
|
|
9
13
|
identity :key, :aliases => 'Key'
|
@@ -27,14 +31,54 @@ module Fog
|
|
27
31
|
attribute :kms_key_id, :aliases => 'x-amz-server-side-encryption-aws-kms-key-id'
|
28
32
|
attribute :tags, :aliases => 'x-amz-tagging'
|
29
33
|
|
34
|
+
UploadPartData = Struct.new(:part_number, :upload_options, :etag)
|
35
|
+
|
36
|
+
class PartList
|
37
|
+
def initialize(parts = [])
|
38
|
+
@parts = parts
|
39
|
+
@mutex = Mutex.new
|
40
|
+
end
|
41
|
+
|
42
|
+
def push(part)
|
43
|
+
@mutex.synchronize { @parts.push(part) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def shift
|
47
|
+
@mutex.synchronize { @parts.shift }
|
48
|
+
end
|
49
|
+
|
50
|
+
def clear!
|
51
|
+
@mutex.synchronize { @parts.clear }
|
52
|
+
end
|
53
|
+
|
54
|
+
def size
|
55
|
+
@mutex.synchronize { @parts.size }
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_a
|
59
|
+
@mutex.synchronize { @parts.dup }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
30
63
|
# @note Chunk size to use for multipart uploads.
|
31
64
|
# Use small chunk sizes to minimize memory. E.g. 5242880 = 5mb
|
32
65
|
attr_reader :multipart_chunk_size
|
33
66
|
def multipart_chunk_size=(mp_chunk_size)
|
34
|
-
raise ArgumentError.new("minimum multipart_chunk_size is
|
67
|
+
raise ArgumentError.new("minimum multipart_chunk_size is #{MIN_MULTIPART_CHUNK_SIZE}") if mp_chunk_size < MIN_MULTIPART_CHUNK_SIZE
|
35
68
|
@multipart_chunk_size = mp_chunk_size
|
36
69
|
end
|
37
70
|
|
71
|
+
# @note Number of threads used to copy files.
|
72
|
+
def concurrency=(concurrency)
|
73
|
+
raise ArgumentError.new('minimum concurrency is 1') if concurrency.to_i < 1
|
74
|
+
|
75
|
+
@concurrency = concurrency.to_i
|
76
|
+
end
|
77
|
+
|
78
|
+
def concurrency
|
79
|
+
@concurrency || 1
|
80
|
+
end
|
81
|
+
|
38
82
|
def acl
|
39
83
|
requires :directory, :key
|
40
84
|
service.get_object_acl(directory.key, key).body['AccessControlList']
|
@@ -99,7 +143,17 @@ module Fog
|
|
99
143
|
#
|
100
144
|
def copy(target_directory_key, target_file_key, options = {})
|
101
145
|
requires :directory, :key
|
102
|
-
|
146
|
+
|
147
|
+
# With a single PUT operation you can upload objects up to 5 GB in size. Automatically set MP for larger objects.
|
148
|
+
self.multipart_chunk_size = MIN_MULTIPART_CHUNK_SIZE * 2 if !multipart_chunk_size && self.content_length.to_i > MAX_SINGLE_PUT_SIZE
|
149
|
+
|
150
|
+
if multipart_chunk_size && self.content_length.to_i >= multipart_chunk_size
|
151
|
+
upload_part_options = options.merge({ 'x-amz-copy-source' => "#{directory.key}/#{key}" })
|
152
|
+
multipart_copy(options, upload_part_options, target_directory_key, target_file_key)
|
153
|
+
else
|
154
|
+
service.copy_object(directory.key, key, target_directory_key, target_file_key, options)
|
155
|
+
end
|
156
|
+
|
103
157
|
target_directory = service.directories.new(:key => target_directory_key)
|
104
158
|
target_directory.files.head(target_file_key)
|
105
159
|
end
|
@@ -214,7 +268,7 @@ module Fog
|
|
214
268
|
options.merge!(encryption_headers)
|
215
269
|
|
216
270
|
# With a single PUT operation you can upload objects up to 5 GB in size. Automatically set MP for larger objects.
|
217
|
-
self.multipart_chunk_size =
|
271
|
+
self.multipart_chunk_size = MIN_MULTIPART_CHUNK_SIZE if !multipart_chunk_size && Fog::Storage.get_body_size(body) > MAX_SINGLE_PUT_SIZE
|
218
272
|
|
219
273
|
if multipart_chunk_size && Fog::Storage.get_body_size(body) >= multipart_chunk_size && body.respond_to?(:read)
|
220
274
|
data = multipart_save(options)
|
@@ -294,6 +348,30 @@ module Fog
|
|
294
348
|
service.complete_multipart_upload(directory.key, key, upload_id, part_tags)
|
295
349
|
end
|
296
350
|
|
351
|
+
def multipart_copy(options, upload_part_options, target_directory_key, target_file_key)
|
352
|
+
# Initiate the upload
|
353
|
+
res = service.initiate_multipart_upload(target_directory_key, target_file_key, options)
|
354
|
+
upload_id = res.body["UploadId"]
|
355
|
+
|
356
|
+
# Store ETags of upload parts
|
357
|
+
part_tags = []
|
358
|
+
pending = PartList.new(create_part_list(upload_part_options))
|
359
|
+
thread_count = self.concurrency
|
360
|
+
completed = PartList.new
|
361
|
+
errors = upload_in_threads(target_directory_key, target_file_key, upload_id, pending, completed, thread_count)
|
362
|
+
|
363
|
+
raise error.first if errors.any?
|
364
|
+
|
365
|
+
part_tags = completed.to_a.sort_by { |part| part.part_number }.map(&:etag)
|
366
|
+
rescue => e
|
367
|
+
# Abort the upload & reraise
|
368
|
+
service.abort_multipart_upload(target_directory_key, target_file_key, upload_id) if upload_id
|
369
|
+
raise
|
370
|
+
else
|
371
|
+
# Complete the upload
|
372
|
+
service.complete_multipart_upload(target_directory_key, target_file_key, upload_id, part_tags)
|
373
|
+
end
|
374
|
+
|
297
375
|
def encryption_headers
|
298
376
|
if encryption && encryption_key
|
299
377
|
encryption_customer_key_headers
|
@@ -318,6 +396,49 @@ module Fog
|
|
318
396
|
'x-amz-server-side-encryption-customer-key-md5' => Base64.encode64(OpenSSL::Digest::MD5.digest(encryption_key.to_s)).chomp!
|
319
397
|
}
|
320
398
|
end
|
399
|
+
|
400
|
+
def create_part_list(upload_part_options)
|
401
|
+
current_pos = 0
|
402
|
+
count = 0
|
403
|
+
pending = []
|
404
|
+
|
405
|
+
while current_pos < self.content_length do
|
406
|
+
start_pos = current_pos
|
407
|
+
end_pos = [current_pos + self.multipart_chunk_size, self.content_length - 1].min
|
408
|
+
range = "bytes=#{start_pos}-#{end_pos}"
|
409
|
+
part_options = upload_part_options.dup
|
410
|
+
part_options['x-amz-copy-source-range'] = range
|
411
|
+
pending << UploadPartData.new(count + 1, part_options, nil)
|
412
|
+
count += 1
|
413
|
+
current_pos = end_pos + 1
|
414
|
+
end
|
415
|
+
|
416
|
+
pending
|
417
|
+
end
|
418
|
+
|
419
|
+
def upload_in_threads(target_directory_key, target_file_key, upload_id, pending, completed, thread_count)
|
420
|
+
threads = []
|
421
|
+
|
422
|
+
thread_count.times do
|
423
|
+
thread = Thread.new do
|
424
|
+
begin
|
425
|
+
while part = pending.shift
|
426
|
+
part_upload = service.upload_part_copy(target_directory_key, target_file_key, upload_id, part.part_number, part.upload_options)
|
427
|
+
part.etag = part_upload.body['ETag']
|
428
|
+
completed.push(part)
|
429
|
+
end
|
430
|
+
rescue => error
|
431
|
+
pending.clear!
|
432
|
+
error
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
thread.abort_on_exception = true
|
437
|
+
threads << thread
|
438
|
+
end
|
439
|
+
|
440
|
+
threads.map(&:value).compact
|
441
|
+
end
|
321
442
|
end
|
322
443
|
end
|
323
444
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Fog
|
2
|
+
module Parsers
|
3
|
+
module AWS
|
4
|
+
module Storage
|
5
|
+
class UploadPartCopyObject < Fog::Parsers::Base
|
6
|
+
def end_element(name)
|
7
|
+
case name
|
8
|
+
when 'ETag'
|
9
|
+
@response[name] = value.gsub('"', '')
|
10
|
+
when 'LastModified'
|
11
|
+
@response[name] = Time.parse(value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -113,7 +113,7 @@ module Fog
|
|
113
113
|
raise Fog::AWS::Compute::Error.new(message)
|
114
114
|
end
|
115
115
|
|
116
|
-
if !image_id.match(/^ami-[a-f0-9]{8}$/)
|
116
|
+
if !image_id.match(/^ami-[a-f0-9]{8,17}$/)
|
117
117
|
message = "The image id '[#{image_id}]' does not exist"
|
118
118
|
raise Fog::AWS::Compute::NotFound.new(message)
|
119
119
|
end
|
@@ -30,6 +30,8 @@ module Fog
|
|
30
30
|
# * 'Ebs.Encrypted'<~Boolean> - specifies whether or not the volume is to be encrypted unless snapshot is specified
|
31
31
|
# * 'Ebs.VolumeType'<~String> - Type of EBS volue. Valid options in ['standard', 'io1'] default is 'standard'.
|
32
32
|
# * 'Ebs.Iops'<~String> - The number of I/O operations per second (IOPS) that the volume supports. Required when VolumeType is 'io1'
|
33
|
+
# * 'HibernationOptions'<~Array>: array of hashes
|
34
|
+
# * 'Configured'<~Boolean> - specifies whether or not the instance is configued for hibernation. This parameter is valid only if the instance meets the hibernation prerequisites.
|
33
35
|
# * 'NetworkInterfaces'<~Array>: array of hashes
|
34
36
|
# * 'NetworkInterfaceId'<~String> - An existing interface to attach to a single instance
|
35
37
|
# * 'DeviceIndex'<~String> - The device index. Applies both to attaching an existing network interface and creating a network interface
|
@@ -75,6 +77,8 @@ module Fog
|
|
75
77
|
# * 'deviceName'<~String> - specifies how volume is exposed to instance
|
76
78
|
# * 'status'<~String> - status of attached volume
|
77
79
|
# * 'volumeId'<~String> - Id of attached volume
|
80
|
+
# * 'hibernationOptions'<~Array>
|
81
|
+
# * 'configured'<~Boolean> - whether or not the instance is enabled for hibernation
|
78
82
|
# * 'dnsName'<~String> - public dns name, blank until instance is running
|
79
83
|
# * 'imageId'<~String> - image id of ami used to launch instance
|
80
84
|
# * 'instanceId'<~String> - id of the instance
|
@@ -111,6 +115,13 @@ module Fog
|
|
111
115
|
end
|
112
116
|
end
|
113
117
|
end
|
118
|
+
if hibernation_options = options.delete('HibernationOptions')
|
119
|
+
hibernation_options.each_with_index do |mapping, index|
|
120
|
+
for key, value in mapping
|
121
|
+
options.merge!({ format("HibernationOptions.%d.#{key}", index) => value })
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
114
125
|
if security_groups = options.delete('SecurityGroup')
|
115
126
|
options.merge!(Fog::AWS.indexed_param('SecurityGroup', [*security_groups]))
|
116
127
|
end
|
@@ -182,6 +193,14 @@ module Fog
|
|
182
193
|
}
|
183
194
|
end
|
184
195
|
|
196
|
+
hibernation_options = (options['HibernationOptions'] || []).reduce([]) do |mapping, device|
|
197
|
+
configure = device.fetch("Configure", true)
|
198
|
+
|
199
|
+
mapping << {
|
200
|
+
"Configure" => configure,
|
201
|
+
}
|
202
|
+
end
|
203
|
+
|
185
204
|
if options['SubnetId']
|
186
205
|
if options['PrivateIpAddress']
|
187
206
|
ni_options = {'PrivateIpAddress' => options['PrivateIpAddress']}
|
@@ -221,6 +240,7 @@ module Fog
|
|
221
240
|
'associatePublicIP' => options['associatePublicIP'] || false,
|
222
241
|
'architecture' => 'i386',
|
223
242
|
'blockDeviceMapping' => block_device_mapping,
|
243
|
+
'hibernationOptions' => hibernation_options,
|
224
244
|
'networkInterfaces' => network_interfaces,
|
225
245
|
'clientToken' => options['clientToken'],
|
226
246
|
'dnsName' => nil,
|
@@ -16,9 +16,17 @@ module Fog
|
|
16
16
|
# * TODO: fill in the blanks
|
17
17
|
#
|
18
18
|
# {Amazon API Reference}[http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/ApiReference-query-StopInstances.html]
|
19
|
-
def stop_instances(instance_id,
|
19
|
+
def stop_instances(instance_id, options = {})
|
20
20
|
params = Fog::AWS.indexed_param('InstanceId', instance_id)
|
21
|
-
|
21
|
+
unless options.is_a?(Hash)
|
22
|
+
Fog::Logger.warning("stop_instances with #{options.class} param is deprecated, use stop_instances('force' => boolean) instead [light_black](#{caller.first})[/]")
|
23
|
+
options = {'force' => options}
|
24
|
+
end
|
25
|
+
params.merge!('Force' => 'true') if options['force']
|
26
|
+
if options['hibernate']
|
27
|
+
params.merge!('Hibernate' => 'true')
|
28
|
+
params.merge!('Force' => 'false')
|
29
|
+
end
|
22
30
|
request({
|
23
31
|
'Action' => 'StopInstances',
|
24
32
|
:idempotent => true,
|
@@ -28,7 +36,7 @@ module Fog
|
|
28
36
|
end
|
29
37
|
|
30
38
|
class Mock
|
31
|
-
def stop_instances(instance_id,
|
39
|
+
def stop_instances(instance_id, options = {})
|
32
40
|
instance_ids = Array(instance_id)
|
33
41
|
|
34
42
|
instance_set = self.data[:instances].values
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Fog
|
2
|
+
module AWS
|
3
|
+
class Storage
|
4
|
+
class Real
|
5
|
+
require 'fog/aws/parsers/storage/upload_part_copy_object'
|
6
|
+
|
7
|
+
# Upload a part for a multipart copy
|
8
|
+
#
|
9
|
+
# @param target_bucket_name [String] Name of bucket to create copy in
|
10
|
+
# @param target_object_name [String] Name for new copy of object
|
11
|
+
# @param upload_id [String] Id of upload to add part to
|
12
|
+
# @param part_number [String] Index of part in upload
|
13
|
+
# @param options [Hash]:
|
14
|
+
# @option options [String] x-amz-metadata-directive Specifies whether to copy metadata from source or replace with data in request. Must be in ['COPY', 'REPLACE']
|
15
|
+
# @option options [String] x-amz-copy_source-if-match Copies object if its etag matches this value
|
16
|
+
# @option options [Time] x-amz-copy_source-if-modified_since Copies object it it has been modified since this time
|
17
|
+
# @option options [String] x-amz-copy_source-if-none-match Copies object if its etag does not match this value
|
18
|
+
# @option options [Time] x-amz-copy_source-if-unmodified-since Copies object it it has not been modified since this time
|
19
|
+
# @option options [Time] x-amz-copy-source-range Specifes the range of bytes to copy from the source object
|
20
|
+
#
|
21
|
+
# @return [Excon::Response]
|
22
|
+
# * body [Hash]:
|
23
|
+
# * ETag [String] - etag of new object
|
24
|
+
# * LastModified [Time] - date object was last modified
|
25
|
+
#
|
26
|
+
# @see https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPartCopy.html
|
27
|
+
#
|
28
|
+
def upload_part_copy(target_bucket_name, target_object_name, upload_id, part_number, options = {})
|
29
|
+
headers = options
|
30
|
+
request({
|
31
|
+
:expects => 200,
|
32
|
+
:idempotent => true,
|
33
|
+
:headers => headers,
|
34
|
+
:bucket_name => target_bucket_name,
|
35
|
+
:object_name => target_object_name,
|
36
|
+
:method => 'PUT',
|
37
|
+
:query => {'uploadId' => upload_id, 'partNumber' => part_number},
|
38
|
+
:parser => Fog::Parsers::AWS::Storage::UploadPartCopyObject.new,
|
39
|
+
})
|
40
|
+
end
|
41
|
+
end # Real
|
42
|
+
|
43
|
+
class Mock # :nodoc:all
|
44
|
+
require 'fog/aws/requests/storage/shared_mock_methods'
|
45
|
+
include Fog::AWS::Storage::SharedMockMethods
|
46
|
+
|
47
|
+
def upload_part_copy(target_bucket_name, target_object_name, upload_id, part_number, options = {})
|
48
|
+
copy_source = options['x-amz-copy-source']
|
49
|
+
copy_range = options['x-amz-copy-source-range']
|
50
|
+
|
51
|
+
raise 'No x-amz-copy-source header provided' unless copy_source
|
52
|
+
raise 'No x-amz-copy-source-range header provided' unless copy_range
|
53
|
+
|
54
|
+
source_bucket_name, source_object_name = copy_source.split('/', 2)
|
55
|
+
verify_mock_bucket_exists(source_bucket_name)
|
56
|
+
|
57
|
+
source_bucket = self.data[:buckets][source_bucket_name]
|
58
|
+
source_object = source_bucket && source_bucket[:objects][source_object_name] && source_bucket[:objects][source_object_name].first
|
59
|
+
upload_info = get_upload_info(target_bucket_name, upload_id)
|
60
|
+
|
61
|
+
response = Excon::Response.new
|
62
|
+
|
63
|
+
if source_object
|
64
|
+
start_pos, end_pos = byte_range(copy_range, source_object[:body].length)
|
65
|
+
upload_info[:parts][part_number] = source_object[:body][start_pos..end_pos]
|
66
|
+
|
67
|
+
response.status = 200
|
68
|
+
response.body = {
|
69
|
+
# just use the part number as the ETag, for simplicity
|
70
|
+
'ETag' => part_number.to_i,
|
71
|
+
'LastModified' => Time.parse(source_object['Last-Modified'])
|
72
|
+
}
|
73
|
+
response
|
74
|
+
else
|
75
|
+
response.status = 404
|
76
|
+
raise(Excon::Errors.status_error({:expects => 200}, response))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def byte_range(range, size)
|
81
|
+
matches = range.match(/bytes=(\d*)-(\d*)/)
|
82
|
+
|
83
|
+
return nil unless matches
|
84
|
+
|
85
|
+
end_pos = [matches[2].to_i, size].min
|
86
|
+
|
87
|
+
[matches[1].to_i, end_pos]
|
88
|
+
end
|
89
|
+
end # Mock
|
90
|
+
end # Storage
|
91
|
+
end # AWS
|
92
|
+
end # Fog
|
data/lib/fog/aws/storage.rb
CHANGED
@@ -112,6 +112,7 @@ module Fog
|
|
112
112
|
request :put_request_payment
|
113
113
|
request :sync_clock
|
114
114
|
request :upload_part
|
115
|
+
request :upload_part_copy
|
115
116
|
|
116
117
|
module Utils
|
117
118
|
attr_accessor :region
|
@@ -227,7 +228,7 @@ module Fog
|
|
227
228
|
when %r{\Acn-.*}
|
228
229
|
"s3.#{region}.amazonaws.com.cn"
|
229
230
|
else
|
230
|
-
"s3
|
231
|
+
"s3.#{region}.amazonaws.com"
|
231
232
|
end
|
232
233
|
end
|
233
234
|
|