train-core 3.4.9 → 3.10.7
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/errors.rb +3 -0
- data/lib/train/extras/command_wrapper.rb +3 -0
- data/lib/train/extras/stat.rb +1 -1
- data/lib/train/file/local.rb +6 -0
- data/lib/train/file/remote/unix.rb +16 -0
- data/lib/train/file/remote/windows.rb +11 -1
- data/lib/train/options.rb +3 -0
- data/lib/train/platforms/detect/helpers/os_common.rb +7 -1
- data/lib/train/platforms/detect/helpers/os_windows.rb +1 -1
- data/lib/train/platforms/detect/specifications/os.rb +9 -1
- data/lib/train/platforms/detect/uuid.rb +3 -2
- 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 +53 -17
- data/lib/train/transports/ssh.rb +49 -8
- data/lib/train/transports/ssh_connection.rb +37 -12
- data/lib/train/version.rb +1 -1
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d396bbb358c284289e040d532236fa26f2f92d73aa41dde56bf0b914a087238
|
4
|
+
data.tar.gz: e378a5a7c8af2cc8bcadc02abc1e99c310f73a182703edac027592074dfa8eba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e37b9360ad1d5c206c700b770e41d552d0a864dfec0df63fde6201c7c842b3f45f9bcb0ffc2f4f6413b55d5354864eae87c1488c2c7dc31228ab9b39da5509e1
|
7
|
+
data.tar.gz: 699adc38633ac5c01a0de55baa35ed4bacf060efff95a7c05d6afb324fff206c4920e29151b0ed23fb1ee2b7b4b57352323654df19a1393c2f22eae6de18678e
|
data/lib/train/errors.rb
CHANGED
@@ -38,6 +38,9 @@ module Train
|
|
38
38
|
# Exception for when no platform can be detected.
|
39
39
|
class PlatformDetectionFailed < Error; end
|
40
40
|
|
41
|
+
# Exception for when no uuid for the platform can be detected.
|
42
|
+
class PlatformUuidDetectionFailed < Error; end
|
43
|
+
|
41
44
|
# Exception for when a invalid cache type is passed.
|
42
45
|
class UnknownCacheType < Error; end
|
43
46
|
|
@@ -81,6 +81,9 @@ module Train::Extras
|
|
81
81
|
when /sudo: sorry, you must have a tty to run sudo/
|
82
82
|
["Sudo requires a TTY. Please see the README on how to configure "\
|
83
83
|
"sudo to allow for non-interactive usage.", :sudo_no_tty]
|
84
|
+
when /sudo: a terminal is required to read the password; either use/
|
85
|
+
["Sudo cannot prompt for password because there is no terminal. "\
|
86
|
+
"Please provide the sudo password directly", :sudo_missing_terminal]
|
84
87
|
else
|
85
88
|
[rawerr, nil]
|
86
89
|
end
|
data/lib/train/extras/stat.rb
CHANGED
@@ -34,7 +34,7 @@ module Train::Extras
|
|
34
34
|
|
35
35
|
def self.linux_stat(shell_escaped_path, backend, follow_symlink)
|
36
36
|
lstat = follow_symlink ? " -L" : ""
|
37
|
-
format = (backend.os.esx? ||
|
37
|
+
format = (backend.os.esx? || %w{alpine yocto ubios}.include?(backend.os[:name])) ? "-c" : "--printf"
|
38
38
|
res = backend.run_command("stat#{lstat} #{shell_escaped_path} 2>/dev/null #{format} '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'")
|
39
39
|
# ignore the exit_code: it is != 0 if selinux labels are not supported
|
40
40
|
# on the system.
|
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}"
|
@@ -12,7 +12,7 @@ module Train
|
|
12
12
|
@spath = path.gsub(/[<>"|?*]/, "")
|
13
13
|
end
|
14
14
|
|
15
|
-
def basename(suffix = nil, sep =
|
15
|
+
def basename(suffix = nil, sep = "\\")
|
16
16
|
super(suffix, sep)
|
17
17
|
end
|
18
18
|
|
@@ -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
|
|
data/lib/train/options.rb
CHANGED
@@ -59,6 +59,9 @@ module Train
|
|
59
59
|
default = hm[:default]
|
60
60
|
if default.is_a? Proc
|
61
61
|
res[field] = default.call(res)
|
62
|
+
elsif hm.key?(:coerce)
|
63
|
+
field_value = hm[:coerce].call(res)
|
64
|
+
res[field] = field_value.nil? ? default : field_value
|
62
65
|
else
|
63
66
|
res[field] = default
|
64
67
|
end
|
@@ -97,9 +97,16 @@ module Train::Platforms::Detect::Helpers
|
|
97
97
|
return @cache[:cisco] = { version: m[2], model: m[1], type: "ios-xe" }
|
98
98
|
end
|
99
99
|
|
100
|
+
# CSR 1000V (for example) does not specify model
|
101
|
+
m = res.match(/Cisco IOS XE Software, Version (\d+\.\d+\.\d+[A-Z]*)/)
|
102
|
+
unless m.nil?
|
103
|
+
return @cache[:cisco] = { version: m[1], type: "ios-xe" }
|
104
|
+
end
|
105
|
+
|
100
106
|
m = res.match(/Cisco Nexus Operating System \(NX-OS\) Software/)
|
101
107
|
unless m.nil?
|
102
108
|
v = res[/^\s*system:\s+version (\d+\.\d+)/, 1]
|
109
|
+
v ||= res[/NXOS: version (\d+\.\d+)/, 1]
|
103
110
|
return @cache[:cisco] = { version: v, type: "nexus" }
|
104
111
|
end
|
105
112
|
|
@@ -122,7 +129,6 @@ module Train::Platforms::Detect::Helpers
|
|
122
129
|
end
|
123
130
|
|
124
131
|
def unix_uuid_from_machine_file
|
125
|
-
# require 'pry';binding.pry
|
126
132
|
%W{
|
127
133
|
/etc/chef/chef_guid
|
128
134
|
#{ENV["HOME"]}/.chef/chef_guid
|
@@ -51,7 +51,7 @@ module Train::Platforms::Detect::Helpers
|
|
51
51
|
|
52
52
|
def local_windows?
|
53
53
|
@backend.class.to_s == "Train::Transports::Local::Connection" &&
|
54
|
-
ruby_host_os(/mswin|
|
54
|
+
ruby_host_os(/mswin|mingw|windows/)
|
55
55
|
end
|
56
56
|
|
57
57
|
# reads os name and version from wmic
|
@@ -74,6 +74,14 @@ module Train::Platforms::Detect::Specifications
|
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
77
|
+
declare_instance("ubios", "Ubiquiti UbiOS", "ubios") do
|
78
|
+
l_o_r = linux_os_release
|
79
|
+
if l_o_r && l_o_r["ID"] == "ubios"
|
80
|
+
@platform[:release] = l_o_r["VERSION_ID"]
|
81
|
+
true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
77
85
|
declare_instance("debian", "Debian Linux", "debian") do
|
78
86
|
# if we get this far we have to be some type of debian
|
79
87
|
@platform[:release] = unix_file_contents("/etc/debian_version").chomp
|
@@ -375,7 +383,7 @@ module Train::Platforms::Detect::Specifications
|
|
375
383
|
def self.load_other
|
376
384
|
plat.family("arista_eos").title("Arista EOS Family").in_family("os")
|
377
385
|
.detect do
|
378
|
-
|
386
|
+
!@backend.run_command("show version").stdout.match(/Arista/).nil?
|
379
387
|
end
|
380
388
|
|
381
389
|
declare_instance("arista_eos", "Arista EOS", "arista_eos") do
|
@@ -20,12 +20,13 @@ module Train::Platforms::Detect
|
|
20
20
|
elsif @platform.windows?
|
21
21
|
windows_uuid
|
22
22
|
else
|
23
|
-
|
23
|
+
# Checking "unknown" :uuid_command which is set for mock transport.
|
24
|
+
if @platform[:uuid_command] && !@platform[:uuid_command] == "unknown"
|
24
25
|
result = @backend.run_command(@platform[:uuid_command])
|
25
26
|
return uuid_from_string(result.stdout.chomp) if result.exit_status == 0 && !result.stdout.empty?
|
26
27
|
end
|
27
28
|
|
28
|
-
raise "Could not find platform uuid! Please set a uuid_command for your platform."
|
29
|
+
raise Train::PlatformUuidDetectionFailed.new("Could not find platform uuid! Please set a uuid_command for your platform.")
|
29
30
|
end
|
30
31
|
end
|
31
32
|
end
|
@@ -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
|
@@ -39,6 +39,18 @@ module Train::Transports
|
|
39
39
|
"local://"
|
40
40
|
end
|
41
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
|
+
|
42
54
|
private
|
43
55
|
|
44
56
|
def select_runner(options)
|
@@ -74,9 +86,9 @@ module Train::Transports
|
|
74
86
|
end
|
75
87
|
end
|
76
88
|
|
77
|
-
def run_command_via_connection(cmd, &_data_handler)
|
89
|
+
def run_command_via_connection(cmd, opts, &_data_handler)
|
78
90
|
# Use the runner if it is available
|
79
|
-
return @runner.run_command(cmd) if defined?(@runner)
|
91
|
+
return @runner.run_command(cmd, opts) if defined?(@runner)
|
80
92
|
|
81
93
|
# If we don't have a runner, such as at the beginning of setting up the
|
82
94
|
# transport and performing the first few steps of OS detection, fall
|
@@ -103,13 +115,18 @@ module Train::Transports
|
|
103
115
|
@cmd_wrapper = Local::CommandWrapper.load(connection, options)
|
104
116
|
end
|
105
117
|
|
106
|
-
def run_command(cmd)
|
118
|
+
def run_command(cmd, opts = {})
|
107
119
|
if defined?(@cmd_wrapper) && !@cmd_wrapper.nil?
|
108
120
|
cmd = @cmd_wrapper.run(cmd)
|
109
121
|
end
|
110
122
|
|
111
123
|
res = Mixlib::ShellOut.new(cmd)
|
112
|
-
res.
|
124
|
+
res.timeout = opts[:timeout]
|
125
|
+
begin
|
126
|
+
res.run_command
|
127
|
+
rescue Mixlib::ShellOut::CommandTimeout
|
128
|
+
raise Train::CommandTimeoutReached
|
129
|
+
end
|
113
130
|
Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
|
114
131
|
end
|
115
132
|
|
@@ -126,7 +143,7 @@ module Train::Transports
|
|
126
143
|
@powershell_cmd = powershell_cmd
|
127
144
|
end
|
128
145
|
|
129
|
-
def run_command(script)
|
146
|
+
def run_command(script, opts)
|
130
147
|
# Prevent progress stream from leaking into stderr
|
131
148
|
script = "$ProgressPreference='SilentlyContinue';" + script
|
132
149
|
|
@@ -137,7 +154,12 @@ module Train::Transports
|
|
137
154
|
cmd = "#{@powershell_cmd} -NoProfile -EncodedCommand #{base64_script}"
|
138
155
|
|
139
156
|
res = Mixlib::ShellOut.new(cmd)
|
140
|
-
res.
|
157
|
+
res.timeout = opts[:timeout]
|
158
|
+
begin
|
159
|
+
res.run_command
|
160
|
+
rescue Mixlib::ShellOut::CommandTimeout
|
161
|
+
raise Train::CommandTimeoutReached
|
162
|
+
end
|
141
163
|
Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
|
142
164
|
end
|
143
165
|
|
@@ -164,11 +186,27 @@ module Train::Transports
|
|
164
186
|
# A command that succeeds without setting an exit code will have exitstatus 0
|
165
187
|
# A command that exits with an exit code will have that value as exitstatus
|
166
188
|
# A command that fails (e.g. throws exception) before setting an exit code will have exitstatus 1
|
167
|
-
def run_command(cmd)
|
189
|
+
def run_command(cmd, _opts)
|
168
190
|
script = "$ProgressPreference='SilentlyContinue';" + cmd
|
169
191
|
encoded_script = Base64.strict_encode64(script)
|
170
|
-
|
171
|
-
|
192
|
+
# TODO: no way to safely implement timeouts here.
|
193
|
+
begin
|
194
|
+
@pipe.puts(encoded_script)
|
195
|
+
@pipe.flush
|
196
|
+
rescue Errno::EPIPE
|
197
|
+
# Retry once if the pipe went away
|
198
|
+
begin
|
199
|
+
# Maybe the pipe went away, but the server didn't? Reset it, to get a clean start.
|
200
|
+
close
|
201
|
+
rescue Errno::EIO
|
202
|
+
# Ignore - server already went away
|
203
|
+
end
|
204
|
+
@pipe = acquire_pipe
|
205
|
+
raise PipeError if @pipe.nil?
|
206
|
+
|
207
|
+
@pipe.puts(encoded_script)
|
208
|
+
@pipe.flush
|
209
|
+
end
|
172
210
|
res = OpenStruct.new(JSON.parse(Base64.decode64(@pipe.readline)))
|
173
211
|
Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
|
174
212
|
end
|
@@ -187,18 +225,16 @@ module Train::Transports
|
|
187
225
|
@server_pid = start_pipe_server(pipe_name)
|
188
226
|
|
189
227
|
# Ensure process is killed when the Train process exits
|
190
|
-
at_exit { close }
|
228
|
+
at_exit { close rescue Errno::EIO }
|
191
229
|
|
192
230
|
pipe = nil
|
193
231
|
|
194
232
|
# PowerShell needs time to create pipe.
|
195
233
|
100.times do
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
sleep 0.1
|
201
|
-
end
|
234
|
+
pipe = open("//./pipe/#{pipe_name}", "r+")
|
235
|
+
break
|
236
|
+
rescue
|
237
|
+
sleep 0.1
|
202
238
|
end
|
203
239
|
|
204
240
|
pipe
|
@@ -254,4 +290,4 @@ module Train::Transports
|
|
254
290
|
end
|
255
291
|
end
|
256
292
|
end
|
257
|
-
end
|
293
|
+
end
|
data/lib/train/transports/ssh.rb
CHANGED
@@ -42,12 +42,12 @@ module Train::Transports
|
|
42
42
|
include_options Train::Extras::CommandWrapper
|
43
43
|
|
44
44
|
# common target configuration
|
45
|
-
option :host,
|
46
|
-
option :
|
47
|
-
option :
|
45
|
+
option :host, required: true
|
46
|
+
option :ssh_config_file, default: true
|
47
|
+
option :port, default: 22, coerce: proc { |v| read_options_from_ssh_config(v, :port) }, required: true
|
48
|
+
option :user, default: "root", coerce: proc { |v| read_options_from_ssh_config(v, :user) }, required: true
|
48
49
|
option :key_files, default: nil
|
49
50
|
option :password, default: nil
|
50
|
-
|
51
51
|
# additional ssh options
|
52
52
|
option :keepalive, default: true
|
53
53
|
option :keepalive_interval, default: 60
|
@@ -75,6 +75,7 @@ module Train::Transports
|
|
75
75
|
|
76
76
|
# (see Base#connection)
|
77
77
|
def connection(state = {}, &block)
|
78
|
+
apply_ssh_config_file(options[:host])
|
78
79
|
opts = merge_options(options, state || {})
|
79
80
|
validate_options(opts)
|
80
81
|
conn_opts = connection_options(opts)
|
@@ -86,8 +87,40 @@ module Train::Transports
|
|
86
87
|
end
|
87
88
|
end
|
88
89
|
|
90
|
+
# Returns the ssh config option like user, port from config files
|
91
|
+
# Params options [Hash], option_type [String]
|
92
|
+
# Return String
|
93
|
+
def self.read_options_from_ssh_config(options, option_type)
|
94
|
+
files = options[:ssh_config_file].nil? || options[:ssh_config_file] == true ? Net::SSH::Config.default_files : options[:ssh_config_file]
|
95
|
+
config_options = Net::SSH::Config.for(options[:host], files)
|
96
|
+
config_options[option_type]
|
97
|
+
end
|
98
|
+
|
99
|
+
def apply_ssh_config_file(host)
|
100
|
+
files = options[:ssh_config_file] == true ? Net::SSH::Config.default_files : options[:ssh_config_file]
|
101
|
+
host_cfg = ssh_config_file_for_host(host, files)
|
102
|
+
host_cfg.each do |key, value|
|
103
|
+
# setting the key_files option to the private keys set in ssh config file
|
104
|
+
if key == :keys && options[:key_files].nil? && !host_cfg[:keys].nil? && options[:password].nil?
|
105
|
+
options[:key_files] = host_cfg[key]
|
106
|
+
elsif options[key].nil?
|
107
|
+
# Precedence is given to the option set by the user manually.
|
108
|
+
# And only assigning value to the option from the ssh config file when it is not set by the user
|
109
|
+
# in the option. When the option has a default value for e.g. option "keepalive_interval" has the "60" as the default
|
110
|
+
# value, then the default value will be used even though the value for "user" is present in the ssh
|
111
|
+
# config file. That is because the precedence is to the options set manually, and currently we don't have
|
112
|
+
# any way to differentiate between the value set by the user or is it the default. This has a future of improvement.
|
113
|
+
options[key] = host_cfg[key]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
89
118
|
private
|
90
119
|
|
120
|
+
def ssh_config_file_for_host(host, files)
|
121
|
+
Net::SSH::Config.for(host, files)
|
122
|
+
end
|
123
|
+
|
91
124
|
def reusable_connection?(conn_opts)
|
92
125
|
return false unless @connection_options
|
93
126
|
|
@@ -101,14 +134,18 @@ module Train::Transports
|
|
101
134
|
key_files = Array(options[:key_files])
|
102
135
|
options[:auth_methods] ||= ["none"]
|
103
136
|
|
104
|
-
|
105
|
-
|
137
|
+
# by default auth_methods has a default values [none publickey password keyboard-interactive]
|
138
|
+
# REF: https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/authentication/session.rb#L48
|
139
|
+
if key_files.empty?
|
140
|
+
options[:auth_methods].delete("publickey")
|
141
|
+
else
|
106
142
|
options[:keys_only] = true if options[:password].nil?
|
107
143
|
options[:key_files] = key_files
|
108
144
|
end
|
109
145
|
|
110
|
-
|
111
|
-
options[:auth_methods].
|
146
|
+
if options[:password].nil?
|
147
|
+
options[:auth_methods].delete("password")
|
148
|
+
options[:auth_methods].delete("keyboard-interactive")
|
112
149
|
end
|
113
150
|
|
114
151
|
if options[:auth_methods] == ["none"]
|
@@ -123,6 +160,8 @@ module Train::Transports
|
|
123
160
|
end
|
124
161
|
end
|
125
162
|
|
163
|
+
options[:auth_methods] = options[:auth_methods].uniq
|
164
|
+
|
126
165
|
if options[:pty]
|
127
166
|
logger.warn("[SSH] PTY requested: stderr will be merged into stdout")
|
128
167
|
end
|
@@ -178,6 +217,7 @@ module Train::Transports
|
|
178
217
|
bastion_port: opts[:bastion_port],
|
179
218
|
non_interactive: opts[:non_interactive],
|
180
219
|
append_all_supported_algorithms: opts[:append_all_supported_algorithms],
|
220
|
+
config: options[:ssh_config_file],
|
181
221
|
transport_options: opts,
|
182
222
|
}
|
183
223
|
# disable host key verification. The hash key and value to use
|
@@ -278,5 +318,6 @@ module Train::Transports
|
|
278
318
|
yield @connection if block_given?
|
279
319
|
@connection
|
280
320
|
end
|
321
|
+
|
281
322
|
end
|
282
323
|
end
|
@@ -32,6 +32,10 @@ class Train::Transports::SSH
|
|
32
32
|
attr_reader :hostname
|
33
33
|
attr_accessor :transport_options
|
34
34
|
|
35
|
+
# If we use the GNU timeout utility to timout a command server-side, it will
|
36
|
+
# exit with this status code if the command timed out.
|
37
|
+
GNU_TIMEOUT_EXIT_STATUS = 124
|
38
|
+
|
35
39
|
def initialize(options)
|
36
40
|
# Track IOS command retries to prevent infinite loop on IOError. This must
|
37
41
|
# be done before `super()` because the parent runs detection commands.
|
@@ -321,9 +325,21 @@ class Train::Transports::SSH
|
|
321
325
|
# wrap commands if that is configured
|
322
326
|
cmd = @cmd_wrapper.run(cmd) if @cmd_wrapper
|
323
327
|
|
328
|
+
# Timeout the command if requested and able
|
329
|
+
if timeout && timeoutable?(cmd)
|
330
|
+
# if cmd start with sudo then we need to make sure the timeout should be prepend with sudo else actual timeout is not working.
|
331
|
+
if cmd.strip.split[0] == "sudo"
|
332
|
+
split_cmd = cmd.strip.split
|
333
|
+
split_cmd[0] = "sudo timeout #{timeout}s"
|
334
|
+
cmd = split_cmd.join(" ")
|
335
|
+
else
|
336
|
+
cmd = "timeout #{timeout}s #{cmd}"
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
324
340
|
logger.debug("[SSH] #{self} cmd = #{cmd}")
|
325
341
|
|
326
|
-
if @transport_options[:pty]
|
342
|
+
if @transport_options[:pty]
|
327
343
|
channel.request_pty do |_ch, success|
|
328
344
|
raise Train::Transports::SSHPTYFailed, "Requesting PTY failed" unless success
|
329
345
|
end
|
@@ -350,21 +366,30 @@ class Train::Transports::SSH
|
|
350
366
|
end
|
351
367
|
end
|
352
368
|
end
|
369
|
+
session.loop
|
353
370
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
unless res
|
359
|
-
logger.debug("train ssh command '#{cmd}' reached requested timeout (#{timeout}s)")
|
360
|
-
session.channels.each_value { |c| c.eof!; c.close }
|
361
|
-
raise Train::CommandTimeoutReached.new "ssh command reached timeout (#{timeout}s)"
|
362
|
-
end
|
363
|
-
else
|
364
|
-
thr.join
|
371
|
+
if timeout && timeoutable?(cmd) && exit_status == GNU_TIMEOUT_EXIT_STATUS
|
372
|
+
logger.debug("train ssh command '#{cmd}' reached requested timeout (#{timeout}s)")
|
373
|
+
session.channels.each_value { |c| c.eof!; c.close }
|
374
|
+
raise Train::CommandTimeoutReached.new "ssh command reached timeout (#{timeout}s)"
|
365
375
|
end
|
366
376
|
|
367
377
|
[exit_status, stdout, stderr]
|
368
378
|
end
|
379
|
+
|
380
|
+
# Returns true if we think we can attempt to timeout the command
|
381
|
+
def timeoutable?(cmd)
|
382
|
+
have_timeout_cli? && !cmd.include?("|") # Don't try to timeout a command that has pipes
|
383
|
+
end
|
384
|
+
|
385
|
+
# Returns true if the GNU timeout command is available
|
386
|
+
def have_timeout_cli?
|
387
|
+
return @have_timeout_cli unless @have_timeout_cli.nil?
|
388
|
+
|
389
|
+
res = session.exec!("timeout --version")
|
390
|
+
@have_timeout_cli = res.exitstatus == 0
|
391
|
+
logger.debug("train ssh have_timeout_cli status is '#{@have_timeout_cli}'")
|
392
|
+
@have_timeout_cli
|
393
|
+
end
|
369
394
|
end
|
370
395
|
end
|
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.10.7
|
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:
|
11
|
+
date: 2022-10-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|
@@ -87,7 +87,7 @@ dependencies:
|
|
87
87
|
version: '1.2'
|
88
88
|
- - "<"
|
89
89
|
- !ruby/object:Gem::Version
|
90
|
-
version: '
|
90
|
+
version: '5.0'
|
91
91
|
type: :runtime
|
92
92
|
prerelease: false
|
93
93
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -97,7 +97,7 @@ dependencies:
|
|
97
97
|
version: '1.2'
|
98
98
|
- - "<"
|
99
99
|
- !ruby/object:Gem::Version
|
100
|
-
version: '
|
100
|
+
version: '5.0'
|
101
101
|
- !ruby/object:Gem::Dependency
|
102
102
|
name: net-ssh
|
103
103
|
requirement: !ruby/object:Gem::Requirement
|
@@ -107,7 +107,7 @@ dependencies:
|
|
107
107
|
version: '2.9'
|
108
108
|
- - "<"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
110
|
+
version: '8.0'
|
111
111
|
type: :runtime
|
112
112
|
prerelease: false
|
113
113
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -117,7 +117,7 @@ dependencies:
|
|
117
117
|
version: '2.9'
|
118
118
|
- - "<"
|
119
119
|
- !ruby/object:Gem::Version
|
120
|
-
version: '
|
120
|
+
version: '8.0'
|
121
121
|
description: A minimal Train with a backends for ssh and winrm.
|
122
122
|
email:
|
123
123
|
- inspec@chef.io
|
@@ -181,7 +181,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
181
181
|
requirements:
|
182
182
|
- - ">="
|
183
183
|
- !ruby/object:Gem::Version
|
184
|
-
version: '2.
|
184
|
+
version: '2.7'
|
185
185
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
186
186
|
requirements:
|
187
187
|
- - ">="
|