beaker 0.0.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 +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
|