bosh_aws_cpi 0.3.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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