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.
@@ -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,17 @@ 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.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 = 5242880 if !multipart_chunk_size && Fog::Storage.get_body_size(body) > 5368709120
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, 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
@@ -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
@@ -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-#{region}.amazonaws.com"
231
+ "s3.#{region}.amazonaws.com"
231
232
  end
232
233
  end
233
234