train-core 2.0.5 → 2.0.8
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
- 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:
|