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.
- checksums.yaml +8 -8
- data/.simplecov +1 -0
- data/beaker.gemspec +2 -0
- data/lib/beaker/dsl/helpers.rb +15 -6
- data/lib/beaker/dsl/install_utils.rb +1 -1
- data/lib/beaker/host.rb +21 -2
- data/lib/beaker/host/aix/exec.rb +7 -0
- data/lib/beaker/host/unix.rb +4 -0
- data/lib/beaker/host/unix/exec.rb +8 -0
- data/lib/beaker/host/windows.rb +2 -0
- data/lib/beaker/host/windows/exec.rb +11 -0
- data/lib/beaker/host_prebuilt_steps.rb +7 -6
- data/lib/beaker/hypervisor.rb +6 -2
- data/lib/beaker/hypervisor/aws_sdk.rb +389 -0
- data/lib/beaker/hypervisor/blimper.rb +8 -23
- data/lib/beaker/hypervisor/ec2_helper.rb +28 -0
- data/lib/beaker/hypervisor/google_compute.rb +10 -8
- data/lib/beaker/hypervisor/google_compute_helper.rb +1 -1
- data/lib/beaker/hypervisor/vagrant.rb +8 -0
- data/lib/beaker/options/command_line_parser.rb +6 -0
- data/lib/beaker/options/parser.rb +1 -1
- data/lib/beaker/options/presets.rb +3 -0
- data/lib/beaker/ssh_connection.rb +2 -3
- data/lib/beaker/test_case.rb +2 -2
- data/lib/beaker/version.rb +1 -1
- data/spec/beaker/dsl/helpers_spec.rb +27 -5
- data/spec/beaker/dsl/install_utils_spec.rb +2 -2
- data/spec/beaker/host_prebuilt_steps_spec.rb +2 -2
- data/spec/beaker/hypervisor/aws_sdk_spec.rb +81 -0
- data/spec/beaker/hypervisor/blimper_spec.rb +0 -35
- data/spec/beaker/hypervisor/ec2_helper_spec.rb +23 -0
- data/spec/beaker/hypervisor/vagrant_spec.rb +22 -1
- data/spec/beaker/options/parser_spec.rb +4 -1
- data/spec/beaker/test_case_spec.rb +85 -1
- data/spec/mocks.rb +4 -0
- metadata +37 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
YmMxMzJlYWRhZjA1M2U5OWUxMGRhZWQ0NWU1Njg2MjA2OGUzNDRkOA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MzBiMjE4MGVmM2YyOTBiNmUzNzcwNGJhMWYyNTMzYzA0YmI1YzMzMQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
YzIyOWQ2MGVkNzk2MWZkZTY2NmQ4YzY1NmRjMGI4ZGMxMzEwNjc2YmI1NWFk
|
10
|
+
MjEzY2NhYjdhNjI0MjNjMDIwN2U4MDk5YzEwY2NhNDk2NTg0NWIxMmY4OTFj
|
11
|
+
YTI2MThkNGExNjVmYjIwNmU4OGY5YjQ5NDA0MTY4ZGE1OGI3NGE=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
YmE3OGU1MGIzMmRjMmM1MGM3MTcwMjdmODM5MzYyYzQ0ZWQyMWQ5N2E4YjQ3
|
14
|
+
NDE2MzQ1YmU0ZDIxOWEwYTAxZDliOTBkOTEwMTQ2ZDJmMTQyNmQ3ZTI3MWU5
|
15
|
+
YWQ1YWE3N2QwYmMyYTRkZjQwMjk2MGJlMGZjMjA4ODVjODEzMDQ=
|
data/.simplecov
CHANGED
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
|
data/lib/beaker/dsl/helpers.rb
CHANGED
@@ -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
|
476
|
-
bounce_service( host, '
|
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
|
492
|
-
bounce_service( host, '
|
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( "
|
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
|
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'] ||
|
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
|
-
|
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'],
|
data/lib/beaker/host/unix.rb
CHANGED
@@ -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
|
data/lib/beaker/host/windows.rb
CHANGED
@@ -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
|
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.
|
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-
|
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-
|
404
|
+
logger.warn("Attempting to disable iptables on non-supported platform: #{host.name}: #{host['platform']}")
|
404
405
|
end
|
405
406
|
end
|
406
407
|
end
|
data/lib/beaker/hypervisor.rb
CHANGED
@@ -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
|
-
|
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
|