train-core 3.4.9 → 3.10.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6272024b67e2dac1df86499e4687dc44d5df58b328146d52cb864433d7c6e6a7
4
- data.tar.gz: a4133877056cc7feed58ac9dab964ecfc435d28cc30a09d6dd104577a8a30374
3
+ metadata.gz: 6d396bbb358c284289e040d532236fa26f2f92d73aa41dde56bf0b914a087238
4
+ data.tar.gz: e378a5a7c8af2cc8bcadc02abc1e99c310f73a182703edac027592074dfa8eba
5
5
  SHA512:
6
- metadata.gz: 53bcc25af9bade96ee63aa1f0940927cb4e4183220737269bb4882c800df94c1bf6cb924abf5cb6b354b18ae000b6fba3d7a35787a3764d9402cd63f7b90bdd2
7
- data.tar.gz: 775d8a24be41a87f8427f3850b829b1acbc78d2a9ef8e299e1e4c9f9dbd352838d7bd9ef906723832171d0f822a10dac39827ab8a1b92c7ebec220bf37e337dd
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
@@ -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? || backend.os[:name] == "alpine" || backend.os[:name] == "yocto") ? "-c" : "--printf"
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.
@@ -15,6 +15,12 @@ module Train
15
15
  nil
16
16
  end
17
17
 
18
+ def content=(new_content)
19
+ ::File.open(@path, "w", encoding: "UTF-8") { |fp| fp.write(new_content) }
20
+
21
+ @content = new_content
22
+ end
23
+
18
24
  def link_path
19
25
  return nil unless symlink?
20
26
 
@@ -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|mingw32|windows/)
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
- true
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
- if @platform[:uuid_command]
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.run_command
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.run_command
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
- @pipe.puts(encoded_script)
171
- @pipe.flush
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
- begin
197
- pipe = open("//./pipe/#{pipe_name}", "r+")
198
- break
199
- rescue
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
@@ -42,12 +42,12 @@ module Train::Transports
42
42
  include_options Train::Extras::CommandWrapper
43
43
 
44
44
  # common target configuration
45
- option :host, required: true
46
- option :port, default: 22, required: true
47
- option :user, default: "root", required: true
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
- unless key_files.empty?
105
- options[:auth_methods].push("publickey")
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
- unless options[:password].nil?
111
- options[:auth_methods].push("password", "keyboard-interactive")
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] || timeout
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
- thr = Thread.new { session.loop }
355
-
356
- if timeout
357
- res = thr.join(timeout)
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
@@ -2,5 +2,5 @@
2
2
  # Author:: Dominik Richter (<dominik.richter@gmail.com>)
3
3
 
4
4
  module Train
5
- VERSION = "3.4.9".freeze
5
+ VERSION = "3.10.7".freeze
6
6
  end
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.9
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: 2021-01-27 00:00:00.000000000 Z
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: '4.0'
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: '4.0'
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: '7.0'
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: '7.0'
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.4'
184
+ version: '2.7'
185
185
  required_rubygems_version: !ruby/object:Gem::Requirement
186
186
  requirements:
187
187
  - - ">="