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.
@@ -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.latest_uploaded_ref
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
- repo = BetweenMeals::Repo.get(
82
- TasteTester::Config.repo_type,
83
- TasteTester::Config.repo,
84
- logger,
85
- )
86
- unless repo.exists?
87
- raise "Could not open repo from #{TasteTester::Config.repo}"
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
- if host.in_test?
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.run
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 fore-upload rather than try to clean up whatever's on the server
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
@@ -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
- if TasteTester::Config.testing_until
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
@@ -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
- end
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.exists?(path)
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
@@ -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
- cmd = "#{TasteTester::Config.ssh_command} " +
46
- "#{TasteTester::Config.user}@#{@name} "
47
- if TasteTester::Config.user != 'root'
48
- cc = Base64.encode64(cmds).delete("\n")
49
- cmd += "\"echo '#{cc}' | base64 --decode | sudo bash -x\""
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
- if TasteTester::Config.locallink
76
- transport = TasteTester::LocalLink.new
66
+ case TasteTester::Config.transport
67
+ when 'locallink'
68
+ TasteTester::LocalLink.new
69
+ when 'noop'
70
+ TasteTester::NoOp.new
77
71
  else
78
- transport = TasteTester::SSH.new(@name)
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
- @serialized_config = Base64.encode64(config).delete("\n")
87
+ serialized_config = Base64.encode64(config).delete("\n")
95
88
 
96
89
  # Then setup the testing
97
90
  transport = get_transport
98
91
 
99
- transport << 'logger -t taste-tester Moving server into taste-tester' +
100
- " for #{@user}"
101
- transport << touchcmd
102
- transport << "echo -n '#{@serialized_config}' | base64 --decode" +
103
- " > #{TasteTester::Config.chef_config_path}/client-taste-tester.rb"
104
- transport << "rm -vf #{TasteTester::Config.chef_config_path}/" +
105
- TasteTester::Config.chef_config
106
- transport << "( ln -vs #{TasteTester::Config.chef_config_path}" +
107
- "/client-taste-tester.rb #{TasteTester::Config.chef_config_path}/" +
108
- "#{TasteTester::Config.chef_config}; true )"
109
- transport.run!
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 && cmds.any?
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
- config_prod = TasteTester::Config.chef_config.split('.').join('-prod.')
131
- [
132
- "rm -vf #{TasteTester::Config.chef_config_path}/" +
133
- TasteTester::Config.chef_config,
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 who_is_testing
150
- transport = get_transport
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
- output = transport.run
167
- if output.first.zero?
168
- user = output.last.match(/client-(.*)-(taste-tester|test).rb/)
169
- if user
170
- return user[1]
171
- end
172
- end
173
-
174
- return nil
175
- end
176
-
177
- def in_test?
178
- transport = get_transport
179
- transport << "test -f #{TasteTester::Config.timestamp_file}"
180
- if transport.run.first.zero? && who_is_testing &&
181
- who_is_testing != ENV['USER']
182
- true
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
- false
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
- touch = Base64.encode64(
206
- "if [ 'Darwin' = $(uname) ]; then touch -t \"$(date -r " +
207
- "#{TasteTester::Config.testing_end_time.to_i} +'%Y%m%d%H%M.%S')\" " +
208
- "#{TasteTester::Config.timestamp_file}; else touch --date \"$(date " +
209
- "-d @#{TasteTester::Config.testing_end_time.to_i} +'%Y-%m-%d %T')\" " +
210
- "#{TasteTester::Config.timestamp_file}; fi",
211
- ).delete("\n")
212
- "echo -n '#{touch}' | base64 --decode | bash"
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
- # rubocop:disable Metrics/LineLength
224
- ttconfig = <<-eos
225
- # TasteTester by #{@user}
226
- # Prevent people from screwing up their permissions
227
- if Process.euid != 0
228
- puts 'Please run chef as root!'
229
- Process.exit!
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
- chef_server_url '#{url}'
235
- ssl_verify_mode :verify_none
236
- ohai.plugin_path << '#{TasteTester::Config.chef_config_path}/ohai_plugins'
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
- eos
239
- # rubocop:enable Metrics/LineLength
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 += <<-eos
244
- # Begin user-hook specified code
245
- #{extra}
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
- eos
408
+ ENDOFSCRIPT
249
409
  end
250
410
 
251
- ttconfig += <<-eos
252
- puts 'INFO: Running on #{@name} in taste-tester by #{@user}'
253
- eos
254
- return ttconfig
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