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.
@@ -22,8 +22,6 @@ module TasteTester
22
22
  include TasteTester::Logging
23
23
  include BetweenMeals::Util
24
24
 
25
- attr_reader :output, :status
26
-
27
25
  def initialize
28
26
  @host = 'localhost'
29
27
  @cmds = []
@@ -36,11 +34,11 @@ module TasteTester
36
34
  alias << add
37
35
 
38
36
  def run
39
- @status, @output = exec(cmd, logger)
37
+ exec(cmd, logger)
40
38
  end
41
39
 
42
40
  def run!
43
- @status, @output = exec!(cmd, logger)
41
+ exec!(cmd, logger)
44
42
  rescue StandardError => e
45
43
  logger.error(e.message)
46
44
  error!
@@ -22,8 +22,6 @@ module TasteTester
22
22
  include TasteTester::Logging
23
23
  include BetweenMeals::Util
24
24
 
25
- attr_reader :output, :status
26
-
27
25
  def initialize
28
26
  print_noop_warning
29
27
  @host = 'localhost'
@@ -48,7 +46,7 @@ module TasteTester
48
46
  alias << add
49
47
 
50
48
  def run
51
- @status, @output = run!
49
+ run!
52
50
  end
53
51
 
54
52
  def run!
@@ -16,7 +16,6 @@
16
16
 
17
17
  require 'fileutils'
18
18
  require 'socket'
19
- require 'timeout'
20
19
 
21
20
  require 'between_meals/util'
22
21
  require 'taste_tester/config'
@@ -65,10 +64,10 @@ module TasteTester
65
64
  # on all addresses - both v4 and v6. Note that on localhost, ::1 is
66
65
  # v6-only, so we default to 127.0.0.1 instead.
67
66
  if TasteTester::Config.use_ssh_tunnels
68
- @addr = '127.0.0.1'
67
+ @addrs = ['127.0.0.1']
69
68
  @host = 'localhost'
70
69
  else
71
- @addr = '::'
70
+ @addrs = ['::', '0.0.0.0']
72
71
  begin
73
72
  @host = TasteTester::Config.my_hostname || Socket.gethostname
74
73
  rescue StandardError
@@ -162,7 +161,7 @@ module TasteTester
162
161
  end
163
162
 
164
163
  def start_chef_zero
165
- File.unlink(@log_file) if File.exists?(@log_file)
164
+ File.unlink(@log_file) if File.exist?(@log_file)
166
165
  @state.update({
167
166
  :port => TasteTester::Config.chef_port,
168
167
  :ssl => TasteTester::Config.use_ssl,
@@ -175,7 +174,8 @@ module TasteTester
175
174
  extend ::TasteTester::Windows
176
175
  start_win_chef_zero_server
177
176
  else
178
- cmd = +"#{chef_zero_path} --host #{@addr} --port #{@state.port} -d"
177
+ hostarg = @addrs.map { |addr| "--host #{addr}" }.join(' ')
178
+ cmd = +"#{chef_zero_path} #{hostarg} --port #{@state.port} -d"
179
179
  if TasteTester::Config.chef_zero_logging
180
180
  cmd << " --log-file #{@log_file}" +
181
181
  ' --log-level debug'
@@ -15,14 +15,14 @@
15
15
  # limitations under the License.
16
16
 
17
17
  require 'taste_tester/exceptions'
18
+ require 'taste_tester/ssh_util'
18
19
 
19
20
  module TasteTester
20
21
  # Thin ssh wrapper
21
22
  class SSH
22
23
  include TasteTester::Logging
23
24
  include BetweenMeals::Util
24
-
25
- attr_reader :output, :status
25
+ include TasteTester::SSH::Util
26
26
 
27
27
  def initialize(host, tunnel = false)
28
28
  @host = host
@@ -36,47 +36,24 @@ module TasteTester
36
36
 
37
37
  alias << add
38
38
 
39
- def run
40
- @status, @output = exec(cmd, logger)
39
+ def run(stream = nil)
40
+ exec(cmd, logger, stream)
41
41
  end
42
42
 
43
- def run!
44
- @status, @output = exec!(cmd, logger)
43
+ def run!(stream = nil)
44
+ exec!(cmd, logger, stream)
45
45
  rescue StandardError => e
46
46
  logger.error(e.message)
47
47
  error!
48
48
  end
49
49
 
50
- def error!
51
- error = <<-ERRORMESSAGE
52
- SSH returned error while connecting to #{TasteTester::Config.user}@#{@host}
53
- The host might be broken or your SSH access is not working properly
54
- Try doing
55
- #{TasteTester::Config.ssh_command} -v #{TasteTester::Config.user}@#{@host}
56
- and come back once that works
57
- ERRORMESSAGE
58
- error.lines.each { |x| logger.error x.strip }
59
- fail TasteTester::Exceptions::SshError
60
- end
61
-
62
50
  private
63
51
 
64
52
  def cmd
65
53
  @cmds.each do |cmd|
66
54
  logger.info("Will run: '#{cmd}' on #{@host}")
67
55
  end
68
- cmds = @cmds.join(' && ')
69
- cmd = "#{TasteTester::Config.ssh_command} " +
70
- '-T -o BatchMode=yes ' +
71
- "-o ConnectTimeout=#{TasteTester::Config.ssh_connect_timeout} " +
72
- "#{TasteTester::Config.user}@#{@host} "
73
- if TasteTester::Config.user != 'root'
74
- cc = Base64.encode64(cmds).delete("\n")
75
- cmd += "\"echo '#{cc}' | base64 --decode | sudo bash -x\""
76
- else
77
- cmd += "\'#{cmds}\'"
78
- end
79
- cmd
56
+ build_ssh_cmd(ssh_base_cmd, @cmds)
80
57
  end
81
58
  end
82
59
  end
@@ -0,0 +1,127 @@
1
+ module TasteTester
2
+ class SSH
3
+ module Util
4
+ def ssh_base_cmd
5
+ jumps = TasteTester::Config.jumps ?
6
+ "-J #{TasteTester::Config.jumps}" : ''
7
+ "#{TasteTester::Config.ssh_command} #{jumps} -T -o BatchMode=yes " +
8
+ '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no ' +
9
+ "-o ConnectTimeout=#{TasteTester::Config.ssh_connect_timeout} " +
10
+ "#{TasteTester::Config.user}@#{@host} "
11
+ end
12
+
13
+ def error!
14
+ error = <<~ERRORMESSAGE
15
+ SSH returned error while connecting to #{TasteTester::Config.user}@#{@host}
16
+ The host might be broken or your SSH access is not working properly
17
+ Try doing
18
+
19
+ #{ssh_base_cmd} -v
20
+
21
+ to see if ssh connection is good.
22
+ If ssh works, add '-v' key to taste-tester to see the list of commands it's
23
+ trying to execute, and try to run them manually on destination host
24
+ ERRORMESSAGE
25
+ logger.error(error)
26
+ fail TasteTester::Exceptions::SshError
27
+ end
28
+
29
+ def build_ssh_cmd(ssh, command_list)
30
+ if TasteTester::Config.windows_target
31
+ # Powershell has no `&&`. So originally we looked into joining the
32
+ # various commands with `; if ($LASTEXITCODE -ne 0) { exit 42 }; `
33
+ # except that it turns out lots of Powershell commands don't set
34
+ # $LASTEXITCODE and so that crashes a lot.
35
+ #
36
+ # There is an `-and`, but it only works if you group things together
37
+ # with `()`, but that loses any output.
38
+ #
39
+ # Technically in the latest preview of Powershell 7, `&&` exists, but
40
+ # we cannot rely on this.
41
+ #
42
+ # So here we are. Thanks Windows Team.
43
+ #
44
+ # Anyway, what we *really* care about is that we exit if we_testing()
45
+ # errors out, and on Windows, we can do that straight from the
46
+ # powershell we generate there (we're not forking off awk), so the
47
+ # `&&` isn't as critical. It's still a bummer that we continue on
48
+ # if one of the commands fails, but... Well, it's Windows,
49
+ # whatchyagonnado?
50
+
51
+ cmds = command_list.join(' ; ')
52
+ else
53
+ cmds = command_list.join(' && ')
54
+ end
55
+ cmd = ssh
56
+ cc = Base64.encode64(cmds).delete("\n")
57
+ if TasteTester::Config.windows_target
58
+
59
+ # This is pretty horrible, but because there's no way I can find to
60
+ # take base64 as stdin and output text, we end up having to do use
61
+ # these PS functions. But they're going to pass through *both* bash
62
+ # *and* powershell, so in order to preserve the quotes, it gets
63
+ # pretty ugly.
64
+ #
65
+ # The tldr here is that in shell you can't escape quotes you're
66
+ # using to quote something. So if you use single quotes, there's no
67
+ # way to escape a single quote inside, and same with double-quotes.
68
+ # As such we switch between quote-styles as necessary. As long as the
69
+ # strings are back-to-back, shell handles this well. To make this
70
+ # clear, imagine you want to echo this:
71
+ # '"'"
72
+ # Exactly like that. You would quote the first single quotes in double
73
+ # quotes: "'"
74
+ # Then the double quotes in single quotes: '"'
75
+ # Now repeat twice and you get: echo "'"'"'"'"'"'
76
+ # And that works reliably.
77
+ #
78
+ # We're doing the same thing here. What we want on the other side of
79
+ # the ssh is:
80
+ # [Text.Encoding]::Utf8.GetString([Convert]::FromBase64String('...'))
81
+ #
82
+ # But for this to work right the command we pass to SSH has to be in
83
+ # single quotes too. For simplicity lets call those two functions
84
+ # above GetString() and Base64(). So we'll start with:
85
+ # ssh host 'GetString(Base64('
86
+ # We've closed that string, now we add the single quote we want there,
87
+ # as well as the stuff inside of those double quotes, so we'll add:
88
+ # '#{cc}'))
89
+ # but that must be in double quotes since we're using single quotes.
90
+ # Put that together:
91
+ # ssh host 'GetString(Base64('"'#{cc}'))"
92
+ # ^-----------------^^---------^
93
+ # string 1 string2
94
+ # No we're doing with needing single quotes inside of our string, go
95
+ # back to using single-quotes so no variables get interpolated. We now
96
+ # add: ' | powershell.exe -c -; exit $LASTEXITCODE'
97
+ # ssh host 'GetString(Base64('"'#{cc}'))"' | powershell.exe ...'
98
+ # ^-----------------^^---------^^---------------------^
99
+ #
100
+ # More than you ever wanted to know about shell. You're welcome.
101
+ #
102
+ # But now we have to put it inside of a ruby string, :)
103
+
104
+ # just for readability, put these crazy function names inside of
105
+ # variables
106
+ fun1 = '[Text.Encoding]::Utf8.GetString'
107
+ fun2 = '[Convert]::FromBase64String'
108
+ cmd += "'#{fun1}(#{fun2}('\"'#{cc}'))\"' | "
109
+ # ^----------------^ ^----------^^---
110
+ # single-q double-q single-q
111
+ # string 1 string2 string3
112
+ cmd += 'powershell.exe -c -; exit $LASTEXITCODE\''
113
+ # ----------------------------------------^
114
+ # continued string3
115
+ else
116
+ cmd += "\"echo '#{cc}' | base64 --decode"
117
+ if TasteTester::Config.user != 'root'
118
+ cmd += ' | sudo bash -x"'
119
+ else
120
+ cmd += ' | bash -x"'
121
+ end
122
+ end
123
+ cmd
124
+ end
125
+ end
126
+ end
127
+ end
@@ -91,7 +91,10 @@ module TasteTester
91
91
  end
92
92
 
93
93
  def bundle
94
- TasteTester::State.read(:bundle)
94
+ val = TasteTester::State.read(:bundle)
95
+ # promote value to symbol to match config value.
96
+ return :compatible if val == 'compatible'
97
+ val
95
98
  end
96
99
 
97
100
  def bundle=(bundle)
@@ -116,7 +119,7 @@ module TasteTester
116
119
 
117
120
  def real_wipe
118
121
  if TasteTester::Config.ref_file &&
119
- File.exists?(TasteTester::Config.ref_file)
122
+ File.exist?(TasteTester::Config.ref_file)
120
123
  File.delete(TasteTester::Config.ref_file)
121
124
  end
122
125
  end
@@ -14,62 +14,49 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require 'taste_tester/logging'
18
+ require 'between_meals/util'
19
+ require 'taste_tester/ssh_util'
20
+
17
21
  module TasteTester
18
22
  # Thin ssh tunnel wrapper
19
23
  class Tunnel
20
24
  include TasteTester::Logging
21
25
  include BetweenMeals::Util
26
+ include TasteTester::SSH::Util
22
27
 
23
28
  attr_reader :port
24
29
 
25
30
  def initialize(host, server)
26
31
  @host = host
27
32
  @server = server
28
- if TasteTester::Config.testing_until
29
- @delta_secs = TasteTester::Config.testing_until.strftime('%s').to_i -
30
- Time.now.strftime('%s').to_i
31
- else
32
- @delta_secs = TasteTester::Config.testing_time
33
- end
34
33
  end
35
34
 
36
35
  def run
37
36
  @port = TasteTester::Config.tunnel_port
38
37
  logger.info("Setting up tunnel on port #{@port}")
39
- @status, @output = exec!(cmd, logger)
38
+ exec!(cmd, logger)
40
39
  rescue StandardError => e
41
40
  logger.error "Failed bringing up ssh tunnel: #{e}"
42
- exit(1)
41
+ error!
43
42
  end
44
43
 
45
44
  def cmd
46
- @max_ping = @delta_secs / 10
47
- pid = '$$'
48
- @ts = TasteTester::Config.testing_end_time.strftime('%y%m%d%H%M.%S')
49
- cmds = "ps -o pgid= -p $(ps -o ppid= -p #{pid}) | sed \"s| ||g\" " +
50
- " > #{TasteTester::Config.timestamp_file} &&" +
51
- " touch -t #{@ts} #{TasteTester::Config.timestamp_file} &&" +
52
- " sleep #{@delta_secs}"
45
+ if TasteTester::Config.windows_target
46
+ cmds = windows_tunnel_cmd
47
+ else
48
+ cmds = sane_os_tunnel_cmd
49
+ end
50
+
53
51
  # As great as it would be to have ExitOnForwardFailure=yes,
54
52
  # we had multiple cases of tunnels dying
55
53
  # if -f and ExitOnForwardFailure are used together.
56
54
  # In most cases the first request from chef was "breaking" the tunnel,
57
55
  # in a way that port was still open, but subsequent requests were hanging.
58
56
  # This is reproducible and should be looked into.
59
- cmd = "#{TasteTester::Config.ssh_command} " +
60
- "-o ConnectTimeout=#{TasteTester::Config.ssh_connect_timeout} " +
61
- '-T -o BatchMode=yes ' +
62
- '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no ' +
63
- "-o ServerAliveInterval=10 -o ServerAliveCountMax=#{@max_ping} " +
64
- "-f -R #{@port}:localhost:#{@server.port} "
65
- if TasteTester::Config.user != 'root'
66
- cc = Base64.encode64(cmds).delete("\n")
67
- cmd += "#{TasteTester::Config.user}@#{@host} \"echo '#{cc}' | base64" +
68
- ' --decode | sudo bash -x"'
69
- else
70
- cmd += "root@#{@host} '#{cmds}'"
71
- end
72
- cmd
57
+ cmd = "#{ssh_base_cmd} -o ServerAliveInterval=10 " +
58
+ "-o ServerAliveCountMax=6 -f -R #{@port}:localhost:#{@server.port} "
59
+ build_ssh_cmd(cmd, [cmds])
73
60
  end
74
61
 
75
62
  def self.kill(name)
@@ -78,15 +65,165 @@ module TasteTester
78
65
  # surround this in paryns, and make sure as a whole it evaluates
79
66
  # to true so it doesn't mess up other things... even though this is
80
67
  # the only thing we're currently executing in this SSH.
81
- if TasteTester::Config.user != 'root'
82
- sudo = 'sudo '
68
+ if TasteTester::Config.windows_target
69
+ cmd = <<~EOPS
70
+ if (Test-Path "#{TasteTester::Config.timestamp_file}") {
71
+ $x = cat "#{TasteTester::Config.timestamp_file}"
72
+ if ($x -ne $null) {
73
+ kill -Force $x 2>$null
74
+ }
75
+ }
76
+ $LASTEXITCODE = 0
77
+ EOPS
78
+ else
79
+ cmd = "( [ -s #{TasteTester::Config.timestamp_file} ]" +
80
+ ' && kill -9 -- ' +
81
+ "-\$(cat #{TasteTester::Config.timestamp_file}) 2>/dev/null; " +
82
+ ' true )'
83
83
  end
84
- cmd = "( [ -s #{TasteTester::Config.timestamp_file} ]" +
85
- " && #{sudo}kill -9 -- " +
86
- "-\$(cat #{TasteTester::Config.timestamp_file}) 2>/dev/null; " +
87
- ' true )'
88
84
  ssh << cmd
89
85
  ssh.run!
90
86
  end
87
+
88
+ private
89
+
90
+ def windows_tunnel_cmd
91
+ # We are powershell. If you walk up you get:
92
+ # ppid - ssh
93
+ # pppid - ssh
94
+ # ppppid - ssh
95
+ # pppppid - services
96
+ #
97
+ # Unlike in Linux you don't need to walk up the tree, however. In fact,
98
+ # killing pppid or ppid didn't actually terminate the session. Only
99
+ # killing our actual powershell instance did.
100
+ #
101
+ # Moreover, it doesn't seem like re-parenting works the same way. So
102
+ # this is pretty simple.
103
+ #
104
+ # For the record, if you want to play with this, you do so with:
105
+ # (gwmi win32_process | ? processid -eq $PID).parentprocessid
106
+ #
107
+ # Also note that backtick is a line-continuation marker in powershell.
108
+ <<~EOS
109
+ $ts = "#{TasteTester::Config.timestamp_file}"
110
+ echo $PID | Out-File -Encoding ASCII "$ts"
111
+ # TODO: pull this from Host.touchcmd
112
+ (Get-Item "$ts").LastWriteTime=("#{TasteTester::Config.testing_end_time}")
113
+
114
+ while ($true) {
115
+ if (-Not (Test-Path $ts)) {
116
+ # if we are here, we know we've created our source
117
+ $splat = @{
118
+ LogName = "Application"
119
+ Source = "taste-tester"
120
+ EventID = 5
121
+ EntryType = "Information"
122
+ Message = "Ending tunnel: timestamp file disappeared"
123
+ }
124
+ Write-EventLog @splat
125
+ break
126
+ }
127
+ sleep 60
128
+ }
129
+ done
130
+ EOS
131
+ end
132
+
133
+ def sane_os_tunnel_cmd
134
+ @ts = TasteTester::Config.testing_end_time.strftime('%y%m%d%H%M.%S')
135
+ # Tie the life of our SSH tunnel with the life of timestamp file.
136
+ # taste-testing can be renewed, so we'll wait until:
137
+ # 1. the timestamp file is entirely gone
138
+ # 2. our parent sshd process dies
139
+ # 3. new taste-tester instance is running (file contains different PGID)
140
+ <<~EOS
141
+ log() {
142
+ [ -e /usr/bin/logger ] || return
143
+ logger -t taste-tester "$*"
144
+ }
145
+ # sets $current_pgid
146
+ # This is important, this should just be called ald let it set the
147
+ # variable. Do NOT call in a subshell like foo=$(get_current_pgid)
148
+ # as then you end up even further down the list of children
149
+ get_current_pgid() {
150
+
151
+ # if TT user is non-root, then it breaks down like this:
152
+ # we are 'bash'
153
+ # our parent is 'sudo'
154
+ # our parent's parent is 'bash "echo ..." | sudo bash -x'
155
+ # our parent's parent's parent is ssh
156
+ # - we want the progress-group ID of *that*
157
+ #
158
+ # EXCEPT... sometimes sudo forks itself one more time so it's
159
+ # we are 'bash'
160
+ # our parent is 'sudo'
161
+ # our parent's parent 'sudo'
162
+ # our parent's parent's parent is 'bash "echo ..." | sudo bash -x'
163
+ # our parent's parent's parent's parent is ssh
164
+ # - we want the progress-group ID of *that*
165
+ #
166
+ # BUT if the TT user is root, no sudo at all...
167
+ # we are 'bash'
168
+ # our parent is 'bash "echo ..." | bash -c
169
+ # our parent's parent is ssh
170
+ # - we want the progress-group ID of *that*
171
+ #
172
+ # We can make all sorts of assumptions, but the most reliable way
173
+ # to do this that's always correct is to is simply to walk parents until
174
+ # we hit something with SSH in the name. Start with PPID and go from
175
+ # there.
176
+ #
177
+ # There's a few commented out 'log's here that are too verbose
178
+ # for operation (since this function runs every minute) but are useful
179
+ # for debugging.
180
+
181
+ relevant_pid=''
182
+ current_pid=$PPID
183
+ while true; do
184
+ name=$(ps -o command= -p $current_pid)
185
+ if [[ "$name" =~ sshd ]]; then
186
+ # Uncomment the following for debugging...
187
+ #log "$current_pid is ssh, that's us!"
188
+ relevant_pid=$current_pid
189
+ break
190
+ fi
191
+ # Uncomment the following for debugging...
192
+ #log "$current_pid is $name, finding parent..."
193
+ current_pid=$(ps -o ppid= -p $current_pid)
194
+ done
195
+ if [ -z "$relevant_pid" ];then
196
+ log "Cannot determine relevant PGID"
197
+ exit 42
198
+ fi
199
+ current_pgid="$(ps -o pgid= -p $relevant_pid | sed "s| ||g")"
200
+ # Uncomment the following for debugging...
201
+ #log "PGID of ssh ($relevant_pid) is $current_pgid"
202
+ }
203
+ get_current_pgid
204
+ SSH_PGID=$current_pgid
205
+
206
+ echo $SSH_PGID > #{TasteTester::Config.timestamp_file} && \
207
+ # TODO: pull this from Host.touchcmd
208
+ touch -t #{@ts} #{TasteTester::Config.timestamp_file} && \
209
+ while true; do
210
+ if ! [ -f "#{TasteTester::Config.timestamp_file}" ]; then
211
+ log "Ending tunnel: timestamp file disappeared"
212
+ break
213
+ fi
214
+ current_pid="$(cat #{TasteTester::Config.timestamp_file})"
215
+ if ! [ "$current_pid" = "$SSH_PGID" ]; then
216
+ log "Ending tunnel: timestamp PGID changed"
217
+ break
218
+ fi
219
+ get_current_pgid
220
+ if ! [ "$current_pgid" = "$SSH_PGID" ]; then
221
+ log "Ending tunnel: timestamp PGID isn't ours"
222
+ break
223
+ fi
224
+ sleep 60
225
+ done
226
+ EOS
227
+ end
91
228
  end
92
229
  end