beaker 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.simplecov +14 -0
- data/DOCUMENTING.md +167 -0
- data/Gemfile +3 -0
- data/LICENSE +17 -0
- data/README.md +332 -0
- data/Rakefile +121 -0
- data/beaker.gemspec +42 -0
- data/beaker.rb +10 -0
- data/bin/beaker +9 -0
- data/lib/beaker.rb +36 -0
- data/lib/beaker/answers.rb +29 -0
- data/lib/beaker/answers/version28.rb +104 -0
- data/lib/beaker/answers/version30.rb +194 -0
- data/lib/beaker/cli.rb +113 -0
- data/lib/beaker/command.rb +241 -0
- data/lib/beaker/command_factory.rb +21 -0
- data/lib/beaker/dsl.rb +85 -0
- data/lib/beaker/dsl/assertions.rb +87 -0
- data/lib/beaker/dsl/helpers.rb +625 -0
- data/lib/beaker/dsl/install_utils.rb +299 -0
- data/lib/beaker/dsl/outcomes.rb +99 -0
- data/lib/beaker/dsl/roles.rb +97 -0
- data/lib/beaker/dsl/structure.rb +63 -0
- data/lib/beaker/dsl/wrappers.rb +100 -0
- data/lib/beaker/host.rb +193 -0
- data/lib/beaker/host/aix.rb +15 -0
- data/lib/beaker/host/aix/file.rb +16 -0
- data/lib/beaker/host/aix/group.rb +35 -0
- data/lib/beaker/host/aix/user.rb +32 -0
- data/lib/beaker/host/unix.rb +54 -0
- data/lib/beaker/host/unix/exec.rb +15 -0
- data/lib/beaker/host/unix/file.rb +16 -0
- data/lib/beaker/host/unix/group.rb +40 -0
- data/lib/beaker/host/unix/pkg.rb +22 -0
- data/lib/beaker/host/unix/user.rb +32 -0
- data/lib/beaker/host/windows.rb +44 -0
- data/lib/beaker/host/windows/exec.rb +18 -0
- data/lib/beaker/host/windows/file.rb +15 -0
- data/lib/beaker/host/windows/group.rb +36 -0
- data/lib/beaker/host/windows/pkg.rb +26 -0
- data/lib/beaker/host/windows/user.rb +32 -0
- data/lib/beaker/hypervisor.rb +37 -0
- data/lib/beaker/hypervisor/aixer.rb +52 -0
- data/lib/beaker/hypervisor/blimper.rb +123 -0
- data/lib/beaker/hypervisor/fusion.rb +56 -0
- data/lib/beaker/hypervisor/solaris.rb +65 -0
- data/lib/beaker/hypervisor/vagrant.rb +118 -0
- data/lib/beaker/hypervisor/vcloud.rb +175 -0
- data/lib/beaker/hypervisor/vsphere.rb +80 -0
- data/lib/beaker/hypervisor/vsphere_helper.rb +200 -0
- data/lib/beaker/logger.rb +167 -0
- data/lib/beaker/network_manager.rb +73 -0
- data/lib/beaker/options_parsing.rb +323 -0
- data/lib/beaker/result.rb +55 -0
- data/lib/beaker/shared.rb +15 -0
- data/lib/beaker/shared/error_handler.rb +17 -0
- data/lib/beaker/shared/host_handler.rb +46 -0
- data/lib/beaker/shared/repetition.rb +28 -0
- data/lib/beaker/ssh_connection.rb +198 -0
- data/lib/beaker/test_case.rb +225 -0
- data/lib/beaker/test_config.rb +148 -0
- data/lib/beaker/test_suite.rb +288 -0
- data/lib/beaker/utils.rb +7 -0
- data/lib/beaker/utils/ntp_control.rb +42 -0
- data/lib/beaker/utils/repo_control.rb +92 -0
- data/lib/beaker/utils/setup_helper.rb +77 -0
- data/lib/beaker/utils/validator.rb +27 -0
- data/spec/beaker/command_spec.rb +94 -0
- data/spec/beaker/dsl/assertions_spec.rb +104 -0
- data/spec/beaker/dsl/helpers_spec.rb +230 -0
- data/spec/beaker/dsl/install_utils_spec.rb +70 -0
- data/spec/beaker/dsl/outcomes_spec.rb +43 -0
- data/spec/beaker/dsl/roles_spec.rb +86 -0
- data/spec/beaker/dsl/structure_spec.rb +60 -0
- data/spec/beaker/dsl/wrappers_spec.rb +52 -0
- data/spec/beaker/host_spec.rb +95 -0
- data/spec/beaker/logger_spec.rb +117 -0
- data/spec/beaker/options_parsing_spec.rb +37 -0
- data/spec/beaker/puppet_command_spec.rb +128 -0
- data/spec/beaker/ssh_connection_spec.rb +39 -0
- data/spec/beaker/test_case_spec.rb +6 -0
- data/spec/beaker/test_suite_spec.rb +44 -0
- data/spec/mocks_and_helpers.rb +34 -0
- data/spec/spec_helper.rb +15 -0
- metadata +359 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'test/unit/assertions'
|
2
|
+
|
3
|
+
module Beaker
|
4
|
+
module DSL
|
5
|
+
# Any custom assertions for Test::Unit or minitest live here. You may
|
6
|
+
# include them in your own testing if you wish, override them, or re-open
|
7
|
+
# the class to register new ones for use within
|
8
|
+
# {Beaker::TestCase}.
|
9
|
+
#
|
10
|
+
# You may use any test/unit assertion within your assertion. The
|
11
|
+
# assertion below assumes access to the method #result which will
|
12
|
+
# contain the output (according to the interface defined in
|
13
|
+
# {Beaker::Result}). When writing your own, to make them more
|
14
|
+
# portable and less brittle it is recommended that you pass the result
|
15
|
+
# or direct object for asserting against into your assertion.
|
16
|
+
#
|
17
|
+
module Assertions
|
18
|
+
include Test::Unit::Assertions
|
19
|
+
|
20
|
+
# Make assertions about the content of console output.
|
21
|
+
#
|
22
|
+
# By default, each line of +output+ is assumed to come from STDOUT.
|
23
|
+
# You may specify the stream explicitly by annotating the line with a
|
24
|
+
# stream marker. (If your line literally requires any stream marker at
|
25
|
+
# the beginning of a line, you must prefix the line with an explicit
|
26
|
+
# stream marker.) The currently recognized markers are:
|
27
|
+
#
|
28
|
+
# * "STDOUT> "
|
29
|
+
# * "STDERR> "
|
30
|
+
# * "OUT> "
|
31
|
+
# * "ERR> "
|
32
|
+
# * "1> "
|
33
|
+
# * "2> "
|
34
|
+
#
|
35
|
+
# Any leading common indentation is automatically removed from the
|
36
|
+
# +output+ parameter. For cases where this matters (e.g. every line
|
37
|
+
# should be indented), you should prefix the line with an explicit
|
38
|
+
# stream marker.
|
39
|
+
#
|
40
|
+
# @example Assert order of interleaved output streams
|
41
|
+
# !!!plain
|
42
|
+
# assert_output <<-CONSOLE
|
43
|
+
# STDOUT> 0123456789
|
44
|
+
# STDERR> ^- This is left aligned
|
45
|
+
# STDOUT> 01234567890
|
46
|
+
# STDERR> ^- This is indented 2 characters.
|
47
|
+
# CONSOLE
|
48
|
+
#
|
49
|
+
# @example Assert all content went to STDOUT
|
50
|
+
# !!!plain
|
51
|
+
# assert_output <<-CONSOLE
|
52
|
+
# 0123456789
|
53
|
+
# ^- This is left aligned
|
54
|
+
# 01234567890
|
55
|
+
# ^- This is indented 2 characters.
|
56
|
+
# CONSOLE
|
57
|
+
#
|
58
|
+
# @param [String] exp_out The expected console output, optionally
|
59
|
+
# annotated with stream markers.
|
60
|
+
# @param [String] msg An explanatory message about why the test
|
61
|
+
# failure is relevant.
|
62
|
+
def assert_output(exp_out, msg='Output lines did not match')
|
63
|
+
# Remove the minimal consistent indentation from the input;
|
64
|
+
# useful for clean HEREDOCs.
|
65
|
+
indentation = exp_out.lines.map { |line| line[/^ */].length }.min
|
66
|
+
cleaned_exp = exp_out.gsub(/^ {#{indentation}}/, '')
|
67
|
+
|
68
|
+
# Divide output based on expected destination
|
69
|
+
out, err = cleaned_exp.lines.partition do |line|
|
70
|
+
line !~ /^((STD)?ERR|2)> /
|
71
|
+
end
|
72
|
+
our_out, our_err, our_output = [
|
73
|
+
out.join, err.join, cleaned_exp
|
74
|
+
].map do |str|
|
75
|
+
str.gsub(/^((STD)?(ERR|OUT)|[12])> /, '')
|
76
|
+
end
|
77
|
+
|
78
|
+
# Exercise assertions about output
|
79
|
+
assert_equal our_output, (result.nil? ? '' : result.output), msg
|
80
|
+
assert_equal our_out, (result.nil? ? '' : result.stdout),
|
81
|
+
'The contents of STDOUT did not match expectations'
|
82
|
+
assert_equal our_err, (result.nil? ? '' : result.stderr),
|
83
|
+
'The contents of STDERR did not match expectations'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,625 @@
|
|
1
|
+
require 'resolv'
|
2
|
+
require 'beaker/dsl/outcomes'
|
3
|
+
|
4
|
+
module Beaker
|
5
|
+
module DSL
|
6
|
+
# This is the heart of the Puppet Acceptance DSL. Here you find a helper
|
7
|
+
# to proxy commands to hosts, more commands to move files between hosts
|
8
|
+
# and execute remote scripts, confine test cases to certain hosts and
|
9
|
+
# prepare the state of a test case.
|
10
|
+
#
|
11
|
+
# To mix this is into a class you need the following:
|
12
|
+
# * a method *hosts* that yields any hosts implementing
|
13
|
+
# {Beaker::Host}'s interface to act upon.
|
14
|
+
# * a method *logger* that yields a logger implementing
|
15
|
+
# {Beaker::Logger}'s interface.
|
16
|
+
#
|
17
|
+
#
|
18
|
+
# @api dsl
|
19
|
+
module Helpers
|
20
|
+
# @!macro common_opts
|
21
|
+
# @param [Hash{Symbol=>String}] opts Options to alter execution.
|
22
|
+
# @option opts [Boolean] :silent (false) Do not produce log output
|
23
|
+
# @option opts [Array<Fixnum>] :acceptable_exit_codes ([0]) An array
|
24
|
+
# (or range) of integer exit codes that should be considered
|
25
|
+
# acceptable. An error will be thrown if the exit code does not
|
26
|
+
# match one of the values in this list.
|
27
|
+
# @option opts [Hash{String=>String}] :environment ({}) These will be
|
28
|
+
# treated as extra environment variables that should be set before
|
29
|
+
# running the command.
|
30
|
+
#
|
31
|
+
|
32
|
+
# The primary method for executing commands *on* some set of hosts.
|
33
|
+
#
|
34
|
+
# @param [Host, Array<Host>] host One or more hosts to act upon.
|
35
|
+
# @param [String, Command] command The command to execute on *host*.
|
36
|
+
# @param [Proc] block Additional actions or assertions.
|
37
|
+
# @!macro common_opts
|
38
|
+
#
|
39
|
+
# @example Most basic usage
|
40
|
+
# on hosts, 'ls /tmp'
|
41
|
+
#
|
42
|
+
# @example Allowing additional exit codes to pass
|
43
|
+
# on agents, 'puppet agent -t', :acceptable_exit_codes => [0,2]
|
44
|
+
#
|
45
|
+
# @example Using the returned result for any kind of checking
|
46
|
+
# if on(host, 'ls -la ~').stdout =~ /\.bin/
|
47
|
+
# ...do some action...
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# @example Using TestCase helpers from within a test.
|
51
|
+
# agents.each do |agent|
|
52
|
+
# on agent, 'cat /etc/puppet/puppet.conf' do
|
53
|
+
# assert_match stdout, /server = #{master}/, 'WTF Mate'
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# @return [Result] An object representing the outcome of *command*.
|
58
|
+
# @raise [FailTest] Raises an exception if *command* obviously fails.
|
59
|
+
def on(host, command, opts = {}, &block)
|
60
|
+
unless command.is_a? Command
|
61
|
+
cmd_opts = opts[:environment] ? { 'ENV' => opts.delete(:environment) } : Hash.new
|
62
|
+
command = Command.new(command.to_s, [], cmd_opts)
|
63
|
+
end
|
64
|
+
if host.is_a? Array
|
65
|
+
host.map { |h| on h, command, opts, &block }
|
66
|
+
else
|
67
|
+
@result = host.exec(command, opts)
|
68
|
+
|
69
|
+
# Also, let additional checking be performed by the caller.
|
70
|
+
yield self if block_given?
|
71
|
+
|
72
|
+
return @result
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Move a file from a remote to a local path
|
77
|
+
# @note If using {Beaker::Host} for the hosts *scp* is not
|
78
|
+
# required on the system as it uses Ruby's net/scp library. The
|
79
|
+
# net-scp gem however is required (and specified in the gemspec).
|
80
|
+
#
|
81
|
+
# @param [Host, #do_scp_from] host One or more hosts (or some object
|
82
|
+
# that responds like
|
83
|
+
# {Beaker::Host#do_scp_from}.
|
84
|
+
# @param [String] from_path A remote path to a file.
|
85
|
+
# @param [String] to_path A local path to copy *from_path* to.
|
86
|
+
# @!macro common_opts
|
87
|
+
#
|
88
|
+
# @return [Result] Returns the result of the SCP operation
|
89
|
+
def scp_from host, from_path, to_path, opts = {}
|
90
|
+
if host.is_a? Array
|
91
|
+
host.each { |h| scp_from h, from_path, to_path, opts }
|
92
|
+
else
|
93
|
+
@result = host.do_scp_from(from_path, to_path, opts)
|
94
|
+
@result.log logger
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Move a local file to a remote host
|
99
|
+
# @note If using {Beaker::Host} for the hosts *scp* is not
|
100
|
+
# required on the system as it uses Ruby's net/scp library. The
|
101
|
+
# net-scp gem however is required (and specified in the gemspec.
|
102
|
+
#
|
103
|
+
# @param [Host, #do_scp_to] host One or more hosts (or some object
|
104
|
+
# that responds like
|
105
|
+
# {Beaker::Host#do_scp_to}.
|
106
|
+
# @param [String] from_path A local path to a file.
|
107
|
+
# @param [String] to_path A remote path to copy *from_path* to.
|
108
|
+
# @!macro common_opts
|
109
|
+
#
|
110
|
+
# @return [Result] Returns the result of the SCP operation
|
111
|
+
def scp_to host, from_path, to_path, opts = {}
|
112
|
+
if host.is_a? Array
|
113
|
+
host.each { |h| scp_to h, from_path, to_path, opts }
|
114
|
+
else
|
115
|
+
@result = host.do_scp_to(from_path, to_path, opts)
|
116
|
+
@result.log logger
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Check to see if a package is installed on a remote host
|
121
|
+
#
|
122
|
+
# @param [Host] host A host object
|
123
|
+
# @param [String] package_name Name of the package to check for.
|
124
|
+
#
|
125
|
+
# @return [Boolean] true/false if the package is found
|
126
|
+
def check_for_package host, package_name
|
127
|
+
host.check_for_package package_name
|
128
|
+
end
|
129
|
+
|
130
|
+
# Install a package on a host
|
131
|
+
#
|
132
|
+
# @param [Host] host A host object
|
133
|
+
# @param [String] package_name Name of the package to install
|
134
|
+
#
|
135
|
+
# @return [Result] An object representing the outcome of *install command*.
|
136
|
+
def install_package host, package_name
|
137
|
+
host.install_package package_name
|
138
|
+
end
|
139
|
+
|
140
|
+
# Create a remote file out of a string
|
141
|
+
# @note This method uses Tempfile in Ruby's STDLIB as well as {#scp_to}.
|
142
|
+
#
|
143
|
+
# @param [Host, #do_scp_to] hosts One or more hosts (or some object
|
144
|
+
# that responds like
|
145
|
+
# {Beaker::Host#do_scp_from}.
|
146
|
+
# @param [String] file_path A remote path to place *file_content* at.
|
147
|
+
# @param [String] file_content The contents of the file to be placed.
|
148
|
+
# @!macro common_opts
|
149
|
+
#
|
150
|
+
# @return [Result] Returns the result of the underlying SCP operation.
|
151
|
+
def create_remote_file(hosts, file_path, file_content, opts = {})
|
152
|
+
Tempfile.open 'beaker' do |tempfile|
|
153
|
+
File.open(tempfile.path, 'w') {|file| file.puts file_content }
|
154
|
+
|
155
|
+
scp_to hosts, tempfile.path, file_path, opts
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Move a local script to a remote host and execute it
|
160
|
+
# @note this relies on {#on} and {#scp_to}
|
161
|
+
#
|
162
|
+
# @param [Host, #do_scp_to] host One or more hosts (or some object
|
163
|
+
# that responds like
|
164
|
+
# {Beaker::Host#do_scp_from}.
|
165
|
+
# @param [String] script A local path to find an executable script at.
|
166
|
+
# @!macro common_opts
|
167
|
+
# @param [Proc] block Additional tests to run after script has executed
|
168
|
+
#
|
169
|
+
# @return [Result] Returns the result of the underlying SCP operation.
|
170
|
+
def run_script_on(host, script, opts = {}, &block)
|
171
|
+
# this is unsafe as it uses the File::SEPARATOR will be set to that
|
172
|
+
# of the coordinator node. This works for us because we use cygwin
|
173
|
+
# which will properly convert the paths. Otherwise this would not
|
174
|
+
# work for running tests on a windows machine when the coordinator
|
175
|
+
# that the harness is running on is *nix. We should use
|
176
|
+
# {Beaker::Host#temp_path} instead. TODO
|
177
|
+
remote_path = File.join("", "tmp", File.basename(script))
|
178
|
+
|
179
|
+
scp_to host, script, remote_path
|
180
|
+
on host, remote_path, opts, &block
|
181
|
+
end
|
182
|
+
|
183
|
+
# Limit the hosts a test case is run against
|
184
|
+
# @note This will modify the {Beaker::TestCase#hosts} member
|
185
|
+
# in place unless an array of hosts is passed into it and
|
186
|
+
# {Beaker::TestCase#logger} yielding an object that responds
|
187
|
+
# like {Beaker::Logger#warn}, as well as
|
188
|
+
# {Beaker::DSL::Outcomes#skip_test}, and optionally
|
189
|
+
# {Beaker::TestCase#hosts}.
|
190
|
+
#
|
191
|
+
# @param [Symbol] type The type of confinement to do. Valid parameters
|
192
|
+
# are *:to* to confine the hosts to only those that
|
193
|
+
# match *criteria* or *:except* to confine the test
|
194
|
+
# case to only those hosts that do not match
|
195
|
+
# criteria.
|
196
|
+
# @param [Hash{Symbol,String=>String,Regexp,Array<String,Regexp>}]
|
197
|
+
# criteria Specify the criteria with which a host should be
|
198
|
+
# considered for inclusion or exclusion. The key is any attribute
|
199
|
+
# of the host that will be yielded by {Beaker::Host#[]}.
|
200
|
+
# The value can be any string/regex or array of strings/regexp.
|
201
|
+
# The values are compared using {Enumerable#any?} so that if one
|
202
|
+
# value of an array matches the host is considered a match for that
|
203
|
+
# criteria.
|
204
|
+
# @param [Array<Host>] host_array This creatively named parameter is
|
205
|
+
# an optional array of hosts to confine to. If not passed in, this
|
206
|
+
# method will modify {Beaker::TestCase#hosts} in place.
|
207
|
+
# @param [Proc] block Addition checks to determine suitability of hosts
|
208
|
+
# for confinement. Each host that is still valid after checking
|
209
|
+
# *criteria* is then passed in turn into this block. The block
|
210
|
+
# should return true if the host matches this additional criteria.
|
211
|
+
#
|
212
|
+
# @example Basic usage to confine to debian OSes.
|
213
|
+
# confine :to, :platform => 'debian'
|
214
|
+
#
|
215
|
+
# @example Confining to anything but Windows and Solaris
|
216
|
+
# confine :except, :platform => ['windows', 'solaris']
|
217
|
+
#
|
218
|
+
# @example Using additional block to confine to Solaris global zone.
|
219
|
+
# confine :to, :platform => 'solaris' do |solaris|
|
220
|
+
# on( solaris, 'zonename' ) =~ /global/
|
221
|
+
# end
|
222
|
+
#
|
223
|
+
# @return [Array<Host>] Returns an array of hosts that are still valid
|
224
|
+
# targets for this tests case.
|
225
|
+
# @raise [SkipTest] Raises skip test if there are no valid hosts for
|
226
|
+
# this test case after confinement.
|
227
|
+
def confine(type, criteria, host_array = nil, &block)
|
228
|
+
provided_hosts = host_array ? true : false
|
229
|
+
hosts_to_modify = host_array || hosts
|
230
|
+
criteria.each_pair do |property, value|
|
231
|
+
case type
|
232
|
+
when :except
|
233
|
+
hosts_to_modify = hosts_to_modify.reject do |host|
|
234
|
+
inspect_host host, property, value
|
235
|
+
end
|
236
|
+
if block_given?
|
237
|
+
hosts_to_modify = hosts_to_modify.reject do |host|
|
238
|
+
yield host
|
239
|
+
end
|
240
|
+
end
|
241
|
+
when :to
|
242
|
+
hosts_to_modify = hosts_to_modify.select do |host|
|
243
|
+
inspect_host host, property, value
|
244
|
+
end
|
245
|
+
if block_given?
|
246
|
+
hosts_to_modify = hosts_to_modify.select do |host|
|
247
|
+
yield host
|
248
|
+
end
|
249
|
+
end
|
250
|
+
else
|
251
|
+
raise "Unknown option #{type}"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
if hosts_to_modify.empty?
|
255
|
+
logger.warn "No suitable hosts with: #{criteria.inspect}"
|
256
|
+
skip_test 'No suitable hosts found'
|
257
|
+
end
|
258
|
+
self.hosts = hosts_to_modify
|
259
|
+
hosts_to_modify
|
260
|
+
end
|
261
|
+
|
262
|
+
# @!visibility private
|
263
|
+
def inspect_host(host, property, one_or_more_values)
|
264
|
+
values = Array(one_or_more_values)
|
265
|
+
return values.any? do |value|
|
266
|
+
true_false = false
|
267
|
+
case value
|
268
|
+
when String
|
269
|
+
true_false = host[property.to_s].include? value
|
270
|
+
when Regexp
|
271
|
+
true_false = host[property.to_s] =~ value
|
272
|
+
end
|
273
|
+
true_false
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
|
278
|
+
# Test Puppet running in a certain run mode with specific options.
|
279
|
+
# This ensures the following steps are performed:
|
280
|
+
# 1. The pre-test Puppet configuration is backed up
|
281
|
+
# 2. A new Puppet configuraton file is layed down
|
282
|
+
# 3. Puppet is started or restarted in the specified run mode
|
283
|
+
# 4. Ensure Puppet has started correctly
|
284
|
+
# 5. Further tests are yielded to
|
285
|
+
# 6. Revert Puppet to the pre-test state
|
286
|
+
# 7. Testing artifacts are saved in a folder named for the test
|
287
|
+
#
|
288
|
+
# @param [Host] hosts One object that act like Host
|
289
|
+
#
|
290
|
+
# @param [Hash{Symbol=>String}]
|
291
|
+
# config_opts Represent puppet settings.
|
292
|
+
# Sections of the puppet.conf may be
|
293
|
+
# specified, if no section is specified the
|
294
|
+
# a puppet.conf file will be written with the
|
295
|
+
# options put in a section named after [mode].
|
296
|
+
#
|
297
|
+
# @param [Block] block The point of this method, yields so
|
298
|
+
# tests may be ran. After the block is finished
|
299
|
+
# puppet will revert to a previous state.
|
300
|
+
#
|
301
|
+
# @example A simple use case to ensure a master is running
|
302
|
+
# with_puppet_running_on( master ) do
|
303
|
+
# ...tests that require a master...
|
304
|
+
# end
|
305
|
+
#
|
306
|
+
# @example Fully utilizing the possiblities of config options
|
307
|
+
# with_puppet_running_on( master,
|
308
|
+
# :main => {:logdest => '/var/blah'},
|
309
|
+
# :master => {:masterlog => '/elswhere'},
|
310
|
+
# :agent => {:server => 'localhost'} ) do
|
311
|
+
#
|
312
|
+
# ...tests to be ran...
|
313
|
+
# end
|
314
|
+
#
|
315
|
+
# @api dsl
|
316
|
+
def with_puppet_running_on host, conf_opts, testdir = host.tmpdir(File.basename(@path)), &block
|
317
|
+
begin
|
318
|
+
backup_file host, host['puppetpath'], testdir, 'puppet.conf'
|
319
|
+
lay_down_new_puppet_conf host, conf_opts, testdir
|
320
|
+
|
321
|
+
if host.is_pe?
|
322
|
+
bounce_service( 'pe-httpd' )
|
323
|
+
|
324
|
+
else
|
325
|
+
start_puppet_from_source_on!( host )
|
326
|
+
end
|
327
|
+
|
328
|
+
yield self if block_given?
|
329
|
+
ensure
|
330
|
+
restore_puppet_conf_from_backup( host )
|
331
|
+
|
332
|
+
if host.is_pe?
|
333
|
+
bounce_service( 'pe-httpd' )
|
334
|
+
|
335
|
+
else
|
336
|
+
stop_puppet_from_source_on( host )
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# @!visibility private
|
342
|
+
def restore_puppet_conf_from_backup( host )
|
343
|
+
puppetpath = host['puppetpath']
|
344
|
+
|
345
|
+
host.exec( Command.new( "if [ -f #{puppetpath}/puppet.conf.bak ]; then " +
|
346
|
+
"cat #{puppetpath}/puppet.conf.bak > " +
|
347
|
+
"#{puppetpath}/puppet.conf; " +
|
348
|
+
"rm -rf #{puppetpath}/puppet.conf.bak; " +
|
349
|
+
"fi" ) )
|
350
|
+
end
|
351
|
+
|
352
|
+
# @!visibility private
|
353
|
+
def backup_file host, current_dir, new_dir, filename = 'puppet.conf'
|
354
|
+
old_location = current_dir + '/' + filename
|
355
|
+
new_location = new_dir + '/' + filename
|
356
|
+
|
357
|
+
host.exec( Command.new( "cp #{old_location} #{new_location}" ) )
|
358
|
+
end
|
359
|
+
|
360
|
+
# @!visibility private
|
361
|
+
def start_puppet_from_source_on! host
|
362
|
+
host.exec( Command.new( puppet( 'master' ) ) )
|
363
|
+
|
364
|
+
logger.debug 'Waiting for the puppet master to start'
|
365
|
+
unless port_open_within?( host, 8140, 10 )
|
366
|
+
raise Beaker::DSL::FailTest, 'Puppet master did not start in a timely fashion'
|
367
|
+
end
|
368
|
+
logger.debug 'The puppet master has started'
|
369
|
+
end
|
370
|
+
|
371
|
+
# @!visibility private
|
372
|
+
def stop_puppet_from_source_on( host )
|
373
|
+
host.exec( Command.new( 'kill $(cat `puppet master --configprint pidfile`)' ) )
|
374
|
+
end
|
375
|
+
|
376
|
+
# @!visibility private
|
377
|
+
def lay_down_new_puppet_conf( host, configuration_options, testdir )
|
378
|
+
new_conf = puppet_conf_for( host )
|
379
|
+
create_remote_file host, "#{testdir}/puppet.conf", new_conf.to_s
|
380
|
+
|
381
|
+
host.exec( Command.new( "cat #{testdir}/puppet.conf > #{host['puppetpath']}/puppet.conf", :silent => true ) )
|
382
|
+
host.exec( Command.new( "cat #{host['puppetpath']}/puppet.conf" ) )
|
383
|
+
end
|
384
|
+
|
385
|
+
# @!visibility private
|
386
|
+
def puppet_conf_for host, conf_opts
|
387
|
+
puppetconf = host.exec( Command.new( "cat #{host['puppetpath']}/puppet.conf" ) ).stdout
|
388
|
+
new_conf = IniFile.new( puppetconf ).merge( conf_opts )
|
389
|
+
|
390
|
+
new_conf
|
391
|
+
end
|
392
|
+
|
393
|
+
# @!visibility private
|
394
|
+
def bounce_service host, service
|
395
|
+
# Any reason to not
|
396
|
+
# host.exec puppet_resource( 'service', service, 'ensure=stopped' )
|
397
|
+
# host.exec puppet_resource( 'service', service, 'ensure=running' )
|
398
|
+
host.exec( Command.new( "/etc/init.d/#{service} restart" ) )
|
399
|
+
end
|
400
|
+
|
401
|
+
# Blocks until the port is open on the host specified, returns false
|
402
|
+
# on failure
|
403
|
+
def port_open_within?( host, port = 8140, seconds = 120 )
|
404
|
+
repeat_for( seconds ) do
|
405
|
+
host.port_open?( port )
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
# Runs 'puppet apply' on a remote host, piping manifest through stdin
|
410
|
+
#
|
411
|
+
# @param [Host] host The host that this command should be run on
|
412
|
+
#
|
413
|
+
# @param [String] manifest The puppet manifest to apply
|
414
|
+
#
|
415
|
+
# @!macro common_opts
|
416
|
+
# @option opts [Boolean] :parseonly (false) If this key is true, the
|
417
|
+
# "--parseonly" command line parameter will
|
418
|
+
# be passed to the 'puppet apply' command.
|
419
|
+
#
|
420
|
+
# @option opts [Boolean] :trace (false) If this key exists in the Hash,
|
421
|
+
# the "--trace" command line parameter will be
|
422
|
+
# passed to the 'puppet apply' command.
|
423
|
+
#
|
424
|
+
# @option opts [Boolean] :catch_failures (false) By default
|
425
|
+
# "puppet --apply" will exit with 0,
|
426
|
+
# which does not count as a test
|
427
|
+
# failure, even if there were errors applying
|
428
|
+
# the manifest. This option enables detailed
|
429
|
+
# exit codes and causes a test failure if
|
430
|
+
# "puppet --apply" indicates there was a
|
431
|
+
# failure during its execution.
|
432
|
+
#
|
433
|
+
# @param [Block] block This method will yield to a block of code passed
|
434
|
+
# by the caller; this can be used for additional
|
435
|
+
# validation, etc.
|
436
|
+
#
|
437
|
+
def apply_manifest_on(host, manifest, opts = {}, &block)
|
438
|
+
on_options = {:stdin => manifest + "\n"}
|
439
|
+
on_options[:acceptable_exit_codes] = opts.delete(:acceptable_exit_codes)
|
440
|
+
args = ["--verbose"]
|
441
|
+
args << "--parseonly" if opts[:parseonly]
|
442
|
+
args << "--trace" if opts[:trace]
|
443
|
+
|
444
|
+
if opts[:catch_failures]
|
445
|
+
args << '--detailed-exitcodes'
|
446
|
+
|
447
|
+
# From puppet help:
|
448
|
+
# "... an exit code of '2' means there were changes, an exit code of
|
449
|
+
# '4' means there were failures during the transaction, and an exit
|
450
|
+
# code of '6' means there were both changes and failures."
|
451
|
+
# We're after failures specifically so catch exit codes 4 and 6 only.
|
452
|
+
on_options[:acceptable_exit_codes] |= [0, 2]
|
453
|
+
end
|
454
|
+
|
455
|
+
# Not really thrilled with this implementation, might want to improve it
|
456
|
+
# later. Basically, there is a magic trick in the constructor of
|
457
|
+
# PuppetCommand which allows you to pass in a Hash for the last value in
|
458
|
+
# the *args Array; if you do so, it will be treated specially. So, here
|
459
|
+
# we check to see if our caller passed us a hash of environment variables
|
460
|
+
# that they want to set for the puppet command. If so, we set the final
|
461
|
+
# value of *args to a new hash with just one entry (the value of which
|
462
|
+
# is our environment variables hash)
|
463
|
+
if opts.has_key?(:environment)
|
464
|
+
args << { :environment => opts[:environment]}
|
465
|
+
end
|
466
|
+
|
467
|
+
on host, puppet( 'apply', *args), on_options, &block
|
468
|
+
end
|
469
|
+
|
470
|
+
# @deprecated
|
471
|
+
def run_agent_on(host, arg='--no-daemonize --verbose --onetime --test',
|
472
|
+
options={}, &block)
|
473
|
+
if host.is_a? Array
|
474
|
+
host.each { |h| run_agent_on h, arg, options, &block }
|
475
|
+
else
|
476
|
+
on host, puppet_agent(arg), options, &block
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# FIX: this should be moved into host/platform
|
481
|
+
# @visibility private
|
482
|
+
def run_cron_on(host, action, user, entry="", &block)
|
483
|
+
platform = host['platform']
|
484
|
+
if platform.include?('solaris') || platform.include?('aix') then
|
485
|
+
case action
|
486
|
+
when :list then args = '-l'
|
487
|
+
when :remove then args = '-r'
|
488
|
+
when :add
|
489
|
+
on( host,
|
490
|
+
"echo '#{entry}' > /var/spool/cron/crontabs/#{user}",
|
491
|
+
&block )
|
492
|
+
end
|
493
|
+
|
494
|
+
else # default for GNU/Linux platforms
|
495
|
+
case action
|
496
|
+
when :list then args = '-l -u'
|
497
|
+
when :remove then args = '-r -u'
|
498
|
+
when :add
|
499
|
+
on( host,
|
500
|
+
"echo '#{entry}' > /tmp/#{user}.cron && " +
|
501
|
+
"crontab -u #{user} /tmp/#{user}.cron",
|
502
|
+
&block )
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
if args
|
507
|
+
case action
|
508
|
+
when :list, :remove then on(host, "crontab #{args} #{user}", &block)
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
# This method accepts a block and using the puppet resource 'host' will
|
514
|
+
# setup host aliases before and after that block.
|
515
|
+
#
|
516
|
+
# A teardown step is also added to make sure unstubbing of the host is
|
517
|
+
# removed always.
|
518
|
+
#
|
519
|
+
# @param machine [String] the host to execute this stub
|
520
|
+
# @param hosts [Hash{String=>String}] a hash containing the host to ip
|
521
|
+
# mappings
|
522
|
+
# @example Stub puppetlabs.com on the master to 127.0.0.1
|
523
|
+
# stub_hosts_on(master, 'puppetlabs.com' => '127.0.0.1')
|
524
|
+
def stub_hosts_on(machine, ip_spec)
|
525
|
+
ip_spec.each do |host, ip|
|
526
|
+
logger.notify("Stubbing host #{host} to IP #{ip} on machine #{machine}")
|
527
|
+
on( machine,
|
528
|
+
puppet('resource', 'host', host, 'ensure=present', "ip=#{ip}") )
|
529
|
+
end
|
530
|
+
|
531
|
+
teardown do
|
532
|
+
ip_spec.each do |host, ip|
|
533
|
+
logger.notify("Unstubbing host #{host} to IP #{ip} on machine #{machine}")
|
534
|
+
on( machine,
|
535
|
+
puppet('resource', 'host', host, 'ensure=absent') )
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
# This wraps the method `stub_hosts_on` and makes the stub specific to
|
541
|
+
# the forge alias.
|
542
|
+
#
|
543
|
+
# @param machine [String] the host to perform the stub on
|
544
|
+
def stub_forge_on(machine)
|
545
|
+
@forge_ip ||= Resolv.getaddress(forge)
|
546
|
+
stub_hosts_on(machine, 'forge.puppetlabs.com' => @forge_ip)
|
547
|
+
end
|
548
|
+
def sleep_until_puppetdb_started(host)
|
549
|
+
curl_with_retries("start puppetdb", host, "http://localhost:8080", 0, 120)
|
550
|
+
curl_with_retries("start puppetdb (ssl)",
|
551
|
+
host, "https://#{host.node_name}:8081", [35, 60])
|
552
|
+
end
|
553
|
+
|
554
|
+
def curl_with_retries(desc, host, url, desired_exit_codes, max_retries = 60, retry_interval = 1)
|
555
|
+
retry_command(desc, host, "curl #{url}", desired_exit_codes, max_retries, retry_interval)
|
556
|
+
end
|
557
|
+
|
558
|
+
def retry_command(desc, host, command, desired_exit_codes = 0, max_retries = 60, retry_interval = 1)
|
559
|
+
desired_exit_codes = [desired_exit_codes].flatten
|
560
|
+
result = on host, command, :acceptable_exit_codes => (0...127)
|
561
|
+
num_retries = 0
|
562
|
+
until desired_exit_codes.include?(result.exit_code)
|
563
|
+
sleep retry_interval
|
564
|
+
result = on host, command, :acceptable_exit_codes => (0...127)
|
565
|
+
num_retries += 1
|
566
|
+
if (num_retries > max_retries)
|
567
|
+
fail("Unable to #{desc}")
|
568
|
+
end
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
#stops the puppet agent running on the host
|
573
|
+
def stop_agent(agent)
|
574
|
+
vardir = agent.puppet['vardir']
|
575
|
+
agent_running = true
|
576
|
+
while agent_running
|
577
|
+
result = on agent, "[ -e '#{vardir}/state/agent_catalog_run.lock' ]", :acceptable_exit_codes => [0,1]
|
578
|
+
agent_running = (result.exit_code == 0)
|
579
|
+
sleep 2 unless agent_running
|
580
|
+
end
|
581
|
+
|
582
|
+
if agent['platform'].include?('solaris')
|
583
|
+
on(agent, '/usr/sbin/svcadm disable -s svc:/network/pe-puppet:default')
|
584
|
+
elsif agent['platform'].include?('aix')
|
585
|
+
on(agent, '/usr/bin/stopsrc -s pe-puppet')
|
586
|
+
elsif agent['platform'].include?('windows')
|
587
|
+
on(agent, 'net stop pe-puppet', :acceptable_exit_codes => [0,2])
|
588
|
+
else
|
589
|
+
# For the sake of not passing the PE version into this method,
|
590
|
+
# we just query the system to find out which service we want to
|
591
|
+
# stop
|
592
|
+
result = on agent, "[ -e /etc/init.d/pe-puppet-agent ]", :acceptable_exit_codes => [0,1]
|
593
|
+
service = (result.exit_code == 0) ? 'pe-puppet-agent' : 'pe-puppet'
|
594
|
+
on(agent, "/etc/init.d/#{service} stop")
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
|
599
|
+
#wait for a given host to appear in the dashboard
|
600
|
+
def wait_for_host_in_dashboard(host)
|
601
|
+
hostname = host.node_name
|
602
|
+
retry_command("Wait for #{hostname} to be in the console", dashboard, "! curl --sslv3 -k -I https://#{dashboard}/nodes/#{hostname} | grep '404 Not Found'")
|
603
|
+
end
|
604
|
+
|
605
|
+
|
606
|
+
#prompt the master to sign certs then check to confirm the cert for this host is signed
|
607
|
+
def sign_certificate(host)
|
608
|
+
return if [master, dashboard, database].include? host
|
609
|
+
|
610
|
+
hostname = Regexp.escape host.node_name
|
611
|
+
|
612
|
+
last_sleep = 0
|
613
|
+
next_sleep = 1
|
614
|
+
(0..10).each do |i|
|
615
|
+
fail_test("Failed to sign cert for #{hostname}") if i == 10
|
616
|
+
|
617
|
+
on master, puppet("cert --sign --all"), :acceptable_exit_codes => [0,24]
|
618
|
+
break if on(master, puppet("cert --list --all")).stdout =~ /\+ "?#{hostname}"?/
|
619
|
+
sleep next_sleep
|
620
|
+
(last_sleep, next_sleep) = next_sleep, last_sleep+next_sleep
|
621
|
+
end
|
622
|
+
end
|
623
|
+
end
|
624
|
+
end
|
625
|
+
end
|