train-core 3.4.7 → 3.5.5
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 +4 -4
- data/lib/train/file/local.rb +6 -0
- data/lib/train/file/remote/unix.rb +16 -0
- data/lib/train/file/remote/windows.rb +10 -0
- data/lib/train/plugins/base_connection.rb +43 -0
- data/lib/train/transports/cisco_ios_connection.rb +8 -0
- data/lib/train/transports/local.rb +54 -13
- data/lib/train/transports/ssh_connection.rb +14 -4
- data/lib/train/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dbe3fad014697c3f0a1d5fb5d3f4ff422f9e552362179f72019f7c2f9784f975
|
4
|
+
data.tar.gz: 337a74095aa5e41b4dd4f24ba2e41857190161a035c2adaf809617205525e442
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a066352b95be252da1de517176e98fe67804484bfb7f9371a23762b04a5050e7a3a7ca814eb27c773c5b23e7eb85a6eb26fd251df67bfd25c500e34a1075f7dc
|
7
|
+
data.tar.gz: fdfa95f6c1278dd04760e5dface6b4feacac3d0dd2b77f8844215d6f9b17760967afa482181415368fd8c3ca869e697e205c5abd45913e6cbfb12dcfc93044c1
|
data/lib/train/file/local.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require "base64" unless defined?(Base64)
|
1
2
|
require "shellwords" unless defined?(Shellwords)
|
2
3
|
|
3
4
|
module Train
|
@@ -19,6 +20,21 @@ module Train
|
|
19
20
|
end
|
20
21
|
end
|
21
22
|
|
23
|
+
def content=(new_content)
|
24
|
+
execute_result = @backend.run_command("base64 --help")
|
25
|
+
if execute_result.exit_status != 0
|
26
|
+
raise TransportError, "#{self.class} found no base64 binary for file writes"
|
27
|
+
end
|
28
|
+
|
29
|
+
unix_cmd = format("echo '%<base64>s' | base64 --decode > %<file>s",
|
30
|
+
base64: Base64.strict_encode64(new_content),
|
31
|
+
file: @spath)
|
32
|
+
|
33
|
+
@backend.run_command(unix_cmd)
|
34
|
+
|
35
|
+
@content = new_content
|
36
|
+
end
|
37
|
+
|
22
38
|
def exist?
|
23
39
|
@exist ||= begin
|
24
40
|
f = @follow_symlink ? "" : " || test -L #{@spath}"
|
@@ -26,6 +26,16 @@ module Train
|
|
26
26
|
@content
|
27
27
|
end
|
28
28
|
|
29
|
+
def content=(new_content)
|
30
|
+
win_cmd = format('[IO.File]::WriteAllBytes("%<file>s", [Convert]::FromBase64String("%<base64>s"))',
|
31
|
+
base64: Base64.strict_encode64(new_content),
|
32
|
+
file: @spath)
|
33
|
+
|
34
|
+
@backend.run_command(win_cmd)
|
35
|
+
|
36
|
+
@content = new_content
|
37
|
+
end
|
38
|
+
|
29
39
|
def exist?
|
30
40
|
return @exist if defined?(@exist)
|
31
41
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative "../errors"
|
2
2
|
require_relative "../extras"
|
3
3
|
require_relative "../file"
|
4
|
+
require "fileutils" unless defined?(FileUtils)
|
4
5
|
require "logger"
|
5
6
|
|
6
7
|
class Train::Plugins::Transport
|
@@ -161,6 +162,48 @@ class Train::Plugins::Transport
|
|
161
162
|
@cache[:file][path] ||= file_via_connection(path, *args)
|
162
163
|
end
|
163
164
|
|
165
|
+
# Uploads local files or directories to remote host.
|
166
|
+
#
|
167
|
+
# @param locals [Array<String>] paths to local files or directories
|
168
|
+
# @param remote [String] path to remote destination
|
169
|
+
# @raise [TransportFailed] if the files could not all be uploaded
|
170
|
+
# successfully, which may vary by implementation
|
171
|
+
def upload(locals, remote)
|
172
|
+
unless file(remote).directory?
|
173
|
+
raise TransportError, "#{self.class} expects remote directory as second upload parameter"
|
174
|
+
end
|
175
|
+
|
176
|
+
Array(locals).each do |local|
|
177
|
+
new_content = File.read(local)
|
178
|
+
remote_file = File.join(remote, File.basename(local))
|
179
|
+
|
180
|
+
logger.debug("Attempting to upload '#{local}' as file #{remote_file}")
|
181
|
+
|
182
|
+
file(remote_file).content = new_content
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Download remote files or directories to local host.
|
187
|
+
#
|
188
|
+
# @param remotes [Array<String>] paths to remote files or directories
|
189
|
+
# @param local [String] path to local destination. If `local` is an
|
190
|
+
# existing directory, `remote` will be downloaded into the directory
|
191
|
+
# using its original name
|
192
|
+
# @raise [TransportFailed] if the files could not all be downloaded
|
193
|
+
# successfully, which may vary by implementation
|
194
|
+
def download(remotes, local)
|
195
|
+
FileUtils.mkdir_p(File.dirname(local))
|
196
|
+
|
197
|
+
Array(remotes).each do |remote|
|
198
|
+
new_content = file(remote).content
|
199
|
+
local_file = File.join(local, File.basename(remote))
|
200
|
+
|
201
|
+
logger.debug("Attempting to download '#{remote}' as file #{local_file}")
|
202
|
+
|
203
|
+
File.open(local_file, "w") { |fp| fp.write(new_content) }
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
164
207
|
# Builds a LoginCommand which can be used to open an interactive
|
165
208
|
# session on the remote host.
|
166
209
|
#
|
@@ -29,6 +29,14 @@ class Train::Transports::SSH
|
|
29
29
|
result.stdout.split(" ")[-1]
|
30
30
|
end
|
31
31
|
|
32
|
+
def upload(locals, remote)
|
33
|
+
raise NotImplementedError, "#{self.class} does not implement #upload()"
|
34
|
+
end
|
35
|
+
|
36
|
+
def download(remotes, local)
|
37
|
+
raise NotImplementedError, "#{self.class} does not implement #download()"
|
38
|
+
end
|
39
|
+
|
32
40
|
private
|
33
41
|
|
34
42
|
def establish_connection
|
@@ -31,10 +31,26 @@ module Train::Transports
|
|
31
31
|
nil # none, open your shell
|
32
32
|
end
|
33
33
|
|
34
|
+
def close
|
35
|
+
@runner.close
|
36
|
+
end
|
37
|
+
|
34
38
|
def uri
|
35
39
|
"local://"
|
36
40
|
end
|
37
41
|
|
42
|
+
def upload(locals, remote)
|
43
|
+
FileUtils.mkdir_p(remote)
|
44
|
+
|
45
|
+
Array(locals).each do |local|
|
46
|
+
FileUtils.cp_r(local, remote)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def download(remotes, local)
|
51
|
+
upload(remotes, local)
|
52
|
+
end
|
53
|
+
|
38
54
|
private
|
39
55
|
|
40
56
|
def select_runner(options)
|
@@ -70,9 +86,9 @@ module Train::Transports
|
|
70
86
|
end
|
71
87
|
end
|
72
88
|
|
73
|
-
def run_command_via_connection(cmd, &_data_handler)
|
89
|
+
def run_command_via_connection(cmd, opts, &_data_handler)
|
74
90
|
# Use the runner if it is available
|
75
|
-
return @runner.run_command(cmd) if defined?(@runner)
|
91
|
+
return @runner.run_command(cmd, opts) if defined?(@runner)
|
76
92
|
|
77
93
|
# If we don't have a runner, such as at the beginning of setting up the
|
78
94
|
# transport and performing the first few steps of OS detection, fall
|
@@ -99,15 +115,24 @@ module Train::Transports
|
|
99
115
|
@cmd_wrapper = Local::CommandWrapper.load(connection, options)
|
100
116
|
end
|
101
117
|
|
102
|
-
def run_command(cmd)
|
118
|
+
def run_command(cmd, opts = {})
|
103
119
|
if defined?(@cmd_wrapper) && !@cmd_wrapper.nil?
|
104
120
|
cmd = @cmd_wrapper.run(cmd)
|
105
121
|
end
|
106
122
|
|
107
123
|
res = Mixlib::ShellOut.new(cmd)
|
108
|
-
res.
|
124
|
+
res.timeout = opts[:timeout]
|
125
|
+
begin
|
126
|
+
res.run_command
|
127
|
+
rescue Mixlib::ShellOut::CommandTimeout
|
128
|
+
raise Train::CommandTimeoutReached
|
129
|
+
end
|
109
130
|
Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
|
110
131
|
end
|
132
|
+
|
133
|
+
def close
|
134
|
+
# nothing to do at the moment
|
135
|
+
end
|
111
136
|
end
|
112
137
|
|
113
138
|
class WindowsShellRunner
|
@@ -118,7 +143,7 @@ module Train::Transports
|
|
118
143
|
@powershell_cmd = powershell_cmd
|
119
144
|
end
|
120
145
|
|
121
|
-
def run_command(script)
|
146
|
+
def run_command(script, opts)
|
122
147
|
# Prevent progress stream from leaking into stderr
|
123
148
|
script = "$ProgressPreference='SilentlyContinue';" + script
|
124
149
|
|
@@ -129,9 +154,18 @@ module Train::Transports
|
|
129
154
|
cmd = "#{@powershell_cmd} -NoProfile -EncodedCommand #{base64_script}"
|
130
155
|
|
131
156
|
res = Mixlib::ShellOut.new(cmd)
|
132
|
-
res.
|
157
|
+
res.timeout = opts[:timeout]
|
158
|
+
begin
|
159
|
+
res.run_command
|
160
|
+
rescue Mixlib::ShellOut::CommandTimeout
|
161
|
+
raise Train::CommandTimeoutReached
|
162
|
+
end
|
133
163
|
Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
|
134
164
|
end
|
165
|
+
|
166
|
+
def close
|
167
|
+
# nothing to do at the moment
|
168
|
+
end
|
135
169
|
end
|
136
170
|
|
137
171
|
class WindowsPipeRunner
|
@@ -141,6 +175,7 @@ module Train::Transports
|
|
141
175
|
|
142
176
|
def initialize(powershell_cmd = "powershell")
|
143
177
|
@powershell_cmd = powershell_cmd
|
178
|
+
@server_pid = nil
|
144
179
|
@pipe = acquire_pipe
|
145
180
|
raise PipeError if @pipe.nil?
|
146
181
|
end
|
@@ -151,21 +186,31 @@ module Train::Transports
|
|
151
186
|
# A command that succeeds without setting an exit code will have exitstatus 0
|
152
187
|
# A command that exits with an exit code will have that value as exitstatus
|
153
188
|
# A command that fails (e.g. throws exception) before setting an exit code will have exitstatus 1
|
154
|
-
def run_command(cmd)
|
189
|
+
def run_command(cmd, _opts)
|
155
190
|
script = "$ProgressPreference='SilentlyContinue';" + cmd
|
156
191
|
encoded_script = Base64.strict_encode64(script)
|
192
|
+
# TODO: no way to safely implement timeouts here.
|
157
193
|
@pipe.puts(encoded_script)
|
158
194
|
@pipe.flush
|
159
195
|
res = OpenStruct.new(JSON.parse(Base64.decode64(@pipe.readline)))
|
160
196
|
Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
|
161
197
|
end
|
162
198
|
|
199
|
+
def close
|
200
|
+
Process.kill("KILL", @server_pid) unless @server_pid.nil?
|
201
|
+
@server_pid = nil
|
202
|
+
end
|
203
|
+
|
163
204
|
private
|
164
205
|
|
165
206
|
def acquire_pipe
|
207
|
+
require "win32/process"
|
166
208
|
pipe_name = "inspec_#{SecureRandom.hex}"
|
167
209
|
|
168
|
-
start_pipe_server(pipe_name)
|
210
|
+
@server_pid = start_pipe_server(pipe_name)
|
211
|
+
|
212
|
+
# Ensure process is killed when the Train process exits
|
213
|
+
at_exit { close rescue Errno::EIO }
|
169
214
|
|
170
215
|
pipe = nil
|
171
216
|
|
@@ -227,11 +272,7 @@ module Train::Transports
|
|
227
272
|
utf8_script = script.encode("UTF-16LE", "UTF-8")
|
228
273
|
base64_script = Base64.strict_encode64(utf8_script)
|
229
274
|
cmd = "#{@powershell_cmd} -NoProfile -ExecutionPolicy bypass -NonInteractive -EncodedCommand #{base64_script}"
|
230
|
-
|
231
|
-
server_pid = Process.create(command_line: cmd).process_id
|
232
|
-
|
233
|
-
# Ensure process is killed when the Train process exits
|
234
|
-
at_exit { Process.kill("KILL", server_pid) }
|
275
|
+
Process.create(command_line: cmd).process_id
|
235
276
|
end
|
236
277
|
end
|
237
278
|
end
|
@@ -29,8 +29,8 @@ class Train::Transports::SSH
|
|
29
29
|
#
|
30
30
|
# @author Fletcher Nichol <fnichol@nichol.ca>
|
31
31
|
class Connection < BaseConnection # rubocop:disable Metrics/ClassLength
|
32
|
-
attr_reader
|
33
|
-
|
32
|
+
attr_reader :hostname
|
33
|
+
attr_accessor :transport_options
|
34
34
|
|
35
35
|
def initialize(options)
|
36
36
|
# Track IOS command retries to prevent infinite loop on IOError. This must
|
@@ -247,6 +247,16 @@ class Train::Transports::SSH
|
|
247
247
|
|
248
248
|
exit_status, stdout, stderr = execute_on_channel(cmd, opts, &data_handler)
|
249
249
|
|
250
|
+
# An interactive console might contain the STDERR in STDOUT
|
251
|
+
# concat both outputs for non-zero exit status.
|
252
|
+
output = "#{stdout} #{stderr}".strip if exit_status != 0
|
253
|
+
|
254
|
+
# Abstract the su - USER authentication failure
|
255
|
+
# raise the Train::UserError and passes message & reason
|
256
|
+
if output && output.match?("su: Authentication failure")
|
257
|
+
raise Train::UserError.new(output, :bad_su_user_password)
|
258
|
+
end
|
259
|
+
|
250
260
|
# Since `@session.loop` succeeded, reset the IOS command retry counter
|
251
261
|
@ios_cmd_retries = 0
|
252
262
|
|
@@ -322,12 +332,12 @@ class Train::Transports::SSH
|
|
322
332
|
channel.exec(cmd) do |_, success|
|
323
333
|
abort "Couldn't execute command on SSH." unless success
|
324
334
|
channel.on_data do |_, data|
|
325
|
-
yield(data) if block_given?
|
335
|
+
yield(data, channel) if block_given?
|
326
336
|
stdout += data
|
327
337
|
end
|
328
338
|
|
329
339
|
channel.on_extended_data do |_, _type, data|
|
330
|
-
yield(data) if block_given?
|
340
|
+
yield(data, channel) if block_given?
|
331
341
|
stderr += data
|
332
342
|
end
|
333
343
|
|
data/lib/train/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: train-core
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.5.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chef InSpec Team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-03-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|