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
data/lib/taste_tester/state.rb
CHANGED
@@ -17,6 +17,7 @@
|
|
17
17
|
require 'fileutils'
|
18
18
|
require 'socket'
|
19
19
|
require 'timeout'
|
20
|
+
require 'chef/mash'
|
20
21
|
|
21
22
|
require 'between_meals/util'
|
22
23
|
require 'taste_tester/config'
|
@@ -29,13 +30,11 @@ module TasteTester
|
|
29
30
|
include ::BetweenMeals::Util
|
30
31
|
|
31
32
|
def initialize
|
32
|
-
ref_dir = File.dirname(File.expand_path(
|
33
|
-
TasteTester::Config.ref_file,
|
34
|
-
))
|
33
|
+
ref_dir = File.dirname(File.expand_path(TasteTester::Config.ref_file))
|
35
34
|
unless File.directory?(ref_dir)
|
36
35
|
begin
|
37
36
|
FileUtils.mkpath(ref_dir)
|
38
|
-
rescue => e
|
37
|
+
rescue StandardError => e
|
39
38
|
logger.error("Chef temp dir #{ref_dir} missing and can't be created")
|
40
39
|
logger.error(e)
|
41
40
|
exit(1)
|
@@ -83,6 +82,25 @@ module TasteTester
|
|
83
82
|
write(:ref, ref)
|
84
83
|
end
|
85
84
|
|
85
|
+
def last_upload_time
|
86
|
+
TasteTester::State.read(:last_upload_time)
|
87
|
+
end
|
88
|
+
|
89
|
+
def last_upload_time=(time)
|
90
|
+
write(:last_upload_time, time)
|
91
|
+
end
|
92
|
+
|
93
|
+
def bundle
|
94
|
+
val = TasteTester::State.read(:bundle)
|
95
|
+
# promote value to symbol to match config value.
|
96
|
+
return :compatible if val == 'compatible'
|
97
|
+
val
|
98
|
+
end
|
99
|
+
|
100
|
+
def bundle=(bundle)
|
101
|
+
write(:bundle, bundle)
|
102
|
+
end
|
103
|
+
|
86
104
|
def update(vals)
|
87
105
|
merge(vals)
|
88
106
|
end
|
@@ -101,14 +119,14 @@ module TasteTester
|
|
101
119
|
|
102
120
|
def real_wipe
|
103
121
|
if TasteTester::Config.ref_file &&
|
104
|
-
File.
|
122
|
+
File.exist?(TasteTester::Config.ref_file)
|
105
123
|
File.delete(TasteTester::Config.ref_file)
|
106
124
|
end
|
107
125
|
end
|
108
126
|
|
109
127
|
def self.read(key)
|
110
128
|
JSON.parse(File.read(TasteTester::Config.ref_file))[key.to_s]
|
111
|
-
rescue => e
|
129
|
+
rescue StandardError => e
|
112
130
|
logger.debug(e)
|
113
131
|
nil
|
114
132
|
end
|
@@ -120,9 +138,13 @@ module TasteTester
|
|
120
138
|
end
|
121
139
|
|
122
140
|
def merge(vals)
|
141
|
+
# we generally use symbols for the keys, but to/from JSON will
|
142
|
+
# give us strings, and thus duplicate keys, which is bad. So
|
143
|
+
# use a Mash
|
144
|
+
state = Mash.new
|
123
145
|
begin
|
124
146
|
state = JSON.parse(File.read(TasteTester::Config.ref_file))
|
125
|
-
rescue
|
147
|
+
rescue StandardError
|
126
148
|
state = {}
|
127
149
|
end
|
128
150
|
state.merge!(vals)
|
@@ -132,7 +154,7 @@ module TasteTester
|
|
132
154
|
)
|
133
155
|
ff.write(state.to_json)
|
134
156
|
ff.close
|
135
|
-
rescue => e
|
157
|
+
rescue StandardError => e
|
136
158
|
logger.error('Unable to write the reffile')
|
137
159
|
logger.debug(e)
|
138
160
|
exit 0
|
data/lib/taste_tester/tunnel.rb
CHANGED
@@ -14,64 +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
|
-
def initialize(host, server
|
30
|
+
def initialize(host, server)
|
26
31
|
@host = host
|
27
32
|
@server = server
|
28
|
-
@timeout = timeout
|
29
|
-
if TasteTester::Config.testing_until
|
30
|
-
@delta_secs = TasteTester::Config.testing_until.strftime('%s').to_i -
|
31
|
-
Time.now.strftime('%s').to_i
|
32
|
-
else
|
33
|
-
@delta_secs = TasteTester::Config.testing_time
|
34
|
-
end
|
35
33
|
end
|
36
34
|
|
37
35
|
def run
|
38
36
|
@port = TasteTester::Config.tunnel_port
|
39
37
|
logger.info("Setting up tunnel on port #{@port}")
|
40
|
-
|
41
|
-
rescue
|
42
|
-
logger.error
|
43
|
-
|
38
|
+
exec!(cmd, logger)
|
39
|
+
rescue StandardError => e
|
40
|
+
logger.error "Failed bringing up ssh tunnel: #{e}"
|
41
|
+
error!
|
44
42
|
end
|
45
43
|
|
46
44
|
def cmd
|
47
|
-
if TasteTester::Config.
|
48
|
-
|
45
|
+
if TasteTester::Config.windows_target
|
46
|
+
cmds = windows_tunnel_cmd
|
49
47
|
else
|
50
|
-
|
48
|
+
cmds = sane_os_tunnel_cmd
|
51
49
|
end
|
52
|
-
|
53
|
-
" > #{TasteTester::Config.timestamp_file} &&" +
|
54
|
-
" touch -t #{TasteTester::Config.testing_end_time}" +
|
55
|
-
" #{TasteTester::Config.timestamp_file} && sleep #{@delta_secs}"
|
50
|
+
|
56
51
|
# As great as it would be to have ExitOnForwardFailure=yes,
|
57
52
|
# we had multiple cases of tunnels dying
|
58
53
|
# if -f and ExitOnForwardFailure are used together.
|
59
54
|
# In most cases the first request from chef was "breaking" the tunnel,
|
60
55
|
# in a way that port was still open, but subsequent requests were hanging.
|
61
56
|
# This is reproducible and should be looked into.
|
62
|
-
cmd = "#{
|
63
|
-
|
64
|
-
|
65
|
-
'-o ServerAliveInterval=10 -o ServerAliveCountMax=6 ' +
|
66
|
-
"-f -R #{@port}:localhost:#{@server.port} "
|
67
|
-
if TasteTester::Config.user != 'root'
|
68
|
-
cc = Base64.encode64(cmds).delete("\n")
|
69
|
-
cmd += "#{TasteTester::Config.user}@#{@host} \"echo '#{cc}' | base64" +
|
70
|
-
' --decode | sudo bash -x"'
|
71
|
-
else
|
72
|
-
cmd += "root@#{@host} \"#{cmds}\""
|
73
|
-
end
|
74
|
-
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])
|
75
60
|
end
|
76
61
|
|
77
62
|
def self.kill(name)
|
@@ -80,14 +65,159 @@ module TasteTester
|
|
80
65
|
# surround this in paryns, and make sure as a whole it evaluates
|
81
66
|
# to true so it doesn't mess up other things... even though this is
|
82
67
|
# the only thing we're currently executing in this SSH.
|
83
|
-
if TasteTester::Config.
|
84
|
-
|
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 )'
|
85
83
|
end
|
86
|
-
cmd = "( [ -s #{TasteTester::Config.timestamp_file} ]" +
|
87
|
-
" && #{sudo}kill -9 -- " +
|
88
|
-
"-\$(cat #{TasteTester::Config.timestamp_file}); true )"
|
89
84
|
ssh << cmd
|
90
85
|
ssh.run!
|
91
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 (1 -eq 1) {
|
115
|
+
if (-Not (Test-Path $ts)) {
|
116
|
+
# if we are here, we know we've created our source
|
117
|
+
Write-EventLog -LogName "Application" -Source "taste-tester" `
|
118
|
+
-EventID 5 -EntryType Information `
|
119
|
+
-Message "Ending tunnel: timestamp file disappeared"
|
120
|
+
}
|
121
|
+
sleep 60
|
122
|
+
}
|
123
|
+
done
|
124
|
+
EOS
|
125
|
+
end
|
126
|
+
|
127
|
+
def sane_os_tunnel_cmd
|
128
|
+
@ts = TasteTester::Config.testing_end_time.strftime('%y%m%d%H%M.%S')
|
129
|
+
# Tie the life of our SSH tunnel with the life of timestamp file.
|
130
|
+
# taste-testing can be renewed, so we'll wait until:
|
131
|
+
# 1. the timestamp file is entirely gone
|
132
|
+
# 2. our parent sshd process dies
|
133
|
+
# 3. new taste-tester instance is running (file contains different PGID)
|
134
|
+
<<~EOS
|
135
|
+
log() {
|
136
|
+
[ -e /usr/bin/logger ] || return
|
137
|
+
logger -t taste-tester "$*"
|
138
|
+
}
|
139
|
+
# sets $current_pgid
|
140
|
+
# This is important, this should just be called ald let it set the
|
141
|
+
# variable. Do NOT call in a subshell like foo=$(get_current_pgid)
|
142
|
+
# as then you end up even further down the list of children
|
143
|
+
get_current_pgid() {
|
144
|
+
|
145
|
+
# if TT user is non-root, then it breaks down like this:
|
146
|
+
# we are 'bash'
|
147
|
+
# our parent is 'sudo'
|
148
|
+
# our parent's parent is 'bash "echo ..." | sudo bash -x'
|
149
|
+
# our parent's parent's parent is ssh
|
150
|
+
# - we want the progress-group ID of *that*
|
151
|
+
#
|
152
|
+
# EXCEPT... sometimes sudo forks itself one more time so it's
|
153
|
+
# we are 'bash'
|
154
|
+
# our parent is 'sudo'
|
155
|
+
# our parent's parent 'sudo'
|
156
|
+
# our parent's parent's parent is 'bash "echo ..." | sudo bash -x'
|
157
|
+
# our parent's parent's parent's parent is ssh
|
158
|
+
# - we want the progress-group ID of *that*
|
159
|
+
#
|
160
|
+
# BUT if the TT user is root, no sudo at all...
|
161
|
+
# we are 'bash'
|
162
|
+
# our parent is 'bash "echo ..." | bash -c
|
163
|
+
# our parent's parent is ssh
|
164
|
+
# - we want the progress-group ID of *that*
|
165
|
+
#
|
166
|
+
# We can make all sorts of assumptions, but the most reliable way
|
167
|
+
# to do this that's always correct is to is simply to walk parents until
|
168
|
+
# we hit something with SSH in the name. Start with PPID and go from
|
169
|
+
# there.
|
170
|
+
#
|
171
|
+
# There's a few commented out 'log's here that are too verbose
|
172
|
+
# for operation (since this function runs every minute) but are useful
|
173
|
+
# for debugging.
|
174
|
+
|
175
|
+
relevant_pid=''
|
176
|
+
current_pid=$PPID
|
177
|
+
while true; do
|
178
|
+
name=$(ps -o command= -p $current_pid)
|
179
|
+
if [[ "$name" =~ sshd ]]; then
|
180
|
+
# Uncomment the following for debugging...
|
181
|
+
#log "$current_pid is ssh, that's us!"
|
182
|
+
relevant_pid=$current_pid
|
183
|
+
break
|
184
|
+
fi
|
185
|
+
# Uncomment the following for debugging...
|
186
|
+
#log "$current_pid is $name, finding parent..."
|
187
|
+
current_pid=$(ps -o ppid= -p $current_pid)
|
188
|
+
done
|
189
|
+
if [ -z "$relevant_pid" ];then
|
190
|
+
log "Cannot determine relevant PGID"
|
191
|
+
exit 42
|
192
|
+
fi
|
193
|
+
current_pgid="$(ps -o pgid= -p $relevant_pid | sed "s| ||g")"
|
194
|
+
# Uncomment the following for debugging...
|
195
|
+
#log "PGID of ssh ($relevant_pid) is $current_pgid"
|
196
|
+
}
|
197
|
+
get_current_pgid
|
198
|
+
SSH_PGID=$current_pgid
|
199
|
+
|
200
|
+
echo $SSH_PGID > #{TasteTester::Config.timestamp_file} && \
|
201
|
+
# TODO: pull this from Host.touchcmd
|
202
|
+
touch -t #{@ts} #{TasteTester::Config.timestamp_file} && \
|
203
|
+
while true; do
|
204
|
+
if ! [ -f "#{TasteTester::Config.timestamp_file}" ]; then
|
205
|
+
log "Ending tunnel: timestamp file disappeared"
|
206
|
+
break
|
207
|
+
fi
|
208
|
+
current_pid="$(cat #{TasteTester::Config.timestamp_file})"
|
209
|
+
if ! [ "$current_pid" = "$SSH_PGID" ]; then
|
210
|
+
log "Ending tunnel: timestamp PGID changed"
|
211
|
+
break
|
212
|
+
fi
|
213
|
+
get_current_pgid
|
214
|
+
if ! [ "$current_pgid" = "$SSH_PGID" ]; then
|
215
|
+
log "Ending tunnel: timestamp PGID isn't ours"
|
216
|
+
break
|
217
|
+
fi
|
218
|
+
sleep 60
|
219
|
+
done
|
220
|
+
EOS
|
221
|
+
end
|
92
222
|
end
|
93
223
|
end
|
data/lib/taste_tester/windows.rb
CHANGED
@@ -40,6 +40,7 @@ module TasteTester
|
|
40
40
|
cmd << " --log-file #{@log_file} --log-level debug"
|
41
41
|
end
|
42
42
|
cmd << ' --ssl' if TasteTester::Config.use_ssl
|
43
|
+
cmd << " --file-store #{@fsroot}" if TasteTester::Config.bundle
|
43
44
|
|
44
45
|
# Mixlib::Shellout will always wait for a process to finish before
|
45
46
|
# returning, so we use `spawn` instead.
|
data/scripts/taste-untester
CHANGED
@@ -14,11 +14,19 @@
|
|
14
14
|
# See the License for the specific language governing permissions and
|
15
15
|
# limitations under the License.
|
16
16
|
|
17
|
+
# default configs
|
17
18
|
CONFLINK='/etc/chef/client.rb'
|
18
19
|
PRODCONF='/etc/chef/client-prod.rb'
|
19
20
|
CERTLINK='/etc/chef/client.pem'
|
20
21
|
PRODCERT='/etc/chef/client-prod.pem'
|
21
22
|
STAMPFILE='/etc/chef/test_timestamp'
|
23
|
+
|
24
|
+
# let the config file overwrite them
|
25
|
+
CONFIG_FILE="${CONFIG_FILE:-/etc/taste-untester-config}"
|
26
|
+
if [ -e "$CONFIG_FILE" ]; then
|
27
|
+
source "$CONFIG_FILE"
|
28
|
+
fi
|
29
|
+
|
22
30
|
MYSELF=$0
|
23
31
|
DRYRUN=0
|
24
32
|
DEBUG=0
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# Copyright 2020-present Facebook
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
[CmdletBinding()]
|
16
|
+
param(
|
17
|
+
[switch]$dryrun
|
18
|
+
)
|
19
|
+
|
20
|
+
# keep these as *forward* slashes
|
21
|
+
$CONFLINK = 'C:/chef/client.rb'
|
22
|
+
$PRODCONF = 'C:/chef/client-prod.rb'
|
23
|
+
$CERTLINK = 'C:/chef/client.pem'
|
24
|
+
$PRODCERT = 'C:/chef/client-prod.pem'
|
25
|
+
$STAMPFILE = 'C:/chef/test_timestamp'
|
26
|
+
$MYSELF = $0
|
27
|
+
|
28
|
+
function log($msg) {
|
29
|
+
Write-EventLog -LogName "Application" -Source "taste-tester" `
|
30
|
+
-EventID 2 -EntryType Warning -Message $msg
|
31
|
+
}
|
32
|
+
|
33
|
+
function set_server_to_prod {
|
34
|
+
if (Test-Path $STAMPFILE) {
|
35
|
+
$content = Get-Content $STAMPFILE
|
36
|
+
if ($content -ne $null) {
|
37
|
+
kill $content -Force 2>$null
|
38
|
+
}
|
39
|
+
}
|
40
|
+
rm -Force $CONFLINK
|
41
|
+
New-Item -ItemType symboliclink -Force -Value $PRODCONF $CONFLINK
|
42
|
+
if (Test-Path $STAMPFILE) {
|
43
|
+
rm -Force $STAMPFILE
|
44
|
+
}
|
45
|
+
log "Reverted to production Chef."
|
46
|
+
}
|
47
|
+
|
48
|
+
function check_server {
|
49
|
+
# this is the only way to check if something is a symlink, apparently
|
50
|
+
if (-Not ((get-item $CONFLINK).Attributes.ToString() -match "ReparsePoint")) {
|
51
|
+
Write-Verbose "$CONFLINK is not a link..."
|
52
|
+
return
|
53
|
+
}
|
54
|
+
$current_config = (Get-Item $CONFLINK).target
|
55
|
+
if ($current_config -eq $PRODCONF) {
|
56
|
+
if (Test-Path $STAMPFILE) {
|
57
|
+
rm -Force $STAMPFILE
|
58
|
+
}
|
59
|
+
return
|
60
|
+
}
|
61
|
+
|
62
|
+
$revert = $false
|
63
|
+
if (-Not (Test-Path $STAMPFILE)) {
|
64
|
+
$revert = $true
|
65
|
+
} else {
|
66
|
+
$now = [int][double]::Parse(
|
67
|
+
$(Get-Date -date (Get-Date).ToUniversalTime()-uformat %s)
|
68
|
+
)
|
69
|
+
$stamp_time = Get-Date -Date `
|
70
|
+
(Get-Item $STAMPFILE).LastWriteTime.ToUniversalTime() -UFormat %s
|
71
|
+
Write-Verbose "$now vs $stamp_time"
|
72
|
+
if ($now -gt $stamp_time) {
|
73
|
+
$revert = $true
|
74
|
+
}
|
75
|
+
}
|
76
|
+
if ($revert) {
|
77
|
+
if ($dryrun) {
|
78
|
+
echo "DRYRUN: Would return server to prod"
|
79
|
+
} else {
|
80
|
+
set_server_to_prod
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
check_server
|