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
@@ -22,8 +22,6 @@ module TasteTester
|
|
22
22
|
include TasteTester::Logging
|
23
23
|
include BetweenMeals::Util
|
24
24
|
|
25
|
-
attr_reader :output
|
26
|
-
|
27
25
|
def initialize
|
28
26
|
@host = 'localhost'
|
29
27
|
@cmds = []
|
@@ -36,14 +34,18 @@ module TasteTester
|
|
36
34
|
alias << add
|
37
35
|
|
38
36
|
def run
|
39
|
-
|
37
|
+
exec(cmd, logger)
|
40
38
|
end
|
41
39
|
|
42
40
|
def run!
|
43
|
-
|
44
|
-
rescue => e
|
41
|
+
exec!(cmd, logger)
|
42
|
+
rescue StandardError => e
|
45
43
|
logger.error(e.message)
|
46
|
-
|
44
|
+
error!
|
45
|
+
end
|
46
|
+
|
47
|
+
def error!
|
48
|
+
fail TasteTester::Exceptions::LocalLinkError
|
47
49
|
end
|
48
50
|
|
49
51
|
private
|
data/lib/taste_tester/logging.rb
CHANGED
@@ -14,7 +14,7 @@
|
|
14
14
|
# See the License for the specific language governing permissions and
|
15
15
|
# limitations under the License.
|
16
16
|
|
17
|
-
# rubocop:disable ClassVars
|
17
|
+
# rubocop:disable ClassVars
|
18
18
|
require 'logger'
|
19
19
|
|
20
20
|
module TasteTester
|
@@ -35,8 +35,8 @@ module TasteTester
|
|
35
35
|
@logger ||= Logger.new(STDOUT)
|
36
36
|
end
|
37
37
|
|
38
|
-
def self.formatterproc=(
|
39
|
-
@@formatter_proc =
|
38
|
+
def self.formatterproc=(process)
|
39
|
+
@@formatter_proc = process
|
40
40
|
end
|
41
41
|
|
42
42
|
def self.use_log_formatter=(use_log_formatter)
|
@@ -49,16 +49,17 @@ module TasteTester
|
|
49
49
|
|
50
50
|
def formatter
|
51
51
|
return @@formatter_proc if @@formatter_proc
|
52
|
+
|
52
53
|
if @@use_log_formatter
|
53
|
-
proc do |severity, datetime,
|
54
|
+
proc do |severity, datetime, _progname, msg|
|
54
55
|
if severity == 'ERROR'
|
55
56
|
msg = msg.red
|
56
57
|
end
|
57
58
|
"[#{datetime.strftime('%Y-%m-%dT%H:%M:%S%:z')}] #{severity}: #{msg}\n"
|
58
59
|
end
|
59
60
|
else
|
60
|
-
proc do |severity,
|
61
|
-
msg.to_s.prepend("#{severity}: ") unless severity == 'WARN'
|
61
|
+
proc do |severity, _datetime, _progname, msg|
|
62
|
+
msg.dup.to_s.prepend("#{severity}: ") unless severity == 'WARN'
|
62
63
|
if severity == 'ERROR'
|
63
64
|
msg = msg.to_s.red
|
64
65
|
end
|
@@ -68,3 +69,4 @@ module TasteTester
|
|
68
69
|
end
|
69
70
|
end
|
70
71
|
end
|
72
|
+
# rubocop:enable ClassVars
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
|
2
|
+
|
3
|
+
# Copyright 2013-present Facebook
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require 'taste_tester/exceptions'
|
18
|
+
|
19
|
+
module TasteTester
|
20
|
+
# Wrapper for running commands on local system
|
21
|
+
class NoOp
|
22
|
+
include TasteTester::Logging
|
23
|
+
include BetweenMeals::Util
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
print_noop_warning
|
27
|
+
@host = 'localhost'
|
28
|
+
@user = ENV['USER']
|
29
|
+
@cmds = []
|
30
|
+
end
|
31
|
+
|
32
|
+
def print_noop_warning
|
33
|
+
# This needs to be a Class var as this class is initialized more
|
34
|
+
# than once in a given tt run and we only want to warn once.
|
35
|
+
# rubocop:disable Style/ClassVars
|
36
|
+
@@printedwarning ||= logger.warn(
|
37
|
+
'No-op plugin active, no remote commands will be run!',
|
38
|
+
)
|
39
|
+
# rubocop:enable Style/ClassVars
|
40
|
+
end
|
41
|
+
|
42
|
+
def add(string)
|
43
|
+
@cmds << string
|
44
|
+
end
|
45
|
+
|
46
|
+
alias << add
|
47
|
+
|
48
|
+
def run
|
49
|
+
run!
|
50
|
+
end
|
51
|
+
|
52
|
+
def run!
|
53
|
+
cmd
|
54
|
+
[0, "# TasteTester by #{@user}"]
|
55
|
+
end
|
56
|
+
|
57
|
+
def error!
|
58
|
+
# never fails, but interface requires a definition
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def cmd
|
64
|
+
@cmds.each do |cmd|
|
65
|
+
logger.info("No-op, faking run of: '#{cmd}' on #{@host}")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/taste_tester/server.rb
CHANGED
@@ -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'
|
@@ -30,22 +29,27 @@ module TasteTester
|
|
30
29
|
include TasteTester::Logging
|
31
30
|
extend ::BetweenMeals::Util
|
32
31
|
|
33
|
-
attr_accessor :user, :host
|
32
|
+
attr_accessor :user, :host, :bundle_dir
|
34
33
|
|
35
34
|
def initialize
|
36
35
|
@state = TasteTester::State.new
|
37
36
|
@ref_file = TasteTester::Config.ref_file
|
38
37
|
ref_dir = File.dirname(File.expand_path(@ref_file))
|
39
|
-
@log_file =
|
38
|
+
@log_file = File.join(ref_dir, 'chef-zero.log')
|
39
|
+
@fsroot = File.join(ref_dir, 'root')
|
40
40
|
@zero_path = TasteTester::Config.chef_zero_path
|
41
41
|
unless File.directory?(ref_dir)
|
42
42
|
begin
|
43
43
|
FileUtils.mkpath(ref_dir)
|
44
|
-
rescue => e
|
44
|
+
rescue StandardError => e
|
45
45
|
logger.warn("Chef temp dir #{ref_dir} missing and can't be created")
|
46
46
|
logger.warn(e)
|
47
47
|
end
|
48
48
|
end
|
49
|
+
if TasteTester::Config.bundle
|
50
|
+
@bundle_dir = File.join(@fsroot, 'organizations/chef/file_store')
|
51
|
+
FileUtils.mkpath(@bundle_dir)
|
52
|
+
end
|
49
53
|
|
50
54
|
@user = ENV['USER']
|
51
55
|
|
@@ -53,19 +57,20 @@ module TasteTester
|
|
53
57
|
# determines if we listen only on localhost or not
|
54
58
|
@need_restart = @state.ssl != TasteTester::Config.use_ssl ||
|
55
59
|
@state.logging != TasteTester::Config.chef_zero_logging ||
|
56
|
-
@state.ssh != TasteTester::Config.use_ssh_tunnels
|
60
|
+
@state.ssh != TasteTester::Config.use_ssh_tunnels ||
|
61
|
+
@state.bundle != TasteTester::Config.bundle
|
57
62
|
|
58
63
|
# If we are using SSH tunneling listen on localhost, otherwise listen
|
59
64
|
# on all addresses - both v4 and v6. Note that on localhost, ::1 is
|
60
65
|
# v6-only, so we default to 127.0.0.1 instead.
|
61
66
|
if TasteTester::Config.use_ssh_tunnels
|
62
|
-
@
|
67
|
+
@addrs = ['127.0.0.1']
|
63
68
|
@host = 'localhost'
|
64
69
|
else
|
65
|
-
@
|
70
|
+
@addrs = ['::', '0.0.0.0']
|
66
71
|
begin
|
67
72
|
@host = TasteTester::Config.my_hostname || Socket.gethostname
|
68
|
-
rescue
|
73
|
+
rescue StandardError
|
69
74
|
logger.error('Unable to find fqdn')
|
70
75
|
exit 1
|
71
76
|
end
|
@@ -121,10 +126,20 @@ module TasteTester
|
|
121
126
|
@state.ref = ref
|
122
127
|
end
|
123
128
|
|
129
|
+
def last_upload_time
|
130
|
+
@state.last_upload_time
|
131
|
+
end
|
132
|
+
|
133
|
+
def last_upload_time=(time)
|
134
|
+
@state.last_upload_time = time
|
135
|
+
end
|
136
|
+
|
124
137
|
def self.running?
|
125
138
|
if TasteTester::State.port
|
126
|
-
return
|
139
|
+
return chef_zero_running?(TasteTester::State.port,
|
140
|
+
TasteTester::Config.use_ssl)
|
127
141
|
end
|
142
|
+
|
128
143
|
false
|
129
144
|
end
|
130
145
|
|
@@ -140,29 +155,33 @@ module TasteTester
|
|
140
155
|
:role_dir => TasteTester::Config.roles,
|
141
156
|
:cookbook_dirs => TasteTester::Config.cookbooks,
|
142
157
|
:checksum_dir => TasteTester::Config.checksum_dir,
|
158
|
+
:config => TasteTester::Config.knife_config,
|
143
159
|
)
|
144
160
|
knife.write_user_config
|
145
161
|
end
|
146
162
|
|
147
163
|
def start_chef_zero
|
148
|
-
File.unlink(@log_file) if File.
|
164
|
+
File.unlink(@log_file) if File.exist?(@log_file)
|
149
165
|
@state.update({
|
150
166
|
:port => TasteTester::Config.chef_port,
|
151
167
|
:ssl => TasteTester::Config.use_ssl,
|
152
168
|
:ssh => TasteTester::Config.use_ssh_tunnels,
|
153
169
|
:logging => TasteTester::Config.chef_zero_logging,
|
170
|
+
:bundle => TasteTester::Config.bundle,
|
154
171
|
})
|
155
172
|
logger.info("Starting chef-zero of port #{@state.port}")
|
156
173
|
if windows?
|
157
174
|
extend ::TasteTester::Windows
|
158
175
|
start_win_chef_zero_server
|
159
176
|
else
|
160
|
-
|
177
|
+
hostarg = @addrs.map { |addr| "--host #{addr}" }.join(' ')
|
178
|
+
cmd = +"#{chef_zero_path} #{hostarg} --port #{@state.port} -d"
|
161
179
|
if TasteTester::Config.chef_zero_logging
|
162
180
|
cmd << " --log-file #{@log_file}" +
|
163
181
|
' --log-level debug'
|
164
182
|
end
|
165
183
|
cmd << ' --ssl' if TasteTester::Config.use_ssl
|
184
|
+
cmd << " --file-store #{@fsroot}" if TasteTester::Config.bundle
|
166
185
|
Mixlib::ShellOut.new(cmd).run_command.error!
|
167
186
|
end
|
168
187
|
end
|
data/lib/taste_tester/ssh.rb
CHANGED
@@ -15,18 +15,17 @@
|
|
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
|
25
|
+
include TasteTester::SSH::Util
|
24
26
|
|
25
|
-
|
26
|
-
|
27
|
-
def initialize(host, timeout = 5, tunnel = false)
|
27
|
+
def initialize(host, tunnel = false)
|
28
28
|
@host = host
|
29
|
-
@timeout = timeout
|
30
29
|
@tunnel = tunnel
|
31
30
|
@cmds = []
|
32
31
|
end
|
@@ -37,25 +36,15 @@ module TasteTester
|
|
37
36
|
|
38
37
|
alias << add
|
39
38
|
|
40
|
-
def run
|
41
|
-
|
39
|
+
def run(stream = nil)
|
40
|
+
exec(cmd, logger, stream)
|
42
41
|
end
|
43
42
|
|
44
|
-
def run!
|
45
|
-
|
46
|
-
rescue => e
|
47
|
-
# rubocop:disable LineLength
|
48
|
-
error = <<-MSG
|
49
|
-
SSH returned error while connecting to #{TasteTester::Config.user}@#{@host}
|
50
|
-
The host might be broken or your SSH access is not working properly
|
51
|
-
Try doing
|
52
|
-
#{TasteTester::Config.ssh_command} -v #{TasteTester::Config.user}@#{@host}
|
53
|
-
and come back once that works
|
54
|
-
MSG
|
55
|
-
# rubocop:enable LineLength
|
56
|
-
error.lines.each { |x| logger.error x.strip }
|
43
|
+
def run!(stream = nil)
|
44
|
+
exec!(cmd, logger, stream)
|
45
|
+
rescue StandardError => e
|
57
46
|
logger.error(e.message)
|
58
|
-
|
47
|
+
error!
|
59
48
|
end
|
60
49
|
|
61
50
|
private
|
@@ -64,17 +53,7 @@ MSG
|
|
64
53
|
@cmds.each do |cmd|
|
65
54
|
logger.info("Will run: '#{cmd}' on #{@host}")
|
66
55
|
end
|
67
|
-
|
68
|
-
cmd = "#{TasteTester::Config.ssh_command} " +
|
69
|
-
"-T -o BatchMode=yes -o ConnectTimeout=#{@timeout} " +
|
70
|
-
"#{TasteTester::Config.user}@#{@host} "
|
71
|
-
if TasteTester::Config.user != 'root'
|
72
|
-
cc = Base64.encode64(cmds).delete("\n")
|
73
|
-
cmd += "\"echo '#{cc}' | base64 --decode | sudo bash -x\""
|
74
|
-
else
|
75
|
-
cmd += "\'#{cmds}\'"
|
76
|
-
end
|
77
|
-
cmd
|
56
|
+
build_ssh_cmd(ssh_base_cmd, @cmds)
|
78
57
|
end
|
79
58
|
end
|
80
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
|