train 3.2.14 → 3.2.20

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. metadata +29 -149
  3. data/LICENSE +0 -201
  4. data/lib/train.rb +0 -193
  5. data/lib/train/errors.rb +0 -44
  6. data/lib/train/extras.rb +0 -11
  7. data/lib/train/extras/command_wrapper.rb +0 -201
  8. data/lib/train/extras/stat.rb +0 -136
  9. data/lib/train/file.rb +0 -212
  10. data/lib/train/file/local.rb +0 -82
  11. data/lib/train/file/local/unix.rb +0 -96
  12. data/lib/train/file/local/windows.rb +0 -68
  13. data/lib/train/file/remote.rb +0 -40
  14. data/lib/train/file/remote/aix.rb +0 -29
  15. data/lib/train/file/remote/linux.rb +0 -21
  16. data/lib/train/file/remote/qnx.rb +0 -41
  17. data/lib/train/file/remote/unix.rb +0 -110
  18. data/lib/train/file/remote/windows.rb +0 -110
  19. data/lib/train/globals.rb +0 -5
  20. data/lib/train/options.rb +0 -81
  21. data/lib/train/platforms.rb +0 -102
  22. data/lib/train/platforms/common.rb +0 -34
  23. data/lib/train/platforms/detect.rb +0 -12
  24. data/lib/train/platforms/detect/helpers/os_common.rb +0 -160
  25. data/lib/train/platforms/detect/helpers/os_linux.rb +0 -80
  26. data/lib/train/platforms/detect/helpers/os_windows.rb +0 -142
  27. data/lib/train/platforms/detect/scanner.rb +0 -85
  28. data/lib/train/platforms/detect/specifications/api.rb +0 -20
  29. data/lib/train/platforms/detect/specifications/os.rb +0 -629
  30. data/lib/train/platforms/detect/uuid.rb +0 -32
  31. data/lib/train/platforms/family.rb +0 -31
  32. data/lib/train/platforms/platform.rb +0 -109
  33. data/lib/train/plugin_test_helper.rb +0 -51
  34. data/lib/train/plugins.rb +0 -40
  35. data/lib/train/plugins/base_connection.rb +0 -198
  36. data/lib/train/plugins/transport.rb +0 -49
  37. data/lib/train/transports/cisco_ios_connection.rb +0 -133
  38. data/lib/train/transports/local.rb +0 -240
  39. data/lib/train/transports/mock.rb +0 -183
  40. data/lib/train/transports/ssh.rb +0 -271
  41. data/lib/train/transports/ssh_connection.rb +0 -342
  42. data/lib/train/version.rb +0 -7
@@ -1,271 +0,0 @@
1
- # encoding: utf-8
2
- #
3
- # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
- # Author:: Dominik Richter (<dominik.richter@gmail.com>)
5
- # Author:: Christoph Hartmann (<chris@lollyrock.com>)
6
- #
7
- # Copyright (C) 2014, Fletcher Nichol
8
- #
9
- # Licensed under the Apache License, Version 2.0 (the "License");
10
- # you may not use this file except in compliance with the License.
11
- # You may obtain a copy of the License at
12
- #
13
- # http://www.apache.org/licenses/LICENSE-2.0
14
- #
15
- # Unless required by applicable law or agreed to in writing, software
16
- # distributed under the License is distributed on an "AS IS" BASIS,
17
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
- # See the License for the specific language governing permissions and
19
- # limitations under the License.
20
-
21
- require "net/ssh"
22
- require "net/scp"
23
- require_relative "../errors"
24
-
25
- module Train::Transports
26
- # Wrapped exception for any internally raised SSH-related errors.
27
- #
28
- # @author Fletcher Nichol <fnichol@nichol.ca>
29
- class SSHFailed < Train::TransportError; end
30
- class SSHPTYFailed < Train::TransportError; end
31
-
32
- # A Transport which uses the SSH protocol to execute commands and transfer
33
- # files.
34
- #
35
- # @author Fletcher Nichol <fnichol@nichol.ca>
36
- class SSH < Train.plugin(1) # rubocop:disable Metrics/ClassLength
37
- name "ssh"
38
-
39
- require_relative "ssh_connection"
40
- require_relative "cisco_ios_connection"
41
-
42
- # add options for submodules
43
- include_options Train::Extras::CommandWrapper
44
-
45
- # common target configuration
46
- option :host, required: true
47
- option :port, default: 22, required: true
48
- option :user, default: "root", required: true
49
- option :key_files, default: nil
50
- option :password, default: nil
51
-
52
- # additional ssh options
53
- option :keepalive, default: true
54
- option :keepalive_interval, default: 60
55
- option :connection_timeout, default: 15
56
- option :connection_retries, default: 5
57
- option :connection_retry_sleep, default: 1
58
- option :max_wait_until_ready, default: 600
59
- option :compression, default: false
60
- option :pty, default: false
61
- option :proxy_command, default: nil
62
- option :bastion_host, default: nil
63
- option :bastion_user, default: "root"
64
- option :bastion_port, default: 22
65
- option :non_interactive, default: false
66
- option :verify_host_key, default: false
67
-
68
- option :compression_level do |opts|
69
- # on nil or false: set compression level to 0
70
- opts[:compression] ? 6 : 0
71
- end
72
-
73
- # (see Base#connection)
74
- def connection(state = {}, &block)
75
- opts = merge_options(options, state || {})
76
- validate_options(opts)
77
- conn_opts = connection_options(opts)
78
-
79
- if defined?(@connection) && @connection_options == conn_opts
80
- reuse_connection(&block)
81
- else
82
- create_new_connection(conn_opts, &block)
83
- end
84
- end
85
-
86
- private
87
-
88
- def validate_options(options)
89
- super(options)
90
-
91
- key_files = Array(options[:key_files])
92
- options[:auth_methods] ||= ["none"]
93
-
94
- unless key_files.empty?
95
- options[:auth_methods].push("publickey")
96
- options[:keys_only] = true if options[:password].nil?
97
- options[:key_files] = key_files
98
- end
99
-
100
- unless options[:password].nil?
101
- options[:auth_methods].push("password", "keyboard-interactive")
102
- end
103
-
104
- if options[:auth_methods] == ["none"]
105
- if ssh_known_identities.empty?
106
- raise Train::ClientError.new(
107
- "Your SSH Agent has no keys added, and you have not specified a password or a key file",
108
- :no_ssh_password_or_key_available
109
- )
110
- else
111
- logger.debug("[SSH] Using Agent keys as no password or key file have been specified")
112
- options[:auth_methods].push("publickey")
113
- end
114
- end
115
-
116
- if options[:pty]
117
- logger.warn("[SSH] PTY requested: stderr will be merged into stdout")
118
- end
119
-
120
- if [options[:proxy_command], options[:bastion_host]].all? { |type| !type.nil? }
121
- raise Train::ClientError, "Only one of proxy_command or bastion_host needs to be specified"
122
- end
123
-
124
- super
125
- self
126
- end
127
-
128
- # Creates an SSH Authentication KeyManager instance and saves it for
129
- # potential future reuse.
130
- #
131
- # @return [Hash] hash of SSH Known Identities
132
- # @api private
133
- def ssh_known_identities
134
- # Force KeyManager to load the key(s)
135
- @manager ||= Net::SSH::Authentication::KeyManager.new(nil).each_identity {}
136
- @manager.known_identities
137
- end
138
-
139
- # Builds the hash of options needed by the Connection object on
140
- # construction.
141
- #
142
- # @param opts [Hash] merged configuration and mutable state data
143
- # @return [Hash] hash of connection options
144
- # @api private
145
- def connection_options(opts)
146
- connection_options = {
147
- logger: logger,
148
- user_known_hosts_file: "/dev/null",
149
- hostname: opts[:host],
150
- port: opts[:port],
151
- username: opts[:user],
152
- compression: opts[:compression],
153
- compression_level: opts[:compression_level],
154
- keepalive: opts[:keepalive],
155
- keepalive_interval: opts[:keepalive_interval],
156
- timeout: opts[:connection_timeout],
157
- connection_retries: opts[:connection_retries],
158
- connection_retry_sleep: opts[:connection_retry_sleep],
159
- max_wait_until_ready: opts[:max_wait_until_ready],
160
- auth_methods: opts[:auth_methods],
161
- keys_only: opts[:keys_only],
162
- keys: opts[:key_files],
163
- password: opts[:password],
164
- forward_agent: opts[:forward_agent],
165
- proxy_command: opts[:proxy_command],
166
- bastion_host: opts[:bastion_host],
167
- bastion_user: opts[:bastion_user],
168
- bastion_port: opts[:bastion_port],
169
- non_interactive: opts[:non_interactive],
170
- transport_options: opts,
171
- }
172
- # disable host key verification. The hash key and value to use
173
- # depends on the version of net-ssh in use.
174
- connection_options[verify_host_key_option] = verify_host_key_value(opts[:verify_host_key])
175
-
176
- connection_options
177
- end
178
-
179
- #
180
- # Returns the correct host-key-verification option key to use depending
181
- # on what version of net-ssh is in use. In net-ssh <= 4.1, the supported
182
- # parameter is `paranoid` but in 4.2, it became `verify_host_key`
183
- #
184
- # `verify_host_key` does not work in <= 4.1, and `paranoid` throws
185
- # deprecation warnings in >= 4.2.
186
- #
187
- # While the "right thing" to do would be to pin train's dependency on
188
- # net-ssh to ~> 4.2, this will prevent InSpec from being used in
189
- # Chef v12 because of it pinning to a v3 of net-ssh.
190
- #
191
- def verify_host_key_option
192
- current_net_ssh = Net::SSH::Version::CURRENT
193
- new_option_version = Net::SSH::Version[4, 2, 0]
194
-
195
- current_net_ssh >= new_option_version ? :verify_host_key : :paranoid
196
- end
197
-
198
- # Likewise, version <5 accepted false; 5+ requires :never or will
199
- # issue a deprecation warning. This method allows a lot of common
200
- # things through.
201
- def verify_host_key_value(given)
202
- current_net_ssh = Net::SSH::Version::CURRENT
203
- new_value_version = Net::SSH::Version[5, 0, 0]
204
- if current_net_ssh >= new_value_version
205
- # 5.0+ style
206
- {
207
- # It's not a boolean anymore.
208
- "true" => :always,
209
- "false" => :never,
210
- true => :always,
211
- false => :never,
212
- # May be correct value, but strings from JSON config
213
- "always" => :always,
214
- "never" => :never,
215
- nil => :never,
216
- }.fetch(given, given)
217
- else
218
- # up to 4.2 style
219
- {
220
- "true" => true,
221
- "false" => false,
222
- nil => false,
223
- }.fetch(given, given)
224
- end
225
- end
226
-
227
- # Creates a new SSH Connection instance and save it for potential future
228
- # reuse.
229
- #
230
- # @param options [Hash] conneciton options
231
- # @return [Ssh::Connection] an SSH Connection instance
232
- # @api private
233
- def create_new_connection(options, &block)
234
- if defined?(@connection)
235
- logger.debug("[SSH] shutting previous connection #{@connection}")
236
- @connection.close
237
- end
238
-
239
- @connection_options = options
240
- conn = Connection.new(options, &block)
241
-
242
- # Cisco IOS requires a special implementation of `Net:SSH`. This uses the
243
- # SSH transport to identify the platform, but then replaces SSHConnection
244
- # with a CiscoIOSConnection in order to behave as expected for the user.
245
- if defined?(conn.platform.cisco_ios?) && conn.platform.cisco_ios?
246
- ios_options = {}
247
- ios_options[:host] = @options[:host]
248
- ios_options[:user] = @options[:user]
249
- # The enable password is used to elevate privileges on Cisco devices
250
- # We will also support the sudo password field for the same purpose
251
- # for the interim. # TODO
252
- ios_options[:enable_password] = @options[:enable_password] || @options[:sudo_password]
253
- ios_options[:logger] = @options[:logger]
254
- ios_options.merge!(@connection_options)
255
- conn = CiscoIOSConnection.new(ios_options)
256
- end
257
-
258
- @connection = conn unless conn.nil?
259
- end
260
-
261
- # Return the last saved SSH connection instance.
262
- #
263
- # @return [Ssh::Connection] an SSH Connection instance
264
- # @api private
265
- def reuse_connection
266
- logger.debug("[SSH] reusing existing connection #{@connection}")
267
- yield @connection if block_given?
268
- @connection
269
- end
270
- end
271
- end
@@ -1,342 +0,0 @@
1
- # encoding: utf-8
2
- #
3
- # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
- # Author:: Dominik Richter (<dominik.richter@gmail.com>)
5
- # Author:: Christoph Hartmann (<chris@lollyrock.com>)
6
- #
7
- # Copyright (C) 2014, Fletcher Nichol
8
- #
9
- # Licensed under the Apache License, Version 2.0 (the "License");
10
- # you may not use this file except in compliance with the License.
11
- # You may obtain a copy of the License at
12
- #
13
- # http://www.apache.org/licenses/LICENSE-2.0
14
- #
15
- # Unless required by applicable law or agreed to in writing, software
16
- # distributed under the License is distributed on an "AS IS" BASIS,
17
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
- # See the License for the specific language governing permissions and
19
- # limitations under the License.
20
-
21
- require "net/ssh"
22
- require "net/scp"
23
- require "timeout"
24
-
25
- class Train::Transports::SSH
26
- # A Connection instance can be generated and re-generated, given new
27
- # connection details such as connection port, hostname, credentials, etc.
28
- # This object is responsible for carrying out the actions on the remote
29
- # host such as executing commands, transferring files, etc.
30
- #
31
- # @author Fletcher Nichol <fnichol@nichol.ca>
32
- class Connection < BaseConnection # rubocop:disable Metrics/ClassLength
33
- attr_reader :hostname
34
- attr_reader :transport_options
35
-
36
- def initialize(options)
37
- # Track IOS command retries to prevent infinite loop on IOError. This must
38
- # be done before `super()` because the parent runs detection commands.
39
- @ios_cmd_retries = 0
40
-
41
- super(options)
42
-
43
- @session = nil
44
- @username = @options.delete(:username)
45
- @hostname = @options.delete(:hostname)
46
- @port = @options[:port] # don't delete from options
47
- @connection_retries = @options.delete(:connection_retries)
48
- @connection_retry_sleep = @options.delete(:connection_retry_sleep)
49
- @max_wait_until_ready = @options.delete(:max_wait_until_ready)
50
- @max_ssh_sessions = @options.delete(:max_ssh_connections) { 9 }
51
- @transport_options = @options.delete(:transport_options)
52
- @proxy_command = @options.delete(:proxy_command)
53
- @bastion_host = @options.delete(:bastion_host)
54
- @bastion_user = @options.delete(:bastion_user)
55
- @bastion_port = @options.delete(:bastion_port)
56
-
57
- @cmd_wrapper = CommandWrapper.load(self, @transport_options)
58
- end
59
-
60
- # (see Base::Connection#close)
61
- def close
62
- return if @session.nil?
63
-
64
- logger.debug("[SSH] closing connection to #{self}")
65
- session.close
66
- ensure
67
- @session = nil
68
- end
69
-
70
- def ssh_opts
71
- level = logger.debug? ? "VERBOSE" : "ERROR"
72
- fwd_agent = options[:forward_agent] ? "yes" : "no"
73
-
74
- args = %w{ -o UserKnownHostsFile=/dev/null }
75
- args += %w{ -o StrictHostKeyChecking=no }
76
- args += %w{ -o IdentitiesOnly=yes } if options[:keys]
77
- args += %w{ -o BatchMode=yes } if options[:non_interactive]
78
- args += %W{ -o LogLevel=#{level} }
79
- args += %W{ -o ForwardAgent=#{fwd_agent} } if options.key?(:forward_agent)
80
- Array(options[:keys]).each do |ssh_key|
81
- args += %W{ -i #{ssh_key} }
82
- end
83
- args
84
- end
85
-
86
- def check_proxy
87
- [@proxy_command, @bastion_host].any? { |type| !type.nil? }
88
- end
89
-
90
- def generate_proxy_command
91
- return @proxy_command unless @proxy_command.nil?
92
-
93
- args = %w{ ssh }
94
- args += ssh_opts
95
- args += %W{ #{@bastion_user}@#{@bastion_host} }
96
- args += %W{ -p #{@bastion_port} }
97
- args += %w{ -W %h:%p }
98
- args.join(" ")
99
- end
100
-
101
- # (see Base::Connection#login_command)
102
- def login_command
103
- args = ssh_opts
104
- args += %W{ -o ProxyCommand='#{generate_proxy_command}' } if check_proxy
105
- args += %W{ -p #{@port} }
106
- args += %W{ #{@username}@#{@hostname} }
107
- LoginCommand.new("ssh", args)
108
- end
109
-
110
- # (see Base::Connection#upload)
111
- def upload(locals, remote)
112
- waits = []
113
- Array(locals).each do |local|
114
- opts = File.directory?(local) ? { recursive: true } : {}
115
-
116
- waits.push session.scp.upload(local, remote, opts) do |_ch, name, sent, total|
117
- logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total
118
- end
119
- waits.shift.wait while waits.length >= @max_ssh_sessions
120
- end
121
- waits.each(&:wait)
122
- rescue Net::SSH::Exception => ex
123
- raise Train::Transports::SSHFailed, "SCP upload failed (#{ex.message})"
124
- end
125
-
126
- def download(remotes, local)
127
- waits = []
128
- Array(remotes).map do |remote|
129
- opts = file(remote).directory? ? { recursive: true } : {}
130
- waits.push session.scp.download(remote, local, opts) do |_ch, name, recv, total|
131
- logger.debug("Downloaded #{name} (#{total} bytes)") if recv == total
132
- end
133
- waits.shift.wait while waits.length >= @max_ssh_sessions
134
- end
135
- waits.each(&:wait)
136
- rescue Net::SSH::Exception => ex
137
- raise Train::Transports::SSHFailed, "SCP download failed (#{ex.message})"
138
- end
139
-
140
- # (see Base::Connection#wait_until_ready)
141
- def wait_until_ready
142
- delay = 3
143
- session(
144
- retries: @max_wait_until_ready / delay,
145
- delay: delay,
146
- message: "Waiting for SSH service on #{@hostname}:#{@port}, " \
147
- "retrying in #{delay} seconds"
148
- )
149
- run_command(PING_COMMAND.dup)
150
- end
151
-
152
- def uri
153
- "ssh://#{@username}@#{@hostname}:#{@port}"
154
- end
155
-
156
- # remote_port_forwarding
157
- def forward_remote(port, host, remote_port, remote_host = "127.0.0.1")
158
- @session.forward.remote(port, host, remote_port, remote_host)
159
- end
160
-
161
- def obscured_options
162
- options_to_print = @options.clone
163
- options_to_print[:password] = "<hidden>" if options_to_print.key?(:password)
164
- options_to_print
165
- end
166
-
167
- def with_sudo_pty
168
- old_pty = transport_options[:pty]
169
- transport_options[:pty] = true if @sudo
170
-
171
- yield
172
- ensure
173
- transport_options[:pty] = old_pty
174
- end
175
-
176
- private
177
-
178
- PING_COMMAND = "echo '[SSH] Established'".freeze
179
-
180
- RESCUE_EXCEPTIONS_ON_ESTABLISH = [
181
- Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
182
- Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH, Errno::EPIPE,
183
- Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout,
184
- Timeout::Error
185
- ].freeze
186
-
187
- # Establish an SSH session on the remote host.
188
- #
189
- # @param opts [Hash] retry options
190
- # @option opts [Integer] :retries the number of times to retry before
191
- # failing
192
- # @option opts [Float] :delay the number of seconds to wait until
193
- # attempting a retry
194
- # @option opts [String] :message an optional message to be logged on
195
- # debug (overriding the default) when a rescuable exception is raised
196
- # @return [Net::SSH::Connection::Session] the SSH connection session
197
- # @api private
198
- def establish_connection(opts)
199
- logger.debug("[SSH] opening connection to #{self}")
200
- logger.debug("[SSH] using options %p" % [obscured_options])
201
- if check_proxy
202
- require "net/ssh/proxy/command"
203
- @options[:proxy] = Net::SSH::Proxy::Command.new(generate_proxy_command)
204
- end
205
- Net::SSH.start(@hostname, @username, @options.clone.delete_if { |_key, value| value.nil? })
206
- rescue *RESCUE_EXCEPTIONS_ON_ESTABLISH => e
207
- if (opts[:retries] -= 1) <= 0
208
- logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
209
- raise Train::Transports::SSHFailed, "SSH session could not be established"
210
- end
211
-
212
- if opts[:message]
213
- logger.debug("[SSH] connection failed (#{e.inspect})")
214
- message = opts[:message]
215
- else
216
- message = "[SSH] connection failed, retrying in #{opts[:delay]}"\
217
- " seconds (#{e.inspect})"
218
- end
219
- logger.info(message)
220
-
221
- sleep(opts[:delay])
222
- retry
223
- end
224
-
225
- def file_via_connection(path, *args)
226
- if os.aix?
227
- Train::File::Remote::Aix.new(self, path, *args)
228
- elsif os.solaris?
229
- Train::File::Remote::Unix.new(self, path, *args)
230
- elsif os[:name] == "qnx"
231
- Train::File::Remote::Qnx.new(self, path, *args)
232
- elsif os.windows?
233
- Train::File::Remote::Windows.new(self, path, *args)
234
- else
235
- Train::File::Remote::Linux.new(self, path, *args)
236
- end
237
- end
238
-
239
- def run_command_via_connection(cmd, &data_handler)
240
- cmd.dup.force_encoding("binary") if cmd.respond_to?(:force_encoding)
241
-
242
- reset_session if session.closed?
243
-
244
- exit_status, stdout, stderr = execute_on_channel(cmd, &data_handler)
245
-
246
- # Since `@session.loop` succeeded, reset the IOS command retry counter
247
- @ios_cmd_retries = 0
248
-
249
- CommandResult.new(stdout, stderr, exit_status)
250
- rescue Net::SSH::Exception => ex
251
- raise Train::Transports::SSHFailed, "SSH command failed (#{ex.message})"
252
- rescue IOError
253
- # Cisco IOS occasionally closes the stream prematurely while we are
254
- # running commands to detect if we need to switch to the Cisco IOS
255
- # transport. This retries the command if this is the case.
256
- # See:
257
- # https://github.com/inspec/train/pull/271
258
- logger.debug("[SSH] Possible Cisco IOS race condition, retrying command")
259
-
260
- # Only attempt retry up to 5 times to avoid infinite loop
261
- @ios_cmd_retries += 1
262
- raise if @ios_cmd_retries >= 5
263
-
264
- retry
265
- end
266
-
267
- # Returns a connection session, or establishes one when invoked the
268
- # first time.
269
- #
270
- # @param retry_options [Hash] retry options for the initial connection
271
- # @return [Net::SSH::Connection::Session] the SSH connection session
272
- # @api private
273
- def session(retry_options = {})
274
- @session ||= establish_connection({
275
- retries: @connection_retries.to_i,
276
- delay: @connection_retry_sleep.to_i,
277
- }.merge(retry_options))
278
- end
279
-
280
- def reset_session
281
- @session = nil
282
- end
283
-
284
- # String representation of object, reporting its connection details and
285
- # configuration.
286
- #
287
- # @api private
288
- def to_s
289
- "#{@username}@#{@hostname}"
290
- end
291
-
292
- # Given a channel and a command string, it will execute the command on the channel
293
- # and accumulate results in @stdout/@stderr.
294
- #
295
- # @param channel [Net::SSH::Connection::Channel] an open ssh channel
296
- # @param cmd [String] the command to execute
297
- # @return [Integer] exit status or nil if exit-status/exit-signal requests
298
- # not received.
299
- #
300
- # @api private
301
- def execute_on_channel(cmd)
302
- stdout = ""
303
- stderr = ""
304
- exit_status = nil
305
- session.open_channel do |channel|
306
- # wrap commands if that is configured
307
- cmd = @cmd_wrapper.run(cmd) if @cmd_wrapper
308
-
309
- logger.debug("[SSH] #{self} cmd = #{cmd}")
310
-
311
- if @transport_options[:pty]
312
- channel.request_pty do |_ch, success|
313
- raise Train::Transports::SSHPTYFailed, "Requesting PTY failed" unless success
314
- end
315
- end
316
-
317
- channel.exec(cmd) do |_, success|
318
- abort "Couldn't execute command on SSH." unless success
319
- channel.on_data do |_, data|
320
- yield(data) if block_given?
321
- stdout += data
322
- end
323
-
324
- channel.on_extended_data do |_, _type, data|
325
- yield(data) if block_given?
326
- stderr += data
327
- end
328
-
329
- channel.on_request("exit-status") do |_, data|
330
- exit_status = data.read_long
331
- end
332
-
333
- channel.on_request("exit-signal") do |_, data|
334
- exit_status = data.read_long
335
- end
336
- end
337
- end
338
- session.loop
339
- [exit_status, stdout, stderr]
340
- end
341
- end
342
- end