taste_tester 0.0.12 → 0.0.17
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 +5 -5
- data/README.md +62 -10
- data/bin/taste-tester +93 -32
- data/lib/taste_tester/client.rb +129 -18
- data/lib/taste_tester/commands.rb +206 -16
- data/lib/taste_tester/config.rb +22 -4
- data/lib/taste_tester/exceptions.rb +9 -0
- data/lib/taste_tester/hooks.rb +24 -16
- data/lib/taste_tester/host.rb +286 -118
- data/lib/taste_tester/locallink.rb +8 -6
- data/lib/taste_tester/logging.rb +8 -6
- data/lib/taste_tester/noop.rb +69 -0
- data/lib/taste_tester/server.rb +30 -11
- data/lib/taste_tester/ssh.rb +10 -31
- data/lib/taste_tester/ssh_util.rb +127 -0
- data/lib/taste_tester/state.rb +30 -8
- data/lib/taste_tester/tunnel.rb +167 -37
- data/lib/taste_tester/windows.rb +1 -0
- data/scripts/taste-untester +8 -0
- data/scripts/taste-untester.ps1 +85 -0
- metadata +22 -19
@@ -14,12 +14,12 @@
|
|
14
14
|
# See the License for the specific language governing permissions and
|
15
15
|
# limitations under the License.
|
16
16
|
|
17
|
-
# rubocop:disable UnusedBlockArgument, UnusedMethodArgument
|
18
17
|
require 'taste_tester/server'
|
19
18
|
require 'taste_tester/host'
|
20
19
|
require 'taste_tester/config'
|
21
20
|
require 'taste_tester/client'
|
22
21
|
require 'taste_tester/logging'
|
22
|
+
require 'taste_tester/exceptions'
|
23
23
|
|
24
24
|
module TasteTester
|
25
25
|
# Functionality dispatch
|
@@ -29,6 +29,7 @@ module TasteTester
|
|
29
29
|
def self.start
|
30
30
|
server = TasteTester::Server.new
|
31
31
|
return if TasteTester::Server.running?
|
32
|
+
|
32
33
|
server.start
|
33
34
|
end
|
34
35
|
|
@@ -46,7 +47,12 @@ module TasteTester
|
|
46
47
|
server = TasteTester::Server.new
|
47
48
|
if TasteTester::Server.running?
|
48
49
|
logger.warn("Local taste-tester server running on port #{server.port}")
|
49
|
-
if server.
|
50
|
+
if TasteTester::Config.no_repo && server.last_upload_time
|
51
|
+
logger.warn("Last upload time was #{server.last_upload_time}")
|
52
|
+
elsif !TasteTester::Config.no_repo && server.latest_uploaded_ref
|
53
|
+
if server.last_upload_time
|
54
|
+
logger.warn("Last upload time was #{server.last_upload_time}")
|
55
|
+
end
|
50
56
|
logger.warn('Latest uploaded revision is ' +
|
51
57
|
server.latest_uploaded_ref)
|
52
58
|
else
|
@@ -78,13 +84,17 @@ module TasteTester
|
|
78
84
|
end
|
79
85
|
server = TasteTester::Server.new
|
80
86
|
unless TasteTester::Config.linkonly
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
87
|
+
if TasteTester::Config.no_repo
|
88
|
+
repo = nil
|
89
|
+
else
|
90
|
+
repo = BetweenMeals::Repo.get(
|
91
|
+
TasteTester::Config.repo_type,
|
92
|
+
TasteTester::Config.repo,
|
93
|
+
logger,
|
94
|
+
)
|
95
|
+
end
|
96
|
+
if repo && !repo.exists?
|
97
|
+
fail "Could not open repo from #{TasteTester::Config.repo}"
|
88
98
|
end
|
89
99
|
end
|
90
100
|
unless TasteTester::Config.skip_pre_test_hook ||
|
@@ -94,12 +104,11 @@ module TasteTester
|
|
94
104
|
tested_hosts = []
|
95
105
|
hosts.each do |hostname|
|
96
106
|
host = TasteTester::Host.new(hostname, server)
|
97
|
-
|
98
|
-
username = host.who_is_testing
|
99
|
-
logger.error("User #{username} is already testing on #{hostname}")
|
100
|
-
else
|
107
|
+
begin
|
101
108
|
host.test
|
102
109
|
tested_hosts << hostname
|
110
|
+
rescue TasteTester::Exceptions::AlreadyTestingError => e
|
111
|
+
logger.error("User #{e.username} is already testing on #{hostname}")
|
103
112
|
end
|
104
113
|
end
|
105
114
|
unless TasteTester::Config.skip_post_test_hook ||
|
@@ -107,6 +116,24 @@ module TasteTester
|
|
107
116
|
TasteTester::Hooks.post_test(TasteTester::Config.dryrun, repo,
|
108
117
|
tested_hosts)
|
109
118
|
end
|
119
|
+
# Strictly: hosts and tested_hosts should be sets to eliminate variance in
|
120
|
+
# order or duplicates. The exact comparison works here because we're
|
121
|
+
# building tested_hosts from hosts directly.
|
122
|
+
if tested_hosts == hosts
|
123
|
+
# No exceptions, complete success: every host listed is now configured
|
124
|
+
# to use our chef-zero instance.
|
125
|
+
exit(0)
|
126
|
+
end
|
127
|
+
if tested_hosts.empty?
|
128
|
+
# All requested hosts are being tested by another user. We didn't change
|
129
|
+
# their configuration.
|
130
|
+
exit(3)
|
131
|
+
end
|
132
|
+
# Otherwise, we got a mix of success and failure due to being tested by
|
133
|
+
# another user. We'll be pessemistic and return an error because the
|
134
|
+
# intent to taste test the complete list was not successful.
|
135
|
+
# code.
|
136
|
+
exit(2)
|
110
137
|
end
|
111
138
|
|
112
139
|
def self.untest
|
@@ -131,7 +158,7 @@ module TasteTester
|
|
131
158
|
server = TasteTester::Server.new
|
132
159
|
hosts.each do |hostname|
|
133
160
|
host = TasteTester::Host.new(hostname, server)
|
134
|
-
host.
|
161
|
+
host.runchef
|
135
162
|
end
|
136
163
|
end
|
137
164
|
|
@@ -150,7 +177,7 @@ module TasteTester
|
|
150
177
|
|
151
178
|
def self.upload
|
152
179
|
server = TasteTester::Server.new
|
153
|
-
# On a
|
180
|
+
# On a force-upload rather than try to clean up whatever's on the server
|
154
181
|
# we'll restart chef-zero which will clear everything and do a full
|
155
182
|
# upload
|
156
183
|
if TasteTester::Config.force_upload
|
@@ -162,7 +189,7 @@ module TasteTester
|
|
162
189
|
client.skip_checks = true if TasteTester::Config.skip_repo_checks
|
163
190
|
client.force = true if TasteTester::Config.force_upload
|
164
191
|
client.upload
|
165
|
-
rescue => exception
|
192
|
+
rescue StandardError => exception
|
166
193
|
# We're trying to recover from common chef-zero errors
|
167
194
|
# Most of them happen due to half finished uploads, which leave
|
168
195
|
# chef-zero in undefined state
|
@@ -183,5 +210,168 @@ module TasteTester
|
|
183
210
|
logger.error(exception.backtrace.join("\n"))
|
184
211
|
exit 1
|
185
212
|
end
|
213
|
+
|
214
|
+
def self.impact
|
215
|
+
# Use the repository specified in config.rb to calculate the changes
|
216
|
+
# that may affect Chef. These changes will be further analyzed to
|
217
|
+
# determine specific roles which may change due to modifed dependencies.
|
218
|
+
repo = BetweenMeals::Repo.get(
|
219
|
+
TasteTester::Config.repo_type,
|
220
|
+
TasteTester::Config.repo,
|
221
|
+
logger,
|
222
|
+
)
|
223
|
+
if repo && !repo.exists?
|
224
|
+
fail "Could not open repo from #{TasteTester::Config.repo}"
|
225
|
+
end
|
226
|
+
|
227
|
+
changes = _find_changeset(repo)
|
228
|
+
|
229
|
+
# Perform preliminary impact analysis. By default, use Knife to find
|
230
|
+
# the roles dependent on modified cookbooks. Custom logic may provide
|
231
|
+
# additional information by defining the find_impact plugin method.
|
232
|
+
basic_impact = TasteTester::Hooks.find_impact(changes)
|
233
|
+
basic_impact ||= _find_roles(changes)
|
234
|
+
|
235
|
+
# Do any post processing required on the list of impacted roles, such
|
236
|
+
# as looking up hostnames associated with each role. By default, pass
|
237
|
+
# the preliminary results through unmodified.
|
238
|
+
final_impact = TasteTester::Hooks.post_impact(basic_impact)
|
239
|
+
final_impact ||= basic_impact
|
240
|
+
|
241
|
+
# Print the calculated impact. If a print hook is defined that
|
242
|
+
# returns true, then the default print function is skipped.
|
243
|
+
unless TasteTester::Hooks.print_impact(final_impact)
|
244
|
+
_print_impact(final_impact)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def self._find_changeset(repo)
|
249
|
+
# We want to compare changes in the current directory (working set) with
|
250
|
+
# the "most recent" commit in the VCS. For SVN, this will be the latest
|
251
|
+
# commit on the checked out repository (i.e. 'trunk'). Git/Hg may have
|
252
|
+
# different tags or labels assigned to the master branch, (i.e. 'master',
|
253
|
+
# 'stable', etc.) and should be configured if different than the default.
|
254
|
+
start_ref = case repo
|
255
|
+
when BetweenMeals::Repo::Svn
|
256
|
+
repo.latest_revision
|
257
|
+
when BetweenMeals::Repo::Git
|
258
|
+
TasteTester::Config.vcs_start_ref_git
|
259
|
+
when BetweenMeals::Repo::Hg
|
260
|
+
TasteTester::Config.vcs_start_ref_hg
|
261
|
+
end
|
262
|
+
end_ref = TasteTester::Config.vcs_end_ref
|
263
|
+
|
264
|
+
changeset = BetweenMeals::Changeset.new(
|
265
|
+
logger,
|
266
|
+
repo,
|
267
|
+
start_ref,
|
268
|
+
end_ref,
|
269
|
+
{
|
270
|
+
:cookbook_dirs =>
|
271
|
+
TasteTester::Config.relative_cookbook_dirs,
|
272
|
+
:role_dir =>
|
273
|
+
TasteTester::Config.relative_role_dir,
|
274
|
+
:databag_dir =>
|
275
|
+
TasteTester::Config.relative_databag_dir,
|
276
|
+
},
|
277
|
+
@track_symlinks,
|
278
|
+
)
|
279
|
+
|
280
|
+
return changeset
|
281
|
+
end
|
282
|
+
|
283
|
+
def self._find_roles(changes)
|
284
|
+
if TasteTester::Config.relative_cookbook_dirs.length > 1
|
285
|
+
logger.error('Knife deps does not support multiple cookbook paths.')
|
286
|
+
logger.error('Please flatten the cookbooks into a single directory' +
|
287
|
+
' or define the find_impact method in a local plugin.')
|
288
|
+
exit(1)
|
289
|
+
end
|
290
|
+
|
291
|
+
cookbooks = Set.new(changes.cookbooks)
|
292
|
+
roles = Set.new(changes.roles)
|
293
|
+
databags = Set.new(changes.databags)
|
294
|
+
|
295
|
+
if cookbooks.empty? && roles.empty?
|
296
|
+
unless TasteTester::Config.json
|
297
|
+
logger.warn('No cookbooks or roles have been modified.')
|
298
|
+
end
|
299
|
+
return Set.new
|
300
|
+
end
|
301
|
+
|
302
|
+
unless cookbooks.empty?
|
303
|
+
logger.info('Modified Cookbooks:')
|
304
|
+
cookbooks.each { |cb| logger.info("\t#{cb}") }
|
305
|
+
end
|
306
|
+
unless roles.empty?
|
307
|
+
logger.info('Modified Roles:')
|
308
|
+
roles.each { |r| logger.info("\t#{r}") }
|
309
|
+
end
|
310
|
+
unless databags.empty?
|
311
|
+
logger.info('Modified Databags:')
|
312
|
+
databags.each { |db| logger.info("\t#{db}") }
|
313
|
+
end
|
314
|
+
|
315
|
+
# Use Knife to list the dependecies for each role in the roles directory.
|
316
|
+
# This creates a recursive tree structure that is then searched for
|
317
|
+
# instances of modified cookbooks. This can be slow since it must read
|
318
|
+
# every line of the Knife output, then search all roles for dependencies.
|
319
|
+
# If you have a custom way to calculate these reverse dependencies, this
|
320
|
+
# is the part you would replace.
|
321
|
+
logger.info('Finding dependencies (this may take a minute or two)...')
|
322
|
+
knife = Mixlib::ShellOut.new(
|
323
|
+
"knife deps /#{TasteTester::Config.role_dir}/*.rb" +
|
324
|
+
" --config #{TasteTester::Config.knife_config}" +
|
325
|
+
" --chef-repo-path #{TasteTester::Config.absolute_base_dir}" +
|
326
|
+
' --tree --recurse',
|
327
|
+
)
|
328
|
+
knife.run_command
|
329
|
+
knife.error!
|
330
|
+
|
331
|
+
# Collapse the output from Knife into a hash structure that maps roles
|
332
|
+
# to the set of their dependencies. This will ignore duplicates in the
|
333
|
+
# Knife output, but must still process each line.
|
334
|
+
logger.info('Processing Dependencies...')
|
335
|
+
deps_hash = {}
|
336
|
+
curr_role = nil
|
337
|
+
|
338
|
+
knife.stdout.each_line do |line|
|
339
|
+
elem = line.rstrip
|
340
|
+
if elem.length == elem.lstrip.length
|
341
|
+
curr_role = elem
|
342
|
+
deps_hash[curr_role] = Set.new
|
343
|
+
else
|
344
|
+
deps_hash[curr_role].add(File.basename(elem, File.extname(elem)))
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Now we can search for modified dependencies by iterating over each
|
349
|
+
# role and checking the hash created earlier. Roles that have been
|
350
|
+
# modified directly are automatically included in the impacted set.
|
351
|
+
impacted_roles = Set.new(roles.map(&:name))
|
352
|
+
deps_hash.each do |role, deplist|
|
353
|
+
cookbooks.each do |cb|
|
354
|
+
if deplist.include?(cb.name)
|
355
|
+
impacted_roles.add(role)
|
356
|
+
logger.info("\tFound dependency: #{role} --> #{cb.name}")
|
357
|
+
break
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
return impacted_roles
|
363
|
+
end
|
364
|
+
|
365
|
+
def self._print_impact(final_impact)
|
366
|
+
if TasteTester::Config.json
|
367
|
+
puts JSON.pretty_generate(final_impact.to_a)
|
368
|
+
elsif final_impact.empty?
|
369
|
+
logger.warn('No impacted roles were found.')
|
370
|
+
else
|
371
|
+
logger.warn('The following roles have modified dependencies.' +
|
372
|
+
' Please test a host in each of these roles.')
|
373
|
+
final_impact.each { |r| logger.warn("\t#{r}") }
|
374
|
+
end
|
375
|
+
end
|
186
376
|
end
|
187
377
|
end
|
data/lib/taste_tester/config.rb
CHANGED
@@ -37,10 +37,12 @@ module TasteTester
|
|
37
37
|
config_file '/etc/taste-tester-config.rb'
|
38
38
|
plugin_path nil
|
39
39
|
chef_zero_path nil
|
40
|
+
bundle false
|
40
41
|
verbosity Logger::WARN
|
41
42
|
timestamp false
|
42
43
|
user 'root'
|
43
44
|
ref_file "#{ENV['HOME']}/.chef/taste-tester-ref.json"
|
45
|
+
knife_config "#{ENV['HOME']}/.chef/knife-#{ENV['USER']}-taste-tester.rb"
|
44
46
|
checksum_dir "#{ENV['HOME']}/.chef/checksums"
|
45
47
|
skip_repo_checks false
|
46
48
|
chef_client_command 'chef-client'
|
@@ -50,11 +52,26 @@ module TasteTester
|
|
50
52
|
timestamp_file '/etc/chef/test_timestamp'
|
51
53
|
use_ssh_tunnels false
|
52
54
|
ssh_command 'ssh'
|
55
|
+
ssh_connect_timeout 5
|
53
56
|
use_ssl true
|
54
57
|
chef_zero_logging true
|
55
58
|
chef_config_path '/etc/chef'
|
56
59
|
chef_config 'client.rb'
|
57
60
|
my_hostname nil
|
61
|
+
track_symlinks false
|
62
|
+
transport 'ssh'
|
63
|
+
no_repo false
|
64
|
+
json false
|
65
|
+
jumps nil
|
66
|
+
windows_target false
|
67
|
+
|
68
|
+
# Start/End refs for calculating changes in the repo.
|
69
|
+
# - start_ref should be the "master" commit of the repository
|
70
|
+
# - end_ref should be nil to compare with the working set,
|
71
|
+
# or something like '.' to compare with the most recent commit
|
72
|
+
vcs_start_ref_git 'origin/HEAD'
|
73
|
+
vcs_start_ref_hg 'master'
|
74
|
+
vcs_end_ref nil
|
58
75
|
|
59
76
|
skip_pre_upload_hook false
|
60
77
|
skip_post_upload_hook false
|
@@ -94,6 +111,10 @@ module TasteTester
|
|
94
111
|
end
|
95
112
|
end
|
96
113
|
|
114
|
+
def self.absolute_base_dir
|
115
|
+
File.join(repo, base_dir)
|
116
|
+
end
|
117
|
+
|
97
118
|
def self.chef_port
|
98
119
|
require 'taste_tester/state'
|
99
120
|
port_range = (
|
@@ -113,11 +134,8 @@ module TasteTester
|
|
113
134
|
end
|
114
135
|
|
115
136
|
def self.testing_end_time
|
116
|
-
|
117
|
-
TasteTester::Config.testing_until
|
118
|
-
else
|
137
|
+
TasteTester::Config.testing_until ||
|
119
138
|
Time.now + TasteTester::Config.testing_time
|
120
|
-
end
|
121
139
|
end
|
122
140
|
end
|
123
141
|
end
|
@@ -20,5 +20,14 @@ module TasteTester
|
|
20
20
|
end
|
21
21
|
class LocalLinkError < StandardError
|
22
22
|
end
|
23
|
+
class NoOpError < StandardError
|
24
|
+
end
|
25
|
+
class AlreadyTestingError < StandardError
|
26
|
+
attr_reader :username
|
27
|
+
|
28
|
+
def initialize(username)
|
29
|
+
@username = username
|
30
|
+
end
|
31
|
+
end
|
23
32
|
end
|
24
33
|
end
|
data/lib/taste_tester/hooks.rb
CHANGED
@@ -26,39 +26,47 @@ module TasteTester
|
|
26
26
|
extend BetweenMeals::Util
|
27
27
|
|
28
28
|
# Do stuff before we upload to chef-zero
|
29
|
-
def self.pre_upload(_dryrun, _repo, _last_ref, _cur_ref)
|
30
|
-
end
|
29
|
+
def self.pre_upload(_dryrun, _repo, _last_ref, _cur_ref); end
|
31
30
|
|
32
31
|
# Do stuff after we upload to chef-zero
|
33
|
-
def self.post_upload(_dryrun, _repo, _last_ref, _cur_ref)
|
34
|
-
end
|
32
|
+
def self.post_upload(_dryrun, _repo, _last_ref, _cur_ref); end
|
35
33
|
|
36
34
|
# Do stuff before we put hosts in test mode
|
37
|
-
def self.pre_test(_dryrun, _repo, _hosts)
|
38
|
-
end
|
35
|
+
def self.pre_test(_dryrun, _repo, _hosts); end
|
39
36
|
|
40
37
|
# This should return an array of commands to execute on
|
41
38
|
# remote systems.
|
42
|
-
def self.test_remote_cmds(_dryrun, _hostname)
|
43
|
-
end
|
39
|
+
def self.test_remote_cmds(_dryrun, _hostname); end
|
44
40
|
|
45
41
|
# Should return a string with extra stuff to shove
|
46
42
|
# in the remote client.rb
|
47
|
-
def self.test_remote_client_rb_extra_code(_hostname)
|
48
|
-
end
|
43
|
+
def self.test_remote_client_rb_extra_code(_hostname); end
|
49
44
|
|
50
45
|
# Do stuff after we put hosts in test mode
|
51
|
-
def self.post_test(_dryrun, _repo, _hosts)
|
52
|
-
end
|
46
|
+
def self.post_test(_dryrun, _repo, _hosts); end
|
53
47
|
|
54
48
|
# Additional checks you want to do on the repo
|
55
|
-
def self.repo_checks(_dryrun, _repo)
|
56
|
-
|
49
|
+
def self.repo_checks(_dryrun, _repo); end
|
50
|
+
|
51
|
+
# Find the set of roles dependent on the changed files.
|
52
|
+
# If returning something other than a set of roles, post_impact and/or
|
53
|
+
# print_impact should be specified to handle the output.
|
54
|
+
def self.find_impact(_changes); end
|
55
|
+
|
56
|
+
# Do stuff after we find impacted roles
|
57
|
+
# This should return a JSON serializable object with the final impact
|
58
|
+
# assessment. You will probably also want to define a print_impact method
|
59
|
+
# which returns true to override the default output logic.
|
60
|
+
def self.post_impact(_impacted_roles); end
|
61
|
+
|
62
|
+
# Customize the printed output of impact
|
63
|
+
# If this method returns true, the default output will not be printed.
|
64
|
+
def self.print_impact(_final_impact); end
|
57
65
|
|
58
66
|
def self.get(file)
|
59
67
|
path = File.expand_path(file)
|
60
|
-
logger.warn("Loading plugin at #{path}")
|
61
|
-
unless File.
|
68
|
+
logger.warn("Loading plugin at #{path}") unless TasteTester::Config.json
|
69
|
+
unless File.exist?(path)
|
62
70
|
logger.error('Plugin file not found')
|
63
71
|
exit(1)
|
64
72
|
end
|
data/lib/taste_tester/host.rb
CHANGED
@@ -20,14 +20,19 @@ require 'open3'
|
|
20
20
|
require 'colorize'
|
21
21
|
|
22
22
|
require 'taste_tester/ssh'
|
23
|
+
require 'taste_tester/noop'
|
23
24
|
require 'taste_tester/locallink'
|
24
25
|
require 'taste_tester/tunnel'
|
26
|
+
require 'taste_tester/exceptions'
|
25
27
|
|
26
28
|
module TasteTester
|
27
29
|
# Manage state of the remote node
|
28
30
|
class Host
|
29
31
|
include TasteTester::Logging
|
30
32
|
|
33
|
+
TASTE_TESTER_CONFIG = 'client-taste-tester.rb'.freeze
|
34
|
+
USER_PREAMBLE = '# TasteTester by '.freeze
|
35
|
+
|
31
36
|
attr_reader :name
|
32
37
|
|
33
38
|
def initialize(name, server)
|
@@ -42,25 +47,11 @@ module TasteTester
|
|
42
47
|
def runchef
|
43
48
|
logger.warn("Running '#{TasteTester::Config.chef_client_command}' " +
|
44
49
|
"on #{@name}")
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
else
|
51
|
-
cmd += "\"#{cmds}\""
|
52
|
-
end
|
53
|
-
status = IO.popen(
|
54
|
-
cmd,
|
55
|
-
) do |io|
|
56
|
-
# rubocop:disable AssignmentInCondition
|
57
|
-
while line = io.gets
|
58
|
-
puts line.chomp!
|
59
|
-
end
|
60
|
-
# rubocop:enable AssignmentInCondition
|
61
|
-
io.close
|
62
|
-
$CHILD_STATUS.to_i
|
63
|
-
end
|
50
|
+
transport = get_transport
|
51
|
+
transport << TasteTester::Config.chef_client_command
|
52
|
+
|
53
|
+
io = IO.new(1)
|
54
|
+
status, = transport.run(io)
|
64
55
|
logger.warn("Finished #{TasteTester::Config.chef_client_command}" +
|
65
56
|
" on #{@name} with status #{status}")
|
66
57
|
if status.zero?
|
@@ -72,12 +63,14 @@ module TasteTester
|
|
72
63
|
end
|
73
64
|
|
74
65
|
def get_transport
|
75
|
-
|
76
|
-
|
66
|
+
case TasteTester::Config.transport
|
67
|
+
when 'locallink'
|
68
|
+
TasteTester::LocalLink.new
|
69
|
+
when 'noop'
|
70
|
+
TasteTester::NoOp.new
|
77
71
|
else
|
78
|
-
|
72
|
+
TasteTester::SSH.new(@name)
|
79
73
|
end
|
80
|
-
transport
|
81
74
|
end
|
82
75
|
|
83
76
|
def test
|
@@ -91,22 +84,35 @@ module TasteTester
|
|
91
84
|
@tunnel.run
|
92
85
|
end
|
93
86
|
|
94
|
-
|
87
|
+
serialized_config = Base64.encode64(config).delete("\n")
|
95
88
|
|
96
89
|
# Then setup the testing
|
97
90
|
transport = get_transport
|
98
91
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
92
|
+
# see if someone else is taste-testing
|
93
|
+
transport << we_testing
|
94
|
+
|
95
|
+
if TasteTester::Config.windows_target
|
96
|
+
add_windows_test_cmds(transport, serialized_config)
|
97
|
+
else
|
98
|
+
add_sane_os_test_cmds(transport, serialized_config)
|
99
|
+
end
|
100
|
+
|
101
|
+
# look again to see if someone else is taste-testing. This is where
|
102
|
+
# we work out if we won or lost a race with another user.
|
103
|
+
transport << we_testing
|
104
|
+
|
105
|
+
status, output = transport.run
|
106
|
+
|
107
|
+
case status
|
108
|
+
when 0
|
109
|
+
# no problem, keep going.
|
110
|
+
nil
|
111
|
+
when 42
|
112
|
+
fail TasteTester::Exceptions::AlreadyTestingError, output.chomp
|
113
|
+
else
|
114
|
+
transport.error!
|
115
|
+
end
|
110
116
|
|
111
117
|
# Then run any other stuff they wanted
|
112
118
|
cmds = TasteTester::Hooks.test_remote_cmds(
|
@@ -114,7 +120,7 @@ module TasteTester
|
|
114
120
|
@name,
|
115
121
|
)
|
116
122
|
|
117
|
-
if cmds
|
123
|
+
if cmds&.any?
|
118
124
|
transport = get_transport
|
119
125
|
cmds.each { |c| transport << c }
|
120
126
|
transport.run!
|
@@ -127,62 +133,49 @@ module TasteTester
|
|
127
133
|
if TasteTester::Config.use_ssh_tunnels
|
128
134
|
TasteTester::Tunnel.kill(@name)
|
129
135
|
end
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
"rm -vf #{TasteTester::Config.chef_config_path}/client-taste-tester.rb",
|
135
|
-
"ln -vs #{TasteTester::Config.chef_config_path}/#{config_prod} " +
|
136
|
-
"#{TasteTester::Config.chef_config_path}/" +
|
137
|
-
TasteTester::Config.chef_config,
|
138
|
-
"rm -vf #{TasteTester::Config.chef_config_path}/client.pem",
|
139
|
-
"ln -vs #{TasteTester::Config.chef_config_path}/client-prod.pem " +
|
140
|
-
"#{TasteTester::Config.chef_config_path}/client.pem",
|
141
|
-
"rm -vf #{TasteTester::Config.timestamp_file}",
|
142
|
-
'logger -t taste-tester Returning server to production',
|
143
|
-
].each do |cmd|
|
144
|
-
transport << cmd
|
136
|
+
if TasteTester::Config.windows_target
|
137
|
+
add_windows_untest_cmds(transport)
|
138
|
+
else
|
139
|
+
add_sane_os_untest_cmds(transport)
|
145
140
|
end
|
146
141
|
transport.run!
|
147
142
|
end
|
148
143
|
|
149
|
-
def
|
150
|
-
|
151
|
-
transport << 'grep "^# TasteTester by"' +
|
152
|
-
" #{TasteTester::Config.chef_config_path}/" +
|
153
|
-
TasteTester::Config.chef_config
|
154
|
-
output = transport.run
|
155
|
-
if output.first.zero?
|
156
|
-
user = output.last.match(/# TasteTester by (.*)$/)
|
157
|
-
if user
|
158
|
-
return user[1]
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
# Legacy FB stuff, remove after migration. Safe for everyone else.
|
163
|
-
transport = get_transport
|
164
|
-
transport << "file #{TasteTester::Config.chef_config_path}/" +
|
144
|
+
def we_testing
|
145
|
+
config_file = "#{TasteTester::Config.chef_config_path}/" +
|
165
146
|
TasteTester::Config.chef_config
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
147
|
+
# Look for signature of TasteTester
|
148
|
+
# 1. Look for USER_PREAMBLE line prefix
|
149
|
+
# 2. See if user is us, or someone else
|
150
|
+
# 3. if someone else is testing: emit username, exit with code 42 which
|
151
|
+
# short circuits the test verb
|
152
|
+
# This is written as a squiggly heredoc so the indentation of the awk is
|
153
|
+
# preserved. Later we remove the newlines to make it a bit easier to read.
|
154
|
+
if TasteTester::Config.windows_target
|
155
|
+
shellcode = <<~ENDOFSHELLCODE
|
156
|
+
Get-Content #{config_file} | ForEach-Object {
|
157
|
+
if (\$_ -match "#{USER_PREAMBLE}" ) {
|
158
|
+
$user = \$_.Split()[-1]
|
159
|
+
if (\$user -ne "#{@user}") {
|
160
|
+
echo \$user
|
161
|
+
exit 42
|
162
|
+
}
|
163
|
+
}
|
164
|
+
}
|
165
|
+
ENDOFSHELLCODE
|
166
|
+
shellcode.chomp
|
183
167
|
else
|
184
|
-
|
168
|
+
shellcode = <<~ENDOFSHELLCODE
|
169
|
+
awk "\\$0 ~ /^#{USER_PREAMBLE}/{
|
170
|
+
if (\\$NF != \\"#{@user}\\"){
|
171
|
+
print \\$NF;
|
172
|
+
exit 42
|
173
|
+
}
|
174
|
+
}" #{config_file}
|
175
|
+
ENDOFSHELLCODE
|
176
|
+
shellcode.delete("\n")
|
185
177
|
end
|
178
|
+
shellcode
|
186
179
|
end
|
187
180
|
|
188
181
|
def keeptesting
|
@@ -201,15 +194,140 @@ module TasteTester
|
|
201
194
|
|
202
195
|
private
|
203
196
|
|
197
|
+
# Sources must be 'registered' with the Eventlog, so check if we have
|
198
|
+
# registered and register if necessary
|
199
|
+
def create_eventlog_if_needed_cmd
|
200
|
+
get_src = 'Get-EventLog -LogName Application -source taste-tester 2>$null'
|
201
|
+
mk_src = 'New-EventLog -source "taste-tester" -LogName Application'
|
202
|
+
"if (-Not (#{get_src})) { #{mk_src} }"
|
203
|
+
end
|
204
|
+
|
205
|
+
# Remote testing commands for most OSes...
|
206
|
+
def add_sane_os_test_cmds(transport, serialized_config)
|
207
|
+
transport << 'logger -t taste-tester Moving server into taste-tester' +
|
208
|
+
" for #{@user}"
|
209
|
+
transport << touchcmd
|
210
|
+
# shell redirection is also racy, so make a temporary file first
|
211
|
+
transport << "tmpconf=$(mktemp #{TasteTester::Config.chef_config_path}/" +
|
212
|
+
"#{TASTE_TESTER_CONFIG}.TMPXXXXXX)"
|
213
|
+
transport << "/bin/echo -n \"#{serialized_config}\" | base64 --decode" +
|
214
|
+
' > "${tmpconf}"'
|
215
|
+
# then rename it to replace any existing file
|
216
|
+
transport << 'mv -f "${tmpconf}" ' +
|
217
|
+
"#{TasteTester::Config.chef_config_path}/#{TASTE_TESTER_CONFIG}"
|
218
|
+
transport << "( ln -vsf #{TasteTester::Config.chef_config_path}" +
|
219
|
+
"/#{TASTE_TESTER_CONFIG} #{TasteTester::Config.chef_config_path}/" +
|
220
|
+
"#{TasteTester::Config.chef_config}; true )"
|
221
|
+
end
|
222
|
+
|
223
|
+
# Remote testing commands for Windows
|
224
|
+
def add_windows_test_cmds(transport, serialized_config)
|
225
|
+
# This is the closest equivalent to 'bash -x' - but if we put it on
|
226
|
+
# by default the way we do with linux it badly breaks our output. So only
|
227
|
+
# set it if we're in debug
|
228
|
+
#
|
229
|
+
# This isn't the most optimal place for this. It should be in ssh_util
|
230
|
+
# and we should jam this into the beggining of the cmds list we get,
|
231
|
+
# but this is early enough and good enough for now and we can think about
|
232
|
+
# that when we refactor tunnel.sh, ssh.sh and ssh_util.sh into one sane
|
233
|
+
# class.
|
234
|
+
if logger.level == Logger::DEBUG
|
235
|
+
transport << 'Set-PSDebug -trace 1'
|
236
|
+
end
|
237
|
+
|
238
|
+
ttconfig =
|
239
|
+
"#{TasteTester::Config.chef_config_path}/#{TASTE_TESTER_CONFIG}"
|
240
|
+
realconfig = "#{TasteTester::Config.chef_config_path}/" +
|
241
|
+
TasteTester::Config.chef_config
|
242
|
+
[
|
243
|
+
create_eventlog_if_needed_cmd,
|
244
|
+
'Write-EventLog -LogName "Application" -Source "taste-tester" ' +
|
245
|
+
'-EventID 1 -EntryType Information ' +
|
246
|
+
"-Message \"Moving server into taste-tester for #{@user}\"",
|
247
|
+
touchcmd,
|
248
|
+
"$b64 = \"#{serialized_config}\"",
|
249
|
+
"$ttconfig = \"#{ttconfig}\"",
|
250
|
+
"$realconfig = \"#{realconfig}\"",
|
251
|
+
|
252
|
+
'$tmp64 = (New-TemporaryFile).name',
|
253
|
+
'$tmp = (New-TemporaryFile).name',
|
254
|
+
|
255
|
+
'$b64 | Out-File -Encoding ASCII $tmp64 -Force',
|
256
|
+
|
257
|
+
# Remove our tmp file before we write to it or certutil crashes...
|
258
|
+
'if (Test-Path $tmp) { rm $tmp }',
|
259
|
+
'certutil -decode $tmp64 $tmp',
|
260
|
+
'mv $tmp $ttconfig -Force',
|
261
|
+
|
262
|
+
'New-Item -ItemType SymbolicLink -Value $ttconfig $realconfig -Force',
|
263
|
+
].each do |cmd|
|
264
|
+
transport << cmd
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
204
268
|
def touchcmd
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
269
|
+
if TasteTester::Config.windows_target
|
270
|
+
# There's no good touch equivalent in Windows. You can force
|
271
|
+
# creation of a new file, but that'll nuke it's contents, which if we're
|
272
|
+
# 'keeptesting'ing, then we'll loose the contents (PID and such).
|
273
|
+
# We can set the timestamp with Get-Item.creationtime, but it must exist
|
274
|
+
# if we're not gonna crash. So do both.
|
275
|
+
[
|
276
|
+
"$ts = \"#{TasteTester::Config.timestamp_file}\"",
|
277
|
+
'if (-Not (Test-Path $ts)) { New-Item -ItemType file $ts }',
|
278
|
+
'(Get-Item "$ts").LastWriteTime=("' +
|
279
|
+
"#{TasteTester::Config.testing_end_time}\")",
|
280
|
+
].join(';')
|
281
|
+
else
|
282
|
+
touch = Base64.encode64(
|
283
|
+
"if [ 'Darwin' = $(uname) ]; then touch -t \"$(date -r " +
|
284
|
+
"#{TasteTester::Config.testing_end_time.to_i} +'%Y%m%d%H%M.%S')\" " +
|
285
|
+
"#{TasteTester::Config.timestamp_file}; else touch --date \"$(date " +
|
286
|
+
"-d @#{TasteTester::Config.testing_end_time.to_i} +'%Y-%m-%d %T')\"" +
|
287
|
+
" #{TasteTester::Config.timestamp_file}; fi",
|
288
|
+
).delete("\n")
|
289
|
+
"/bin/echo -n '#{touch}' | base64 --decode | bash"
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Remote untesting commands for Windows
|
294
|
+
def add_windows_untest_cmds(transport)
|
295
|
+
config_prod = TasteTester::Config.chef_config.split('.').join('-prod.')
|
296
|
+
[
|
297
|
+
'New-Item -ItemType SymbolicLink -Force -Value ' +
|
298
|
+
"#{TasteTester::Config.chef_config_path}/#{config_prod} " +
|
299
|
+
"#{TasteTester::Config.chef_config_path}/" +
|
300
|
+
TasteTester::Config.chef_config,
|
301
|
+
'New-Item -ItemType SymbolicLink -Force -Value ' +
|
302
|
+
"#{TasteTester::Config.chef_config_path}/client-prod.pem " +
|
303
|
+
"#{TasteTester::Config.chef_config_path}/client.pem",
|
304
|
+
'rm -Force ' +
|
305
|
+
"#{TasteTester::Config.chef_config_path}/#{TASTE_TESTER_CONFIG}",
|
306
|
+
"rm -Force #{TasteTester::Config.timestamp_file}",
|
307
|
+
create_eventlog_if_needed_cmd,
|
308
|
+
'Write-EventLog -LogName "Application" -Source "taste-tester" ' +
|
309
|
+
'-EventID 4 -EntryType Information -Message "Returning server ' +
|
310
|
+
'to production"',
|
311
|
+
].each do |cmd|
|
312
|
+
transport << cmd
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# Remote untesting commands for most OSes...
|
317
|
+
def add_sane_os_untest_cmds(transport)
|
318
|
+
config_prod = TasteTester::Config.chef_config.split('.').join('-prod.')
|
319
|
+
[
|
320
|
+
"ln -vsf #{TasteTester::Config.chef_config_path}/#{config_prod} " +
|
321
|
+
"#{TasteTester::Config.chef_config_path}/" +
|
322
|
+
TasteTester::Config.chef_config,
|
323
|
+
"ln -vsf #{TasteTester::Config.chef_config_path}/client-prod.pem " +
|
324
|
+
"#{TasteTester::Config.chef_config_path}/client.pem",
|
325
|
+
"rm -vf #{TasteTester::Config.chef_config_path}/#{TASTE_TESTER_CONFIG}",
|
326
|
+
"rm -vf #{TasteTester::Config.timestamp_file}",
|
327
|
+
'logger -t taste-tester Returning server to production',
|
328
|
+
].each do |cmd|
|
329
|
+
transport << cmd
|
330
|
+
end
|
213
331
|
end
|
214
332
|
|
215
333
|
def config
|
@@ -217,41 +335,91 @@ module TasteTester
|
|
217
335
|
if TasteTester::Config.use_ssh_tunnels
|
218
336
|
url = "#{scheme}://localhost:#{@tunnel.port}"
|
219
337
|
else
|
220
|
-
url = "#{scheme}://#{@server.host}"
|
338
|
+
url = +"#{scheme}://#{@server.host}"
|
221
339
|
url << ":#{TasteTester::State.port}" if TasteTester::State.port
|
222
340
|
end
|
223
|
-
|
224
|
-
|
225
|
-
#
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
end
|
341
|
+
ttconfig = <<~ENDOFSCRIPT
|
342
|
+
#{USER_PREAMBLE}#{@user}
|
343
|
+
# Prevent people from screwing up their permissions
|
344
|
+
if Process.euid != 0
|
345
|
+
puts 'Please run chef as root!'
|
346
|
+
Process.exit!
|
347
|
+
end
|
231
348
|
|
232
|
-
log_level :info
|
233
|
-
log_location STDOUT
|
234
|
-
|
235
|
-
|
236
|
-
|
349
|
+
log_level :info
|
350
|
+
log_location STDOUT
|
351
|
+
ssl_verify_mode :verify_none
|
352
|
+
ohai.plugin_path << File.join('#{TasteTester::Config.chef_config_path}', 'ohai_plugins')
|
353
|
+
ENDOFSCRIPT
|
237
354
|
|
238
|
-
|
239
|
-
|
355
|
+
if TasteTester::Config.bundle
|
356
|
+
ttconfig += <<~ENDOFSCRIPT
|
357
|
+
taste_tester_dest = File.join(Dir.tmpdir, 'taste-tester')
|
358
|
+
puts 'INFO: Downloading bundle from #{url}...'
|
359
|
+
FileUtils.rmtree(taste_tester_dest)
|
360
|
+
FileUtils.mkpath(taste_tester_dest)
|
361
|
+
FileUtils.touch(File.join(taste_tester_dest, 'chefignore'))
|
362
|
+
uri = URI('#{url}/file_store/tt.tgz')
|
363
|
+
Net::HTTP.start(
|
364
|
+
uri.host,
|
365
|
+
uri.port,
|
366
|
+
:use_ssl => #{TasteTester::Config.use_ssl},
|
367
|
+
# we expect self signed certificates
|
368
|
+
:verify_mode => OpenSSL::SSL::VERIFY_NONE,
|
369
|
+
) do |http|
|
370
|
+
http.request_get(uri) do |response|
|
371
|
+
# the use of stringIO means we are buffering the entire file in
|
372
|
+
# memory. This isn't very efficient, but it should work for
|
373
|
+
# most practical cases.
|
374
|
+
stream = Zlib::GzipReader.new(StringIO.new(response.body))
|
375
|
+
Gem::Package::TarReader.new(stream).each do |e|
|
376
|
+
dest = File.join(taste_tester_dest, e.full_name)
|
377
|
+
FileUtils.mkpath(File.dirname(dest))
|
378
|
+
if e.symlink?
|
379
|
+
File.symlink(e.header.linkname, dest)
|
380
|
+
else
|
381
|
+
File.open(dest, 'wb+') do |f|
|
382
|
+
# https://github.com/rubygems/rubygems/pull/2303
|
383
|
+
# IO.copy_stream(e, f)
|
384
|
+
# workaround:
|
385
|
+
f.write(e.read)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
puts 'INFO: Download complete'
|
392
|
+
solo true
|
393
|
+
local_mode true
|
394
|
+
ENDOFSCRIPT
|
395
|
+
else
|
396
|
+
ttconfig += <<~ENDOFSCRIPT
|
397
|
+
chef_server_url '#{url}'
|
398
|
+
ENDOFSCRIPT
|
399
|
+
end
|
240
400
|
|
241
401
|
extra = TasteTester::Hooks.test_remote_client_rb_extra_code(@name)
|
242
402
|
if extra
|
243
|
-
ttconfig +=
|
244
|
-
# Begin user-hook specified code
|
245
|
-
|
246
|
-
# End user-hook secified code
|
403
|
+
ttconfig += <<~ENDOFSCRIPT
|
404
|
+
# Begin user-hook specified code
|
405
|
+
#{extra}
|
406
|
+
# End user-hook secified code
|
247
407
|
|
248
|
-
|
408
|
+
ENDOFSCRIPT
|
249
409
|
end
|
250
410
|
|
251
|
-
ttconfig +=
|
252
|
-
puts 'INFO: Running on #{@name} in taste-tester by #{@user}'
|
253
|
-
|
254
|
-
|
411
|
+
ttconfig += <<~ENDOFSCRIPT
|
412
|
+
puts 'INFO: Running on #{@name} in taste-tester by #{@user}'
|
413
|
+
ENDOFSCRIPT
|
414
|
+
|
415
|
+
if TasteTester::Config.bundle
|
416
|
+
# This is last in the configuration file because it needs to override
|
417
|
+
# any values in test_remote_client_rb_extra_code
|
418
|
+
ttconfig += <<~ENDOFSCRIPT
|
419
|
+
chef_repo_path taste_tester_dest
|
420
|
+
ENDOFSCRIPT
|
421
|
+
end
|
422
|
+
ttconfig
|
255
423
|
end
|
256
424
|
end
|
257
425
|
end
|