taste_tester 0.0.14 → 0.0.19

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbe88f9386cbb7c68b721ea057a30881dd98ba374a0e84528e43da739eb88d31
4
- data.tar.gz: 642d43ce2aa006331680178e0ab15afba8d88323ab89d2e1cb0321f74c663b07
3
+ metadata.gz: a2fa17de1672814a680aa44b592bc649f3b3ec80b057a0199aed9b3ce13e1fe5
4
+ data.tar.gz: e9150bc89798e17651102239224763468ca8a65affb7706c4080a409577bf108
5
5
  SHA512:
6
- metadata.gz: 3a256a6d7e67b346aad620a1782032394f68848ef9c085c24854b4d70ffdd718b64d4970c479b7080aba11b468423905759d0ae8f92d305aa38fe654187745b7
7
- data.tar.gz: '048097e573d03b748a08d09438820d9ee144c90d0fb43e0375fbd91e1354215d7c82d8a0b39f97452b0752ab8dc79fe5b5863c9108727ba87fc2810271f27ee8'
6
+ metadata.gz: 12bf2e2e631987187b455b294bae9db28bd1f50c7aba35bbe9d04b3cbceabec3f65fd665f5ef5c70424bb296438742ebd0a918a5545480ec058ff760ac6f81fa
7
+ data.tar.gz: 4b6781fe213f7e9447077d6f629c3facc4f2fa596212b8a8f1f8cde40e1f1aba4c66542f1b939b587c87d4e05878320e410e2bd8b9bbec98a40ff97ac52c4806
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # Taste Tester
2
2
 
3
- [![TravisCI](https://travis-ci.org/facebook/taste-tester.svg)](http://travis-ci.org/facebook/taste-tester)
4
- [![CircleCI](https://circleci.com/gh/facebook/taste-tester.svg?style=svg)](https://circleci.com/gh/facebook/taste-tester)
3
+ ![Continuous Integration](https://github.com/facebook/taste-tester/workflows/Continuous%20Integration/badge.svg?event=push)
5
4
 
6
5
  ## Intro
7
6
  Ohai!
@@ -25,6 +24,7 @@ Typical usage is:
25
24
 
26
25
  ```text
27
26
  vi cookbooks/... # Make your changes and commit locally
27
+ taste-tester impact # Check where your changes are used
28
28
  taste-tester test -s [host] # Put host in Taste Tester mode
29
29
  ssh root@[host] # Log in to host
30
30
  # Run chef and watch it break
@@ -62,6 +62,7 @@ you want to test on, i.e. SSH public/private keys, SSH certificates, Kerberos
62
62
  * Colorize
63
63
  * BetweenMeals
64
64
  * Minitar
65
+ * Chef
65
66
 
66
67
  ## Automatic Untesting
67
68
 
@@ -91,7 +92,8 @@ All command-line options are available in the config file:
91
92
  * plugin_path (string, no default)
92
93
  * repo (string, default: `#{ENV['HOME']}/ops`)
93
94
  * testing_time (int, default: `3600`)
94
- * chef_client_command (strng, default: `chef-client`)
95
+ * chef_client_command (string, default: `chef-client`)
96
+ * json (bool, default: `false`)
95
97
  * skip_repo_checks (bool, default: `false`)
96
98
  * skip_pre_upload_hook (bool, default: `false`)
97
99
  * skip_post_upload_hook (bool, default: `false`)
@@ -103,7 +105,7 @@ The following options are also available:
103
105
  * base_dir - The directory in the repo under which to find chef configs.
104
106
  Default: `chef`
105
107
  * cookbook_dirs - An array of cookbook directories relative to base_dir.
106
- Default: `['cookbooks']
108
+ Default: `['cookbooks']`
107
109
  * role_dir - A directory of roles, relative to base_dir. Default: `roles`
108
110
  * databag_dir - A directory of databags, relative to base_dir.
109
111
  Default: `databags`
@@ -112,9 +114,13 @@ The following options are also available:
112
114
  * checksum_dir - The checksum directory to put in knife.conf for users. Default:
113
115
  `#{ENV['HOME']}/.chef/checksums`
114
116
  * bundle - use a single tar.gz file for transporting cookbooks, roles and
115
- databags to clients. Experimental.
117
+ databags to clients. Experimental. Value is tri-state:
118
+ * `false` - server uses knife upload, client uses `chef_server`
119
+ * `:compatible` - make server support both methods, client uses tar.gz
120
+ * `true` - server only creates tar.gz, client uses tar.gz
121
+ Default: false
122
+ * impact - analyze local changes to determine which hosts/roles to test.
116
123
  Default: false
117
-
118
124
 
119
125
  ## Plugin
120
126
 
@@ -156,10 +162,28 @@ Stuff to do after putting all hosts in test mode.
156
162
 
157
163
  Additional checks you want to do on the repo as sanity checks.
158
164
 
159
- **Plugin example**
165
+ * self.find_impact(changes)
166
+
167
+ Custom implementation of impact analysis. Uses `knife deps` by default. May
168
+ return any data structure, provided one or both of `self.post_impact` or
169
+ `self.print_impact` are defined.
170
+
171
+ * self.post_impact(basic_impact)
172
+
173
+ Stuff to do after preliminary impact analysis. May be used to extend the
174
+ information generated by `self.find_impact`, reformat the data structure, etc.
175
+
176
+ * self.print_impact(final_impact)
177
+
178
+ Custom output of calculated impact, useful if defining either of the other
179
+ impact hooks. Must return a truthy value to prevent the default output from
180
+ printing.
181
+
182
+ ## Plugin example
160
183
 
161
184
  This is an example `/etc/taste-tester-plugin.rb` to add a user-defined string
162
185
  to `client-taste-tester.rb` on the remote system:
186
+
163
187
  ```
164
188
  Hooks.class_eval do
165
189
  def self.test_remote_client_rb_extra_code(_hostname)
@@ -170,7 +194,8 @@ Hooks.class_eval do
170
194
  end
171
195
  end
172
196
  ```
173
- Be sure to pass this plugin file with `-p` on the command line or set it as
197
+
198
+ Be sure to pass this plugin file with `-p` on the command line or set it as
174
199
  `plugin_path` in your `taste-tester-config.rb` file.
175
200
 
176
201
  ## License
@@ -41,7 +41,7 @@ module TasteTester
41
41
 
42
42
  # Do an initial read of the config file if it's in the default place, so
43
43
  # that if people override chef_client_command the help message is correct.
44
- if File.exists?(File.expand_path(TasteTester::Config.config_file))
44
+ if File.exist?(File.expand_path(TasteTester::Config.config_file))
45
45
  TasteTester::Config.from_file(
46
46
  File.expand_path(TasteTester::Config.config_file),
47
47
  )
@@ -66,7 +66,8 @@ TLDR; Most common usage is:
66
66
  taste-tester untest -s [host] # Put host back in production
67
67
  # (optional - will revert itself after 1 hour)
68
68
 
69
- And you're done! See the above wiki page for more details.
69
+ And you're done!
70
+ Note: There may be site specific testing instructions, see local documentation for details.
70
71
 
71
72
  MODES:
72
73
  test
@@ -135,7 +136,6 @@ MODES:
135
136
  end
136
137
 
137
138
  options = { :config_file => TasteTester::Config.config_file }
138
- # rubocop:disable Metrics/BlockLength
139
139
  parser = OptionParser.new do |opts|
140
140
  opts.banner = description
141
141
 
@@ -143,7 +143,7 @@ MODES:
143
143
  opts.separator 'Global options:'.upcase
144
144
 
145
145
  opts.on('-c', '--config FILE', 'Config file') do |file|
146
- unless File.exists?(File.expand_path(file))
146
+ unless File.exist?(File.expand_path(file))
147
147
  logger.error("Sorry, cannot find #{file}")
148
148
  exit(1)
149
149
  end
@@ -160,7 +160,7 @@ MODES:
160
160
  end
161
161
 
162
162
  opts.on('-p', '--plugin-path FILE', String, 'Plugin file') do |file|
163
- unless File.exists?(File.expand_path(file))
163
+ unless File.exist?(File.expand_path(file))
164
164
  logger.error("Sorry, cannot find #{file}")
165
165
  exit(1)
166
166
  end
@@ -244,6 +244,7 @@ MODES:
244
244
  'Until when should the host remain in testing.' +
245
245
  ' Anything parsable is ok, such as "5/18 4:35" or "16/9/13".'
246
246
  ) do |time|
247
+ # can make this an implicit rescue after we drop ruby 2.4
247
248
  begin
248
249
  options[:testing_until] = Time.parse(time)
249
250
  rescue StandardError
@@ -305,6 +306,17 @@ MODES:
305
306
  options[:roles] = roles
306
307
  end
307
308
 
309
+ opts.on(
310
+ '-J', '--jumps JUMP',
311
+ 'Uses ssh\'s `ProxyJump` support to ssh across bastion/jump hosts. ' +
312
+ 'This is particularly useful in tunnel mode to test machines that ' +
313
+ 'your workstatation doesn\'t have direct access to. The format is ' +
314
+ 'the same as `ssh -J`: a comma-separated list of hosts to forward ' +
315
+ 'through.'
316
+ ) do |jumps|
317
+ options[:jumps] = jumps
318
+ end
319
+
308
320
  opts.on('--really', 'Really do link-only. DANGEROUS!') do |r|
309
321
  options[:really] = r
310
322
  end
@@ -356,6 +368,15 @@ MODES:
356
368
  options[:json] = true
357
369
  end
358
370
 
371
+ opts.on(
372
+ '-w', '--windows-target',
373
+ 'The target is a Windows machine. You will likely want to override ' +
374
+ '`test_timestamp` and `chef_config_path`, but *not* `config_file`. ' +
375
+ 'Requires the target be running PowerShell >= 5.1 as the default shell.'
376
+ ) do
377
+ options[:windows_target] = true
378
+ end
379
+
359
380
  opts.separator ''
360
381
  opts.separator 'Control local hook behavior with these options:'
361
382
 
@@ -389,7 +410,6 @@ MODES:
389
410
  options[:skip_post_test_hook] = true
390
411
  end
391
412
  end
392
- # rubocop:enable Metrics/BlockLength
393
413
 
394
414
  if mode == 'help'
395
415
  puts parser
@@ -398,7 +418,7 @@ MODES:
398
418
 
399
419
  parser.parse!
400
420
 
401
- if File.exists?(File.expand_path(options[:config_file]))
421
+ if File.exist?(File.expand_path(options[:config_file]))
402
422
  TasteTester::Config.from_file(File.expand_path(options[:config_file]))
403
423
  end
404
424
  TasteTester::Config.merge!(options)
@@ -407,7 +427,7 @@ MODES:
407
427
 
408
428
  if TasteTester::Config.plugin_path
409
429
  path = File.expand_path(TasteTester::Config.plugin_path)
410
- unless File.exists?(path)
430
+ unless File.exist?(path)
411
431
  logger.error("Plugin not found (#{path})")
412
432
  exit(1)
413
433
  end
@@ -447,5 +467,7 @@ MODES:
447
467
  end
448
468
 
449
469
  if $PROGRAM_NAME == __FILE__
450
- include TasteTester
470
+ module TasteTester
471
+ include TasteTester
472
+ end
451
473
  end
@@ -20,6 +20,8 @@ require 'taste_tester/logging'
20
20
  require 'between_meals/repo'
21
21
  require 'between_meals/knife'
22
22
  require 'between_meals/changeset'
23
+ require 'chef/log'
24
+ require 'chef/cookbook/chefignore'
23
25
 
24
26
  module TasteTester
25
27
  # Client side upload functionality
@@ -130,46 +132,52 @@ module TasteTester
130
132
  def populate(stream, writer, path, destination)
131
133
  full_path = File.join(File.join(TasteTester::Config.repo, path))
132
134
  return unless File.directory?(full_path)
135
+ chefignores = Chef::Cookbook::Chefignore.new(full_path)
133
136
  # everything is relative to the repo dir. chdir makes handling all the
134
137
  # paths within this simpler
135
138
  Dir.chdir(full_path) do
136
- Find.find('.') do |p|
137
- # ignore current directory. The File.directory? would also skip it,
138
- # but we need to do it early because the string is too short for the
139
- # next statement.
140
- next if p == '.'
141
- # paths are enumerated as relative to the input path '.', so we get
142
- # './dir/file'. Stripping off the first two characters gives us a
143
- # a cleaner 'dir/file' path.
144
- name = File.join(destination, p[2..-1])
145
- if File.directory?(p)
146
- # skip it. This also handles symlinks to directories which aren't
147
- # useful either.
148
- elsif File.symlink?(p)
149
- # tar handling of filenames > 100 characters gets complex. We'd use
150
- # split_name from Minitar, but it's a private method. It's
151
- # reasonable to assume that all symlink names in the bundle are
152
- # less than 100 characters long. Long term, the version of minitar
153
- # in chefdk should be upgraded.
154
- fail 'Add support for long symlink paths' if name.size > 100
155
- # The version of Minitar included in chefdk does not support
156
- # symlinks directly. Therefore we use direct writes to the
157
- # underlying stream to reproduce the symlinks
158
- symlink = {
159
- :name => name,
160
- :mode => 0644,
161
- :typeflag => '2',
162
- :size => 0,
163
- :linkname => File.readlink(p),
164
- :prefix => '',
165
- }
166
- stream.write(Minitar::PosixHeader.new(symlink))
167
- else
168
- File.open(p, 'rb') do |r|
169
- writer.add_file_simple(
170
- name, :mode => 0644, :size => File.size(r)
171
- ) do |d, _opts|
172
- IO.copy_stream(r, d)
139
+ look_at = ['']
140
+ while (prefix = look_at.pop)
141
+ Dir.glob(File.join("#{prefix}**", '*'), File::FNM_DOTMATCH) do |p|
142
+ minus_first = p.split(
143
+ File::SEPARATOR,
144
+ )[1..-1].join(File::SEPARATOR)
145
+ next if chefignores.ignored?(p) ||
146
+ chefignores.ignored?(minus_first)
147
+ name = File.join(destination, p)
148
+ if File.directory?(p)
149
+ # we don't store directories in the tar, but we do want to follow
150
+ # top level symlinked directories as they are used to share
151
+ # cookbooks between codebases.
152
+ if minus_first == '' && File.symlink?(p)
153
+ look_at.push("#{p}#{File::SEPARATOR}")
154
+ end
155
+ elsif File.symlink?(p)
156
+ # tar handling of filenames > 100 characters gets complex. We'd
157
+ # use split_name from Minitar, but it's a private method. It's
158
+ # reasonable to assume that all symlink names in the bundle are
159
+ # less than 100 characters long. Long term, the version of minitar
160
+ # in chefdk should be upgraded.
161
+ fail 'Add support for long symlink paths' if name.size > 100
162
+ # The version of Minitar included in chefdk does not support
163
+ # symlinks directly. Therefore we use direct writes to the
164
+ # underlying stream to reproduce the symlinks
165
+ symlink = {
166
+ :name => name,
167
+ :mode => 0644,
168
+ :typeflag => '2',
169
+ :size => 0,
170
+ :linkname => File.readlink(p),
171
+ :prefix => '',
172
+ }
173
+ stream.write(Minitar::PosixHeader.new(symlink))
174
+ else
175
+ File.open(p, 'rb') do |r|
176
+ writer.add_file_simple(
177
+ name, :mode => 0644, :size => File.size(r)
178
+ ) do |d, _opts|
179
+ IO.copy_stream(r, d)
180
+ end
173
181
  end
174
182
  end
175
183
  end
@@ -206,11 +214,12 @@ module TasteTester
206
214
  end
207
215
 
208
216
  def full
217
+ logger.warn('Doing full upload')
209
218
  if TasteTester::Config.bundle
210
219
  bundle_upload
211
- return
220
+ # only leave early if true (strictly bundle mode only)
221
+ return if TasteTester::Config.bundle == true
212
222
  end
213
- logger.warn('Doing full upload')
214
223
  @knife.cookbook_upload_all
215
224
  @knife.role_upload_all
216
225
  @knife.databag_upload_all
@@ -220,7 +229,7 @@ module TasteTester
220
229
  if TasteTester::Config.bundle
221
230
  logger.info('No partial support for bundle mode, doing full upload')
222
231
  bundle_upload
223
- return
232
+ return if TasteTester::Config.bundle == true
224
233
  end
225
234
  logger.info('Doing differential upload from ' +
226
235
  @server.latest_uploaded_ref)
@@ -158,7 +158,7 @@ module TasteTester
158
158
  server = TasteTester::Server.new
159
159
  hosts.each do |hostname|
160
160
  host = TasteTester::Host.new(hostname, server)
161
- host.run
161
+ host.runchef
162
162
  end
163
163
  end
164
164
 
@@ -226,16 +226,17 @@ module TasteTester
226
226
 
227
227
  changes = _find_changeset(repo)
228
228
 
229
- # Use Knife (or custom logic) to check the dependencies of each role
230
- # against the list of changes. `impacted_roles` will contian the set
231
- # of roles with direct or indirect (dependency) modifications.
232
- impacted_roles = TasteTester::Hooks.impact_find_roles(changes)
233
- impacted_roles ||= _find_roles(changes)
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
234
 
235
235
  # Do any post processing required on the list of impacted roles, such
236
- # as looking up hostnames associated with each role.
237
- final_impact = TasteTester::Hooks.post_impact(impacted_roles)
238
- final_impact ||= impacted_roles
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
239
240
 
240
241
  # Print the calculated impact. If a print hook is defined that
241
242
  # returns true, then the default print function is skipped.
@@ -283,7 +284,7 @@ module TasteTester
283
284
  if TasteTester::Config.relative_cookbook_dirs.length > 1
284
285
  logger.error('Knife deps does not support multiple cookbook paths.')
285
286
  logger.error('Please flatten the cookbooks into a single directory' +
286
- ' or override the impact_find_roles function.')
287
+ ' or define the find_impact method in a local plugin.')
287
288
  exit(1)
288
289
  end
289
290
 
@@ -292,7 +293,9 @@ module TasteTester
292
293
  databags = Set.new(changes.databags)
293
294
 
294
295
  if cookbooks.empty? && roles.empty?
295
- logger.warn('No cookbooks or roles have been modified.')
296
+ unless TasteTester::Config.json
297
+ logger.warn('No cookbooks or roles have been modified.')
298
+ end
296
299
  return Set.new
297
300
  end
298
301
 
@@ -62,6 +62,8 @@ module TasteTester
62
62
  transport 'ssh'
63
63
  no_repo false
64
64
  json false
65
+ jumps nil
66
+ windows_target false
65
67
 
66
68
  # Start/End refs for calculating changes in the repo.
67
69
  # - start_ref should be the "master" commit of the repository
@@ -51,22 +51,22 @@ module TasteTester
51
51
  # Find the set of roles dependent on the changed files.
52
52
  # If returning something other than a set of roles, post_impact and/or
53
53
  # print_impact should be specified to handle the output.
54
- def self.impact_find_roles(_changes); end
54
+ def self.find_impact(_changes); end
55
55
 
56
56
  # Do stuff after we find impacted roles
57
- # This should return a Set object with the final impact. To return more
58
- # complex data, you must also provide a print_impact function which returns
59
- # true to override the default output.
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
60
  def self.post_impact(_impacted_roles); end
61
61
 
62
- # Customized the printed output of impact
62
+ # Customize the printed output of impact
63
63
  # If this method returns true, the default output will not be printed.
64
64
  def self.print_impact(_final_impact); end
65
65
 
66
66
  def self.get(file)
67
67
  path = File.expand_path(file)
68
- logger.warn("Loading plugin at #{path}")
69
- unless File.exists?(path)
68
+ logger.warn("Loading plugin at #{path}") unless TasteTester::Config.json
69
+ unless File.exist?(path)
70
70
  logger.error('Plugin file not found')
71
71
  exit(1)
72
72
  end
@@ -47,25 +47,11 @@ module TasteTester
47
47
  def runchef
48
48
  logger.warn("Running '#{TasteTester::Config.chef_client_command}' " +
49
49
  "on #{@name}")
50
- cmd = "#{TasteTester::Config.ssh_command} " +
51
- "#{TasteTester::Config.user}@#{@name} "
52
- if TasteTester::Config.user != 'root'
53
- cc = Base64.encode64(cmds).delete("\n")
54
- cmd += "\"echo '#{cc}' | base64 --decode | sudo bash -x\""
55
- else
56
- cmd += "\"#{cmds}\""
57
- end
58
- status = IO.popen(
59
- cmd,
60
- ) do |io|
61
- # rubocop:disable AssignmentInCondition
62
- while line = io.gets
63
- puts line.chomp!
64
- end
65
- # rubocop:enable AssignmentInCondition
66
- io.close
67
- $CHILD_STATUS.to_i
68
- end
50
+ transport = get_transport
51
+ transport << TasteTester::Config.chef_client_command
52
+
53
+ io = IO.new(1)
54
+ status, = transport.run(io)
69
55
  logger.warn("Finished #{TasteTester::Config.chef_client_command}" +
70
56
  " on #{@name} with status #{status}")
71
57
  if status.zero?
@@ -106,34 +92,24 @@ module TasteTester
106
92
  # see if someone else is taste-testing
107
93
  transport << we_testing
108
94
 
109
- transport << 'logger -t taste-tester Moving server into taste-tester' +
110
- " for #{@user}"
111
- transport << touchcmd
112
- # shell redirection is also racy, so make a temporary file first
113
- transport << "tmpconf=$(mktemp #{TasteTester::Config.chef_config_path}/" +
114
- "#{TASTE_TESTER_CONFIG}.TMPXXXXXX)"
115
- transport << "/bin/echo -n \"#{serialized_config}\" | base64 --decode" +
116
- ' > "${tmpconf}"'
117
- # then rename it to replace any existing file
118
- transport << 'mv -f "${tmpconf}" ' +
119
- "#{TasteTester::Config.chef_config_path}/#{TASTE_TESTER_CONFIG}"
120
- transport << "( ln -vsf #{TasteTester::Config.chef_config_path}" +
121
- "/#{TASTE_TESTER_CONFIG} #{TasteTester::Config.chef_config_path}/" +
122
- "#{TasteTester::Config.chef_config}; true )"
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
123
100
 
124
101
  # look again to see if someone else is taste-testing. This is where
125
102
  # we work out if we won or lost a race with another user.
126
103
  transport << we_testing
127
104
 
128
- transport.run
105
+ status, output = transport.run
129
106
 
130
- case transport.status
107
+ case status
131
108
  when 0
132
109
  # no problem, keep going.
133
110
  nil
134
111
  when 42
135
- fail TasteTester::Exceptions::AlreadyTestingError,
136
- transport.output.chomp
112
+ fail TasteTester::Exceptions::AlreadyTestingError, output.chomp
137
113
  else
138
114
  transport.error!
139
115
  end
@@ -157,18 +133,10 @@ module TasteTester
157
133
  if TasteTester::Config.use_ssh_tunnels
158
134
  TasteTester::Tunnel.kill(@name)
159
135
  end
160
- config_prod = TasteTester::Config.chef_config.split('.').join('-prod.')
161
- [
162
- "ln -vsf #{TasteTester::Config.chef_config_path}/#{config_prod} " +
163
- "#{TasteTester::Config.chef_config_path}/" +
164
- TasteTester::Config.chef_config,
165
- "ln -vsf #{TasteTester::Config.chef_config_path}/client-prod.pem " +
166
- "#{TasteTester::Config.chef_config_path}/client.pem",
167
- "rm -vf #{TasteTester::Config.chef_config_path}/#{TASTE_TESTER_CONFIG}",
168
- "rm -vf #{TasteTester::Config.timestamp_file}",
169
- 'logger -t taste-tester Returning server to production',
170
- ].each do |cmd|
171
- transport << cmd
136
+ if TasteTester::Config.windows_target
137
+ add_windows_untest_cmds(transport)
138
+ else
139
+ add_sane_os_untest_cmds(transport)
172
140
  end
173
141
  transport.run!
174
142
  end
@@ -183,15 +151,30 @@ module TasteTester
183
151
  # short circuits the test verb
184
152
  # This is written as a squiggly heredoc so the indentation of the awk is
185
153
  # preserved. Later we remove the newlines to make it a bit easier to read.
186
- shellcode = <<~ENDOFSHELLCODE
187
- awk "\\$0 ~ /^#{USER_PREAMBLE}/{
188
- if (\\$NF != \\"#{@user}\\"){
189
- print \\$NF;
190
- exit 42
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
+ }
191
164
  }
192
- }" #{config_file}
193
- ENDOFSHELLCODE
194
- shellcode.delete("\n")
165
+ ENDOFSHELLCODE
166
+ else
167
+ shellcode = <<~ENDOFSHELLCODE
168
+ awk "\\$0 ~ /^#{USER_PREAMBLE}/{
169
+ if (\\$NF != \\"#{@user}\\"){
170
+ print \\$NF;
171
+ exit 42
172
+ }
173
+ }" #{config_file}
174
+ ENDOFSHELLCODE
175
+ shellcode.chomp!
176
+ end
177
+ shellcode
195
178
  end
196
179
 
197
180
  def keeptesting
@@ -210,15 +193,147 @@ module TasteTester
210
193
 
211
194
  private
212
195
 
196
+ # Sources must be 'registered' with the Eventlog, so check if we have
197
+ # registered and register if necessary
198
+ def create_eventlog_if_needed_cmd
199
+ get_src = 'Get-EventLog -LogName Application -source taste-tester 2>$null'
200
+ mk_src = 'New-EventLog -source "taste-tester" -LogName Application'
201
+ "if (-Not (#{get_src})) { #{mk_src} }"
202
+ end
203
+
204
+ # Remote testing commands for most OSes...
205
+ def add_sane_os_test_cmds(transport, serialized_config)
206
+ transport << 'logger -t taste-tester Moving server into taste-tester' +
207
+ " for #{@user}"
208
+ transport << touchcmd
209
+ # shell redirection is also racy, so make a temporary file first
210
+ transport << "tmpconf=$(mktemp #{TasteTester::Config.chef_config_path}/" +
211
+ "#{TASTE_TESTER_CONFIG}.TMPXXXXXX)"
212
+ transport << "/bin/echo -n \"#{serialized_config}\" | base64 --decode" +
213
+ ' > "${tmpconf}"'
214
+ # then rename it to replace any existing file
215
+ transport << 'mv -f "${tmpconf}" ' +
216
+ "#{TasteTester::Config.chef_config_path}/#{TASTE_TESTER_CONFIG}"
217
+ transport << "( ln -vsf #{TasteTester::Config.chef_config_path}" +
218
+ "/#{TASTE_TESTER_CONFIG} #{TasteTester::Config.chef_config_path}/" +
219
+ "#{TasteTester::Config.chef_config}; true )"
220
+ end
221
+
222
+ # Remote testing commands for Windows
223
+ def add_windows_test_cmds(transport, serialized_config)
224
+ # This is the closest equivalent to 'bash -x' - but if we put it on
225
+ # by default the way we do with linux it badly breaks our output. So only
226
+ # set it if we're in debug
227
+ #
228
+ # This isn't the most optimal place for this. It should be in ssh_util
229
+ # and we should jam this into the beggining of the cmds list we get,
230
+ # but this is early enough and good enough for now and we can think about
231
+ # that when we refactor tunnel.sh, ssh.sh and ssh_util.sh into one sane
232
+ # class.
233
+ if logger.level == Logger::DEBUG
234
+ transport << 'Set-PSDebug -trace 1'
235
+ end
236
+
237
+ ttconfig =
238
+ "#{TasteTester::Config.chef_config_path}/#{TASTE_TESTER_CONFIG}"
239
+ realconfig = "#{TasteTester::Config.chef_config_path}/" +
240
+ TasteTester::Config.chef_config
241
+ [
242
+ create_eventlog_if_needed_cmd,
243
+ 'Write-EventLog -LogName "Application" -Source "taste-tester" ' +
244
+ '-EventID 1 -EntryType Information ' +
245
+ "-Message \"Moving server into taste-tester for #{@user}\"",
246
+ touchcmd,
247
+ "$b64 = \"#{serialized_config}\"",
248
+ "$ttconfig = \"#{ttconfig}\"",
249
+ "$realconfig = \"#{realconfig}\"",
250
+
251
+ '$tmp64 = (New-TemporaryFile).name',
252
+ '$tmp = (New-TemporaryFile).name',
253
+
254
+ '$b64 | Out-File -Encoding ASCII $tmp64 -Force',
255
+
256
+ # Remove our tmp file before we write to it or certutil crashes...
257
+ "#{win_rm_f} $tmp",
258
+ 'certutil -decode $tmp64 $tmp',
259
+ 'mv $tmp $ttconfig -Force',
260
+
261
+ 'New-Item -ItemType SymbolicLink -Value $ttconfig $realconfig -Force',
262
+ ].each do |cmd|
263
+ transport << cmd
264
+ end
265
+ end
266
+
213
267
  def touchcmd
214
- touch = Base64.encode64(
215
- "if [ 'Darwin' = $(uname) ]; then touch -t \"$(date -r " +
216
- "#{TasteTester::Config.testing_end_time.to_i} +'%Y%m%d%H%M.%S')\" " +
217
- "#{TasteTester::Config.timestamp_file}; else touch --date \"$(date " +
218
- "-d @#{TasteTester::Config.testing_end_time.to_i} +'%Y-%m-%d %T')\" " +
219
- "#{TasteTester::Config.timestamp_file}; fi",
220
- ).delete("\n")
221
- "/bin/echo -n '#{touch}' | base64 --decode | bash"
268
+ if TasteTester::Config.windows_target
269
+ # There's no good touch equivalent in Windows. You can force
270
+ # creation of a new file, but that'll nuke it's contents, which if we're
271
+ # 'keeptesting'ing, then we'll loose the contents (PID and such).
272
+ # We can set the timestamp with Get-Item.creationtime, but it must exist
273
+ # if we're not gonna crash. So do both.
274
+ [
275
+ "$ts = \"#{TasteTester::Config.timestamp_file}\"",
276
+ 'if (-Not (Test-Path $ts)) { New-Item -ItemType file $ts }',
277
+ '(Get-Item "$ts").LastWriteTime=("' +
278
+ "#{TasteTester::Config.testing_end_time}\")",
279
+ ].join(';')
280
+ else
281
+ touch = Base64.encode64(
282
+ "if [ 'Darwin' = $(uname) ]; then touch -t \"$(date -r " +
283
+ "#{TasteTester::Config.testing_end_time.to_i} +'%Y%m%d%H%M.%S')\" " +
284
+ "#{TasteTester::Config.timestamp_file}; else touch --date \"$(date " +
285
+ "-d @#{TasteTester::Config.testing_end_time.to_i} +'%Y-%m-%d %T')\"" +
286
+ " #{TasteTester::Config.timestamp_file}; fi",
287
+ ).delete("\n")
288
+ "/bin/echo -n '#{touch}' | base64 --decode | bash"
289
+ end
290
+ end
291
+
292
+ # Remote untesting commands for Windows
293
+ def add_windows_untest_cmds(transport)
294
+ config_prod = TasteTester::Config.chef_config.split('.').join('-prod.')
295
+ tt_config =
296
+ "#{TasteTester::Config.chef_config_path}/#{TASTE_TESTER_CONFIG}"
297
+ pem_file = "#{TasteTester::Config.chef_config_path}/client-prod.pem"
298
+ pem_link = "#{TasteTester::Config.chef_config_path}/client.pem"
299
+
300
+ [
301
+ 'New-Item -ItemType SymbolicLink -Force -Value ' +
302
+ "#{TasteTester::Config.chef_config_path}/#{config_prod} " +
303
+ "#{TasteTester::Config.chef_config_path}/" +
304
+ TasteTester::Config.chef_config,
305
+ 'New-Item -ItemType SymbolicLink -Force -Value ' +
306
+ "#{pem_file} #{pem_link}",
307
+ "#{win_rm_f} #{tt_config}",
308
+ "#{win_rm_f} #{TasteTester::Config.timestamp_file}",
309
+ create_eventlog_if_needed_cmd,
310
+ 'Write-EventLog -LogName "Application" -Source "taste-tester" ' +
311
+ '-EventID 4 -EntryType Information -Message "Returning server ' +
312
+ 'to production"',
313
+ ].each do |cmd|
314
+ transport << cmd
315
+ end
316
+ end
317
+
318
+ # Remote untesting commands for most OSes...
319
+ def add_sane_os_untest_cmds(transport)
320
+ config_prod = TasteTester::Config.chef_config.split('.').join('-prod.')
321
+ [
322
+ "ln -vsf #{TasteTester::Config.chef_config_path}/#{config_prod} " +
323
+ "#{TasteTester::Config.chef_config_path}/" +
324
+ TasteTester::Config.chef_config,
325
+ "ln -vsf #{TasteTester::Config.chef_config_path}/client-prod.pem " +
326
+ "#{TasteTester::Config.chef_config_path}/client.pem",
327
+ "rm -vf #{TasteTester::Config.chef_config_path}/#{TASTE_TESTER_CONFIG}",
328
+ "rm -vf #{TasteTester::Config.timestamp_file}",
329
+ 'logger -t taste-tester Returning server to production',
330
+ ].each do |cmd|
331
+ transport << cmd
332
+ end
333
+ end
334
+
335
+ def win_rm_f
336
+ 'Remove-Item -Force -ErrorAction SilentlyContinue'
222
337
  end
223
338
 
224
339
  def config
@@ -310,7 +425,7 @@ module TasteTester
310
425
  chef_repo_path taste_tester_dest
311
426
  ENDOFSCRIPT
312
427
  end
313
- return ttconfig
428
+ ttconfig
314
429
  end
315
430
  end
316
431
  end