train-core 2.0.5 → 2.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/train/transports/ssh.rb +269 -0
- data/lib/train/transports/ssh_connection.rb +311 -0
- data/lib/train/transports/winrm.rb +207 -0
- data/lib/train/transports/winrm_connection.rb +200 -0
- data/lib/train/version.rb +1 -1
- metadata +106 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6296676d2fb3138e2e4e3f660e6bc9d871f10419318ba3344ca472c1eca7f0d2
|
4
|
+
data.tar.gz: 8c5d1f00ee0c1e4dffa9520c8c622cd5333716a90576d52dfc5856f2ce7bf6de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97150adffc1f18041eb3c1f7c77c439920674a2f1d40ef90b7720eeb808bc98709d4cd646af0fae949a47229703dc19988ac2ca54b21e1de5aeb2b71a873a4d1
|
7
|
+
data.tar.gz: '028b9286800362e7e945c6fa5d3efde308e5b917233e7e9367b00ba289e87902ac4a8ccfcb55bf6879d7548340a40156e0cf2a05035c139d86d216dd27bda036'
|
@@ -0,0 +1,269 @@
|
|
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 'train/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 'train/transports/ssh_connection'
|
40
|
+
require 'train/transports/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
|
+
fail Train::ClientError,
|
107
|
+
'Your SSH Agent has no keys added, and you have not specified a password or a key file'
|
108
|
+
else
|
109
|
+
logger.debug('[SSH] Using Agent keys as no password or key file have been specified')
|
110
|
+
options[:auth_methods].push('publickey')
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if options[:pty]
|
115
|
+
logger.warn('[SSH] PTY requested: stderr will be merged into stdout')
|
116
|
+
end
|
117
|
+
|
118
|
+
if [options[:proxy_command], options[:bastion_host]].all? { |type| !type.nil? }
|
119
|
+
fail Train::ClientError, 'Only one of proxy_command or bastion_host needs to be specified'
|
120
|
+
end
|
121
|
+
|
122
|
+
super
|
123
|
+
self
|
124
|
+
end
|
125
|
+
|
126
|
+
# Creates an SSH Authentication KeyManager instance and saves it for
|
127
|
+
# potential future reuse.
|
128
|
+
#
|
129
|
+
# @return [Hash] hash of SSH Known Identities
|
130
|
+
# @api private
|
131
|
+
def ssh_known_identities
|
132
|
+
# Force KeyManager to load the key(s)
|
133
|
+
@manager ||= Net::SSH::Authentication::KeyManager.new(nil).each_identity {}
|
134
|
+
@manager.known_identities
|
135
|
+
end
|
136
|
+
|
137
|
+
# Builds the hash of options needed by the Connection object on
|
138
|
+
# construction.
|
139
|
+
#
|
140
|
+
# @param opts [Hash] merged configuration and mutable state data
|
141
|
+
# @return [Hash] hash of connection options
|
142
|
+
# @api private
|
143
|
+
def connection_options(opts)
|
144
|
+
connection_options = {
|
145
|
+
logger: logger,
|
146
|
+
user_known_hosts_file: '/dev/null',
|
147
|
+
hostname: opts[:host],
|
148
|
+
port: opts[:port],
|
149
|
+
username: opts[:user],
|
150
|
+
compression: opts[:compression],
|
151
|
+
compression_level: opts[:compression_level],
|
152
|
+
keepalive: opts[:keepalive],
|
153
|
+
keepalive_interval: opts[:keepalive_interval],
|
154
|
+
timeout: opts[:connection_timeout],
|
155
|
+
connection_retries: opts[:connection_retries],
|
156
|
+
connection_retry_sleep: opts[:connection_retry_sleep],
|
157
|
+
max_wait_until_ready: opts[:max_wait_until_ready],
|
158
|
+
auth_methods: opts[:auth_methods],
|
159
|
+
keys_only: opts[:keys_only],
|
160
|
+
keys: opts[:key_files],
|
161
|
+
password: opts[:password],
|
162
|
+
forward_agent: opts[:forward_agent],
|
163
|
+
proxy_command: opts[:proxy_command],
|
164
|
+
bastion_host: opts[:bastion_host],
|
165
|
+
bastion_user: opts[:bastion_user],
|
166
|
+
bastion_port: opts[:bastion_port],
|
167
|
+
non_interactive: opts[:non_interactive],
|
168
|
+
transport_options: opts,
|
169
|
+
}
|
170
|
+
# disable host key verification. The hash key and value to use
|
171
|
+
# depends on the version of net-ssh in use.
|
172
|
+
connection_options[verify_host_key_option] = verify_host_key_value(opts[:verify_host_key])
|
173
|
+
|
174
|
+
connection_options
|
175
|
+
end
|
176
|
+
|
177
|
+
#
|
178
|
+
# Returns the correct host-key-verification option key to use depending
|
179
|
+
# on what version of net-ssh is in use. In net-ssh <= 4.1, the supported
|
180
|
+
# parameter is `paranoid` but in 4.2, it became `verify_host_key`
|
181
|
+
#
|
182
|
+
# `verify_host_key` does not work in <= 4.1, and `paranoid` throws
|
183
|
+
# deprecation warnings in >= 4.2.
|
184
|
+
#
|
185
|
+
# While the "right thing" to do would be to pin train's dependency on
|
186
|
+
# net-ssh to ~> 4.2, this will prevent InSpec from being used in
|
187
|
+
# Chef v12 because of it pinning to a v3 of net-ssh.
|
188
|
+
#
|
189
|
+
def verify_host_key_option
|
190
|
+
current_net_ssh = Net::SSH::Version::CURRENT
|
191
|
+
new_option_version = Net::SSH::Version[4, 2, 0]
|
192
|
+
|
193
|
+
current_net_ssh >= new_option_version ? :verify_host_key : :paranoid
|
194
|
+
end
|
195
|
+
|
196
|
+
# Likewise, version <5 accepted false; 5+ requires :never or will
|
197
|
+
# issue a deprecation warning. This method allows a lot of common
|
198
|
+
# things through.
|
199
|
+
def verify_host_key_value(given)
|
200
|
+
current_net_ssh = Net::SSH::Version::CURRENT
|
201
|
+
new_value_version = Net::SSH::Version[5, 0, 0]
|
202
|
+
if current_net_ssh >= new_value_version
|
203
|
+
# 5.0+ style
|
204
|
+
{
|
205
|
+
# It's not a boolean anymore.
|
206
|
+
'true' => :always,
|
207
|
+
'false' => :never,
|
208
|
+
true => :always,
|
209
|
+
false => :never,
|
210
|
+
# May be correct value, but strings from JSON config
|
211
|
+
'always' => :always,
|
212
|
+
'never' => :never,
|
213
|
+
nil => :never,
|
214
|
+
}.fetch(given, given)
|
215
|
+
else
|
216
|
+
# up to 4.2 style
|
217
|
+
{
|
218
|
+
'true' => true,
|
219
|
+
'false' => false,
|
220
|
+
nil => false,
|
221
|
+
}.fetch(given, given)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Creates a new SSH Connection instance and save it for potential future
|
226
|
+
# reuse.
|
227
|
+
#
|
228
|
+
# @param options [Hash] conneciton options
|
229
|
+
# @return [Ssh::Connection] an SSH Connection instance
|
230
|
+
# @api private
|
231
|
+
def create_new_connection(options, &block)
|
232
|
+
if defined?(@connection)
|
233
|
+
logger.debug("[SSH] shutting previous connection #{@connection}")
|
234
|
+
@connection.close
|
235
|
+
end
|
236
|
+
|
237
|
+
@connection_options = options
|
238
|
+
conn = Connection.new(options, &block)
|
239
|
+
|
240
|
+
# Cisco IOS requires a special implementation of `Net:SSH`. This uses the
|
241
|
+
# SSH transport to identify the platform, but then replaces SSHConnection
|
242
|
+
# with a CiscoIOSConnection in order to behave as expected for the user.
|
243
|
+
if defined?(conn.platform.cisco_ios?) && conn.platform.cisco_ios?
|
244
|
+
ios_options = {}
|
245
|
+
ios_options[:host] = @options[:host]
|
246
|
+
ios_options[:user] = @options[:user]
|
247
|
+
# The enable password is used to elevate privileges on Cisco devices
|
248
|
+
# We will also support the sudo password field for the same purpose
|
249
|
+
# for the interim. # TODO
|
250
|
+
ios_options[:enable_password] = @options[:enable_password] || @options[:sudo_password]
|
251
|
+
ios_options[:logger] = @options[:logger]
|
252
|
+
ios_options.merge!(@connection_options)
|
253
|
+
conn = CiscoIOSConnection.new(ios_options)
|
254
|
+
end
|
255
|
+
|
256
|
+
@connection = conn unless conn.nil?
|
257
|
+
end
|
258
|
+
|
259
|
+
# Return the last saved SSH connection instance.
|
260
|
+
#
|
261
|
+
# @return [Ssh::Connection] an SSH Connection instance
|
262
|
+
# @api private
|
263
|
+
def reuse_connection
|
264
|
+
logger.debug("[SSH] reusing existing connection #{@connection}")
|
265
|
+
yield @connection if block_given?
|
266
|
+
@connection
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
@@ -0,0 +1,311 @@
|
|
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
|
+
def initialize(options)
|
35
|
+
# Track IOS command retries to prevent infinite loop on IOError. This must
|
36
|
+
# be done before `super()` because the parent runs detection commands.
|
37
|
+
@ios_cmd_retries = 0
|
38
|
+
super(options)
|
39
|
+
@username = @options.delete(:username)
|
40
|
+
@hostname = @options.delete(:hostname)
|
41
|
+
@port = @options[:port] # don't delete from options
|
42
|
+
@connection_retries = @options.delete(:connection_retries)
|
43
|
+
@connection_retry_sleep = @options.delete(:connection_retry_sleep)
|
44
|
+
@max_wait_until_ready = @options.delete(:max_wait_until_ready)
|
45
|
+
@max_ssh_sessions = @options.delete(:max_ssh_connections) { 9 }
|
46
|
+
@session = nil
|
47
|
+
@transport_options = @options.delete(:transport_options)
|
48
|
+
@cmd_wrapper = nil
|
49
|
+
@proxy_command = @options.delete(:proxy_command)
|
50
|
+
@bastion_host = @options.delete(:bastion_host)
|
51
|
+
@bastion_user = @options.delete(:bastion_user)
|
52
|
+
@bastion_port = @options.delete(:bastion_port)
|
53
|
+
@cmd_wrapper = CommandWrapper.load(self, @transport_options)
|
54
|
+
end
|
55
|
+
|
56
|
+
# (see Base::Connection#close)
|
57
|
+
def close
|
58
|
+
return if @session.nil?
|
59
|
+
logger.debug("[SSH] closing connection to #{self}")
|
60
|
+
session.close
|
61
|
+
ensure
|
62
|
+
@session = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def ssh_opts
|
66
|
+
level = logger.debug? ? 'VERBOSE' : 'ERROR'
|
67
|
+
fwd_agent = options[:forward_agent] ? 'yes' : 'no'
|
68
|
+
|
69
|
+
args = %w{ -o UserKnownHostsFile=/dev/null }
|
70
|
+
args += %w{ -o StrictHostKeyChecking=no }
|
71
|
+
args += %w{ -o IdentitiesOnly=yes } if options[:keys]
|
72
|
+
args += %w{ -o BatchMode=yes } if options[:non_interactive]
|
73
|
+
args += %W( -o LogLevel=#{level} )
|
74
|
+
args += %W( -o ForwardAgent=#{fwd_agent} ) if options.key?(:forward_agent)
|
75
|
+
Array(options[:keys]).each do |ssh_key|
|
76
|
+
args += %W( -i #{ssh_key} )
|
77
|
+
end
|
78
|
+
args
|
79
|
+
end
|
80
|
+
|
81
|
+
def check_proxy
|
82
|
+
[@proxy_command, @bastion_host].any? { |type| !type.nil? }
|
83
|
+
end
|
84
|
+
|
85
|
+
def generate_proxy_command
|
86
|
+
return @proxy_command unless @proxy_command.nil?
|
87
|
+
args = %w{ ssh }
|
88
|
+
args += ssh_opts
|
89
|
+
args += %W( #{@bastion_user}@#{@bastion_host} )
|
90
|
+
args += %W( -p #{@bastion_port} )
|
91
|
+
args += %w{ -W %h:%p }
|
92
|
+
args.join(' ')
|
93
|
+
end
|
94
|
+
|
95
|
+
# (see Base::Connection#login_command)
|
96
|
+
def login_command
|
97
|
+
args = ssh_opts
|
98
|
+
args += %W( -o ProxyCommand='#{generate_proxy_command}' ) if check_proxy
|
99
|
+
args += %W( -p #{@port} )
|
100
|
+
args += %W( #{@username}@#{@hostname} )
|
101
|
+
LoginCommand.new('ssh', args)
|
102
|
+
end
|
103
|
+
|
104
|
+
# (see Base::Connection#upload)
|
105
|
+
def upload(locals, remote)
|
106
|
+
waits = []
|
107
|
+
Array(locals).each do |local|
|
108
|
+
opts = File.directory?(local) ? { recursive: true } : {}
|
109
|
+
|
110
|
+
waits.push session.scp.upload(local, remote, opts) do |_ch, name, sent, total|
|
111
|
+
logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total
|
112
|
+
end
|
113
|
+
waits.shift.wait while waits.length >= @max_ssh_sessions
|
114
|
+
end
|
115
|
+
waits.each(&:wait)
|
116
|
+
rescue Net::SSH::Exception => ex
|
117
|
+
raise Train::Transports::SSHFailed, "SCP upload failed (#{ex.message})"
|
118
|
+
end
|
119
|
+
|
120
|
+
def download(remotes, local)
|
121
|
+
waits = []
|
122
|
+
Array(remotes).map do |remote|
|
123
|
+
opts = file(remote).directory? ? { recursive: true } : {}
|
124
|
+
waits.push session.scp.download(remote, local, opts) do |_ch, name, recv, total|
|
125
|
+
logger.debug("Downloaded #{name} (#{total} bytes)") if recv == total
|
126
|
+
end
|
127
|
+
waits.shift.wait while waits.length >= @max_ssh_sessions
|
128
|
+
end
|
129
|
+
waits.each(&:wait)
|
130
|
+
rescue Net::SSH::Exception => ex
|
131
|
+
raise Train::Transports::SSHFailed, "SCP download failed (#{ex.message})"
|
132
|
+
end
|
133
|
+
|
134
|
+
# (see Base::Connection#wait_until_ready)
|
135
|
+
def wait_until_ready
|
136
|
+
delay = 3
|
137
|
+
session(
|
138
|
+
retries: @max_wait_until_ready / delay,
|
139
|
+
delay: delay,
|
140
|
+
message: "Waiting for SSH service on #{@hostname}:#{@port}, " \
|
141
|
+
"retrying in #{delay} seconds",
|
142
|
+
)
|
143
|
+
run_command(PING_COMMAND.dup)
|
144
|
+
end
|
145
|
+
|
146
|
+
def uri
|
147
|
+
"ssh://#{@username}@#{@hostname}:#{@port}"
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
PING_COMMAND = "echo '[SSH] Established'".freeze
|
153
|
+
|
154
|
+
RESCUE_EXCEPTIONS_ON_ESTABLISH = [
|
155
|
+
Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
|
156
|
+
Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH, Errno::EPIPE,
|
157
|
+
Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout,
|
158
|
+
Timeout::Error
|
159
|
+
].freeze
|
160
|
+
|
161
|
+
# Establish an SSH session on the remote host.
|
162
|
+
#
|
163
|
+
# @param opts [Hash] retry options
|
164
|
+
# @option opts [Integer] :retries the number of times to retry before
|
165
|
+
# failing
|
166
|
+
# @option opts [Float] :delay the number of seconds to wait until
|
167
|
+
# attempting a retry
|
168
|
+
# @option opts [String] :message an optional message to be logged on
|
169
|
+
# debug (overriding the default) when a rescuable exception is raised
|
170
|
+
# @return [Net::SSH::Connection::Session] the SSH connection session
|
171
|
+
# @api private
|
172
|
+
def establish_connection(opts)
|
173
|
+
logger.debug("[SSH] opening connection to #{self}")
|
174
|
+
if check_proxy
|
175
|
+
require 'net/ssh/proxy/command'
|
176
|
+
@options[:proxy] = Net::SSH::Proxy::Command.new(generate_proxy_command)
|
177
|
+
end
|
178
|
+
Net::SSH.start(@hostname, @username, @options.clone.delete_if { |_key, value| value.nil? })
|
179
|
+
rescue *RESCUE_EXCEPTIONS_ON_ESTABLISH => e
|
180
|
+
if (opts[:retries] -= 1) <= 0
|
181
|
+
logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
|
182
|
+
raise Train::Transports::SSHFailed, 'SSH session could not be established'
|
183
|
+
end
|
184
|
+
|
185
|
+
if opts[:message]
|
186
|
+
logger.debug("[SSH] connection failed (#{e.inspect})")
|
187
|
+
message = opts[:message]
|
188
|
+
else
|
189
|
+
message = "[SSH] connection failed, retrying in #{opts[:delay]}"\
|
190
|
+
" seconds (#{e.inspect})"
|
191
|
+
end
|
192
|
+
logger.info(message)
|
193
|
+
|
194
|
+
sleep(opts[:delay])
|
195
|
+
retry
|
196
|
+
end
|
197
|
+
|
198
|
+
def file_via_connection(path)
|
199
|
+
if os.aix?
|
200
|
+
Train::File::Remote::Aix.new(self, path)
|
201
|
+
elsif os.solaris?
|
202
|
+
Train::File::Remote::Unix.new(self, path)
|
203
|
+
elsif os[:name] == 'qnx'
|
204
|
+
Train::File::Remote::Qnx.new(self, path)
|
205
|
+
else
|
206
|
+
Train::File::Remote::Linux.new(self, path)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def run_command_via_connection(cmd, &data_handler)
|
211
|
+
cmd.dup.force_encoding('binary') if cmd.respond_to?(:force_encoding)
|
212
|
+
logger.debug("[SSH] #{self} (#{cmd})")
|
213
|
+
|
214
|
+
reset_session if session.closed?
|
215
|
+
exit_status, stdout, stderr = execute_on_channel(cmd, &data_handler)
|
216
|
+
|
217
|
+
# Since `@session.loop` succeeded, reset the IOS command retry counter
|
218
|
+
@ios_cmd_retries = 0
|
219
|
+
|
220
|
+
CommandResult.new(stdout, stderr, exit_status)
|
221
|
+
rescue Net::SSH::Exception => ex
|
222
|
+
raise Train::Transports::SSHFailed, "SSH command failed (#{ex.message})"
|
223
|
+
rescue IOError
|
224
|
+
# Cisco IOS occasionally closes the stream prematurely while we are
|
225
|
+
# running commands to detect if we need to switch to the Cisco IOS
|
226
|
+
# transport. This retries the command if this is the case.
|
227
|
+
# See:
|
228
|
+
# https://github.com/inspec/train/pull/271
|
229
|
+
logger.debug('[SSH] Possible Cisco IOS race condition, retrying command')
|
230
|
+
|
231
|
+
# Only attempt retry up to 5 times to avoid infinite loop
|
232
|
+
@ios_cmd_retries += 1
|
233
|
+
raise if @ios_cmd_retries >= 5
|
234
|
+
|
235
|
+
retry
|
236
|
+
end
|
237
|
+
|
238
|
+
# Returns a connection session, or establishes one when invoked the
|
239
|
+
# first time.
|
240
|
+
#
|
241
|
+
# @param retry_options [Hash] retry options for the initial connection
|
242
|
+
# @return [Net::SSH::Connection::Session] the SSH connection session
|
243
|
+
# @api private
|
244
|
+
def session(retry_options = {})
|
245
|
+
@session ||= establish_connection({
|
246
|
+
retries: @connection_retries.to_i,
|
247
|
+
delay: @connection_retry_sleep.to_i,
|
248
|
+
}.merge(retry_options))
|
249
|
+
end
|
250
|
+
|
251
|
+
def reset_session
|
252
|
+
@session = nil
|
253
|
+
end
|
254
|
+
|
255
|
+
# String representation of object, reporting its connection details and
|
256
|
+
# configuration.
|
257
|
+
#
|
258
|
+
# @api private
|
259
|
+
def to_s
|
260
|
+
options_to_print = @options.clone
|
261
|
+
options_to_print[:password] = '<hidden>' if options_to_print.key?(:password)
|
262
|
+
"#{@username}@#{@hostname}<#{options_to_print.inspect}>"
|
263
|
+
end
|
264
|
+
|
265
|
+
# Given a channel and a command string, it will execute the command on the channel
|
266
|
+
# and accumulate results in @stdout/@stderr.
|
267
|
+
#
|
268
|
+
# @param channel [Net::SSH::Connection::Channel] an open ssh channel
|
269
|
+
# @param cmd [String] the command to execute
|
270
|
+
# @return [Integer] exit status or nil if exit-status/exit-signal requests
|
271
|
+
# not received.
|
272
|
+
#
|
273
|
+
# @api private
|
274
|
+
def execute_on_channel(cmd, &data_handler)
|
275
|
+
stdout = stderr = ''
|
276
|
+
exit_status = nil
|
277
|
+
session.open_channel do |channel|
|
278
|
+
# wrap commands if that is configured
|
279
|
+
cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
|
280
|
+
|
281
|
+
if @transport_options[:pty]
|
282
|
+
channel.request_pty do |_ch, success|
|
283
|
+
fail Train::Transports::SSHPTYFailed, 'Requesting PTY failed' unless success
|
284
|
+
end
|
285
|
+
end
|
286
|
+
channel.exec(cmd) do |_, success|
|
287
|
+
abort 'Couldn\'t execute command on SSH.' unless success
|
288
|
+
channel.on_data do |_, data|
|
289
|
+
yield(data) unless data_handler.nil?
|
290
|
+
stdout += data
|
291
|
+
end
|
292
|
+
|
293
|
+
channel.on_extended_data do |_, _type, data|
|
294
|
+
yield(data) unless data_handler.nil?
|
295
|
+
stderr += data
|
296
|
+
end
|
297
|
+
|
298
|
+
channel.on_request('exit-status') do |_, data|
|
299
|
+
exit_status = data.read_long
|
300
|
+
end
|
301
|
+
|
302
|
+
channel.on_request('exit-signal') do |_, data|
|
303
|
+
exit_status = data.read_long
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
session.loop
|
308
|
+
[exit_status, stdout, stderr]
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Salim Afiune (<salim@afiunemaya.com.mx>)
|
4
|
+
# Author:: Matt Wrock (<matt@mattwrock.com>)
|
5
|
+
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
6
|
+
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
7
|
+
# Author:: Christoph Hartmann (<chris@lollyrock.com>)
|
8
|
+
#
|
9
|
+
# Copyright (C) 2014, Salim Afiune
|
10
|
+
#
|
11
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
12
|
+
# you may not use this file except in compliance with the License.
|
13
|
+
# You may obtain a copy of the License at
|
14
|
+
#
|
15
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
16
|
+
#
|
17
|
+
# Unless required by applicable law or agreed to in writing, software
|
18
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
19
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
20
|
+
# See the License for the specific language governing permissions and
|
21
|
+
# limitations under the License.
|
22
|
+
|
23
|
+
require 'rbconfig'
|
24
|
+
require 'uri'
|
25
|
+
require 'train/errors'
|
26
|
+
|
27
|
+
module Train::Transports
|
28
|
+
# Wrapped exception for any internally raised WinRM-related errors.
|
29
|
+
#
|
30
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
31
|
+
class WinRMFailed < Train::TransportError; end
|
32
|
+
|
33
|
+
# A Transport which uses WinRM to execute commands and transfer files.
|
34
|
+
#
|
35
|
+
# @author Matt Wrock <matt@mattwrock.com>
|
36
|
+
# @author Salim Afiune <salim@afiunemaya.com.mx>
|
37
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
38
|
+
class WinRM < Train.plugin(1) # rubocop:disable ClassLength
|
39
|
+
name 'winrm'
|
40
|
+
|
41
|
+
require 'train/transports/winrm_connection'
|
42
|
+
|
43
|
+
# ref: https://github.com/winrb/winrm#transports
|
44
|
+
SUPPORTED_WINRM_TRANSPORTS = %i(negotiate ssl plaintext kerberos).freeze
|
45
|
+
|
46
|
+
# common target configuration
|
47
|
+
option :host, required: true
|
48
|
+
option :port
|
49
|
+
option :user, default: 'administrator', required: true
|
50
|
+
option :password, nil
|
51
|
+
option :winrm_transport, default: :negotiate
|
52
|
+
option :winrm_disable_sspi, default: false
|
53
|
+
option :winrm_basic_auth_only, default: false
|
54
|
+
option :path, default: '/wsman'
|
55
|
+
option :ssl, default: false
|
56
|
+
option :self_signed, default: false
|
57
|
+
|
58
|
+
# additional winrm options
|
59
|
+
option :rdp_port, default: 3389
|
60
|
+
option :connection_retries, default: 5
|
61
|
+
option :connection_retry_sleep, default: 1
|
62
|
+
option :max_wait_until_ready, default: 600
|
63
|
+
option :ssl_peer_fingerprint, default: nil
|
64
|
+
option :kerberos_realm, default: nil
|
65
|
+
option :kerberos_service, default: nil
|
66
|
+
option :ca_trust_file, default: nil
|
67
|
+
# The amount of time in SECONDS for which each operation must get an ack
|
68
|
+
# from the winrm endpoint. Does not mean that the command has
|
69
|
+
# completed in this time, only that the server has ack'd the request.
|
70
|
+
option :operation_timeout, default: nil
|
71
|
+
|
72
|
+
def initialize(opts)
|
73
|
+
super(opts)
|
74
|
+
load_needed_dependencies!
|
75
|
+
end
|
76
|
+
|
77
|
+
# (see Base#connection)
|
78
|
+
def connection(state = nil, &block)
|
79
|
+
opts = merge_options(options, state || {})
|
80
|
+
validate_options(opts)
|
81
|
+
conn_opts = connection_options(opts)
|
82
|
+
|
83
|
+
if @connection && @connection_options == conn_opts
|
84
|
+
reuse_connection(&block)
|
85
|
+
else
|
86
|
+
create_new_connection(conn_opts, &block)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def validate_options(opts)
|
93
|
+
super(opts)
|
94
|
+
|
95
|
+
# set scheme and port based on ssl activation
|
96
|
+
scheme = opts[:ssl] ? 'https' : 'http'
|
97
|
+
port = opts[:port]
|
98
|
+
port = (opts[:ssl] ? 5986 : 5985) if port.nil?
|
99
|
+
winrm_transport = opts[:winrm_transport].to_sym
|
100
|
+
unless SUPPORTED_WINRM_TRANSPORTS.include?(winrm_transport)
|
101
|
+
fail Train::ClientError, "Unsupported transport type: #{winrm_transport.inspect}"
|
102
|
+
end
|
103
|
+
|
104
|
+
# remove leading '/'
|
105
|
+
path = (opts[:path] || '').sub(%r{^/+}, '')
|
106
|
+
|
107
|
+
opts[:endpoint] = "#{scheme}://#{opts[:host]}:#{port}/#{path}"
|
108
|
+
end
|
109
|
+
|
110
|
+
WINRM_FS_SPEC_VERSION = '~> 1.0'.freeze
|
111
|
+
|
112
|
+
# Builds the hash of options needed by the Connection object on
|
113
|
+
# construction.
|
114
|
+
#
|
115
|
+
# @param data [Hash] merged configuration and mutable state data
|
116
|
+
# @return [Hash] hash of connection options
|
117
|
+
# @api private
|
118
|
+
def connection_options(opts)
|
119
|
+
{
|
120
|
+
logger: logger,
|
121
|
+
transport: opts[:winrm_transport].to_sym,
|
122
|
+
disable_sspi: opts[:winrm_disable_sspi],
|
123
|
+
basic_auth_only: opts[:winrm_basic_auth_only],
|
124
|
+
hostname: opts[:host],
|
125
|
+
endpoint: opts[:endpoint],
|
126
|
+
user: opts[:user],
|
127
|
+
password: opts[:password],
|
128
|
+
rdp_port: opts[:rdp_port],
|
129
|
+
connection_retries: opts[:connection_retries],
|
130
|
+
connection_retry_sleep: opts[:connection_retry_sleep],
|
131
|
+
max_wait_until_ready: opts[:max_wait_until_ready],
|
132
|
+
no_ssl_peer_verification: opts[:self_signed],
|
133
|
+
realm: opts[:kerberos_realm],
|
134
|
+
service: opts[:kerberos_service],
|
135
|
+
ca_trust_file: opts[:ca_trust_file],
|
136
|
+
ssl_peer_fingerprint: opts[:ssl_peer_fingerprint],
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
# Creates a new WinRM Connection instance and save it for potential
|
141
|
+
# future reuse.
|
142
|
+
#
|
143
|
+
# @param options [Hash] conneciton options
|
144
|
+
# @return [WinRM::Connection] a WinRM Connection instance
|
145
|
+
# @api private
|
146
|
+
def create_new_connection(options, &block)
|
147
|
+
if @connection
|
148
|
+
logger.debug("[WinRM] shutting previous connection #{@connection}")
|
149
|
+
@connection.close
|
150
|
+
end
|
151
|
+
|
152
|
+
@connection_options = options
|
153
|
+
@connection = Connection.new(options, &block)
|
154
|
+
end
|
155
|
+
|
156
|
+
# (see Base#load_needed_dependencies!)
|
157
|
+
def load_needed_dependencies!
|
158
|
+
spec_version = WINRM_FS_SPEC_VERSION.dup
|
159
|
+
logger.debug('winrm-fs requested,' \
|
160
|
+
" loading WinRM::FS gem (#{spec_version})")
|
161
|
+
gem 'winrm-fs', spec_version
|
162
|
+
first_load = require 'winrm-fs'
|
163
|
+
load_winrm_transport!
|
164
|
+
|
165
|
+
if first_load
|
166
|
+
logger.debug('WinRM::FS library loaded')
|
167
|
+
else
|
168
|
+
logger.debug('WinRM::FS previously loaded')
|
169
|
+
end
|
170
|
+
rescue LoadError => e
|
171
|
+
logger.fatal(
|
172
|
+
"The `winrm-fs' gem is missing and must" \
|
173
|
+
' be installed or cannot be properly activated. Run' \
|
174
|
+
" `gem install winrm-fs --version '#{spec_version}'`" \
|
175
|
+
' or add the following to your Gemfile if you are using Bundler:' \
|
176
|
+
" `gem 'winrm-fs', '#{spec_version}'`.",
|
177
|
+
)
|
178
|
+
raise Train::UserError,
|
179
|
+
"Could not load or activate WinRM::FS (#{e.message})"
|
180
|
+
end
|
181
|
+
|
182
|
+
# Load WinRM::Transport code.
|
183
|
+
#
|
184
|
+
# @api private
|
185
|
+
def load_winrm_transport!
|
186
|
+
silence_warnings { require 'winrm-fs' }
|
187
|
+
end
|
188
|
+
|
189
|
+
# Return the last saved WinRM connection instance.
|
190
|
+
#
|
191
|
+
# @return [Winrm::Connection] a WinRM Connection instance
|
192
|
+
# @api private
|
193
|
+
def reuse_connection
|
194
|
+
logger.debug("[WinRM] reusing existing connection #{@connection}")
|
195
|
+
yield @connection if block_given?
|
196
|
+
@connection
|
197
|
+
end
|
198
|
+
|
199
|
+
def silence_warnings
|
200
|
+
old_verbose = $VERBOSE
|
201
|
+
$VERBOSE = nil
|
202
|
+
yield
|
203
|
+
ensure
|
204
|
+
$VERBOSE = old_verbose
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Salim Afiune (<salim@afiunemaya.com.mx>)
|
4
|
+
# Author:: Matt Wrock (<matt@mattwrock.com>)
|
5
|
+
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
6
|
+
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
7
|
+
# Author:: Christoph Hartmann (<chris@lollyrock.com>)
|
8
|
+
#
|
9
|
+
# Copyright (C) 2014, Salim Afiune
|
10
|
+
#
|
11
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
12
|
+
# you may not use this file except in compliance with the License.
|
13
|
+
# You may obtain a copy of the License at
|
14
|
+
#
|
15
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
16
|
+
#
|
17
|
+
# Unless required by applicable law or agreed to in writing, software
|
18
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
19
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
20
|
+
# See the License for the specific language governing permissions and
|
21
|
+
# limitations under the License.
|
22
|
+
|
23
|
+
class Train::Transports::WinRM
|
24
|
+
# A Connection instance can be generated and re-generated, given new
|
25
|
+
# connection details such as connection port, hostname, credentials, etc.
|
26
|
+
# This object is responsible for carrying out the actions on the remote
|
27
|
+
# host such as executing commands, transferring files, etc.
|
28
|
+
#
|
29
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
30
|
+
class Connection < BaseConnection # rubocop:disable Metrics/ClassLength
|
31
|
+
attr_reader :hostname
|
32
|
+
def initialize(options)
|
33
|
+
super(options)
|
34
|
+
@hostname = @options.delete(:hostname)
|
35
|
+
@rdp_port = @options.delete(:rdp_port)
|
36
|
+
@connection_retries = @options.delete(:connection_retries)
|
37
|
+
@connection_retry_sleep = @options.delete(:connection_retry_sleep)
|
38
|
+
@max_wait_until_ready = @options.delete(:max_wait_until_ready)
|
39
|
+
@operation_timeout = @options.delete(:operation_timeout)
|
40
|
+
end
|
41
|
+
|
42
|
+
# (see Base::Connection#close)
|
43
|
+
def close
|
44
|
+
return if @session.nil?
|
45
|
+
session.close
|
46
|
+
ensure
|
47
|
+
@session = nil
|
48
|
+
end
|
49
|
+
|
50
|
+
# (see Base::Connection#login_command)
|
51
|
+
def login_command
|
52
|
+
case RbConfig::CONFIG['host_os']
|
53
|
+
when /darwin/
|
54
|
+
login_command_for_mac
|
55
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
56
|
+
login_command_for_windows
|
57
|
+
when /linux/
|
58
|
+
login_command_for_linux
|
59
|
+
else
|
60
|
+
fail ActionFailed,
|
61
|
+
"Remote login not supported in #{self.class} " \
|
62
|
+
"from host OS '#{RbConfig::CONFIG['host_os']}'."
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# (see Base::Connection#upload)
|
67
|
+
def upload(locals, remote)
|
68
|
+
file_manager.upload(locals, remote)
|
69
|
+
end
|
70
|
+
|
71
|
+
def download(remotes, local)
|
72
|
+
Array(remotes).each do |remote|
|
73
|
+
file_manager.download(remote, local)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# (see Base::Connection#wait_until_ready)
|
78
|
+
def wait_until_ready
|
79
|
+
delay = 3
|
80
|
+
session(
|
81
|
+
retry_limit: @max_wait_until_ready / delay,
|
82
|
+
retry_delay: delay,
|
83
|
+
)
|
84
|
+
run_command_via_connection(PING_COMMAND.dup)
|
85
|
+
end
|
86
|
+
|
87
|
+
def uri
|
88
|
+
"winrm://#{options[:user]}@#{options[:endpoint]}:#{@rdp_port}"
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
PING_COMMAND = "Write-Host '[WinRM] Established\n'".freeze
|
94
|
+
|
95
|
+
def file_via_connection(path)
|
96
|
+
Train::File::Remote::Windows.new(self, path)
|
97
|
+
end
|
98
|
+
|
99
|
+
def run_command_via_connection(command, &data_handler)
|
100
|
+
return if command.nil?
|
101
|
+
logger.debug("[WinRM] #{self} (#{command})")
|
102
|
+
out = ''
|
103
|
+
|
104
|
+
response = session.run(command) do |stdout, _|
|
105
|
+
yield(stdout) if data_handler && stdout
|
106
|
+
out << stdout if stdout
|
107
|
+
end
|
108
|
+
|
109
|
+
CommandResult.new(out, response.stderr, response.exitcode)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Create a local RDP document and return it
|
113
|
+
#
|
114
|
+
# @param opts [Hash] configuration options
|
115
|
+
# @option opts [true,false] :mac whether or not the document is for a
|
116
|
+
# Mac system
|
117
|
+
# @api private
|
118
|
+
def rdp_doc(opts = {})
|
119
|
+
host = URI.parse(options[:endpoint]).host
|
120
|
+
content = [
|
121
|
+
"full address:s:#{host}:#{@rdp_port}",
|
122
|
+
'prompt for credentials:i:1',
|
123
|
+
"username:s:#{options[:user]}",
|
124
|
+
].join("\n")
|
125
|
+
|
126
|
+
content.prepend("drivestoredirect:s:*\n") if opts[:mac]
|
127
|
+
|
128
|
+
content
|
129
|
+
end
|
130
|
+
|
131
|
+
# @return [Winrm::FileManager] a file transporter
|
132
|
+
# @api private
|
133
|
+
def file_manager
|
134
|
+
@file_manager ||= begin
|
135
|
+
# Ensure @service is available:
|
136
|
+
wait_until_ready
|
137
|
+
WinRM::FS::FileManager.new(@service)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Builds a `LoginCommand` for use by Linux-based platforms.
|
142
|
+
#
|
143
|
+
# TODO: determine whether or not `desktop` exists
|
144
|
+
#
|
145
|
+
# @return [LoginCommand] a login command
|
146
|
+
# @api private
|
147
|
+
def login_command_for_linux
|
148
|
+
args = %W( -u #{options[:user]} )
|
149
|
+
args += %W( -p #{options[:pass]} ) if options.key?(:pass)
|
150
|
+
args += %W( #{URI.parse(options[:endpoint]).host}:#{@rdp_port} )
|
151
|
+
LoginCommand.new('rdesktop', args)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Builds a `LoginCommand` for use by Mac-based platforms.
|
155
|
+
#
|
156
|
+
# @return [LoginCommand] a login command
|
157
|
+
# @api private
|
158
|
+
def login_command_for_mac
|
159
|
+
LoginCommand.new('open', rdp_doc(mac: true))
|
160
|
+
end
|
161
|
+
|
162
|
+
# Builds a `LoginCommand` for use by Windows-based platforms.
|
163
|
+
#
|
164
|
+
# @return [LoginCommand] a login command
|
165
|
+
# @api private
|
166
|
+
def login_command_for_windows
|
167
|
+
LoginCommand.new('mstsc', rdp_doc)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Establishes a remote shell session, or establishes one when invoked
|
171
|
+
# the first time.
|
172
|
+
#
|
173
|
+
# @param retry_options [Hash] retry options for the initial connection
|
174
|
+
# @return [Winrm::CommandExecutor] the command executor session
|
175
|
+
# @api private
|
176
|
+
def session(retry_options = {})
|
177
|
+
@session ||= begin
|
178
|
+
opts = {
|
179
|
+
retry_limit: @connection_retries.to_i,
|
180
|
+
retry_delay: @connection_retry_sleep.to_i,
|
181
|
+
}.merge(retry_options)
|
182
|
+
|
183
|
+
opts[:operation_timeout] = @operation_timeout unless @operation_timeout.nil?
|
184
|
+
@service = ::WinRM::Connection.new(options.merge(opts))
|
185
|
+
@service.logger = logger
|
186
|
+
@service.shell(:powershell)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# String representation of object, reporting its connection details and
|
191
|
+
# configuration.
|
192
|
+
#
|
193
|
+
# @api private
|
194
|
+
def to_s
|
195
|
+
options_to_print = @options.clone
|
196
|
+
options_to_print[:password] = '<hidden>' if options_to_print.key?(:password)
|
197
|
+
"#{@username}@#{@hostname}<#{options_to_print.inspect}>"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
data/lib/train/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: train-core
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dominik Richter
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-04-
|
11
|
+
date: 2019-04-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.8'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.8'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3.0'
|
13
33
|
- !ruby/object:Gem::Dependency
|
14
34
|
name: mixlib-shellout
|
15
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -31,12 +51,32 @@ dependencies:
|
|
31
51
|
- !ruby/object:Gem::Version
|
32
52
|
version: '4.0'
|
33
53
|
- !ruby/object:Gem::Dependency
|
34
|
-
name:
|
54
|
+
name: net-ssh
|
35
55
|
requirement: !ruby/object:Gem::Requirement
|
36
56
|
requirements:
|
37
57
|
- - ">="
|
38
58
|
- !ruby/object:Gem::Version
|
39
|
-
version: '
|
59
|
+
version: '2.9'
|
60
|
+
- - "<"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '6.0'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '2.9'
|
70
|
+
- - "<"
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '6.0'
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: net-scp
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '1.2'
|
40
80
|
- - "<"
|
41
81
|
- !ruby/object:Gem::Version
|
42
82
|
version: '3.0'
|
@@ -46,11 +86,67 @@ dependencies:
|
|
46
86
|
requirements:
|
47
87
|
- - ">="
|
48
88
|
- !ruby/object:Gem::Version
|
49
|
-
version: '1.
|
89
|
+
version: '1.2'
|
50
90
|
- - "<"
|
51
91
|
- !ruby/object:Gem::Version
|
52
92
|
version: '3.0'
|
53
|
-
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: ed25519
|
95
|
+
requirement: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - "~>"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '1.2'
|
100
|
+
type: :runtime
|
101
|
+
prerelease: false
|
102
|
+
version_requirements: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - "~>"
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '1.2'
|
107
|
+
- !ruby/object:Gem::Dependency
|
108
|
+
name: bcrypt_pbkdf
|
109
|
+
requirement: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - "~>"
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '1.0'
|
114
|
+
type: :runtime
|
115
|
+
prerelease: false
|
116
|
+
version_requirements: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - "~>"
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '1.0'
|
121
|
+
- !ruby/object:Gem::Dependency
|
122
|
+
name: winrm
|
123
|
+
requirement: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - "~>"
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '2.0'
|
128
|
+
type: :runtime
|
129
|
+
prerelease: false
|
130
|
+
version_requirements: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - "~>"
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '2.0'
|
135
|
+
- !ruby/object:Gem::Dependency
|
136
|
+
name: winrm-fs
|
137
|
+
requirement: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - "~>"
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '1.0'
|
142
|
+
type: :runtime
|
143
|
+
prerelease: false
|
144
|
+
version_requirements: !ruby/object:Gem::Requirement
|
145
|
+
requirements:
|
146
|
+
- - "~>"
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: '1.0'
|
149
|
+
description: A minimal Train with a backends for ssh and winrm.
|
54
150
|
email:
|
55
151
|
- drichter@chef.io
|
56
152
|
executables: []
|
@@ -93,6 +189,10 @@ files:
|
|
93
189
|
- lib/train/plugins/transport.rb
|
94
190
|
- lib/train/transports/local.rb
|
95
191
|
- lib/train/transports/mock.rb
|
192
|
+
- lib/train/transports/ssh.rb
|
193
|
+
- lib/train/transports/ssh_connection.rb
|
194
|
+
- lib/train/transports/winrm.rb
|
195
|
+
- lib/train/transports/winrm_connection.rb
|
96
196
|
- lib/train/version.rb
|
97
197
|
homepage: https://github.com/inspec/train/
|
98
198
|
licenses:
|