fog-aws 3.6.4 → 3.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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(force = false)
232
+ def stop(options = {})
231
233
  requires :id
232
- service.stop_instances(id, force)
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"],
@@ -39,7 +39,7 @@ module Fog
39
39
  end
40
40
 
41
41
  def is_default?
42
- require :is_default
42
+ requires :is_default
43
43
  is_default
44
44
  end
45
45
 
@@ -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 5242880") if mp_chunk_size < 5242880
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,18 @@ module Fog
99
143
  #
100
144
  def copy(target_directory_key, target_file_key, options = {})
101
145
  requires :directory, :key
102
- service.copy_object(directory.key, key, target_directory_key, target_file_key, options)
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.select { |key, _| ALLOWED_UPLOAD_PART_OPTIONS.include?(key.to_sym) }
152
+ upload_part_options = upload_part_options.merge({ 'x-amz-copy-source' => "#{directory.key}/#{key}" })
153
+ multipart_copy(options, upload_part_options, target_directory_key, target_file_key)
154
+ else
155
+ service.copy_object(directory.key, key, target_directory_key, target_file_key, options)
156
+ end
157
+
103
158
  target_directory = service.directories.new(:key => target_directory_key)
104
159
  target_directory.files.head(target_file_key)
105
160
  end
@@ -214,7 +269,7 @@ module Fog
214
269
  options.merge!(encryption_headers)
215
270
 
216
271
  # 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 = 5242880 if !multipart_chunk_size && Fog::Storage.get_body_size(body) > 5368709120
272
+ self.multipart_chunk_size = MIN_MULTIPART_CHUNK_SIZE if !multipart_chunk_size && Fog::Storage.get_body_size(body) > MAX_SINGLE_PUT_SIZE
218
273
 
219
274
  if multipart_chunk_size && Fog::Storage.get_body_size(body) >= multipart_chunk_size && body.respond_to?(:read)
220
275
  data = multipart_save(options)
@@ -294,6 +349,30 @@ module Fog
294
349
  service.complete_multipart_upload(directory.key, key, upload_id, part_tags)
295
350
  end
296
351
 
352
+ def multipart_copy(options, upload_part_options, target_directory_key, target_file_key)
353
+ # Initiate the upload
354
+ res = service.initiate_multipart_upload(target_directory_key, target_file_key, options)
355
+ upload_id = res.body["UploadId"]
356
+
357
+ # Store ETags of upload parts
358
+ part_tags = []
359
+ pending = PartList.new(create_part_list(upload_part_options))
360
+ thread_count = self.concurrency
361
+ completed = PartList.new
362
+ errors = upload_in_threads(target_directory_key, target_file_key, upload_id, pending, completed, thread_count)
363
+
364
+ raise errors.first if errors.any?
365
+
366
+ part_tags = completed.to_a.sort_by { |part| part.part_number }.map(&:etag)
367
+ rescue => e
368
+ # Abort the upload & reraise
369
+ service.abort_multipart_upload(target_directory_key, target_file_key, upload_id) if upload_id
370
+ raise
371
+ else
372
+ # Complete the upload
373
+ service.complete_multipart_upload(target_directory_key, target_file_key, upload_id, part_tags)
374
+ end
375
+
297
376
  def encryption_headers
298
377
  if encryption && encryption_key
299
378
  encryption_customer_key_headers
@@ -318,6 +397,49 @@ module Fog
318
397
  'x-amz-server-side-encryption-customer-key-md5' => Base64.encode64(OpenSSL::Digest::MD5.digest(encryption_key.to_s)).chomp!
319
398
  }
320
399
  end
400
+
401
+ def create_part_list(upload_part_options)
402
+ current_pos = 0
403
+ count = 0
404
+ pending = []
405
+
406
+ while current_pos < self.content_length do
407
+ start_pos = current_pos
408
+ end_pos = [current_pos + self.multipart_chunk_size, self.content_length - 1].min
409
+ range = "bytes=#{start_pos}-#{end_pos}"
410
+ part_options = upload_part_options.dup
411
+ part_options['x-amz-copy-source-range'] = range
412
+ pending << UploadPartData.new(count + 1, part_options, nil)
413
+ count += 1
414
+ current_pos = end_pos + 1
415
+ end
416
+
417
+ pending
418
+ end
419
+
420
+ def upload_in_threads(target_directory_key, target_file_key, upload_id, pending, completed, thread_count)
421
+ threads = []
422
+
423
+ thread_count.times do
424
+ thread = Thread.new do
425
+ begin
426
+ while part = pending.shift
427
+ part_upload = service.upload_part_copy(target_directory_key, target_file_key, upload_id, part.part_number, part.upload_options)
428
+ part.etag = part_upload.body['ETag']
429
+ completed.push(part)
430
+ end
431
+ rescue => error
432
+ pending.clear!
433
+ error
434
+ end
435
+ end
436
+
437
+ thread.abort_on_exception = true
438
+ threads << thread
439
+ end
440
+
441
+ threads.map(&:value).compact
442
+ end
321
443
  end
322
444
  end
323
445
  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, force = false)
19
+ def stop_instances(instance_id, options = {})
20
20
  params = Fog::AWS.indexed_param('InstanceId', instance_id)
21
- params.merge!('Force' => 'true') if force
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, force = false)
39
+ def stop_instances(instance_id, options = {})
32
40
  instance_ids = Array(instance_id)
33
41
 
34
42
  instance_set = self.data[:instances].values
@@ -15,6 +15,7 @@ module Fog
15
15
  def parse_mock_data(data)
16
16
  data = Fog::Storage.parse_data(data)
17
17
  unless data[:body].is_a?(String)
18
+ data[:body].rewind if data[:body].eof?
18
19
  data[:body] = data[:body].read
19
20
  end
20
21
  data
@@ -0,0 +1,119 @@
1
+ module Fog
2
+ module AWS
3
+ class Storage
4
+ # From https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPartCopy.html
5
+ ALLOWED_UPLOAD_PART_OPTIONS = %i(
6
+ x-amz-copy-source
7
+ x-amz-copy-source-if-match
8
+ x-amz-copy-source-if-modified-since
9
+ x-amz-copy-source-if-none-match
10
+ x-amz-copy-source-if-unmodified-since
11
+ x-amz-copy-source-range
12
+ x-amz-copy-source-server-side-encryption-customer-algorithm
13
+ x-amz-copy-source-server-side-encryption-customer-key
14
+ x-amz-copy-source-server-side-encryption-customer-key-MD5
15
+ x-amz-expected-bucket-owner
16
+ x-amz-request-payer
17
+ x-amz-server-side-encryption-customer-algorithm
18
+ x-amz-server-side-encryption-customer-key
19
+ x-amz-server-side-encryption-customer-key-MD5
20
+ x-amz-source-expected-bucket-owner
21
+ ).freeze
22
+
23
+ class Real
24
+ require 'fog/aws/parsers/storage/upload_part_copy_object'
25
+
26
+ # Upload a part for a multipart copy
27
+ #
28
+ # @param target_bucket_name [String] Name of bucket to create copy in
29
+ # @param target_object_name [String] Name for new copy of object
30
+ # @param upload_id [String] Id of upload to add part to
31
+ # @param part_number [String] Index of part in upload
32
+ # @param options [Hash]:
33
+ # @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']
34
+ # @option options [String] x-amz-copy_source-if-match Copies object if its etag matches this value
35
+ # @option options [Time] x-amz-copy_source-if-modified_since Copies object it it has been modified since this time
36
+ # @option options [String] x-amz-copy_source-if-none-match Copies object if its etag does not match this value
37
+ # @option options [Time] x-amz-copy_source-if-unmodified-since Copies object it it has not been modified since this time
38
+ # @option options [Time] x-amz-copy-source-range Specifes the range of bytes to copy from the source object
39
+ #
40
+ # @return [Excon::Response]
41
+ # * body [Hash]:
42
+ # * ETag [String] - etag of new object
43
+ # * LastModified [Time] - date object was last modified
44
+ #
45
+ # @see https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPartCopy.html
46
+ #
47
+ def upload_part_copy(target_bucket_name, target_object_name, upload_id, part_number, options = {})
48
+ headers = options
49
+ request({
50
+ :expects => 200,
51
+ :idempotent => true,
52
+ :headers => headers,
53
+ :bucket_name => target_bucket_name,
54
+ :object_name => target_object_name,
55
+ :method => 'PUT',
56
+ :query => {'uploadId' => upload_id, 'partNumber' => part_number},
57
+ :parser => Fog::Parsers::AWS::Storage::UploadPartCopyObject.new,
58
+ })
59
+ end
60
+ end # Real
61
+
62
+ class Mock # :nodoc:all
63
+ require 'fog/aws/requests/storage/shared_mock_methods'
64
+ include Fog::AWS::Storage::SharedMockMethods
65
+
66
+ def upload_part_copy(target_bucket_name, target_object_name, upload_id, part_number, options = {})
67
+ validate_options!(options)
68
+
69
+ copy_source = options['x-amz-copy-source']
70
+ copy_range = options['x-amz-copy-source-range']
71
+
72
+ raise 'No x-amz-copy-source header provided' unless copy_source
73
+ raise 'No x-amz-copy-source-range header provided' unless copy_range
74
+
75
+ source_bucket_name, source_object_name = copy_source.split('/', 2)
76
+ verify_mock_bucket_exists(source_bucket_name)
77
+
78
+ source_bucket = self.data[:buckets][source_bucket_name]
79
+ source_object = source_bucket && source_bucket[:objects][source_object_name] && source_bucket[:objects][source_object_name].first
80
+ upload_info = get_upload_info(target_bucket_name, upload_id)
81
+
82
+ response = Excon::Response.new
83
+
84
+ if source_object
85
+ start_pos, end_pos = byte_range(copy_range, source_object[:body].length)
86
+ upload_info[:parts][part_number] = source_object[:body][start_pos..end_pos]
87
+
88
+ response.status = 200
89
+ response.body = {
90
+ # just use the part number as the ETag, for simplicity
91
+ 'ETag' => part_number.to_i,
92
+ 'LastModified' => Time.parse(source_object['Last-Modified'])
93
+ }
94
+ response
95
+ else
96
+ response.status = 404
97
+ raise(Excon::Errors.status_error({:expects => 200}, response))
98
+ end
99
+ end
100
+
101
+ def byte_range(range, size)
102
+ matches = range.match(/bytes=(\d*)-(\d*)/)
103
+
104
+ return nil unless matches
105
+
106
+ end_pos = [matches[2].to_i, size].min
107
+
108
+ [matches[1].to_i, end_pos]
109
+ end
110
+
111
+ def validate_options!(options)
112
+ options.keys.each do |key|
113
+ raise "Invalid UploadPart option: #{key}" unless ::Fog::AWS::Storage::ALLOWED_UPLOAD_PART_OPTIONS.include?(key.to_sym)
114
+ end
115
+ end
116
+ end # Mock
117
+ end # Storage
118
+ end # AWS
119
+ end # Fog