train 3.2.14 → 3.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- 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
|