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 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
  - - ">="