bosh_aws_cpi 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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