bosh_aws_cpi 0.3.3 → 0.5.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.
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright (c) 2009-2012 VMware, Inc.
4
+
5
+ # Usage example:
6
+ # irb(main):001:0> cpi.create_vm("test", "ami-809a48e9",
7
+ # {"instance_type" => "m1.small"}, {}, [], {"foo" =>"bar"})
8
+
9
+ gemfile = File.expand_path("../../Gemfile", __FILE__)
10
+
11
+ if File.exists?(gemfile)
12
+ ENV["BUNDLE_GEMFILE"] = gemfile
13
+ require "rubygems"
14
+ require "bundler/setup"
15
+ end
16
+
17
+ $:.unshift(File.expand_path("../../lib", __FILE__))
18
+ require "bosh_aws_cpi"
19
+ require "irb"
20
+ require "irb/completion"
21
+ require "ostruct"
22
+ require "optparse"
23
+
24
+ config_file = nil
25
+
26
+ opts_parser = OptionParser.new do |opts|
27
+ opts.on("-c", "--config FILE") { |file| config_file = file }
28
+ end
29
+
30
+ opts_parser.parse!
31
+
32
+ unless config_file
33
+ puts opts_parser
34
+ exit(1)
35
+ end
36
+
37
+ @config = YAML.load_file(config_file)
38
+
39
+ module ConsoleHelpers
40
+ def cpi
41
+ @cpi ||= Bosh::AwsCloud::Cloud.new(@config)
42
+ end
43
+
44
+ def ec2
45
+ cpi.ec2
46
+ end
47
+
48
+ def registry
49
+ cpi.registry
50
+ end
51
+ end
52
+
53
+ cloud_config = OpenStruct.new(:logger => Logger.new(STDOUT))
54
+
55
+ Bosh::Clouds::Config.configure(cloud_config)
56
+
57
+ include ConsoleHelpers
58
+
59
+ begin
60
+ require "ruby-debug"
61
+ puts "=> Debugger enabled"
62
+ rescue LoadError
63
+ puts "=> ruby-debug not found, debugger disabled"
64
+ end
65
+
66
+ puts "=> Welcome to BOSH AWS CPI console"
67
+ puts "You can use 'cpi' to access CPI methods"
68
+
69
+ IRB.start
70
+
71
+
72
+
@@ -11,13 +11,15 @@ module Bosh::AwsCloud
11
11
  METADATA_TIMEOUT = 5 # seconds
12
12
  DEVICE_POLL_TIMEOUT = 60 # seconds
13
13
 
14
- DEFAULT_AKI = "aki-825ea7eb"
14
+ DEFAULT_AKI = "aki-b4aa75dd"
15
+ DEFAULT_ROOT_DEVICE_NAME = "/dev/sda1"
15
16
 
16
17
  # UBUNTU_10_04_32_BIT_US_EAST_EBS = "ami-3e9b4957"
17
18
  # UBUNTU_10_04_32_BIT_US_EAST = "ami-809a48e9"
18
19
 
19
20
  attr_reader :ec2
20
21
  attr_reader :registry
22
+ attr_accessor :logger
21
23
 
22
24
  ##
23
25
  # Initialize BOSH AWS CPI
@@ -94,15 +96,13 @@ module Bosh::AwsCloud
94
96
  }
95
97
  }
96
98
 
97
- if disk_locality
98
- # TODO: use as hint for availability zones
99
- @logger.debug("Disk locality is ignored by AWS CPI")
100
- end
101
-
102
99
  security_groups =
103
100
  network_configurator.security_groups(@default_security_groups)
104
101
  @logger.debug("using security groups: #{security_groups.join(', ')}")
105
102
 
103
+ response = @ec2.client.describe_images(:image_ids => [stemcell_id])
104
+ root_device_name = response.images_set.first.root_device_name
105
+
106
106
  instance_params = {
107
107
  :image_id => stemcell_id,
108
108
  :count => 1,
@@ -112,23 +112,20 @@ module Bosh::AwsCloud
112
112
  :user_data => Yajl::Encoder.encode(user_data)
113
113
  }
114
114
 
115
- availability_zone = resource_pool["availability_zone"]
116
- if availability_zone
117
- instance_params[:availability_zone] = availability_zone
118
- end
115
+ instance_params[:availability_zone] =
116
+ select_availability_zone(disk_locality,
117
+ resource_pool["availability_zone"])
119
118
 
120
119
  @logger.info("Creating new instance...")
121
120
  instance = @ec2.instances.create(instance_params)
122
- state = instance.status
123
121
 
124
- @logger.info("Creating new instance `#{instance.id}', " \
125
- "state is `#{state}'")
126
-
127
- wait_resource(instance, state, :running)
122
+ @logger.info("Creating new instance `#{instance.id}'")
123
+ wait_resource(instance, :running)
128
124
 
129
125
  network_configurator.configure(@ec2, instance)
130
126
 
131
- settings = initial_agent_settings(agent_id, network_spec, environment)
127
+ settings = initial_agent_settings(agent_id, network_spec, environment,
128
+ root_device_name)
132
129
  @registry.update_settings(instance.id, settings)
133
130
 
134
131
  instance.id
@@ -137,22 +134,23 @@ module Bosh::AwsCloud
137
134
 
138
135
  ##
139
136
  # Terminates EC2 instance and waits until it reports as terminated
140
- # @param [String] vm_id Running instance id
137
+ # @param [String] instance_id Running instance id
141
138
  def delete_vm(instance_id)
142
139
  with_thread_name("delete_vm(#{instance_id})") do
143
140
  instance = @ec2.instances[instance_id]
144
141
 
145
142
  instance.terminate
146
- state = instance.status
147
-
148
- # TODO: should this be done before or after deleting VM?
149
- @logger.info("Deleting instance settings for `#{instance.id}'")
150
- @registry.delete_settings(instance.id)
151
143
 
152
- @logger.info("Deleting instance `#{instance.id}', " \
153
- "state is `#{state}'")
154
-
155
- wait_resource(instance, state, :terminated)
144
+ begin
145
+ # TODO: should this be done before or after deleting VM?
146
+ @logger.info("Deleting instance settings for `#{instance.id}'")
147
+ @registry.delete_settings(instance.id)
148
+
149
+ @logger.info("Deleting instance `#{instance.id}'")
150
+ wait_resource(instance, :terminated)
151
+ rescue AWS::EC2::Errors::InvalidInstanceID::NotFound
152
+ # It's OK, just means that instance has already been deleted
153
+ end
156
154
  end
157
155
  end
158
156
 
@@ -178,11 +176,11 @@ module Bosh::AwsCloud
178
176
  raise ArgumentError, "disk size needs to be an integer"
179
177
  end
180
178
 
181
- if (size < 1024)
179
+ if size < 1024
182
180
  cloud_error("AWS CPI minimum disk size is 1 GiB")
183
181
  end
184
182
 
185
- if (size > 1024 * 1000)
183
+ if size > 1024 * 1000
186
184
  cloud_error("AWS CPI maximum disk size is 1 TiB")
187
185
  end
188
186
 
@@ -199,12 +197,8 @@ module Bosh::AwsCloud
199
197
  }
200
198
 
201
199
  volume = @ec2.volumes.create(volume_params)
202
- state = volume.state
203
-
204
- @logger.info("Creating volume `#{volume.id}', " \
205
- "state is `#{state}'")
206
-
207
- wait_resource(volume, state, :available)
200
+ @logger.info("Creating volume `#{volume.id}'")
201
+ wait_resource(volume, :available)
208
202
 
209
203
  volume.id
210
204
  end
@@ -227,12 +221,10 @@ module Bosh::AwsCloud
227
221
  volume.delete
228
222
 
229
223
  begin
230
- state = volume.state
231
- @logger.info("Deleting volume `#{volume.id}', " \
232
- "state is `#{state}'")
233
-
234
- wait_resource(volume, state, :deleted)
224
+ @logger.info("Deleting volume `#{volume.id}'")
225
+ wait_resource(volume, :deleted)
235
226
  rescue AWS::EC2::Errors::InvalidVolume::NotFound
227
+ # It's OK, just means the volume has already been deleted
236
228
  end
237
229
 
238
230
  @logger.info("Volume `#{disk_id}' has been deleted")
@@ -310,32 +302,31 @@ module Bosh::AwsCloud
310
302
  ebs_volume = find_ebs_device(sd_name)
311
303
 
312
304
  # 2. Copy image to new EBS volume
313
- Dir.mktmpdir do |tmp_dir|
314
- @logger.info("Extracting stemcell to `#{tmp_dir}'")
315
-
316
- unpack_image(tmp_dir, image_path)
317
- copy_root_image(tmp_dir, ebs_volume)
318
-
319
- # 3. Create snapshot and then an image using this snapshot
320
- snapshot = volume.create_snapshot
321
- wait_resource(snapshot, snapshot.status, :completed)
322
-
323
- image_params = {
324
- :name => "BOSH-#{generate_unique_name}",
325
- :architecture => "x86_64",
326
- :kernel_id => cloud_properties["kernel_id"] || DEFAULT_AKI,
327
- :root_device_name => "/dev/sda",
328
- :block_device_mappings => {
329
- "/dev/sda" => { :snapshot_id => snapshot.id },
330
- "/dev/sdb" => "ephemeral0"
331
- }
305
+ @logger.info("Copying stemcell disk image to '#{ebs_volume}'")
306
+ copy_root_image(image_path, ebs_volume)
307
+
308
+ # 3. Create snapshot and then an image using this snapshot
309
+ snapshot = volume.create_snapshot
310
+ wait_resource(snapshot, :completed)
311
+
312
+ root_device_name = cloud_properties["root_device_name"] ||
313
+ DEFAULT_ROOT_DEVICE_NAME
314
+
315
+ image_params = {
316
+ :name => "BOSH-#{generate_unique_name}",
317
+ :architecture => "x86_64", # TODO should this be configurable?
318
+ :kernel_id => cloud_properties["kernel_id"] || DEFAULT_AKI,
319
+ :root_device_name => root_device_name,
320
+ :block_device_mappings => {
321
+ "/dev/sda" => { :snapshot_id => snapshot.id },
322
+ "/dev/sdb" => "ephemeral0"
332
323
  }
324
+ }
333
325
 
334
- image = @ec2.images.create(image_params)
335
- wait_resource(image, image.state, :available, :state)
326
+ image = @ec2.images.create(image_params)
327
+ wait_resource(image, :available, :state)
336
328
 
337
- image.id
338
- end
329
+ image.id
339
330
  rescue => e
340
331
  # TODO: delete snapshot?
341
332
  @logger.error(e)
@@ -361,6 +352,32 @@ module Bosh::AwsCloud
361
352
  not_implemented(:validate_deployment)
362
353
  end
363
354
 
355
+ # Selects the availability zone to use from a list of disk volumes,
356
+ # resource pool availability zone (if any) and the default availability
357
+ # zone.
358
+ # @param [Hash] volumes volume ids to attach to the vm
359
+ # @param [String] resource_pool_az availability zone specified in
360
+ # the resource pool (may be nil)
361
+ # @return [String] availability zone to use
362
+ def select_availability_zone(volumes, resource_pool_az)
363
+ if volumes && !volumes.empty?
364
+ disks = volumes.map { |vid| @ec2.volumes[vid] }
365
+ ensure_same_availability_zone(disks, resource_pool_az)
366
+ disks.first.availability_zone
367
+ else
368
+ resource_pool_az || DEFAULT_AVAILABILITY_ZONE
369
+ end
370
+ end
371
+
372
+ # ensure all supplied availability zones are the same
373
+ def ensure_same_availability_zone(disks, default)
374
+ zones = disks.map { |disk| disk.availability_zone }
375
+ zones << default if default
376
+ zones.uniq!
377
+ cloud_error "can't use multiple availability zones: %s" %
378
+ zones.join(", ") unless zones.size == 1 || zones.empty?
379
+ end
380
+
364
381
  private
365
382
 
366
383
  ##
@@ -377,7 +394,8 @@ module Bosh::AwsCloud
377
394
  # @param [Hash] network_spec Agent network spec
378
395
  # @param [Hash] environment
379
396
  # @return [Hash]
380
- def initial_agent_settings(agent_id, network_spec, environment)
397
+ def initial_agent_settings(agent_id, network_spec, environment,
398
+ root_device_name)
381
399
  settings = {
382
400
  "vm" => {
383
401
  "name" => "vm-#{generate_unique_name}"
@@ -385,7 +403,7 @@ module Bosh::AwsCloud
385
403
  "agent_id" => agent_id,
386
404
  "networks" => network_spec,
387
405
  "disks" => {
388
- "system" => "/dev/sda",
406
+ "system" => root_device_name,
389
407
  "ephemeral" => "/dev/sdb",
390
408
  "persistent" => {}
391
409
  }
@@ -438,7 +456,9 @@ module Bosh::AwsCloud
438
456
  end
439
457
 
440
458
  def attach_ebs_volume(instance, volume)
441
- device_names = Set.new(instance.block_device_mappings.keys)
459
+ # TODO once we upgrade the aws-sdk gem to > 1.3.9, we need to use:
460
+ # instance.block_device_mappings.to_hash.keys
461
+ device_names = Set.new(instance.block_device_mappings.to_hash.keys)
442
462
  new_attachment = nil
443
463
 
444
464
  ("f".."p").each do |char| # f..p is what console suggests
@@ -458,12 +478,9 @@ module Bosh::AwsCloud
458
478
  cloud_error("Instance has too many disks attached")
459
479
  end
460
480
 
461
- state = new_attachment.status
481
+ @logger.info("Attaching `#{volume.id}' to `#{instance.id}'")
482
+ wait_resource(new_attachment, :attached)
462
483
 
463
- @logger.info("Attaching `#{volume.id}' to #{instance.id}, " \
464
- "state is #{state}'")
465
-
466
- wait_resource(new_attachment, state, :attached)
467
484
  device_name = new_attachment.device
468
485
 
469
486
  @logger.info("Attached `#{volume.id}' to `#{instance.id}', " \
@@ -473,7 +490,9 @@ module Bosh::AwsCloud
473
490
  end
474
491
 
475
492
  def detach_ebs_volume(instance, volume)
476
- mappings = instance.block_device_mappings
493
+ # TODO once we upgrade the aws-sdk gem to > 1.3.9, we need to use:
494
+ # instance.block_device_mappings.to_hash.keys
495
+ mappings = instance.block_device_mappings.to_hash
477
496
 
478
497
  device_map = mappings.inject({}) do |hash, (device_name, attachment)|
479
498
  hash[attachment.volume.id] = device_name
@@ -486,40 +505,51 @@ module Bosh::AwsCloud
486
505
  end
487
506
 
488
507
  attachment = volume.detach_from(instance, device_map[volume.id])
489
- state = attachment.status
490
-
491
- @logger.info("Detaching `#{volume.id}' from `#{instance.id}', " \
492
- "state is #{state}'")
508
+ @logger.info("Detaching `#{volume.id}' from `#{instance.id}'")
493
509
 
494
510
  begin
495
- wait_resource(attachment, state, :detached)
511
+ wait_resource(attachment, :detached)
496
512
  rescue AWS::Core::Resource::NotFound
497
- # It's OK, just means attachment is gone when we're asking for state
513
+ # It's OK, just means attachment is gone by now
498
514
  end
499
515
  end
500
516
 
501
- def unpack_image(tmp_dir, image_path)
502
- output = `tar -C #{tmp_dir} -xzf #{image_path} 2>&1`
503
- if $?.exitstatus != 0
504
- cloud_error("Failed to unpack stemcell root image" \
505
- "tar exit status #{$?.exitstatus}: #{output}")
517
+ # This method tries to execute the helper script stemcell-copy
518
+ # as root using sudo, since it needs to write to the ebs_volume.
519
+ # If stemcell-copy isn't available, it falls back to writing directly
520
+ # to the device, which is used in the micro bosh deployer.
521
+ # The stemcell-copy script must be in the PATH of the user running
522
+ # the director, and needs sudo privileges to execute without
523
+ # password.
524
+ def copy_root_image(image_path, ebs_volume)
525
+ path = ENV["PATH"]
526
+
527
+ if stemcell_copy = has_stemcell_copy(path)
528
+ @logger.debug("copying stemcell using stemcell-copy script")
529
+ # note that is is a potentially dangerous operation, but as the
530
+ # stemcell-copy script sets PATH to a sane value this is safe
531
+ out = `sudo #{stemcell_copy} #{image_path} #{ebs_volume} 2>&1`
532
+ else
533
+ @logger.info("falling back to using dd to copy stemcell")
534
+ out = `tar -xzf #{image_path} -O root.img | dd of=#{ebs_volume} 2>&1`
506
535
  end
507
536
 
508
- root_image = File.join(tmp_dir, "root.img")
509
- unless File.exists?(root_image)
510
- cloud_error("Root image is missing from stemcell archive")
537
+ unless $?.exitstatus == 0
538
+ cloud_error("Unable to copy stemcell root image, " \
539
+ "exit status #{$?.exitstatus}: #{out}")
511
540
  end
541
+
542
+ @logger.debug("stemcell copy output:\n#{out}")
512
543
  end
513
544
 
514
- def copy_root_image(dir, ebs_volume)
515
- Dir.chdir(dir) do
516
- dd_out = `dd if=root.img of=#{ebs_volume} 2>&1`
517
- if $?.exitstatus != 0
518
- cloud_error("Unable to copy stemcell root image, " \
519
- "dd exit status #{$?.exitstatus}: " \
520
- "#{dd_out}")
521
- end
545
+ # checks if the stemcell-copy script can be found in
546
+ # the current PATH
547
+ def has_stemcell_copy(path)
548
+ path.split(":").each do |dir|
549
+ stemcell_copy = File.join(dir, "stemcell-copy")
550
+ return stemcell_copy if File.exist?(stemcell_copy)
522
551
  end
552
+ nil
523
553
  end
524
554
 
525
555
  def find_ebs_device(sd_name)
@@ -554,20 +584,13 @@ module Bosh::AwsCloud
554
584
  # N.B. This will only work with ebs-store instances,
555
585
  # as instance-store instances don't support stop/start.
556
586
  instance.stop
557
- state = instance.status
558
587
 
559
- @logger.info("Stopping instance `#{instance.id}', " \
560
- "state is `#{state}'")
561
-
562
- wait_resource(instance, state, :stopped)
588
+ @logger.info("Stopping instance `#{instance.id}'")
589
+ wait_resource(instance, :stopped)
563
590
 
564
591
  instance.start
565
- state = instance.status
566
-
567
- @logger.info("Starting instance `#{instance.id}', " \
568
- "state is `#{state}'")
569
-
570
- wait_resource(instance, state, :running)
592
+ @logger.info("Starting instance `#{instance.id}'")
593
+ wait_resource(instance, :running)
571
594
  end
572
595
 
573
596
  ##
@@ -4,7 +4,7 @@ module Bosh::AwsCloud
4
4
 
5
5
  module Helpers
6
6
 
7
- DEFAULT_TIMEOUT = 3600
7
+ DEFAULT_TIMEOUT = 3600 # seconds
8
8
 
9
9
  ##
10
10
  # Raises CloudError exception
@@ -16,43 +16,61 @@ module Bosh::AwsCloud
16
16
  raise Bosh::Clouds::CloudError, message
17
17
  end
18
18
 
19
- def wait_resource(resource, start_state,
20
- target_state, state_method = :status,
19
+ def wait_resource(resource, target_state, state_method = :status,
21
20
  timeout = DEFAULT_TIMEOUT)
22
21
 
23
22
  started_at = Time.now
24
- state = resource.send(state_method)
23
+ failures = 0
25
24
  desc = resource.to_s
26
25
 
27
- while state == start_state && state != target_state
26
+ loop do
28
27
  duration = Time.now - started_at
29
28
 
30
29
  if duration > timeout
31
- cloud_error("Timed out waiting for #{desc} " \
32
- "to be #{target_state}")
30
+ cloud_error("Timed out waiting for #{desc} to be #{target_state}")
33
31
  end
34
32
 
35
33
  if @logger
36
- @logger.debug("Waiting for #{desc} " \
37
- "to be #{target_state} (#{duration})")
34
+ @logger.debug("Waiting for #{desc} to be #{target_state} " \
35
+ "(#{duration}s)")
38
36
  end
39
37
 
40
- sleep(1)
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
53
+ end
54
+
55
+ # This is not a very strong convention, but some resources
56
+ # have 'error' and 'failed' states, we probably don't want to keep
57
+ # waiting if we're in these states. Alternatively we could introduce a
58
+ # set of 'loop breaker' states but that doesn't seem very helpful
59
+ # at the moment
60
+ if state == :error || state == :failed
61
+ cloud_error("#{desc} state is #{state}, expected #{target_state}")
62
+ end
63
+
64
+ break if state == target_state
41
65
 
42
- state = resource.send(state_method)
66
+ sleep(1)
43
67
  end
44
68
 
45
- if state == target_state
46
- if @logger
47
- @logger.info("#{desc} is #{target_state} " \
48
- "after #{Time.now - started_at}s")
49
- end
50
- else
51
- cloud_error("#{desc} is #{state}, " \
52
- "expected to be #{target_state}")
69
+ if @logger
70
+ total = Time.now - started_at
71
+ @logger.info("#{desc} is now #{target_state}, took #{total}s")
53
72
  end
54
73
  end
55
74
  end
56
-
57
75
  end
58
76
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bosh
4
4
  module AwsCloud
5
- VERSION = "0.3.3"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -0,0 +1,31 @@
1
+ #!/bin/sh
2
+ #
3
+ # This script runs as root through sudo without the need for a password,
4
+ # so it needs to make sure it can't be abused.
5
+ #
6
+
7
+ # make sure we have a secure PATH
8
+ PATH=/bin:/usr/bin
9
+ export PATH
10
+
11
+ if [ $# -ne 1 ]; then
12
+ echo "usage: $0 <block device>"
13
+ exit 1
14
+ fi
15
+
16
+ OUTPUT="$1"
17
+
18
+ # TODO perhaps this should be more restrictive?
19
+ echo ${OUTPUT} | egrep '^/dev/[a-z0-9]+$' > /dev/null 2>&1
20
+ if [ $? -ne 0 ]; then
21
+ echo "ERROR: illegal device: ${OUTPUT}"
22
+ exit 1
23
+ fi
24
+
25
+ if [ ! -b ${OUTPUT} ]; then
26
+ echo "ERROR: missing device: ${OUTPUT}"
27
+ exit 1
28
+ fi
29
+
30
+ # copy image to block device with 1 MB block size
31
+ dd if=root.img if=${OUTPUT} bs=1M
@@ -0,0 +1,78 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ require File.expand_path("../../spec_helper", __FILE__)
4
+
5
+ require "tempfile"
6
+
7
+ describe Bosh::AwsCloud::Cloud do
8
+
9
+ before(:each) do
10
+ unless ENV["CPI_CONFIG_FILE"]
11
+ raise "Please provide CPI_CONFIG_FILE environment variable"
12
+ end
13
+ @config = YAML.load_file(ENV["CPI_CONFIG_FILE"])
14
+ @logger = Logger.new(STDOUT)
15
+ end
16
+
17
+ let(:cpi) do
18
+ cpi = Bosh::AwsCloud::Cloud.new(@config)
19
+ cpi.logger = @logger
20
+
21
+ # As we inject the configuration file from the outside, we don't care
22
+ # about spinning up the registry ourselves. However we don't want to bother
23
+ # EC2 at all if registry is not working, so just in case we perform a test
24
+ # health check against whatever has been provided.
25
+ cpi.registry.update_settings("foo", { "bar" => "baz" })
26
+ cpi.registry.read_settings("foo").should == { "bar" => "baz"}
27
+
28
+ cpi
29
+ end
30
+
31
+ it "exercises a VM lifecycle" do
32
+ instance_id = cpi.create_vm(
33
+ "agent-007", "ami-809a48e9",
34
+ { "instance_type" => "m1.small" },
35
+ { "default" => { "type" => "dynamic" }},
36
+ [], { "key" => "value" })
37
+
38
+ instance_id.should_not be_nil
39
+
40
+ settings = cpi.registry.read_settings(instance_id)
41
+ settings["vm"].should be_a(Hash)
42
+ settings["vm"]["name"].should_not be_nil
43
+ settings["agent_id"].should == "agent-007"
44
+ settings["networks"].should == { "default" => { "type" => "dynamic" }}
45
+ settings["disks"].should == {
46
+ "system" => "/dev/sda",
47
+ "ephemeral" => "/dev/sdb",
48
+ "persistent" => {}
49
+ }
50
+
51
+ settings["env"].should == { "key" => "value" }
52
+
53
+ volume_id = cpi.create_disk(2048)
54
+ volume_id.should_not be_nil
55
+
56
+ cpi.attach_disk(instance_id, volume_id)
57
+ settings = cpi.registry.read_settings(instance_id)
58
+ settings["disks"]["persistent"].should == { volume_id => "/dev/sdf" }
59
+
60
+ cpi.detach_disk(instance_id, volume_id)
61
+ settings = cpi.registry.read_settings(instance_id)
62
+ settings["disks"]["persistent"].should == {}
63
+
64
+ # TODO: test configure_networks (need an elastic IP at hand for that)
65
+
66
+ cpi.delete_vm(instance_id)
67
+ cpi.delete_disk(volume_id)
68
+
69
+ # Test below would fail: EC2 still reports the instance as 'terminated'
70
+ # for some time.
71
+ # cpi.ec2.instances[instance_id].should be_nil
72
+
73
+ expect {
74
+ cpi.registry.read_settings(instance_id)
75
+ }.to raise_error(/HTTP 404/)
76
+ end
77
+
78
+ end
data/spec/spec_helper.rb CHANGED
@@ -25,6 +25,20 @@ Bosh::Clouds::Config.configure(aws_config)
25
25
  MOCK_AWS_ACCESS_KEY_ID = "foo"
26
26
  MOCK_AWS_SECRET_ACCESS_KEY = "bar"
27
27
 
28
+ def internal_to(*args, &block)
29
+ example = describe *args, &block
30
+ klass = args[0]
31
+ if klass.is_a? Class
32
+ saved_private_instance_methods = klass.private_instance_methods
33
+ example.before do
34
+ klass.class_eval { public *saved_private_instance_methods }
35
+ end
36
+ example.after do
37
+ klass.class_eval { private *saved_private_instance_methods }
38
+ end
39
+ end
40
+ end
41
+
28
42
  def mock_cloud_options
29
43
  {
30
44
  "aws" => {
@@ -23,8 +23,7 @@ describe Bosh::AwsCloud::Cloud do
23
23
 
24
24
  instance.should_receive(:block_device_mappings).and_return({})
25
25
 
26
- attachment.should_receive(:status).and_return(:attaching)
27
- cloud.should_receive(:wait_resource).with(attachment, :attaching, :attached)
26
+ cloud.should_receive(:wait_resource).with(attachment, :attached)
28
27
 
29
28
  old_settings = { "foo" => "bar" }
30
29
  new_settings = {
@@ -61,8 +60,7 @@ describe Bosh::AwsCloud::Cloud do
61
60
  volume.should_receive(:attach_to).
62
61
  with(instance, "/dev/sdh").and_return(attachment)
63
62
 
64
- attachment.should_receive(:status).and_return(:attaching)
65
- cloud.should_receive(:wait_resource).with(attachment, :attaching, :attached)
63
+ cloud.should_receive(:wait_resource).with(attachment, :attached)
66
64
 
67
65
  old_settings = { "foo" => "bar" }
68
66
  new_settings = {
@@ -99,8 +97,7 @@ describe Bosh::AwsCloud::Cloud do
99
97
  volume.should_receive(:attach_to).
100
98
  with(instance, "/dev/sdh").and_return(attachment)
101
99
 
102
- attachment.should_receive(:status).and_return(:attaching)
103
- cloud.should_receive(:wait_resource).with(attachment, :attaching, :attached)
100
+ cloud.should_receive(:wait_resource).with(attachment, :attached)
104
101
 
105
102
  old_settings = { "foo" => "bar" }
106
103
  new_settings = {
@@ -13,4 +13,20 @@ describe Bosh::AwsCloud::Cloud do
13
13
 
14
14
  end
15
15
 
16
+ internal_to Bosh::AwsCloud::Cloud do
17
+
18
+ it "should not find stemcell-copy" do
19
+ cloud = Bosh::Clouds::Provider.create(:aws, mock_cloud_options)
20
+ cloud.has_stemcell_copy("/usr/bin:/usr/sbin").should be_nil
21
+ end
22
+
23
+ it "should find stemcell-copy" do
24
+ cloud = Bosh::Clouds::Provider.create(:aws, mock_cloud_options)
25
+ path = ENV["PATH"]
26
+ path += ":#{File.expand_path('../../assets', __FILE__)}"
27
+ cloud.has_stemcell_copy(path).should_not be_nil
28
+ end
29
+
30
+ end
31
+
16
32
  end
@@ -16,8 +16,7 @@ describe Bosh::AwsCloud::Cloud do
16
16
  ec2.volumes.should_receive(:create).with(disk_params).and_return(volume)
17
17
  end
18
18
 
19
- volume.should_receive(:state).and_return(:creating)
20
- cloud.should_receive(:wait_resource).with(volume, :creating, :available)
19
+ cloud.should_receive(:wait_resource).with(volume, :available)
21
20
 
22
21
  cloud.create_disk(2048).should == "v-foobar"
23
22
  end
@@ -34,8 +33,7 @@ describe Bosh::AwsCloud::Cloud do
34
33
  ec2.volumes.should_receive(:create).with(disk_params).and_return(volume)
35
34
  end
36
35
 
37
- volume.should_receive(:state).and_return(:creating)
38
- cloud.should_receive(:wait_resource).with(volume, :creating, :available)
36
+ cloud.should_receive(:wait_resource).with(volume, :available)
39
37
 
40
38
  cloud.create_disk(2049)
41
39
  end
@@ -67,8 +65,7 @@ describe Bosh::AwsCloud::Cloud do
67
65
  ec2.instances.stub(:[]).with("i-test").and_return(instance)
68
66
  end
69
67
 
70
- volume.should_receive(:state).and_return(:creating)
71
- cloud.should_receive(:wait_resource).with(volume, :creating, :available)
68
+ cloud.should_receive(:wait_resource).with(volume, :available)
72
69
 
73
70
  cloud.create_disk(1024, "i-test")
74
71
  end
@@ -27,8 +27,8 @@ describe Bosh::AwsCloud::Cloud do
27
27
  image_params = {
28
28
  :name => "BOSH-#{unique_name}",
29
29
  :architecture => "x86_64",
30
- :kernel_id => "aki-825ea7eb",
31
- :root_device_name => "/dev/sda",
30
+ :kernel_id => "aki-b4aa75dd",
31
+ :root_device_name => "/dev/sda1",
32
32
  :block_device_mappings => {
33
33
  "/dev/sda" => { :snapshot_id => "s-baz" },
34
34
  "/dev/sdb" => "ephemeral0"
@@ -68,35 +68,24 @@ describe Bosh::AwsCloud::Cloud do
68
68
  volume.should_receive(:attach_to).with(current_instance, "/dev/sdh").
69
69
  and_return(attachment)
70
70
 
71
- attachment.should_receive(:status).and_return(:attaching)
72
- cloud.should_receive(:wait_resource).
73
- with(attachment, :attaching, :attached)
71
+ cloud.should_receive(:wait_resource).with(attachment, :attached)
74
72
 
75
73
  cloud.stub(:sleep)
76
74
 
77
75
  File.stub(:blockdev?).with("/dev/sdh").and_return(false, false, false)
78
76
  File.stub(:blockdev?).with("/dev/xvdh").and_return(false, false, true)
79
77
 
80
- Dir.should_receive(:mktmpdir).and_yield(@tmp_dir)
81
-
82
- cloud.should_receive(:unpack_image).with(@tmp_dir, "/tmp/foo")
83
- cloud.should_receive(:copy_root_image).with(@tmp_dir, "/dev/xvdh")
78
+ cloud.should_receive(:copy_root_image).with("/tmp/foo", "/dev/xvdh")
84
79
 
85
80
  volume.should_receive(:create_snapshot).and_return(snapshot)
86
- snapshot.should_receive(:status).and_return(:in_progress)
87
- cloud.should_receive(:wait_resource).
88
- with(snapshot, :in_progress, :completed)
81
+ cloud.should_receive(:wait_resource).with(snapshot, :completed)
89
82
 
90
- image.should_receive(:state).and_return(:creating)
91
- cloud.should_receive(:wait_resource).with(image,
92
- :creating, :available, :state)
83
+ cloud.should_receive(:wait_resource).with(image, :available, :state)
93
84
 
94
85
  volume.should_receive(:detach_from).with(current_instance, "/dev/sdh").
95
86
  and_return(attachment)
96
87
 
97
- attachment.should_receive(:status).and_return(:detaching)
98
- cloud.should_receive(:wait_resource).
99
- with(attachment, :detaching, :detached)
88
+ cloud.should_receive(:wait_resource).with(attachment, :detached)
100
89
 
101
90
  cloud.should_receive(:delete_disk).with("v-foo")
102
91
 
@@ -12,7 +12,7 @@ describe Bosh::AwsCloud::Cloud, "create_vm" do
12
12
  "agent_id" => "agent-id",
13
13
  "networks" => { "network_a" => network_spec },
14
14
  "disks" => {
15
- "system" => "/dev/sda",
15
+ "system" => "/dev/sda1",
16
16
  "ephemeral" => "/dev/sdb",
17
17
  "persistent" => {}
18
18
  },
@@ -24,6 +24,11 @@ describe Bosh::AwsCloud::Cloud, "create_vm" do
24
24
  }
25
25
  end
26
26
 
27
+ def fake_image_set
28
+ image = double("image", :root_device_name => "/dev/sda1")
29
+ double("images_set", :images_set => [image])
30
+ end
31
+
27
32
  def ec2_params(user_data, security_groups=[])
28
33
  {
29
34
  :image_id => "sc-id",
@@ -52,16 +57,17 @@ describe Bosh::AwsCloud::Cloud, "create_vm" do
52
57
  instance = double("instance",
53
58
  :id => "i-test",
54
59
  :elastic_ip => nil)
60
+ client = double("client", :describe_images => fake_image_set)
55
61
 
56
62
  cloud = mock_cloud do |ec2|
57
63
  ec2.instances.should_receive(:create).
58
64
  with(ec2_params(user_data)).
59
65
  and_return(instance)
66
+ ec2.should_receive(:client).and_return(client)
60
67
  end
61
68
 
62
- instance.should_receive(:status).and_return(:pending)
63
69
  cloud.should_receive(:generate_unique_name).and_return(unique_name)
64
- cloud.should_receive(:wait_resource).with(instance, :pending, :running)
70
+ cloud.should_receive(:wait_resource).with(instance, :running)
65
71
  @registry.should_receive(:update_settings)
66
72
  .with("i-test", agent_settings(unique_name))
67
73
 
@@ -85,6 +91,7 @@ describe Bosh::AwsCloud::Cloud, "create_vm" do
85
91
  instance = double("instance",
86
92
  :id => "i-test",
87
93
  :elastic_ip => nil)
94
+ client = double("client", :describe_images => fake_image_set)
88
95
 
89
96
  security_groups = %w[foo bar]
90
97
  network_spec = dynamic_network_spec
@@ -96,11 +103,11 @@ describe Bosh::AwsCloud::Cloud, "create_vm" do
96
103
  ec2.instances.should_receive(:create).
97
104
  with(ec2_params(user_data, security_groups)).
98
105
  and_return(instance)
106
+ ec2.should_receive(:client).and_return(client)
99
107
  end
100
108
 
101
- instance.should_receive(:status).and_return(:pending)
102
109
  cloud.should_receive(:generate_unique_name).and_return(unique_name)
103
- cloud.should_receive(:wait_resource).with(instance, :pending, :running)
110
+ cloud.should_receive(:wait_resource).with(instance, :running)
104
111
  @registry.should_receive(:update_settings)
105
112
  .with("i-test", agent_settings(unique_name, network_spec))
106
113
 
@@ -116,14 +123,15 @@ describe Bosh::AwsCloud::Cloud, "create_vm" do
116
123
  instance = double("instance",
117
124
  :id => "i-test",
118
125
  :elastic_ip => nil)
126
+ client = double("client", :describe_images => fake_image_set)
119
127
 
120
128
  cloud = mock_cloud do |ec2|
121
129
  ec2.instances.should_receive(:create).and_return(instance)
130
+ ec2.should_receive(:client).and_return(client)
122
131
  end
123
132
 
124
- instance.should_receive(:status).and_return(:pending)
125
133
  instance.should_receive(:associate_elastic_ip).with("10.0.0.1")
126
- cloud.should_receive(:wait_resource).with(instance, :pending, :running)
134
+ cloud.should_receive(:wait_resource).with(instance, :running)
127
135
  @registry.should_receive(:update_settings)
128
136
 
129
137
  vm_id = cloud.create_vm("agent-id", "sc-id",
@@ -131,4 +139,56 @@ describe Bosh::AwsCloud::Cloud, "create_vm" do
131
139
  combined_network_spec)
132
140
  end
133
141
 
142
+ def volume(zone)
143
+ vol = double("volume")
144
+ vol.stub(:availability_zone).and_return(zone)
145
+ vol
146
+ end
147
+
148
+ describe "#select_availability_zone" do
149
+
150
+ it "should select the default availability_zone when all values are nil" do
151
+ cloud = mock_cloud
152
+ cloud.select_availability_zone(nil, nil).should ==
153
+ Bosh::AwsCloud::Cloud::DEFAULT_AVAILABILITY_ZONE
154
+ end
155
+
156
+ it "should select the zone from a list of disks" do
157
+ cloud = mock_cloud do |ec2|
158
+ ec2.volumes.stub(:[]).and_return(volume("foo"), volume("foo"))
159
+ end
160
+ cloud.select_availability_zone(%w[cid1 cid2], nil).should == "foo"
161
+ end
162
+
163
+ it "should select the zone from a list of disks and a default" do
164
+ cloud = mock_cloud do |ec2|
165
+ ec2.volumes.stub(:[]).and_return(volume("foo"), volume("foo"))
166
+ end
167
+ cloud.select_availability_zone(%w[cid1 cid2], "foo").should == "foo"
168
+ end
169
+ end
170
+
171
+ describe "#ensure_same_availability_zone" do
172
+ it "should raise an error when the zones differ" do
173
+ cloud = mock_cloud
174
+ lambda {
175
+ cloud.ensure_same_availability_zone([volume("foo"), volume("bar")], nil)
176
+ }.should raise_error Bosh::Clouds::CloudError
177
+ end
178
+
179
+ it "should raise an error when the zones differ" do
180
+ cloud = mock_cloud
181
+ lambda {
182
+ cloud.ensure_same_availability_zone([volume("foo"), volume("bar")], "foo")
183
+ }.should raise_error Bosh::Clouds::CloudError
184
+ end
185
+
186
+ it "should raise an error when the zones differ" do
187
+ cloud = mock_cloud
188
+ lambda {
189
+ cloud.ensure_same_availability_zone([volume("foo"), volume("foo")], "bar")
190
+ }.should raise_error Bosh::Clouds::CloudError
191
+ end
192
+ end
193
+
134
194
  end
@@ -11,9 +11,9 @@ describe Bosh::AwsCloud::Cloud do
11
11
  ec2.volumes.stub(:[]).with("v-foo").and_return(volume)
12
12
  end
13
13
 
14
- volume.should_receive(:state).and_return(:available, :deleting)
14
+ volume.should_receive(:state).and_return(:available)
15
15
  volume.should_receive(:delete)
16
- cloud.should_receive(:wait_resource).with(volume, :deleting, :deleted)
16
+ cloud.should_receive(:wait_resource).with(volume, :deleted)
17
17
 
18
18
  cloud.delete_disk("v-foo")
19
19
  end
@@ -16,8 +16,7 @@ describe Bosh::AwsCloud::Cloud do
16
16
  end
17
17
 
18
18
  instance.should_receive(:terminate)
19
- instance.should_receive(:status).and_return(:deleting)
20
- cloud.should_receive(:wait_resource).with(instance, :deleting, :terminated)
19
+ cloud.should_receive(:wait_resource).with(instance, :terminated)
21
20
 
22
21
  @registry.should_receive(:delete_settings).with("i-foobar")
23
22
 
@@ -30,8 +30,7 @@ describe Bosh::AwsCloud::Cloud do
30
30
  volume.should_receive(:detach_from).
31
31
  with(instance, "/dev/sdf").and_return(attachment)
32
32
 
33
- attachment.should_receive(:status).and_return(:detaching)
34
- cloud.should_receive(:wait_resource).with(attachment, :detaching, :detached)
33
+ cloud.should_receive(:wait_resource).with(attachment, :detached)
35
34
 
36
35
  old_settings = {
37
36
  "foo" => "bar",
@@ -11,7 +11,7 @@ describe Bosh::AwsCloud::Helpers do
11
11
  cloud.stub(:sleep)
12
12
 
13
13
  lambda {
14
- cloud.wait_resource(resource, :start, :stop, :status, 0.1)
14
+ cloud.wait_resource(resource, :stop, :status, 0.1)
15
15
  }.should raise_error Bosh::Clouds::CloudError, /Timed out/
16
16
  end
17
17
 
@@ -19,11 +19,11 @@ describe Bosh::AwsCloud::Helpers do
19
19
  cloud = mock_cloud
20
20
 
21
21
  resource = double("resource")
22
- resource.stub(:status).and_return(:start, :stop)
22
+ resource.stub(:status).and_return(:start, :stopping, :stopping, :stop)
23
23
  cloud.stub(:sleep)
24
24
 
25
25
  lambda {
26
- cloud.wait_resource(resource, :start, :stop, :status, 0.1)
26
+ cloud.wait_resource(resource, :stop, :status, 0.1)
27
27
  }.should_not raise_error Bosh::Clouds::CloudError
28
28
  end
29
29
 
@@ -35,8 +35,8 @@ describe Bosh::AwsCloud::Helpers do
35
35
  cloud.stub(:sleep)
36
36
 
37
37
  lambda {
38
- cloud.wait_resource(resource, :started, :stopped, :status, 0.1)
38
+ cloud.wait_resource(resource, :stopped, :status, 0.1)
39
39
  }.should raise_error Bosh::Clouds::CloudError,
40
- /is failed, expected to be stopped/
40
+ /is failed, expected stopped/
41
41
  end
42
42
  end
@@ -25,14 +25,12 @@ describe Bosh::AwsCloud::Cloud do
25
25
  it "hard reboots an EC2 instance" do
26
26
  # N.B. This requires ebs-store instance
27
27
  @instance.should_receive(:stop).ordered
28
- @instance.should_receive(:status).ordered.and_return(:stopping)
29
28
  @cloud.should_receive(:wait_resource).
30
- with(@instance, :stopping, :stopped).ordered
29
+ with(@instance, :stopped).ordered
31
30
 
32
31
  @instance.should_receive(:start)
33
- @instance.should_receive(:status).and_return(:starting)
34
32
  @cloud.should_receive(:wait_resource).ordered.
35
- with(@instance, :starting, :running)
33
+ with(@instance, :running)
36
34
 
37
35
  @cloud.send(:hard_reboot, @instance)
38
36
  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.3.3
4
+ version: 0.5.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-04-08 00:00:00.000000000 Z
12
+ date: 2012-07-10 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.2
53
+ version: 0.4.3
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.2
61
+ version: 0.4.3
62
62
  - !ruby/object:Gem::Dependency
63
63
  name: httpclient
64
64
  requirement: !ruby/object:Gem::Requirement
@@ -109,10 +109,12 @@ dependencies:
109
109
  version: 0.8.2
110
110
  description: BOSH AWS CPI
111
111
  email: support@vmware.com
112
- executables: []
112
+ executables:
113
+ - bosh_aws_console
113
114
  extensions: []
114
115
  extra_rdoc_files: []
115
116
  files:
117
+ - bin/bosh_aws_console
116
118
  - lib/bosh_aws_cpi.rb
117
119
  - lib/cloud/aws.rb
118
120
  - lib/cloud/aws/cloud.rb
@@ -125,6 +127,8 @@ files:
125
127
  - lib/cloud/aws/vip_network.rb
126
128
  - README
127
129
  - Rakefile
130
+ - spec/assets/stemcell-copy
131
+ - spec/integration/cpi_test.rb
128
132
  - spec/spec_helper.rb
129
133
  - spec/unit/attach_disk_spec.rb
130
134
  - spec/unit/cloud_spec.rb
@@ -154,7 +158,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
154
158
  version: '0'
155
159
  segments:
156
160
  - 0
157
- hash: 3401725189883691334
161
+ hash: -2403392629027305829
158
162
  required_rubygems_version: !ruby/object:Gem::Requirement
159
163
  none: false
160
164
  requirements:
@@ -163,14 +167,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
167
  version: '0'
164
168
  segments:
165
169
  - 0
166
- hash: 3401725189883691334
170
+ hash: -2403392629027305829
167
171
  requirements: []
168
172
  rubyforge_project:
169
- rubygems_version: 1.8.21
173
+ rubygems_version: 1.8.24
170
174
  signing_key:
171
175
  specification_version: 3
172
176
  summary: BOSH AWS CPI
173
177
  test_files:
178
+ - spec/assets/stemcell-copy
179
+ - spec/integration/cpi_test.rb
174
180
  - spec/spec_helper.rb
175
181
  - spec/unit/attach_disk_spec.rb
176
182
  - spec/unit/cloud_spec.rb