beaker 1.9.1 → 1.10.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.
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