beaker 1.9.1 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +8 -8
  2. data/.simplecov +1 -0
  3. data/beaker.gemspec +2 -0
  4. data/lib/beaker/dsl/helpers.rb +15 -6
  5. data/lib/beaker/dsl/install_utils.rb +1 -1
  6. data/lib/beaker/host.rb +21 -2
  7. data/lib/beaker/host/aix/exec.rb +7 -0
  8. data/lib/beaker/host/unix.rb +4 -0
  9. data/lib/beaker/host/unix/exec.rb +8 -0
  10. data/lib/beaker/host/windows.rb +2 -0
  11. data/lib/beaker/host/windows/exec.rb +11 -0
  12. data/lib/beaker/host_prebuilt_steps.rb +7 -6
  13. data/lib/beaker/hypervisor.rb +6 -2
  14. data/lib/beaker/hypervisor/aws_sdk.rb +389 -0
  15. data/lib/beaker/hypervisor/blimper.rb +8 -23
  16. data/lib/beaker/hypervisor/ec2_helper.rb +28 -0
  17. data/lib/beaker/hypervisor/google_compute.rb +10 -8
  18. data/lib/beaker/hypervisor/google_compute_helper.rb +1 -1
  19. data/lib/beaker/hypervisor/vagrant.rb +8 -0
  20. data/lib/beaker/options/command_line_parser.rb +6 -0
  21. data/lib/beaker/options/parser.rb +1 -1
  22. data/lib/beaker/options/presets.rb +3 -0
  23. data/lib/beaker/ssh_connection.rb +2 -3
  24. data/lib/beaker/test_case.rb +2 -2
  25. data/lib/beaker/version.rb +1 -1
  26. data/spec/beaker/dsl/helpers_spec.rb +27 -5
  27. data/spec/beaker/dsl/install_utils_spec.rb +2 -2
  28. data/spec/beaker/host_prebuilt_steps_spec.rb +2 -2
  29. data/spec/beaker/hypervisor/aws_sdk_spec.rb +81 -0
  30. data/spec/beaker/hypervisor/blimper_spec.rb +0 -35
  31. data/spec/beaker/hypervisor/ec2_helper_spec.rb +23 -0
  32. data/spec/beaker/hypervisor/vagrant_spec.rb +22 -1
  33. data/spec/beaker/options/parser_spec.rb +4 -1
  34. data/spec/beaker/test_case_spec.rb +85 -1
  35. data/spec/mocks.rb +4 -0
  36. metadata +37 -2
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- YjM4YTIwYzFlZjZlNWZhODY4MWE3ODdiOTVhNzU5ZGEwOTI5MTE2Ng==
4
+ YmMxMzJlYWRhZjA1M2U5OWUxMGRhZWQ0NWU1Njg2MjA2OGUzNDRkOA==
5
5
  data.tar.gz: !binary |-
6
- ZTg1Y2Y1NzMzZTE2YjY4OWY3MTE5MDc5NTZjZGMyNWYzZWQyNmFhNg==
6
+ MzBiMjE4MGVmM2YyOTBiNmUzNzcwNGJhMWYyNTMzYzA0YmI1YzMzMQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- NWZkNWFlZTUxMTdhYWZjNDM3MTJjM2M2MTliNzI2MWVmN2MzN2ZkMzQwYzY5
10
- NTM2N2ZmYjZmZWFlMzgxOWJkMmMzNzIyOTgyNTMyMGU3MmRiYjNkOTI0NjI5
11
- MTI3MGY2Zjc0YjkwMTc1YmE0OTgxM2YwNWNmYmE3ZmE4OWEwZTY=
9
+ YzIyOWQ2MGVkNzk2MWZkZTY2NmQ4YzY1NmRjMGI4ZGMxMzEwNjc2YmI1NWFk
10
+ MjEzY2NhYjdhNjI0MjNjMDIwN2U4MDk5YzEwY2NhNDk2NTg0NWIxMmY4OTFj
11
+ YTI2MThkNGExNjVmYjIwNmU4OGY5YjQ5NDA0MTY4ZGE1OGI3NGE=
12
12
  data.tar.gz: !binary |-
13
- OTRhZjk0ODYxZDI2ZWNmZGFiMjVkNTUzNGFhN2I0M2FmMTIwMTVkYjQ0NDJm
14
- YmI4MzFhY2I0MzFkNTQyYjYzODA2OTg3MzNhNmQ2MDRjNzI5YTdjZWM3ZTMx
15
- OGQwYzlmMjY4OTQwZDg3NGI5Y2M0ZGEwNjZhNDNlNjNhZGZiMWE=
13
+ YmE3OGU1MGIzMmRjMmM1MGM3MTcwMjdmODM5MzYyYzQ0ZWQyMWQ5N2E4YjQ3
14
+ NDE2MzQ1YmU0ZDIxOWEwYTAxZDliOTBkOTEwMTQ2ZDJmMTQyNmQ3ZTI3MWU5
15
+ YWQ1YWE3N2QwYmMyYTRkZjQwMjk2MGJlMGZjMjA4ODVjODEzMDQ=
data/.simplecov CHANGED
@@ -9,6 +9,7 @@ SimpleCov.configure do
9
9
  files = %w(cli.rb logger.rb options_parsing.rb test_config.rb utils/)
10
10
  files.any? {|f| file.filename =~ Regexp.new( Regexp.quote(f) ) }
11
11
  end
12
+ add_group 'Hypervisors', '/hypervisor'
12
13
  end
13
14
 
14
15
  SimpleCov.start if ENV['COVERAGE']
data/beaker.gemspec CHANGED
@@ -18,6 +18,7 @@ Gem::Specification.new do |s|
18
18
  s.require_paths = ["lib"]
19
19
 
20
20
  # Testing dependencies
21
+ s.add_development_dependency 'minitest', '~> 4.0'
21
22
  s.add_development_dependency 'rspec', '~> 2.14.0'
22
23
  s.add_development_dependency 'fakefs', '0.4'
23
24
  s.add_development_dependency 'rake', '~> 10.1.0'
@@ -39,6 +40,7 @@ Gem::Specification.new do |s|
39
40
  s.add_runtime_dependency 'blimpy', '~> 0.6'
40
41
  s.add_runtime_dependency 'fission', '~> 0.4'
41
42
  s.add_runtime_dependency 'google-api-client', '~> 0.6.4'
43
+ s.add_runtime_dependency 'aws-sdk', '~> 1.38'
42
44
 
43
45
  # These are transitive dependencies that we include or pin to because...
44
46
  # Ruby 1.8 compatibility
@@ -472,8 +472,8 @@ module Beaker
472
472
  backup_file = backup_the_file(host, host['puppetpath'], testdir, 'puppet.conf')
473
473
  lay_down_new_puppet_conf host, conf_opts, testdir
474
474
 
475
- if host.is_pe?
476
- bounce_service( host, 'pe-httpd' )
475
+ if host['puppetservice']
476
+ bounce_service( host, host['puppetservice'] )
477
477
  else
478
478
  puppet_master_started = start_puppet_from_source_on!( host, cmdline_args )
479
479
  end
@@ -488,8 +488,8 @@ module Beaker
488
488
  begin
489
489
  restore_puppet_conf_from_backup( host, backup_file )
490
490
 
491
- if host.is_pe?
492
- bounce_service( host, 'pe-httpd' )
491
+ if host['puppetservice']
492
+ bounce_service( host, host['puppetservice'] )
493
493
  else
494
494
  if puppet_master_started
495
495
  stop_puppet_from_source_on( host )
@@ -631,7 +631,10 @@ module Beaker
631
631
  # Any reason to not
632
632
  # host.exec puppet_resource( 'service', service, 'ensure=stopped' )
633
633
  # host.exec puppet_resource( 'service', service, 'ensure=running' )
634
- host.exec( Command.new( "/etc/init.d/#{service} restart" ) )
634
+ host.exec( Command.new( "#{host['service-prefix']}#{service} restart" ) )
635
+ if host['service-wait']
636
+ curl_with_retries(" #{service} ", host, "http://localhost:8140", [0, 52], 120)
637
+ end
635
638
  end
636
639
 
637
640
  # Blocks until the port is open on the host specified, returns false
@@ -690,6 +693,11 @@ module Beaker
690
693
  # if `puppet --apply` indicates there were no
691
694
  # failure during its execution.
692
695
  #
696
+ # @option opts [Boolean] :future_parser (false) This option enables
697
+ # the future parser option that is available
698
+ # from Puppet verion 3.2
699
+ # By default it will use the 'current' parser.
700
+ #
693
701
  # @param [Block] block This method will yield to a block of code passed
694
702
  # by the caller; this can be used for additional
695
703
  # validation, etc.
@@ -707,6 +715,7 @@ module Beaker
707
715
  args = ["--verbose"]
708
716
  args << "--parseonly" if opts[:parseonly]
709
717
  args << "--trace" if opts[:trace]
718
+ args << "--parser future" if opts[:future_parser]
710
719
 
711
720
  # From puppet help:
712
721
  # "... an exit code of '2' means there were changes, an exit code of
@@ -872,7 +881,7 @@ module Beaker
872
881
  end
873
882
 
874
883
  def curl_with_retries(desc, host, url, desired_exit_codes, max_retries = 60, retry_interval = 1)
875
- retry_command(desc, host, "curl #{url}", desired_exit_codes, max_retries, retry_interval)
884
+ retry_command(desc, host, "curl -m 1 #{url}", desired_exit_codes, max_retries, retry_interval)
876
885
  end
877
886
 
878
887
  def retry_command(desc, host, command, desired_exit_codes = 0, max_retries = 60, retry_interval = 1)
@@ -454,7 +454,7 @@ module Beaker
454
454
  end
455
455
  on host, 'curl -O http://apt.puppetlabs.com/puppetlabs-release-$(lsb_release -c -s).deb'
456
456
  on host, 'dpkg -i puppetlabs-release-$(lsb_release -c -s).deb'
457
- on host, 'apt-get -y -f -m update'
457
+ on host, 'apt-get update'
458
458
  on host, 'apt-get install -y puppet'
459
459
  else
460
460
  raise "install_puppet() called for unsupported platform '#{host['platform']}' on '#{host.name}'"
data/lib/beaker/host.rb CHANGED
@@ -92,8 +92,9 @@ module Beaker
92
92
  end
93
93
  end
94
94
 
95
+ # Return the preferred method to reach the host, will use IP is available and then default to {#hostname}.
95
96
  def reachable_name
96
- self['ip'] || self['vmhostname'] || name
97
+ self['ip'] || hostname
97
98
  end
98
99
 
99
100
  # Returning our PuppetConfigReader here allows users of the Host
@@ -116,11 +117,19 @@ module Beaker
116
117
  @defaults.has_key?(k)
117
118
  end
118
119
 
120
+ # The {#hostname} of this host.
119
121
  def to_str
120
- @defaults['vmhostname'] || @name
122
+ hostname
121
123
  end
122
124
 
125
+ # The {#hostname} of this host.
123
126
  def to_s
127
+ hostname
128
+ end
129
+
130
+ # Return the public name of the particular host, which may be different then the name of the host provided in
131
+ # the configuration file as some provisioners create random, unique hostnames.
132
+ def hostname
124
133
  @defaults['vmhostname'] || @name
125
134
  end
126
135
 
@@ -140,6 +149,16 @@ module Beaker
140
149
  end
141
150
  end
142
151
 
152
+ #Determine the ip address of this host
153
+ def get_ip
154
+ @logger.warn("Uh oh, this should be handled by sub-classes but hasn't been")
155
+ end
156
+
157
+ #Return the ip address of this host
158
+ def ip
159
+ self[:ip] ||= get_ip
160
+ end
161
+
143
162
  def connection
144
163
  @connection ||= SshConnection.connect( reachable_name,
145
164
  self['user'],
@@ -0,0 +1,7 @@
1
+ module Aix::Exec
2
+ include Beaker::CommandFactory
3
+
4
+ def get_ip
5
+ execute("ifconfig -a inet| awk '/broadcast/ {print $2}' | cut -d/ -f1 | head -1").strip
6
+ end
7
+ end
@@ -22,6 +22,9 @@ module Unix
22
22
  h.merge({
23
23
  'user' => 'root',
24
24
  'group' => 'pe-puppet',
25
+ 'service-wait' => false,
26
+ 'service-prefix'=> '/etc/init.d/',
27
+ 'puppetservice' => 'pe-httpd',
25
28
  'puppetpath' => '/etc/puppetlabs/puppet',
26
29
  'puppetbin' => '/opt/puppet/bin/puppet',
27
30
  'puppetbindir' => '/opt/puppet/bin',
@@ -39,6 +42,7 @@ module Unix
39
42
  h.merge({
40
43
  'user' => 'root',
41
44
  'group' => 'puppet',
45
+ 'service-wait' => false,
42
46
  'puppetpath' => '/etc/puppet',
43
47
  'puppetvardir' => '/var/lib/puppet',
44
48
  'puppetbin' => '/usr/bin/puppet',
@@ -12,4 +12,12 @@ module Unix::Exec
12
12
  def path
13
13
  '/bin:/usr/bin'
14
14
  end
15
+
16
+ def get_ip
17
+ if self['platform'].include? 'solaris'
18
+ execute("ifconfig -a inet| awk '/broadcast/ {print $2}' | cut -d/ -f1 | head -1").strip
19
+ else
20
+ execute("ip a|awk '/global/{print$2}' | cut -d/ -f1 | head -1").strip
21
+ end
22
+ end
15
23
  end
@@ -22,6 +22,8 @@ module Windows
22
22
  h.merge({
23
23
  'user' => 'Administrator',
24
24
  'group' => 'Administrators',
25
+ 'service-wait' => false,
26
+ 'puppetservice' => 'pe-httpd',
25
27
  'puppetpath' => '`cygpath -smF 35`/PuppetLabs/puppet/etc',
26
28
  'puppetvardir' => '`cygpath -smF 35`/PuppetLabs/puppet/var',
27
29
  #if an x86 Program Files dir exists then use it, default to just Program Files
@@ -15,4 +15,15 @@ module Windows::Exec
15
15
  def path
16
16
  'c:/windows/system32;c:/windows'
17
17
  end
18
+
19
+ def get_ip
20
+ ip = execute("ipconfig | grep -i 'IP Address' | cut -d: -f2 | head -1").strip
21
+ if ip == ''
22
+ ip = execute("ipconfig | grep -i 'IPv4 Address' | cut -d: -f2 | head -1").strip
23
+ end
24
+ if ip == ''
25
+ ip = execute("ipconfig | grep -i 'IPv6 Address' | cut -d: -f2 | head -1").strip
26
+ end
27
+ ip
28
+ end
18
29
  end
@@ -199,7 +199,7 @@ module Beaker
199
199
  host.map { |h| apt_get_update(h) }
200
200
  else
201
201
  if host[:platform] =~ /(ubuntu)|(debian)/
202
- host.exec(Command.new("apt-get -y -f -m update"))
202
+ host.exec(Command.new("apt-get update"))
203
203
  end
204
204
  end
205
205
  end
@@ -302,8 +302,9 @@ module Beaker
302
302
 
303
303
  #Determine the ip address of the provided host
304
304
  # @param [Host] host the host to act upon
305
+ # @deprecated use {Host#get_ip}
305
306
  def get_ip(host)
306
- host.exec(Command.new("ip a|awk '/global/{print$2}' | cut -d/ -f1 | head -1")).stdout.chomp
307
+ host.get_ip
307
308
  end
308
309
 
309
310
  #Append the provided string to the /etc/hosts file of the provided host
@@ -378,11 +379,11 @@ module Beaker
378
379
  if host.is_a? Array
379
380
  host.map { |h| copy_ssh_to_root(h, opts) }
380
381
  else
381
- if host['platform'] =~ /centos/
382
+ if host['platform'] =~ /centos|el-|redhat|fedora/
382
383
  @logger.debug("Disabling se_linux on #{host.name}")
383
384
  host.exec(Command.new("sudo su -c \"setenforce 0\""), {:pty => true})
384
385
  else
385
- @logger.warn("Attempting to disable SELinux on non-centos platform: #{host.name}: #{host['platform']}")
386
+ @logger.warn("Attempting to disable SELinux on non-supported platform: #{host.name}: #{host['platform']}")
386
387
  end
387
388
  end
388
389
  end
@@ -396,11 +397,11 @@ module Beaker
396
397
  if host.is_a? Array
397
398
  host.map { |h| copy_ssh_to_root(h, opts) }
398
399
  else
399
- if host['platform'] =~ /centos/
400
+ if host['platform'] =~ /centos|el-|redhat|fedora/
400
401
  logger.debug("Disabling iptables on #{host.name}")
401
402
  host.exec(Command.new("sudo su -c \"/etc/init.d/iptables stop\""), {:pty => true})
402
403
  else
403
- logger.warn("Attempting to disable iptables on non-centos platform: #{host.name}: #{host['platform']}")
404
+ logger.warn("Attempting to disable iptables on non-supported platform: #{host.name}: #{host['platform']}")
404
405
  end
405
406
  end
406
407
  end
@@ -34,6 +34,8 @@ module Beaker
34
34
  Beaker::Fusion
35
35
  when /blimpy/
36
36
  Beaker::Blimper
37
+ when /ec2/
38
+ Beaker::AwsSdk
37
39
  when /vcloud/
38
40
  if options['pooling_api']
39
41
  Beaker::VcloudPooled
@@ -89,7 +91,9 @@ module Beaker
89
91
 
90
92
  #Default validation steps to be run for a given hypervisor
91
93
  def validate
92
- validate_host(@hosts, @options)
94
+ if @options[:validate]
95
+ validate_host(@hosts, @options)
96
+ end
93
97
  end
94
98
 
95
99
  #Generate a random straing composted of letter and numbers
@@ -100,7 +104,7 @@ module Beaker
100
104
  end
101
105
  end
102
106
 
103
- %w( vsphere_helper vagrant fusion blimper vsphere vcloud vcloud_pooled aixer solaris google_compute_helper google_compute).each do |lib|
107
+ %w( vsphere_helper vagrant fusion blimper aws_sdk vsphere vcloud vcloud_pooled aixer solaris google_compute_helper google_compute).each do |lib|
104
108
  begin
105
109
  require "hypervisor/#{lib}"
106
110
  rescue LoadError
@@ -0,0 +1,389 @@
1
+ require 'aws/ec2'
2
+ require 'set'
3
+ require 'zlib'
4
+ require 'beaker/hypervisor/ec2_helper'
5
+
6
+ module Beaker
7
+ # This is an alternate EC2 driver that implements direct API access using
8
+ # Amazon's AWS-SDK library: http://aws.amazon.com/documentation/sdkforruby/
9
+ #
10
+ # It is built for full control, to reduce any other layers beyond the pure
11
+ # vendor API.
12
+ class AwsSdk < Beaker::Hypervisor
13
+ # Initialize AwsSdk hypervisor driver
14
+ #
15
+ # @param [Array<Beaker::Host>] hosts Array of Beaker::Host objects
16
+ # @param [Hash<String, String>] options Options hash
17
+ def initialize(hosts, options)
18
+ @hosts = hosts
19
+ @options = options
20
+ @logger = options[:logger]
21
+
22
+ # Get fog credentials from the local .fog file
23
+ creds = load_fog_credentials(@options[:dot_fog])
24
+
25
+ config = {
26
+ :access_key_id => creds[:access_key],
27
+ :secret_access_key => creds[:secret_key],
28
+ :logger => Logger.new($stdout),
29
+ :log_level => :debug,
30
+ :log_formatter => AWS::Core::LogFormatter.colored,
31
+ :max_retries => 12,
32
+ }
33
+ AWS.config(config)
34
+
35
+ @ec2 = AWS::EC2.new()
36
+ end
37
+
38
+ # Provision all hosts on EC2 using the AWS::EC2 API
39
+ #
40
+ # @return [void]
41
+ def provision
42
+ start_time = Time.now
43
+
44
+ # Perform the main launch work
45
+ launch_all_nodes()
46
+
47
+ # Wait for each node to reach status :running
48
+ wait_for_status(:running)
49
+
50
+ # Grab the ip addresses and dns from EC2 for each instance to use for ssh
51
+ populate_dns()
52
+
53
+ # Set the hostname for each box
54
+ set_hostnames()
55
+
56
+ # Configure /etc/hosts on each host
57
+ configure_hosts()
58
+
59
+ @logger.notify("aws-sdk: Provisioning complete in #{Time.now - start_time} seconds")
60
+
61
+ nil #void
62
+ end
63
+
64
+ # Cleanup all earlier provisioned hosts on EC2 using the AWS::EC2 library
65
+ #
66
+ # It goes without saying, but a #cleanup does nothing without a #provision
67
+ # method call first.
68
+ #
69
+ # @return [void]
70
+ def cleanup
71
+ @logger.notify("aws-sdk: Cleanup, iterating across all hosts and terminating them")
72
+ @hosts.each do |host|
73
+ # This was set previously during provisioning
74
+ instance = host['instance']
75
+
76
+ # Only attempt a terminate if the instance actually is set by provision
77
+ # and the instance actually 'exists'.
78
+ if !instance.nil? and instance.exists?
79
+ instance.terminate
80
+ end
81
+ end
82
+
83
+ nil #void
84
+ end
85
+
86
+ # Launch all nodes
87
+ #
88
+ # This is where the main launching work occurs for each node. Here we take
89
+ # care of feeding the information from the image required into the config
90
+ # for the new host, we perform the launch operation and ensure that the
91
+ # instance is properly tagged for identification.
92
+ #
93
+ # @return [void]
94
+ # @api private
95
+ def launch_all_nodes
96
+ # Load the ec2_yaml spec file
97
+ ami_spec = YAML.load_file(@options[:ec2_yaml])["AMI"]
98
+
99
+ # Iterate across all hosts and launch them, adding tags along the way
100
+ @logger.notify("aws-sdk: Iterate across all hosts in configuration and launch them")
101
+ @hosts.each do |host|
102
+ amitype = host['vmname'] || host['platform']
103
+ amisize = host['amisize'] || 'm1.small'
104
+
105
+ # Use snapshot provided for this host
106
+ image_type = host['snapshot']
107
+ if not image_type
108
+ raise RuntimeError, "No snapshot/image_type provided for EC2 provisioning"
109
+ end
110
+ ami = ami_spec[amitype]
111
+ ami_region = ami[:region]
112
+
113
+ # Main region object for ec2 operations
114
+ region = @ec2.regions[ami_region]
115
+
116
+ # Grab image object
117
+ image_id = ami[:image][image_type.to_sym]
118
+ @logger.notify("aws-sdk: Checking image #{image_id} exists and getting its root device")
119
+ image = region.images[image_id]
120
+ if image.nil? and not image.exists?
121
+ raise RuntimeError, "Image not found: #{image_id}"
122
+ end
123
+
124
+ # Transform the images block_device_mappings output into a format
125
+ # ready for a create.
126
+ orig_bdm = image.block_device_mappings()
127
+ @logger.notify("aws-sdk: Image block_device_mappings: #{orig_bdm.to_hash}")
128
+ block_device_mappings = []
129
+ orig_bdm.each do |device_name, rest|
130
+ block_device_mappings << {
131
+ :device_name => device_name,
132
+ :ebs => {
133
+ # This is required to override the images default for
134
+ # delete_on_termination, forcing all volumes to be deleted once the
135
+ # instance is terminated.
136
+ :delete_on_termination => true,
137
+ }
138
+ }
139
+ end
140
+
141
+ # Launch the node, filling in the blanks from previous work.
142
+ @logger.notify("aws-sdk: Launch instance")
143
+ config = {
144
+ :count => 1,
145
+ :image_id => image_id,
146
+ :monitoring_enabled => true,
147
+ :key_pair => ensure_key_pair(region),
148
+ :security_groups => [ensure_group(region, Beaker::EC2Helper.amiports(host['roles']))],
149
+ :instance_type => amisize,
150
+ :disable_api_termination => false,
151
+ :instance_initiated_shutdown_behavior => "terminate",
152
+ :block_device_mappings => block_device_mappings,
153
+ }
154
+ instance = region.instances.create(config)
155
+
156
+ # Persist the instance object for this host, so later it can be
157
+ # manipulated by 'cleanup' for example.
158
+ host['instance'] = instance
159
+
160
+ # Define tags for the instance
161
+ @logger.notify("aws-sdk: Add tags")
162
+ instance.add_tag("jenkins_build_url", :value => @options[:jenkins_build_url])
163
+ instance.add_tag("Name", :value => host.name)
164
+ instance.add_tag("department", :value => @options[:department])
165
+ instance.add_tag("project", :value => @options[:project])
166
+
167
+ @logger.notify("aws-sdk: Launched #{host.name} (#{amitype}:#{amisize}) using snapshot/image_type #{image_type}")
168
+ end
169
+
170
+ nil
171
+ end
172
+
173
+ # Waits until all boxes reach the desired status
174
+ #
175
+ # @param status [Symbol] EC2 state to wait for, :running :stopped etc.
176
+ # @return [void]
177
+ # @api private
178
+ def wait_for_status(status)
179
+ # Wait for each node to reach status :running
180
+ @logger.notify("aws-sdk: Now wait for all hosts to reach state #{status}")
181
+ @hosts.each do |host|
182
+ instance = host['instance']
183
+ name = host.name
184
+
185
+ @logger.notify("aws-sdk: Wait for status #{status} for node #{name}")
186
+
187
+ # Here we keep waiting for the machine state to reach ':running' with an
188
+ # exponential backoff for each poll.
189
+ # TODO: should probably be a in a shared method somewhere
190
+ for tries in 1..10
191
+ if instance.status == status
192
+ # Always sleep, so the next command won't cause a throttle
193
+ backoff_sleep(tries)
194
+ break
195
+ elsif tries == 10
196
+ raise "Instance never reached state #{status}"
197
+ end
198
+ backoff_sleep(tries)
199
+ end
200
+
201
+ # Set the IP to be the dns_name of the host, yes I know its not an IP.
202
+ host['ip'] = instance.dns_name
203
+ end
204
+ end
205
+
206
+ # Populate the hosts IP address and vmhostname entry from the EC2 dns_name
207
+ #
208
+ # @return [void]
209
+ # @api private
210
+ def populate_dns
211
+ # Obtain the IP addresses and dns_name for each host
212
+ @hosts.each do |host|
213
+ instance = host['instance']
214
+ host['vmhostname'] = instance.dns_name
215
+ host['ip'] = instance.ip_address
216
+ host['private_ip'] = instance.private_ip_address
217
+ @logger.notify("aws-sdk: name: #{host.name} vmhostname: #{host['vmhostname']} ip: #{host['ip']} private_ip: #{host['private_ip']}")
218
+ end
219
+
220
+ nil
221
+ end
222
+
223
+ # Configure /etc/hosts for each node
224
+ #
225
+ # @return [void]
226
+ # @api private
227
+ def configure_hosts
228
+ base = "127.0.0.1\tlocalhost localhost.localdomain\n"
229
+ @hosts.each do |host|
230
+ hn = host.hostname
231
+ etc_hosts = base + "#{host['private_ip']}\t#{hn} #{hn.split(".")[0]}\n"
232
+ set_etc_hosts(host, etc_hosts)
233
+ end
234
+ end
235
+
236
+ # Set the hostname of all instances to be the hostname defined in the
237
+ # beaker configuration.
238
+ #
239
+ # @return [void]
240
+ # @api private
241
+ def set_hostnames
242
+ @hosts.each do |host|
243
+ host.exec(Command.new("hostname #{host['vmhostname']}"))
244
+ end
245
+ end
246
+
247
+ # Calculates and waits a back-off period based on the number of tries
248
+ #
249
+ # Logs each backupoff time and retry value to the console.
250
+ #
251
+ # @param tries [Number] number of tries to calculate back-off period
252
+ # @return [void]
253
+ # @api private
254
+ def backoff_sleep(tries)
255
+ # Exponential with some randomization
256
+ sleep_time = 2 ** tries
257
+ @logger.notify("aws-sdk: Sleeping #{sleep_time} seconds for attempt #{tries}.")
258
+ sleep sleep_time
259
+ nil
260
+ end
261
+
262
+ # Retrieve the public key locally from the executing users ~/.ssh directory
263
+ #
264
+ # @return [String] contents of public key
265
+ # @api private
266
+ def public_key
267
+ filename = File.expand_path('~/.ssh/id_rsa.pub')
268
+ unless File.exists? filename
269
+ filename = File.expand_path('~/.ssh/id_dsa.pub')
270
+ unless File.exists? filename
271
+ raise RuntimeError, 'Expected either ~/.ssh/id_rsa.pub or ~/.ssh/id_dsa.pub but found neither'
272
+ end
273
+ end
274
+
275
+ File.read(filename)
276
+ end
277
+
278
+ # Generate a reusable key name from the local hosts hostname
279
+ #
280
+ # @return [String] safe key name for current host
281
+ # @api private
282
+ def key_name
283
+ safe_hostname = Socket.gethostname.gsub('.', '-')
284
+ "Beaker-#{local_user}-#{safe_hostname}"
285
+ end
286
+
287
+ # Returns the local user running this tool
288
+ #
289
+ # @return [String] username of local user
290
+ # @api private
291
+ def local_user
292
+ ENV['USER']
293
+ end
294
+
295
+ # Returns the KeyPair for this host, creating it if needed
296
+ #
297
+ # @param region [AWS::EC2::Region] region to create the key pair in
298
+ # @return [AWS::EC2::KeyPair] created key_pair
299
+ # @api private
300
+ def ensure_key_pair(region)
301
+ @logger.notify("aws-sdk: Ensure key pair exists, create if not")
302
+ key_pairs = region.key_pairs
303
+ pair_name = key_name()
304
+ kp = key_pairs[pair_name]
305
+ unless kp.exists?
306
+ ssh_string = public_key()
307
+ kp = key_pairs.import(pair_name, ssh_string)
308
+ end
309
+
310
+ kp
311
+ end
312
+
313
+ # Return a reproducable security group identifier based on input ports
314
+ #
315
+ # @param ports [Array<Number>] array of port numbers
316
+ # @return [String] group identifier
317
+ # @api private
318
+ def group_id(ports)
319
+ if ports.nil? or ports.empty?
320
+ raise ArgumentError, "Ports list cannot be nil or empty"
321
+ end
322
+
323
+ unless ports.is_a? Set
324
+ ports = Set.new(ports)
325
+ end
326
+
327
+ # Lolwut, #hash is inconsistent between ruby processes
328
+ "Beaker-#{Zlib.crc32(ports.inspect)}"
329
+ end
330
+
331
+ # Return an existing group, or create new one
332
+ #
333
+ # @param region [AWS::EC2::Region] the AWS region control object
334
+ # @param ports [Array<Number>] an array of port numbers
335
+ # @return [AWS::EC2::SecurityGroup] created security group
336
+ # @api private
337
+ def ensure_group(region, ports)
338
+ @logger.notify("aws-sdk: Ensure security group exists for ports #{ports.to_s}, create if not")
339
+ name = group_id(ports)
340
+
341
+ group = region.security_groups.filter('group-name', name).first
342
+
343
+ if group.nil?
344
+ group = create_group(region, ports)
345
+ end
346
+
347
+ group
348
+ end
349
+
350
+ # Create a new security group
351
+ #
352
+ # @param region [AWS::EC2::Region] the AWS region control object
353
+ # @param ports [Array<Number>] an array of port numbers
354
+ # @return [AWS::EC2::SecurityGroup] created security group
355
+ # @api private
356
+ def create_group(region, ports)
357
+ name = group_id(ports)
358
+ @logger.notify("aws-sdk: Creating group #{name} for ports #{ports.to_s}")
359
+ group = region.security_groups.create(name,
360
+ :description => "Custom Beaker security group for #{ports.to_a}")
361
+
362
+ unless ports.is_a? Set
363
+ ports = Set.new(ports)
364
+ end
365
+
366
+ ports.each do |port|
367
+ group.authorize_ingress(:tcp, port)
368
+ end
369
+
370
+ group
371
+ end
372
+
373
+ # Return a hash containing the fog credentials for EC2
374
+ #
375
+ # @param dot_fog [String] dot fog path
376
+ # @return [Hash<Symbol, String>] ec2 credentials
377
+ # @api private
378
+ def load_fog_credentials(dot_fog = '.fog')
379
+ fog = YAML.load_file( dot_fog )
380
+
381
+ default = fog[:default]
382
+
383
+ creds = {}
384
+ creds[:access_key] = default[:aws_access_key_id]
385
+ creds[:secret_key] = default[:aws_secret_access_key]
386
+ creds
387
+ end
388
+ end
389
+ end