bosh_aws_cpi 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # BOSH AWS Cloud Provider Interface
2
+ Copyright (c) 2009-2012 VMware, Inc.
3
+
4
+ For online documentation see: http://rubydoc.info/gems/bosh_aws_cpi/
5
+
6
+ ## Options
7
+
8
+ These options are passed to the AWS CPI when it is instantiated.
9
+
10
+ ### AWS options
11
+
12
+ * `access_key_id` (required)
13
+ AWS IAM user access key
14
+ * `secret_access_key` (required)
15
+ AWS IAM secret access key
16
+ * `default_key_name` (required)
17
+ default AWS ssh key name to assign to created virtual machines
18
+ * `default_security_group` (required)
19
+ default AWS security group to assign to created virtual machines
20
+ * `ec2_private_key` (required)
21
+ local path to the ssh private key, must match `default_key_name`
22
+ * `region` (optional)
23
+ EC2 region, defaults to `us-east-1`
24
+ * `ec2_endpoint` (optional)
25
+ URL of the EC2 endpoint to connect to, defaults to the endpoint corresponding to the selected region,
26
+ or `DEFAULT_EC2_ENDPOINT` if no region has been selected
27
+ * `max_retries` (optional)
28
+ maximum number of time to retry an AWS API call, defaults to `DEFAULT_MAX_RETRIES`
29
+
30
+ ### Registry options
31
+
32
+ The registry options are passed to the AWS CPI by the BOSH director based on the settings in `director.yml`, but can be
33
+ overridden if needed.
34
+
35
+ * `endpoint` (required)
36
+ registry URL
37
+ * `user` (required)
38
+ registry user
39
+ * `password` (required)
40
+ registry password
41
+
42
+ ### Agent options
43
+
44
+ Agent options are passed to the AWS CPI by the BOSH director based on the settings in `director.yml`, but can be
45
+ overridden if needed.
46
+
47
+ ### Resource pool options
48
+
49
+ These options are specified under `cloud_options` in the `resource_pools` section of a BOSH deployment manifest.
50
+
51
+ * `availability_zone` (optional)
52
+ the EC2 availability zone the VMs should be created in
53
+ * `instance_type` (required)
54
+ which [type of instance](http://aws.amazon.com/ec2/instance-types/) the VMs should belong to
55
+
56
+ ### Network options
57
+
58
+ These options are specified under `cloud_options` in the `networks` section of a BOSH deployment manifest.
59
+
60
+ * `type` (required)
61
+ can be either `dynamic` for a DHCP assigned IP by AWS, or `vip` to use an Elastic IP (which needs to be already
62
+ allocated)
63
+
64
+ ## Example
65
+
66
+ This is a sample of how AWS specific properties are used in a BOSH deployment manifest:
67
+
68
+ ---
69
+ name: sample
70
+ director_uuid: 38ce80c3-e9e9-4aac-ba61-97c676631b91
71
+
72
+ ...
73
+
74
+ networks:
75
+ - name: nginx_network
76
+ type: vip
77
+ cloud_properties: {}
78
+ - name: default
79
+ type: dynamic
80
+ cloud_properties:
81
+ security_groups:
82
+ - default
83
+
84
+ ...
85
+
86
+ resource_pools:
87
+ - name: common
88
+ network: default
89
+ size: 3
90
+ stemcell:
91
+ name: bosh-stemcell
92
+ version: 0.6.7
93
+ cloud_properties:
94
+ instance_type: m1.small
95
+
96
+ ...
97
+
98
+ properties:
99
+ aws:
100
+ access_key_id: AKIAIYJWVDUP4KRWBESQ
101
+ secret_access_key: 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
102
+ default_key_name: bosh
103
+ default_security_groups: ["bosh"]
104
+ ec2_private_key: /home/bosh/.ssh/bosh
@@ -0,0 +1,68 @@
1
+ module Bosh::AwsCloud
2
+ class AKIPicker
3
+
4
+ # @param [AWS::Core::ServiceInterface] ec2
5
+ def initialize(ec2)
6
+ @ec2 = ec2
7
+ end
8
+
9
+ # finds the correct aki for the current region
10
+ # @param [String] architecture instruction architecture to find
11
+ # @param [String] root_device_name
12
+ # @return [String] EC2 image id
13
+ def pick(architecture, root_device_name)
14
+ candidate = pick_candidate(fetch_akis(architecture), root_device_name)
15
+ raise Bosh::Clouds::CloudError, "unable to find AKI" unless candidate
16
+ logger.info("auto-selected AKI: #{candidate.image_id}")
17
+
18
+ candidate.image_id
19
+ end
20
+
21
+ private
22
+
23
+ def fetch_akis(architecture)
24
+ filter = aki_filter(architecture)
25
+ @ec2.client.describe_images(:filters => filter).images_set
26
+ end
27
+
28
+ # @return [Hash] search filter
29
+ def aki_filter(architecture)
30
+ [
31
+ {:name => "architecture", :values => [architecture]},
32
+ {:name => "image-type", :values => %w[kernel]},
33
+ {:name => "owner-alias", :values => %w[amazon]}
34
+ ]
35
+ end
36
+
37
+ def regexp(root_device_name)
38
+ # do nasty hackery to select boot device and version from
39
+ # the image_location string e.g. pv-grub-hd00_1.03-x86_64.gz
40
+ if root_device_name == "/dev/sda1"
41
+ /-hd00[-_](\d+)\.(\d+)/
42
+ else
43
+ /-hd0[-_](\d+)\.(\d+)/
44
+ end
45
+ end
46
+
47
+ # @param [AWS::EC2::ImageCollection] akis
48
+ def pick_candidate(akis, root_device_name)
49
+ candidate = nil
50
+ major = 0
51
+ minor = 0
52
+ akis.each do |image|
53
+ match = image.image_location.match(regexp(root_device_name))
54
+ if match && match[1].to_i > major && match[2].to_i > minor
55
+ candidate = image
56
+ major = match[1].to_i
57
+ minor = match[2].to_i
58
+ end
59
+ end
60
+
61
+ candidate
62
+ end
63
+
64
+ def logger
65
+ Bosh::Clouds::Config.logger
66
+ end
67
+ end
68
+ end
@@ -5,20 +5,26 @@ module Bosh::AwsCloud
5
5
  class Cloud < Bosh::Cloud
6
6
  include Helpers
7
7
 
8
+ # default maximum number of times to retry an AWS API call
8
9
  DEFAULT_MAX_RETRIES = 2
10
+ # default availability zone for instances and disks
9
11
  DEFAULT_AVAILABILITY_ZONE = "us-east-1a"
10
12
  DEFAULT_EC2_ENDPOINT = "ec2.amazonaws.com"
11
- METADATA_TIMEOUT = 5 # seconds
12
- DEVICE_POLL_TIMEOUT = 60 # seconds
13
+ METADATA_TIMEOUT = 5 # in seconds
14
+ DEVICE_POLL_TIMEOUT = 60 # in seconds
15
+ MAX_TAG_KEY_LENGTH = 127
16
+ MAX_TAG_VALUE_LENGTH = 255
13
17
 
14
18
  attr_reader :ec2
15
19
  attr_reader :registry
16
20
  attr_accessor :logger
17
21
 
18
22
  ##
19
- # Initialize BOSH AWS CPI
23
+ # Initialize BOSH AWS CPI. The contents of sub-hashes are defined in the {file:README.md}
20
24
  # @param [Hash] options CPI options
21
- #
25
+ # @option options [Hash] aws AWS specific options
26
+ # @option options [Hash] agent agent options
27
+ # @option options [Hash] registry agent options
22
28
  def initialize(options)
23
29
  @options = options.dup
24
30
 
@@ -30,6 +36,7 @@ module Bosh::AwsCloud
30
36
 
31
37
  @agent_properties = @options["agent"] || {}
32
38
  @aws_properties = @options["aws"]
39
+ @aws_region = @aws_properties.delete("region")
33
40
  @registry_properties = @options["registry"]
34
41
 
35
42
  @default_key_name = @aws_properties["default_key_name"]
@@ -38,7 +45,7 @@ module Bosh::AwsCloud
38
45
  aws_params = {
39
46
  :access_key_id => @aws_properties["access_key_id"],
40
47
  :secret_access_key => @aws_properties["secret_access_key"],
41
- :ec2_endpoint => @aws_properties["ec2_endpoint"] || DEFAULT_EC2_ENDPOINT,
48
+ :ec2_endpoint => @aws_properties["ec2_endpoint"] || default_ec2_endpoint,
42
49
  :max_retries => @aws_properties["max_retries"] || DEFAULT_MAX_RETRIES,
43
50
  :logger => @aws_logger
44
51
  }
@@ -59,37 +66,31 @@ module Bosh::AwsCloud
59
66
  registry_user,
60
67
  registry_password)
61
68
 
69
+ @aki_picker = AKIPicker.new(@ec2)
62
70
  @metadata_lock = Mutex.new
63
71
  end
64
72
 
65
73
  ##
66
- # Creates EC2 instance and waits until it's in running state
67
- # @param [String] agent_id Agent id associated with new VM
68
- # @param [String] stemcell_id AMI id that will be used
69
- # to power on new instance
70
- # @param [Hash] resource_pool Resource pool specification
71
- # @param [Hash] network_spec Network specification, if it contains
72
- # security groups they must be existing
73
- # @param [optional, Array] disk_locality List of disks that
74
+ # Create an EC2 instance and wait until it's in running state
75
+ # @param [String] agent_id agent id associated with new VM
76
+ # @param [String] stemcell_id AMI id of the stemcell used to
77
+ # create the new instance
78
+ # @param [Hash] resource_pool resource pool specification
79
+ # @param [Hash] network_spec network specification, if it contains
80
+ # security groups they must already exist
81
+ # @param [optional, Array] disk_locality list of disks that
74
82
  # might be attached to this instance in the future, can be
75
83
  # used as a placement hint (i.e. instance will only be created
76
84
  # if resource pool availability zone is the same as disk
77
85
  # availability zone)
78
- # @param [optional, Hash] environment Data to be merged into
86
+ # @param [optional, Hash] environment data to be merged into
79
87
  # agent settings
80
- #
81
- # @return [String] created instance id
88
+ # @return [String] EC2 instance id of the new virtual machine
82
89
  def create_vm(agent_id, stemcell_id, resource_pool,
83
90
  network_spec, disk_locality = nil, environment = nil)
84
91
  with_thread_name("create_vm(#{agent_id}, ...)") do
85
92
  network_configurator = NetworkConfigurator.new(network_spec)
86
93
 
87
- user_data = {
88
- "registry" => {
89
- "endpoint" => @registry.endpoint
90
- }
91
- }
92
-
93
94
  security_groups =
94
95
  network_configurator.security_groups(@default_security_groups)
95
96
  @logger.debug("using security groups: #{security_groups.join(', ')}")
@@ -107,7 +108,7 @@ module Bosh::AwsCloud
107
108
  :key_name => resource_pool["key_name"] || @default_key_name,
108
109
  :security_groups => security_groups,
109
110
  :instance_type => resource_pool["instance_type"],
110
- :user_data => Yajl::Encoder.encode(user_data)
111
+ :user_data => Yajl::Encoder.encode(user_data(network_spec))
111
112
  }
112
113
 
113
114
  instance_params[:availability_zone] =
@@ -131,8 +132,9 @@ module Bosh::AwsCloud
131
132
  end
132
133
 
133
134
  ##
134
- # Terminates EC2 instance and waits until it reports as terminated
135
- # @param [String] instance_id Running instance id
135
+ # Delete EC2 instance ("terminate" in AWS language) and wait until
136
+ # it reports as terminated
137
+ # @param [String] instance_id EC2 instance id
136
138
  def delete_vm(instance_id)
137
139
  with_thread_name("delete_vm(#{instance_id})") do
138
140
  instance = @ec2.instances[instance_id]
@@ -153,8 +155,8 @@ module Bosh::AwsCloud
153
155
  end
154
156
 
155
157
  ##
156
- # Reboots EC2 instance
157
- # @param [String] instance_id Running instance id
158
+ # Reboot EC2 instance
159
+ # @param [String] instance_id EC2 instance id
158
160
  def reboot_vm(instance_id)
159
161
  with_thread_name("reboot_vm(#{instance_id})") do
160
162
  instance = @ec2.instances[instance_id]
@@ -165,7 +167,7 @@ module Bosh::AwsCloud
165
167
  ##
166
168
  # Creates a new EBS volume
167
169
  # @param [Integer] size disk size in MiB
168
- # @param [optional, String] instance_id vm id
170
+ # @param [optional, String] instance_id EC2 instance id
169
171
  # of the VM that this disk will be attached to
170
172
  # @return [String] created EBS volume id
171
173
  def create_disk(size, instance_id = nil)
@@ -182,11 +184,13 @@ module Bosh::AwsCloud
182
184
  cloud_error("AWS CPI maximum disk size is 1 TiB")
183
185
  end
184
186
 
187
+ # if the disk is created for an instance, use the same availability
188
+ # zone as they must match
185
189
  if instance_id
186
190
  instance = @ec2.instances[instance_id]
187
191
  availability_zone = instance.availability_zone
188
192
  else
189
- availability_zone = DEFAULT_AVAILABILITY_ZONE
193
+ availability_zone = default_availability_zone
190
194
  end
191
195
 
192
196
  volume_params = {
@@ -203,10 +207,9 @@ module Bosh::AwsCloud
203
207
  end
204
208
 
205
209
  ##
206
- # Deletes EBS volume
207
- # @param [String] disk_id volume id
210
+ # Delete EBS volume
211
+ # @param [String] disk_id EBS volume id
208
212
  # @raise [Bosh::Clouds::CloudError] if disk is not in available state
209
- # @return nil
210
213
  def delete_disk(disk_id)
211
214
  with_thread_name("delete_disk(#{disk_id})") do
212
215
  volume = @ec2.volumes[disk_id]
@@ -229,6 +232,9 @@ module Bosh::AwsCloud
229
232
  end
230
233
  end
231
234
 
235
+ # Attach an EBS volume to an EC2 instance
236
+ # @param [String] instance_id EC2 instance id of the virtual machine to attach the disk to
237
+ # @param [String] disk_id EBS volume id of the disk to attach
232
238
  def attach_disk(instance_id, disk_id)
233
239
  with_thread_name("attach_disk(#{instance_id}, #{disk_id})") do
234
240
  instance = @ec2.instances[instance_id]
@@ -241,9 +247,13 @@ module Bosh::AwsCloud
241
247
  settings["disks"]["persistent"] ||= {}
242
248
  settings["disks"]["persistent"][disk_id] = device_name
243
249
  end
250
+ @logger.info("Attached `#{disk_id}' to `#{instance_id}'")
244
251
  end
245
252
  end
246
253
 
254
+ # Detach an EBS volume from an EC2 instance
255
+ # @param [String] instance_id EC2 instance id of the virtual machine to detach the disk from
256
+ # @param [String] disk_id EBS volume id of the disk to detach
247
257
  def detach_disk(instance_id, disk_id)
248
258
  with_thread_name("detach_disk(#{instance_id}, #{disk_id})") do
249
259
  instance = @ec2.instances[instance_id]
@@ -261,10 +271,10 @@ module Bosh::AwsCloud
261
271
  end
262
272
  end
263
273
 
264
- # Configures network for a running instance
265
- # @param [String] instance_id instance identifier
274
+ # Configure network for an EC2 instance
275
+ # @param [String] instance_id EC2 instance id
266
276
  # @param [Hash] network_spec network properties
267
- # @raises [Bosh::Clouds:NotSupported] if the security groups change
277
+ # @raise [Bosh::Clouds:NotSupported] if the security groups change
268
278
  def configure_networks(instance_id, network_spec)
269
279
  with_thread_name("configure_networks(#{instance_id}, ...)") do
270
280
  @logger.info("Configuring `#{instance_id}' to use the following " \
@@ -294,24 +304,26 @@ module Bosh::AwsCloud
294
304
  end
295
305
 
296
306
  ##
297
- # Creates a new AMI using stemcell image.
307
+ # Creates a new EC2 AMI using stemcell image.
298
308
  # This method can only be run on an EC2 instance, as image creation
299
309
  # involves creating and mounting new EBS volume as local block device.
300
310
  # @param [String] image_path local filesystem path to a stemcell image
301
- # @param [Hash] cloud_properties CPI-specific properties
311
+ # @param [Hash] cloud_properties AWS-specific stemcell properties
302
312
  # @option cloud_properties [String] kernel_id
303
- # AKI, auto-selected based on the region
313
+ # AKI, auto-selected based on the region, unless specified
304
314
  # @option cloud_properties [String] root_device_name
305
- # provided by the stemcell manifest
315
+ # block device path (e.g. /dev/sda1), provided by the stemcell manifest, unless specified
306
316
  # @option cloud_properties [String] architecture
307
- # provided by the stemcell manifest
317
+ # instruction set architecture (e.g. x86_64), provided by the stemcell manifest,
318
+ # unless specified
308
319
  # @option cloud_properties [String] disk (2048)
309
320
  # root disk size
321
+ # @return [String] EC2 AMI name of the stemcell
310
322
  def create_stemcell(image_path, cloud_properties)
311
323
  # TODO: refactor into several smaller methods
312
324
  with_thread_name("create_stemcell(#{image_path}...)") do
313
325
  begin
314
- # These two variables are used in 'ensure' clause
326
+ # These three variables are used in 'ensure' clause
315
327
  instance = nil
316
328
  volume = nil
317
329
  # 1. Create and mount new EBS volume (2GB default)
@@ -331,31 +343,14 @@ module Bosh::AwsCloud
331
343
  snapshot = volume.create_snapshot
332
344
  wait_resource(snapshot, :completed)
333
345
 
334
- root_device_name = cloud_properties["root_device_name"]
335
- architecture = cloud_properties["architecture"]
336
-
337
- aki = find_aki(architecture, root_device_name)
338
-
339
- # we could set :description here, but since we don't have a
340
- # handle to the stemcell name and version, we can't set it
341
- # to something useful :(
342
- image_params = {
343
- :name => "BOSH-#{generate_unique_name}",
344
- :architecture => architecture,
345
- :kernel_id => aki,
346
- :root_device_name => root_device_name,
347
- :block_device_mappings => {
348
- "/dev/sda" => { :snapshot_id => snapshot.id },
349
- "/dev/sdb" => "ephemeral0"
350
- }
351
- }
352
-
353
- image = @ec2.images.create(image_params)
346
+ params = image_params(cloud_properties, snapshot.id)
347
+ image = @ec2.images.create(params)
354
348
  wait_resource(image, :available, :state)
355
349
 
350
+ tag(image, "Name", params[:description]) if params[:description]
351
+
356
352
  image.id
357
353
  rescue => e
358
- # TODO: delete snapshot?
359
354
  @logger.error(e)
360
355
  raise e
361
356
  ensure
@@ -367,49 +362,53 @@ module Bosh::AwsCloud
367
362
  end
368
363
  end
369
364
 
370
- # finds the correct aki for the current region
371
- def find_aki(arch, root_device_name)
372
-
373
- filters = []
374
- filters << {:name => "architecture", :values => [arch]}
375
- filters << {:name => "image-type", :values => %w[kernel]}
376
- filters << {:name => "owner-alias", :values => %w[amazon]}
365
+ # Delete a stemcell and the accompanying snapshots
366
+ # @param [String] stemcell_id EC2 AMI name of the stemcell to be deleted
367
+ def delete_stemcell(stemcell_id)
368
+ with_thread_name("delete_stemcell(#{stemcell_id})") do
369
+ snapshots = []
370
+ image = @ec2.images[stemcell_id]
377
371
 
378
- response = @ec2.client.describe_images(:filters => filters)
372
+ image.block_device_mappings.each do |device, map|
373
+ id = map[:snapshot_id]
374
+ if id
375
+ @logger.debug("queuing snapshot #{id} for deletion")
376
+ snapshots << id
377
+ end
378
+ end
379
379
 
380
- # do nasty hackery to select boot device and version from
381
- # the image_location string e.g. pv-grub-hd00_1.03-x86_64.gz
382
- if root_device_name == "/dev/sda1"
383
- regexp = /-hd00[-_](\d+)\.(\d+)/
384
- else
385
- regexp = /-hd0[-_](\d+)\.(\d+)/
386
- end
380
+ image.deregister
387
381
 
388
- candidate = nil
389
- major = 0
390
- minor = 0
391
- response.images_set.each do |image|
392
- match = image.image_location.match(regexp)
393
- if match && match[1].to_i > major && match[2].to_i > minor
394
- candidate = image
395
- major = match[1].to_i
396
- minor = match[2].to_i
382
+ snapshots.each do |id|
383
+ @logger.info("cleaning up snapshot #{id}")
384
+ snapshot = @ec2.snapshots[id]
385
+ snapshot.delete
397
386
  end
398
387
  end
399
-
400
- cloud_error("unable to find AKI") unless candidate
401
- @logger.info("auto-selected AKI: #{candidate.image_id}")
402
-
403
- candidate.image_id
404
388
  end
405
389
 
406
- def delete_stemcell(stemcell_id)
407
- with_thread_name("delete_stemcell(#{stemcell_id})") do
408
- image = @ec2.images[stemcell_id]
409
- image.deregister
390
+ # Add tags to an instance. In addition to the suplied tags,
391
+ # it adds a 'Name' tag as it is shown in the AWS console.
392
+ # @param [String] vm vm id that was once returned by {#create_vm}
393
+ # @param [Hash] metadata metadata key/value pairs
394
+ # @return [void]
395
+ def set_vm_metadata(vm, metadata)
396
+ instance = @ec2.instances[vm]
397
+
398
+ # TODO should we clear existing tags that don't exist in metadata?
399
+ metadata.each_pair do |key, value|
400
+ tag(instance, key, value)
410
401
  end
402
+
403
+ # should deployment name be included too?
404
+ job = metadata[:job]
405
+ index = metadata[:index]
406
+ tag(instance, "Name", "#{job}/#{index}") if job && index
407
+ rescue AWS::EC2::Errors::TagLimitExceeded => e
408
+ @logger.error("could not tag #{instance.id}: #{e.message}")
411
409
  end
412
410
 
411
+ # @note Not implemented in the AWS CPI
413
412
  def validate_deployment(old_manifest, new_manifest)
414
413
  # Not implemented in VSphere CPI as well
415
414
  not_implemented(:validate_deployment)
@@ -422,17 +421,19 @@ module Bosh::AwsCloud
422
421
  # @param [String] resource_pool_az availability zone specified in
423
422
  # the resource pool (may be nil)
424
423
  # @return [String] availability zone to use
424
+ # @note this is a private method that is public to make it easier to test
425
425
  def select_availability_zone(volumes, resource_pool_az)
426
426
  if volumes && !volumes.empty?
427
427
  disks = volumes.map { |vid| @ec2.volumes[vid] }
428
428
  ensure_same_availability_zone(disks, resource_pool_az)
429
429
  disks.first.availability_zone
430
430
  else
431
- resource_pool_az || DEFAULT_AVAILABILITY_ZONE
431
+ resource_pool_az || default_availability_zone
432
432
  end
433
433
  end
434
434
 
435
435
  # ensure all supplied availability zones are the same
436
+ # @note this is a private method that is public to make it easier to test
436
437
  def ensure_same_availability_zone(disks, default)
437
438
  zones = disks.map { |disk| disk.availability_zone }
438
439
  zones << default if default
@@ -443,6 +444,70 @@ module Bosh::AwsCloud
443
444
 
444
445
  private
445
446
 
447
+ # add a tag to something
448
+ def tag(taggable, key, value)
449
+ trimmed_key = key[0..(MAX_TAG_KEY_LENGTH - 1)]
450
+ trimmed_value = value[0..(MAX_TAG_VALUE_LENGTH - 1)]
451
+ taggable.add_tag(trimmed_key, :value => trimmed_value)
452
+ rescue AWS::EC2::Errors::InvalidParameterValue => e
453
+ @logger.error("could not tag #{taggable.id}: #{e.message}")
454
+ end
455
+
456
+ # Prepare EC2 user data
457
+ # @param [Hash] network_spec network specification
458
+ # @return [Hash] EC2 user data
459
+ def user_data(network_spec)
460
+ data = {}
461
+
462
+ data["registry"] = { "endpoint" => @registry.endpoint }
463
+
464
+ with_dns(network_spec) do |servers|
465
+ data["dns"] = { "nameserver" => servers }
466
+ end
467
+
468
+ data
469
+ end
470
+
471
+ # extract dns server list from network spec and yield the the list
472
+ # @param [Hash] network_spec network specification for instance
473
+ # @yield [Array]
474
+ def with_dns(network_spec)
475
+ network_spec.each_value do |properties|
476
+ if properties["dns"]
477
+ yield properties["dns"]
478
+ return
479
+ end
480
+ end
481
+ end
482
+
483
+ def image_params(cloud_properties, snapshot_id)
484
+ root_device_name = cloud_properties["root_device_name"]
485
+ architecture = cloud_properties["architecture"]
486
+
487
+ params = {
488
+ :name => "BOSH-#{generate_unique_name}",
489
+ :architecture => architecture,
490
+ :kernel_id => find_aki(architecture, root_device_name),
491
+ :root_device_name => root_device_name,
492
+ :block_device_mappings => {
493
+ "/dev/sda" => { :snapshot_id => snapshot_id },
494
+ "/dev/sdb" => "ephemeral0"
495
+ }
496
+ }
497
+
498
+ # old stemcells doesn't have name & version
499
+ if cloud_properties["name"] && cloud_properties["version"]
500
+ name = "#{cloud_properties['name']} #{cloud_properties['version']}"
501
+ params[:description] = name
502
+ end
503
+
504
+ params
505
+ end
506
+
507
+ def find_aki(architecture, root_device_name)
508
+ @aki_picker.pick(architecture, root_device_name)
509
+ end
510
+
446
511
  ##
447
512
  # Generates initial agent settings. These settings will be read by agent
448
513
  # from AWS registry (also a BOSH component) on a target instance. Disk
@@ -571,10 +636,11 @@ module Bosh::AwsCloud
571
636
  attachment = volume.detach_from(instance, device_map[volume.id])
572
637
  @logger.info("Detaching `#{volume.id}' from `#{instance.id}'")
573
638
 
574
- begin
575
- wait_resource(attachment, :detached)
576
- rescue AWS::Core::Resource::NotFound
577
- # It's OK, just means attachment is gone by now
639
+ wait_resource(attachment, :detached) do |error|
640
+ if error.is_a? AWS::Core::Resource::NotFound
641
+ @logger.info("attachment is no longer found, assuming it to be detached")
642
+ :detached
643
+ end
578
644
  end
579
645
  end
580
646
 
@@ -678,6 +744,25 @@ module Bosh::AwsCloud
678
744
  end
679
745
  end
680
746
 
747
+ def default_ec2_endpoint
748
+ if @aws_region
749
+ "ec2.#{@aws_region}.amazonaws.com"
750
+ else
751
+ DEFAULT_EC2_ENDPOINT
752
+ end
753
+ end
754
+
755
+ def default_availability_zone
756
+ if @aws_region
757
+ "#{@aws_region}b"
758
+ else
759
+ DEFAULT_AVAILABILITY_ZONE
760
+ end
761
+ end
762
+
763
+ def task_checkpoint
764
+ Bosh::Clouds::Config.task_checkpoint
765
+ end
681
766
  end
682
767
 
683
768
  end
@@ -21,9 +21,13 @@ module Bosh::AwsCloud
21
21
 
22
22
  started_at = Time.now
23
23
  failures = 0
24
- desc = resource.to_s
24
+
25
+ # all resources but Attachment have id
26
+ desc = resource.respond_to?(:id) ? resource.id : resource.to_s
25
27
 
26
28
  loop do
29
+ task_checkpoint
30
+
27
31
  duration = Time.now - started_at
28
32
 
29
33
  if duration > timeout
@@ -35,21 +39,13 @@ module Bosh::AwsCloud
35
39
  "(#{duration}s)")
36
40
  end
37
41
 
38
- begin
39
- state = resource.send(state_method)
40
- rescue AWS::EC2::Errors::InvalidAMIID::NotFound,
41
- AWS::EC2::Errors::InvalidInstanceID::NotFound => e
42
- # ugly workaround for AWS race conditions:
43
- # 1) sometimes when we upload a stemcell and proceed to create a VM
44
- # from it, AWS reports that the AMI is missing
45
- # 2) sometimes when we create a new EC2 instance, AWS reports that
46
- # the instance it returns is missing
47
- # in both cases we just wait a little and retry...
48
- raise e if failures > 3
49
- failures += 1
50
- @logger.error("#{e.message}: #{desc}")
51
- sleep(1)
52
- next
42
+ state = get_state_for(resource, state_method) do |error|
43
+ if block_given?
44
+ yield error
45
+ else
46
+ @logger.error("#{error.message}: #{desc}")
47
+ nil
48
+ end
53
49
  end
54
50
 
55
51
  # This is not a very strong convention, but some resources
@@ -71,6 +67,22 @@ module Bosh::AwsCloud
71
67
  @logger.info("#{desc} is now #{target_state}, took #{total}s")
72
68
  end
73
69
  end
70
+
71
+ private
72
+
73
+ def get_state_for(resource, state_method)
74
+ resource.send(state_method)
75
+ rescue AWS::EC2::Errors::InvalidAMIID::NotFound,
76
+ AWS::EC2::Errors::InvalidInstanceID::NotFound,
77
+ AWS::Core::Resource::NotFound => e
78
+ # ugly workaround for AWS race conditions:
79
+ # 1) sometimes when we upload a stemcell and proceed to create a VM
80
+ # from it, AWS reports that the AMI is missing
81
+ # 2) sometimes when we create a new EC2 instance, AWS reports that
82
+ # the instance it returns is missing
83
+ # in both cases we just catch the exception, wait a little and retry...
84
+ yield e
85
+ end
74
86
  end
75
87
  end
76
88
 
@@ -42,7 +42,7 @@ module Bosh::AwsCloud
42
42
  payload = Yajl::Encoder.encode(settings)
43
43
  url = "#{@endpoint}/instances/#{instance_id}/settings"
44
44
 
45
- response = @client.put(url, payload, @headers)
45
+ response = @client.put(url, {:body => payload, :header => @headers})
46
46
 
47
47
  if response.status != 200
48
48
  cloud_error("Cannot update settings for `#{instance_id}', " \
@@ -59,7 +59,7 @@ module Bosh::AwsCloud
59
59
  def read_settings(instance_id)
60
60
  url = "#{@endpoint}/instances/#{instance_id}/settings"
61
61
 
62
- response = @client.get(url, {}, @headers)
62
+ response = @client.get(url, {:header => @headers})
63
63
 
64
64
  if response.status != 200
65
65
  cloud_error("Cannot read settings for `#{instance_id}', " \
@@ -94,7 +94,7 @@ module Bosh::AwsCloud
94
94
  def delete_settings(instance_id)
95
95
  url = "#{@endpoint}/instances/#{instance_id}/settings"
96
96
 
97
- response = @client.delete(url, @headers)
97
+ response = @client.delete(url, {:header => @headers})
98
98
 
99
99
  if response.status != 200
100
100
  cloud_error("Cannot delete settings for `#{instance_id}', " \
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bosh
4
4
  module AwsCloud
5
- VERSION = "0.6.2"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
data/lib/cloud/aws.rb CHANGED
@@ -21,6 +21,7 @@ require "cloud/aws/cloud"
21
21
  require "cloud/aws/registry_client"
22
22
  require "cloud/aws/version"
23
23
 
24
+ require "cloud/aws/aki_picker"
24
25
  require "cloud/aws/network_configurator"
25
26
  require "cloud/aws/network"
26
27
  require "cloud/aws/dynamic_network"
data/spec/spec_helper.rb CHANGED
@@ -68,21 +68,23 @@ def mock_registry(endpoint = "http://registry:3333")
68
68
  end
69
69
 
70
70
  def mock_cloud(options = nil)
71
- instances = double("instances")
72
- volumes = double("volumes")
73
- images = double("images")
71
+ ec2 = mock_ec2
72
+ AWS::EC2.stub(:new).and_return(ec2)
74
73
 
75
- ec2 = double(AWS::EC2)
74
+ yield ec2 if block_given?
76
75
 
77
- ec2.stub(:instances).and_return(instances)
78
- ec2.stub(:volumes).and_return(volumes)
79
- ec2.stub(:images).and_return(images)
76
+ Bosh::AwsCloud::Cloud.new(options || mock_cloud_options)
77
+ end
80
78
 
81
- AWS::EC2.stub(:new).and_return(ec2)
79
+ def mock_ec2
80
+ ec2 = double(AWS::EC2,
81
+ :instances => double("instances"),
82
+ :volumes => double("volumes"),
83
+ :images =>double("images"))
82
84
 
83
85
  yield ec2 if block_given?
84
86
 
85
- Bosh::AwsCloud::Cloud.new(options || mock_cloud_options)
87
+ ec2
86
88
  end
87
89
 
88
90
  def dynamic_network_spec
@@ -0,0 +1,29 @@
1
+ require "spec_helper"
2
+
3
+ describe Bosh::AwsCloud::AKIPicker do
4
+ let(:akis) {
5
+ [
6
+ double("image-1", :root_device_name => "/dev/sda1",
7
+ :image_location => "pv-grub-hd00_1.03-x86_64.gz",
8
+ :image_id => "aki-b4aa75dd"),
9
+ double("image-2", :root_device_name => "/dev/sda1",
10
+ :image_location => "pv-grub-hd00_1.02-x86_64.gz",
11
+ :image_id => "aki-b4aa75d0")
12
+ ]
13
+ }
14
+ let(:logger) {double("logger", :info => nil)}
15
+ let(:picker) {Bosh::AwsCloud::AKIPicker.new(double("ec2"))}
16
+
17
+ it "should pick the AKI with the highest version" do
18
+ picker.should_receive(:logger).and_return(logger)
19
+ picker.should_receive(:fetch_akis).and_return(akis)
20
+ picker.pick("x86_64", "/dev/sda1").should == "aki-b4aa75dd"
21
+ end
22
+
23
+ it "should raise an error when it can't pick an AKI" do
24
+ picker.should_receive(:fetch_akis).and_return(akis)
25
+ expect {
26
+ picker.pick("foo", "bar")
27
+ }.to raise_error Bosh::Clouds::CloudError, "unable to find AKI"
28
+ end
29
+ end
@@ -1,6 +1,6 @@
1
1
  # Copyright (c) 2009-2012 VMware, Inc.
2
2
 
3
- require File.expand_path("../../spec_helper", __FILE__)
3
+ require "spec_helper"
4
4
 
5
5
  describe Bosh::AwsCloud::Cloud do
6
6
 
@@ -8,6 +8,10 @@ describe Bosh::AwsCloud::Cloud do
8
8
  @tmp_dir = Dir.mktmpdir
9
9
  end
10
10
 
11
+ after(:each) do
12
+ FileUtils.rm_rf(@tmp_dir)
13
+ end
14
+
11
15
  describe "EBS-volume based flow" do
12
16
 
13
17
  it "creates stemcell by copying an image to a new EBS volume" do
@@ -19,7 +23,7 @@ describe Bosh::AwsCloud::Cloud do
19
23
  :device => "/dev/sdh",
20
24
  :volume => volume)
21
25
 
22
- snapshot = double("snapshot", :id => "s-baz")
26
+ snapshot = double("snapshot", :id => "s-baz", :delete => nil)
23
27
  image = double("image", :id => "i-bar")
24
28
 
25
29
  unique_name = UUIDTools::UUID.random_create.to_s
@@ -29,29 +33,25 @@ describe Bosh::AwsCloud::Cloud do
29
33
  :architecture => "x86_64",
30
34
  :kernel_id => "aki-b4aa75dd",
31
35
  :root_device_name => "/dev/sda1",
36
+ :description => "bosh-stemcell 1.2.3",
32
37
  :block_device_mappings => {
33
38
  "/dev/sda" => { :snapshot_id => "s-baz" },
34
39
  "/dev/sdb" => "ephemeral0"
35
40
  }
36
41
  }
37
42
 
43
+
44
+ image.should_receive(:add_tag).with("Name", {:value=>"bosh-stemcell 1.2.3"})
45
+
38
46
  cloud = mock_cloud do |ec2|
39
47
  ec2.volumes.stub(:[]).with("v-foo").and_return(volume)
40
48
  ec2.instances.stub(:[]).with("i-current").and_return(current_instance)
41
49
  ec2.images.should_receive(:create).with(image_params).and_return(image)
42
-
43
- i1 = double("image-1", :root_device_name => "/dev/sda1",
44
- :image_location => "pv-grub-hd00_1.03-x86_64.gz",
45
- :image_id => "aki-b4aa75dd")
46
- i2 = double("image-2", :root_device_name => "/dev/sda1",
47
- :image_location => "pv-grub-hd00_1.02-x86_64.gz",
48
- :image_id => "aki-b4aa75d0")
49
-
50
- result = double("images_set", :images_set => [i1, i2])
51
- client = double("client", :describe_images => result)
52
- ec2.should_receive(:client).and_return(client)
53
50
  end
54
51
 
52
+ cloud.should_receive(:find_aki).with("x86_64", "/dev/sda1")
53
+ .and_return("aki-b4aa75dd")
54
+
55
55
  cloud.stub(:generate_unique_name).and_return(unique_name)
56
56
  cloud.stub(:current_instance_id).and_return("i-current")
57
57
 
@@ -102,7 +102,9 @@ describe Bosh::AwsCloud::Cloud do
102
102
 
103
103
  cloud_properties = {
104
104
  "root_device_name" => "/dev/sda1",
105
- "architecture" => "x86_64"
105
+ "architecture" => "x86_64",
106
+ "name" => "bosh-stemcell",
107
+ "version" => "1.2.3"
106
108
  }
107
109
  cloud.create_stemcell("/tmp/foo", cloud_properties).should == "i-bar"
108
110
  end
@@ -1,6 +1,6 @@
1
1
  # Copyright (c) 2009-2012 VMware, Inc.
2
2
 
3
- require File.expand_path("../../spec_helper", __FILE__)
3
+ require "spec_helper"
4
4
 
5
5
  describe Bosh::AwsCloud::Cloud, "create_vm" do
6
6
 
@@ -81,6 +81,46 @@ describe Bosh::AwsCloud::Cloud, "create_vm" do
81
81
  vm_id.should == "i-test"
82
82
  end
83
83
 
84
+ it "passes dns servers in ec2 user data when present" do
85
+ unique_name = UUIDTools::UUID.random_create.to_s
86
+
87
+ user_data = {
88
+ "registry" => {
89
+ "endpoint" => "http://registry:3333"
90
+ },
91
+ "dns" => { "nameserver" => ["1.2.3.4"] }
92
+ }
93
+
94
+ sec_grp = double("security_group", :name => "default")
95
+ instance = double("instance",
96
+ :id => "i-test",
97
+ :elastic_ip => nil,
98
+ :security_groups => sec_grp)
99
+ client = double("client", :describe_images => fake_image_set)
100
+
101
+ network_spec = dynamic_network_spec
102
+ network_spec["dns"] = ["1.2.3.4"]
103
+
104
+ cloud = mock_cloud do |ec2|
105
+ ec2.instances.should_receive(:create).
106
+ with(ec2_params(user_data, %w[default])).
107
+ and_return(instance)
108
+ ec2.should_receive(:client).and_return(client)
109
+ end
110
+
111
+ cloud.should_receive(:generate_unique_name).and_return(unique_name)
112
+ cloud.should_receive(:wait_resource).with(instance, :running)
113
+ @registry.should_receive(:update_settings)
114
+ .with("i-test", agent_settings(unique_name, network_spec))
115
+
116
+ vm_id = cloud.create_vm("agent-id", "sc-id",
117
+ resource_pool_spec,
118
+ { "network_a" => network_spec },
119
+ nil, { "test_env" => "value" })
120
+
121
+ vm_id.should == "i-test"
122
+ end
123
+
84
124
  it "creates EC2 instance with security group" do
85
125
  unique_name = UUIDTools::UUID.random_create.to_s
86
126
 
@@ -7,11 +7,22 @@ describe Bosh::AwsCloud::Cloud do
7
7
  it "deregisters EC2 image" do
8
8
  image = double("image", :id => "i-foo")
9
9
 
10
+ snapshot = double("snapshot")
11
+ snapshot.should_receive(:delete)
12
+
13
+ snapshots = double("snapshots")
14
+ snapshots.should_receive(:[]).with("snap-123").and_return(snapshot)
15
+
10
16
  cloud = mock_cloud do |ec2|
11
17
  ec2.images.stub(:[]).with("i-foo").and_return(image)
18
+ ec2.should_receive(:snapshots).and_return(snapshots)
12
19
  end
13
20
 
14
21
  image.should_receive(:deregister)
22
+
23
+ map = { "/dev/sda" => {:snapshot_id => "snap-123"} }
24
+ image.should_receive(:block_device_mappings).and_return(map)
25
+
15
26
  cloud.delete_stemcell("i-foo")
16
27
  end
17
28
 
@@ -4,15 +4,16 @@ require File.expand_path("../../spec_helper", __FILE__)
4
4
 
5
5
  describe Bosh::AwsCloud::Helpers do
6
6
  it "should time out" do
7
+ Bosh::Clouds::Config.stub(:task_checkpoint)
7
8
  cloud = mock_cloud
8
9
 
9
10
  resource = double("resource")
10
11
  resource.stub(:status).and_return(:start)
11
12
  cloud.stub(:sleep)
12
13
 
13
- lambda {
14
+ expect {
14
15
  cloud.wait_resource(resource, :stop, :status, 0.1)
15
- }.should raise_error Bosh::Clouds::CloudError, /Timed out/
16
+ }.to raise_error Bosh::Clouds::CloudError, /Timed out/
16
17
  end
17
18
 
18
19
  it "should not time out" do
@@ -28,15 +29,36 @@ describe Bosh::AwsCloud::Helpers do
28
29
  end
29
30
 
30
31
  it "should raise error when target state is wrong" do
32
+ Bosh::Clouds::Config.stub(:task_checkpoint)
31
33
  cloud = mock_cloud
32
34
 
33
35
  resource = double("resource")
34
36
  resource.stub(:status).and_return(:started, :failed)
35
37
  cloud.stub(:sleep)
36
38
 
37
- lambda {
39
+ expect {
38
40
  cloud.wait_resource(resource, :stopped, :status, 0.1)
39
- }.should raise_error Bosh::Clouds::CloudError,
40
- /is failed, expected stopped/
41
+ }.to raise_error Bosh::Clouds::CloudError, /is failed, expected stopped/
42
+ end
43
+
44
+ it "should swallow AWS::EC2::Errors::InvalidInstanceID::NotFound" do
45
+ Bosh::Clouds::Config.stub(:task_checkpoint)
46
+ cloud = mock_cloud
47
+
48
+ resource = double("resource")
49
+ return_values = [:raise, :raise, :raise, :start, :start, :stop]
50
+ i = 0
51
+ resource.stub(:status) do
52
+ i += 1
53
+ if return_values[i] == :raise
54
+ raise AWS::EC2::Errors::InvalidInstanceID::NotFound
55
+ end
56
+ return_values[i]
57
+ end
58
+ cloud.stub(:sleep)
59
+
60
+ #lambda {
61
+ cloud.wait_resource(resource, :stop, :status, 0.1)
62
+ #}.should_not raise_error AWS::EC2::Errors::InvalidInstanceID::NotFound
41
63
  end
42
64
  end
@@ -0,0 +1,30 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ require "spec_helper"
4
+
5
+ describe Bosh::AwsCloud::Cloud, "#set_vm_metadata" do
6
+ before :each do
7
+ @instance = double("instance", :id => "i-foobar")
8
+
9
+ @cloud = mock_cloud(mock_cloud_options) do |ec2|
10
+ ec2.instances.stub(:[]).with("i-foobar").and_return(@instance)
11
+ end
12
+ end
13
+
14
+ it "should add new tags" do
15
+ metadata = {:job => "job", :index => "index"}
16
+ @cloud.should_receive(:tag).with(@instance, :job, "job")
17
+ @cloud.should_receive(:tag).with(@instance, :index, "index")
18
+ @cloud.should_receive(:tag).with(@instance, "Name", "job/index")
19
+ @cloud.set_vm_metadata("i-foobar", metadata)
20
+ end
21
+
22
+ it "should trim key and value length" do
23
+ metadata = {"x"*128 => "y"*256}
24
+ @instance.should_receive(:add_tag) do |key, options|
25
+ key.size.should == 127
26
+ options[:value].size.should == 255
27
+ end
28
+ @cloud.set_vm_metadata("i-foobar", metadata)
29
+ end
30
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bosh_aws_cpi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-10 00:00:00.000000000 Z
12
+ date: 2013-01-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: aws-sdk
@@ -50,7 +50,7 @@ dependencies:
50
50
  requirements:
51
51
  - - ! '>='
52
52
  - !ruby/object:Gem::Version
53
- version: 0.4.4
53
+ version: 0.5.1
54
54
  type: :runtime
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
@@ -58,7 +58,7 @@ dependencies:
58
58
  requirements:
59
59
  - - ! '>='
60
60
  - !ruby/object:Gem::Version
61
- version: 0.4.4
61
+ version: 0.5.1
62
62
  - !ruby/object:Gem::Dependency
63
63
  name: httpclient
64
64
  requirement: !ruby/object:Gem::Requirement
@@ -117,6 +117,7 @@ files:
117
117
  - bin/bosh_aws_console
118
118
  - lib/bosh_aws_cpi.rb
119
119
  - lib/cloud/aws.rb
120
+ - lib/cloud/aws/aki_picker.rb
120
121
  - lib/cloud/aws/cloud.rb
121
122
  - lib/cloud/aws/dynamic_network.rb
122
123
  - lib/cloud/aws/helpers.rb
@@ -125,11 +126,12 @@ files:
125
126
  - lib/cloud/aws/registry_client.rb
126
127
  - lib/cloud/aws/version.rb
127
128
  - lib/cloud/aws/vip_network.rb
128
- - README
129
+ - README.md
129
130
  - Rakefile
130
131
  - spec/assets/stemcell-copy
131
132
  - spec/integration/cpi_test.rb
132
133
  - spec/spec_helper.rb
134
+ - spec/unit/aki_picker_spec.rb
133
135
  - spec/unit/attach_disk_spec.rb
134
136
  - spec/unit/cloud_spec.rb
135
137
  - spec/unit/configure_networks_spec.rb
@@ -143,6 +145,7 @@ files:
143
145
  - spec/unit/helpers_spec.rb
144
146
  - spec/unit/network_configurator_spec.rb
145
147
  - spec/unit/reboot_vm_spec.rb
148
+ - spec/unit/set_vm_metadata_spec.rb
146
149
  - spec/unit/validate_deployment_spec.rb
147
150
  homepage: http://www.vmware.com
148
151
  licenses: []
@@ -158,7 +161,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
158
161
  version: '0'
159
162
  segments:
160
163
  - 0
161
- hash: -2760126620918151960
164
+ hash: -1915781663242535072
162
165
  required_rubygems_version: !ruby/object:Gem::Requirement
163
166
  none: false
164
167
  requirements:
@@ -167,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
170
  version: '0'
168
171
  segments:
169
172
  - 0
170
- hash: -2760126620918151960
173
+ hash: -1915781663242535072
171
174
  requirements: []
172
175
  rubyforge_project:
173
176
  rubygems_version: 1.8.24
@@ -178,6 +181,7 @@ test_files:
178
181
  - spec/assets/stemcell-copy
179
182
  - spec/integration/cpi_test.rb
180
183
  - spec/spec_helper.rb
184
+ - spec/unit/aki_picker_spec.rb
181
185
  - spec/unit/attach_disk_spec.rb
182
186
  - spec/unit/cloud_spec.rb
183
187
  - spec/unit/configure_networks_spec.rb
@@ -191,4 +195,6 @@ test_files:
191
195
  - spec/unit/helpers_spec.rb
192
196
  - spec/unit/network_configurator_spec.rb
193
197
  - spec/unit/reboot_vm_spec.rb
198
+ - spec/unit/set_vm_metadata_spec.rb
194
199
  - spec/unit/validate_deployment_spec.rb
200
+ has_rdoc:
data/README DELETED
@@ -1,3 +0,0 @@
1
- # Copyright (c) 2009-2012 VMware, Inc.
2
-
3
- BOSH AWS Cloud Provider Interface