train 3.2.14 → 3.2.20
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- metadata +29 -149
- data/LICENSE +0 -201
- data/lib/train.rb +0 -193
- data/lib/train/errors.rb +0 -44
- data/lib/train/extras.rb +0 -11
- data/lib/train/extras/command_wrapper.rb +0 -201
- data/lib/train/extras/stat.rb +0 -136
- data/lib/train/file.rb +0 -212
- data/lib/train/file/local.rb +0 -82
- data/lib/train/file/local/unix.rb +0 -96
- data/lib/train/file/local/windows.rb +0 -68
- data/lib/train/file/remote.rb +0 -40
- data/lib/train/file/remote/aix.rb +0 -29
- data/lib/train/file/remote/linux.rb +0 -21
- data/lib/train/file/remote/qnx.rb +0 -41
- data/lib/train/file/remote/unix.rb +0 -110
- data/lib/train/file/remote/windows.rb +0 -110
- data/lib/train/globals.rb +0 -5
- data/lib/train/options.rb +0 -81
- data/lib/train/platforms.rb +0 -102
- data/lib/train/platforms/common.rb +0 -34
- data/lib/train/platforms/detect.rb +0 -12
- data/lib/train/platforms/detect/helpers/os_common.rb +0 -160
- data/lib/train/platforms/detect/helpers/os_linux.rb +0 -80
- data/lib/train/platforms/detect/helpers/os_windows.rb +0 -142
- data/lib/train/platforms/detect/scanner.rb +0 -85
- data/lib/train/platforms/detect/specifications/api.rb +0 -20
- data/lib/train/platforms/detect/specifications/os.rb +0 -629
- data/lib/train/platforms/detect/uuid.rb +0 -32
- data/lib/train/platforms/family.rb +0 -31
- data/lib/train/platforms/platform.rb +0 -109
- data/lib/train/plugin_test_helper.rb +0 -51
- data/lib/train/plugins.rb +0 -40
- data/lib/train/plugins/base_connection.rb +0 -198
- data/lib/train/plugins/transport.rb +0 -49
- data/lib/train/transports/cisco_ios_connection.rb +0 -133
- data/lib/train/transports/local.rb +0 -240
- data/lib/train/transports/mock.rb +0 -183
- data/lib/train/transports/ssh.rb +0 -271
- data/lib/train/transports/ssh_connection.rb +0 -342
- data/lib/train/version.rb +0 -7
data/lib/train/transports/ssh.rb
DELETED
@@ -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
|