train 0.12.1
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 +7 -0
- data/.rubocop.yml +71 -0
- data/CHANGELOG.md +308 -0
- data/Gemfile +30 -0
- data/LICENSE +201 -0
- data/README.md +156 -0
- data/Rakefile +148 -0
- data/lib/train.rb +117 -0
- data/lib/train/errors.rb +23 -0
- data/lib/train/extras.rb +17 -0
- data/lib/train/extras/command_wrapper.rb +148 -0
- data/lib/train/extras/file_aix.rb +20 -0
- data/lib/train/extras/file_common.rb +161 -0
- data/lib/train/extras/file_linux.rb +16 -0
- data/lib/train/extras/file_unix.rb +79 -0
- data/lib/train/extras/file_windows.rb +91 -0
- data/lib/train/extras/linux_lsb.rb +60 -0
- data/lib/train/extras/os_common.rb +136 -0
- data/lib/train/extras/os_detect_darwin.rb +32 -0
- data/lib/train/extras/os_detect_linux.rb +148 -0
- data/lib/train/extras/os_detect_unix.rb +99 -0
- data/lib/train/extras/os_detect_windows.rb +57 -0
- data/lib/train/extras/stat.rb +133 -0
- data/lib/train/options.rb +80 -0
- data/lib/train/plugins.rb +40 -0
- data/lib/train/plugins/base_connection.rb +86 -0
- data/lib/train/plugins/transport.rb +49 -0
- data/lib/train/transports/docker.rb +103 -0
- data/lib/train/transports/local.rb +52 -0
- data/lib/train/transports/local_file.rb +90 -0
- data/lib/train/transports/local_os.rb +51 -0
- data/lib/train/transports/mock.rb +147 -0
- data/lib/train/transports/ssh.rb +163 -0
- data/lib/train/transports/ssh_connection.rb +225 -0
- data/lib/train/transports/winrm.rb +184 -0
- data/lib/train/transports/winrm_connection.rb +194 -0
- data/lib/train/version.rb +7 -0
- data/test/integration/.kitchen.yml +43 -0
- data/test/integration/Berksfile +3 -0
- data/test/integration/bootstrap.sh +17 -0
- data/test/integration/chefignore +1 -0
- data/test/integration/cookbooks/test/metadata.rb +1 -0
- data/test/integration/cookbooks/test/recipes/default.rb +100 -0
- data/test/integration/cookbooks/test/recipes/prep_files.rb +47 -0
- data/test/integration/docker_run.rb +153 -0
- data/test/integration/docker_test.rb +24 -0
- data/test/integration/docker_test_container.rb +24 -0
- data/test/integration/helper.rb +61 -0
- data/test/integration/sudo/customcommand.rb +15 -0
- data/test/integration/sudo/nopasswd.rb +16 -0
- data/test/integration/sudo/passwd.rb +21 -0
- data/test/integration/sudo/reqtty.rb +17 -0
- data/test/integration/sudo/run_as.rb +12 -0
- data/test/integration/test-travis-1.yaml +13 -0
- data/test/integration/test-travis-2.yaml +13 -0
- data/test/integration/test_local.rb +19 -0
- data/test/integration/test_ssh.rb +39 -0
- data/test/integration/tests/path_block_device_test.rb +74 -0
- data/test/integration/tests/path_character_device_test.rb +74 -0
- data/test/integration/tests/path_file_test.rb +79 -0
- data/test/integration/tests/path_folder_test.rb +90 -0
- data/test/integration/tests/path_missing_test.rb +77 -0
- data/test/integration/tests/path_pipe_test.rb +78 -0
- data/test/integration/tests/path_symlink_test.rb +95 -0
- data/test/integration/tests/run_command_test.rb +28 -0
- data/test/unit/extras/command_wrapper_test.rb +78 -0
- data/test/unit/extras/file_common_test.rb +180 -0
- data/test/unit/extras/linux_file_test.rb +167 -0
- data/test/unit/extras/os_common_test.rb +269 -0
- data/test/unit/extras/os_detect_linux_test.rb +189 -0
- data/test/unit/extras/os_detect_windows_test.rb +99 -0
- data/test/unit/extras/stat_test.rb +148 -0
- data/test/unit/extras/windows_file_test.rb +44 -0
- data/test/unit/helper.rb +7 -0
- data/test/unit/plugins/connection_test.rb +44 -0
- data/test/unit/plugins/transport_test.rb +111 -0
- data/test/unit/plugins_test.rb +22 -0
- data/test/unit/train_test.rb +156 -0
- data/test/unit/transports/local_file_test.rb +184 -0
- data/test/unit/transports/local_test.rb +87 -0
- data/test/unit/transports/mock_test.rb +87 -0
- data/test/unit/transports/ssh_test.rb +109 -0
- data/test/unit/version_test.rb +8 -0
- data/test/windows/local_test.rb +46 -0
- data/test/windows/winrm_test.rb +52 -0
- data/train.gemspec +38 -0
- metadata +295 -0
@@ -0,0 +1,225 @@
|
|
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
|
+
def initialize(options)
|
34
|
+
super(options)
|
35
|
+
@username = @options.delete(:username)
|
36
|
+
@hostname = @options.delete(:hostname)
|
37
|
+
@port = @options[:port] # don't delete from options
|
38
|
+
@connection_retries = @options.delete(:connection_retries)
|
39
|
+
@connection_retry_sleep = @options.delete(:connection_retry_sleep)
|
40
|
+
@max_wait_until_ready = @options.delete(:max_wait_until_ready)
|
41
|
+
@files = {}
|
42
|
+
@session = nil
|
43
|
+
@transport_options = @options.delete(:transport_options)
|
44
|
+
@cmd_wrapper = nil
|
45
|
+
@cmd_wrapper = CommandWrapper.load(self, @transport_options)
|
46
|
+
end
|
47
|
+
|
48
|
+
# (see Base::Connection#close)
|
49
|
+
def close
|
50
|
+
return if @session.nil?
|
51
|
+
logger.debug("[SSH] closing connection to #{self}")
|
52
|
+
session.close
|
53
|
+
ensure
|
54
|
+
@session = nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def os
|
58
|
+
@os ||= OS.new(self)
|
59
|
+
end
|
60
|
+
|
61
|
+
def file(path)
|
62
|
+
@files[path] ||= \
|
63
|
+
if os.aix?
|
64
|
+
AixFile.new(self, path)
|
65
|
+
elsif os.solaris?
|
66
|
+
UnixFile.new(self, path)
|
67
|
+
else
|
68
|
+
LinuxFile.new(self, path)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# (see Base::Connection#run_command)
|
73
|
+
def run_command(cmd)
|
74
|
+
stdout = stderr = ''
|
75
|
+
exit_status = nil
|
76
|
+
cmd.force_encoding('binary') if cmd.respond_to?(:force_encoding)
|
77
|
+
logger.debug("[SSH] #{self} (#{cmd})")
|
78
|
+
|
79
|
+
session.open_channel do |channel|
|
80
|
+
# wrap commands if that is configured
|
81
|
+
cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
|
82
|
+
|
83
|
+
channel.exec(cmd) do |_, success|
|
84
|
+
abort 'Couldn\'t execute command on SSH.' unless success
|
85
|
+
|
86
|
+
channel.on_data do |_, data|
|
87
|
+
stdout += data
|
88
|
+
end
|
89
|
+
|
90
|
+
channel.on_extended_data do |_, _type, data|
|
91
|
+
stderr += data
|
92
|
+
end
|
93
|
+
|
94
|
+
channel.on_request('exit-status') do |_, data|
|
95
|
+
exit_status = data.read_long
|
96
|
+
end
|
97
|
+
|
98
|
+
channel.on_request('exit-signal') do |_, data|
|
99
|
+
exit_status = data.read_long
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
@session.loop
|
104
|
+
|
105
|
+
CommandResult.new(stdout, stderr, exit_status)
|
106
|
+
rescue Net::SSH::Exception => ex
|
107
|
+
raise Train::Transports::SSHFailed, "SSH command failed (#{ex.message})"
|
108
|
+
end
|
109
|
+
|
110
|
+
# (see Base::Connection#login_command)
|
111
|
+
def login_command
|
112
|
+
level = logger.debug? ? 'VERBOSE' : 'ERROR'
|
113
|
+
fwd_agent = options[:forward_agent] ? 'yes' : 'no'
|
114
|
+
|
115
|
+
args = %w{ -o UserKnownHostsFile=/dev/null }
|
116
|
+
args += %w{ -o StrictHostKeyChecking=no }
|
117
|
+
args += %w{ -o IdentitiesOnly=yes } if options[:keys]
|
118
|
+
args += %W( -o LogLevel=#{level} )
|
119
|
+
args += %W( -o ForwardAgent=#{fwd_agent} ) if options.key?(:forward_agent)
|
120
|
+
Array(options[:keys]).each do |ssh_key|
|
121
|
+
args += %W( -i #{ssh_key} )
|
122
|
+
end
|
123
|
+
args += %W( -p #{@port} )
|
124
|
+
args += %W( #{@username}@#{@hostname} )
|
125
|
+
|
126
|
+
LoginCommand.new('ssh', args)
|
127
|
+
end
|
128
|
+
|
129
|
+
# (see Base::Connection#upload)
|
130
|
+
def upload(locals, remote)
|
131
|
+
Array(locals).each do |local|
|
132
|
+
opts = File.directory?(local) ? { recursive: true } : {}
|
133
|
+
|
134
|
+
session.scp.upload!(local, remote, opts) do |_ch, name, sent, total|
|
135
|
+
logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total
|
136
|
+
end
|
137
|
+
end
|
138
|
+
rescue Net::SSH::Exception => ex
|
139
|
+
raise Train::Transports::SSHFailed, "SCP upload failed (#{ex.message})"
|
140
|
+
end
|
141
|
+
|
142
|
+
# (see Base::Connection#wait_until_ready)
|
143
|
+
def wait_until_ready
|
144
|
+
delay = 3
|
145
|
+
session(
|
146
|
+
retries: @max_wait_until_ready / delay,
|
147
|
+
delay: delay,
|
148
|
+
message: "Waiting for SSH service on #{@hostname}:#{@port}, " \
|
149
|
+
"retrying in #{delay} seconds",
|
150
|
+
)
|
151
|
+
execute(PING_COMMAND.dup)
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
PING_COMMAND = "echo '[SSH] Established'".freeze
|
157
|
+
|
158
|
+
RESCUE_EXCEPTIONS_ON_ESTABLISH = [
|
159
|
+
Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
|
160
|
+
Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
|
161
|
+
Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout,
|
162
|
+
Timeout::Error
|
163
|
+
].freeze
|
164
|
+
|
165
|
+
# Establish an SSH session on the remote host.
|
166
|
+
#
|
167
|
+
# @param opts [Hash] retry options
|
168
|
+
# @option opts [Integer] :retries the number of times to retry before
|
169
|
+
# failing
|
170
|
+
# @option opts [Float] :delay the number of seconds to wait until
|
171
|
+
# attempting a retry
|
172
|
+
# @option opts [String] :message an optional message to be logged on
|
173
|
+
# debug (overriding the default) when a rescuable exception is raised
|
174
|
+
# @return [Net::SSH::Connection::Session] the SSH connection session
|
175
|
+
# @api private
|
176
|
+
def establish_connection(opts)
|
177
|
+
logger.debug("[SSH] opening connection to #{self}")
|
178
|
+
Net::SSH.start(@hostname, @username, @options)
|
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
|
+
# Returns a connection session, or establishes one when invoked the
|
199
|
+
# first time.
|
200
|
+
#
|
201
|
+
# @param retry_options [Hash] retry options for the initial connection
|
202
|
+
# @return [Net::SSH::Connection::Session] the SSH connection session
|
203
|
+
# @api private
|
204
|
+
def session(retry_options = {})
|
205
|
+
@session ||= establish_connection({
|
206
|
+
retries: @connection_retries.to_i,
|
207
|
+
delay: @connection_retry_sleep.to_i,
|
208
|
+
}.merge(retry_options))
|
209
|
+
end
|
210
|
+
|
211
|
+
# String representation of object, reporting its connection details and
|
212
|
+
# configuration.
|
213
|
+
#
|
214
|
+
# @api private
|
215
|
+
def to_s
|
216
|
+
"#{@username}@#{@hostname}<#{@options.inspect}>"
|
217
|
+
end
|
218
|
+
|
219
|
+
class OS < OSCommon
|
220
|
+
def initialize(backend)
|
221
|
+
super(backend, { family: 'unix' })
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,184 @@
|
|
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)
|
39
|
+
name 'winrm'
|
40
|
+
|
41
|
+
autoload :Connection, 'train/transports/winrm_connection'
|
42
|
+
|
43
|
+
# common target configuration
|
44
|
+
option :host, required: true
|
45
|
+
option :port
|
46
|
+
option :user, default: 'administrator', required: true
|
47
|
+
option :password, nil
|
48
|
+
option :path, default: '/wsman'
|
49
|
+
option :ssl, default: false
|
50
|
+
option :self_signed, default: false
|
51
|
+
|
52
|
+
# additional winrm options
|
53
|
+
option :rdp_port, default: 3389
|
54
|
+
option :connection_retries, default: 5
|
55
|
+
option :connection_retry_sleep, default: 1
|
56
|
+
option :max_wait_until_ready, default: 600
|
57
|
+
|
58
|
+
def initialize(opts)
|
59
|
+
super(opts)
|
60
|
+
load_needed_dependencies!
|
61
|
+
end
|
62
|
+
|
63
|
+
# (see Base#connection)
|
64
|
+
def connection(state = nil, &block)
|
65
|
+
opts = merge_options(options, state || {})
|
66
|
+
validate_options(opts)
|
67
|
+
conn_opts = connection_options(opts)
|
68
|
+
|
69
|
+
if @connection && @connection_options == conn_opts
|
70
|
+
reuse_connection(&block)
|
71
|
+
else
|
72
|
+
create_new_connection(conn_opts, &block)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def validate_options(opts)
|
79
|
+
super(opts)
|
80
|
+
|
81
|
+
# set scheme and port based on ssl activation
|
82
|
+
scheme = opts[:ssl] ? 'https' : 'http'
|
83
|
+
port = opts[:port]
|
84
|
+
port = (opts[:ssl] ? 5986 : 5985) if port.nil?
|
85
|
+
|
86
|
+
# remove leading '/'
|
87
|
+
path = (opts[:path] || '').sub(%r{^/+}, '')
|
88
|
+
|
89
|
+
opts[:endpoint] = "#{scheme}://#{opts[:host]}:#{port}/#{path}"
|
90
|
+
end
|
91
|
+
|
92
|
+
WINRM_FS_SPEC_VERSION = '~> 0.3'.freeze
|
93
|
+
|
94
|
+
# Builds the hash of options needed by the Connection object on
|
95
|
+
# construction.
|
96
|
+
#
|
97
|
+
# @param data [Hash] merged configuration and mutable state data
|
98
|
+
# @return [Hash] hash of connection options
|
99
|
+
# @api private
|
100
|
+
def connection_options(opts)
|
101
|
+
{
|
102
|
+
logger: logger,
|
103
|
+
winrm_transport: :negotiate,
|
104
|
+
disable_sspi: false,
|
105
|
+
basic_auth_only: false,
|
106
|
+
endpoint: opts[:endpoint],
|
107
|
+
user: opts[:user],
|
108
|
+
pass: opts[:password],
|
109
|
+
rdp_port: opts[:rdp_port],
|
110
|
+
connection_retries: opts[:connection_retries],
|
111
|
+
connection_retry_sleep: opts[:connection_retry_sleep],
|
112
|
+
max_wait_until_ready: opts[:max_wait_until_ready],
|
113
|
+
no_ssl_peer_verification: opts[:self_signed],
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
# Creates a new WinRM Connection instance and save it for potential
|
118
|
+
# future reuse.
|
119
|
+
#
|
120
|
+
# @param options [Hash] conneciton options
|
121
|
+
# @return [WinRM::Connection] a WinRM Connection instance
|
122
|
+
# @api private
|
123
|
+
def create_new_connection(options, &block)
|
124
|
+
if @connection
|
125
|
+
logger.debug("[WinRM] shutting previous connection #{@connection}")
|
126
|
+
@connection.close
|
127
|
+
end
|
128
|
+
|
129
|
+
@connection_options = options
|
130
|
+
@connection = Connection.new(options, &block)
|
131
|
+
end
|
132
|
+
|
133
|
+
# (see Base#load_needed_dependencies!)
|
134
|
+
def load_needed_dependencies!
|
135
|
+
spec_version = WINRM_FS_SPEC_VERSION.dup
|
136
|
+
logger.debug('winrm-fs requested,' \
|
137
|
+
" loading WinRM::FS gem (#{spec_version})")
|
138
|
+
gem 'winrm-fs', spec_version
|
139
|
+
first_load = require 'winrm-fs'
|
140
|
+
load_winrm_transport!
|
141
|
+
|
142
|
+
if first_load
|
143
|
+
logger.debug('WinRM::FS library loaded')
|
144
|
+
else
|
145
|
+
logger.debug('WinRM::FS previously loaded')
|
146
|
+
end
|
147
|
+
rescue LoadError => e
|
148
|
+
logger.fatal(
|
149
|
+
"The `winrm-fs' gem is missing and must" \
|
150
|
+
' be installed or cannot be properly activated. Run' \
|
151
|
+
" `gem install winrm-fs --version '#{spec_version}'`" \
|
152
|
+
' or add the following to your Gemfile if you are using Bundler:' \
|
153
|
+
" `gem 'winrm-fs', '#{spec_version}'`.",
|
154
|
+
)
|
155
|
+
raise Train::UserError,
|
156
|
+
"Could not load or activate WinRM::FS (#{e.message})"
|
157
|
+
end
|
158
|
+
|
159
|
+
# Load WinRM::Transport code.
|
160
|
+
#
|
161
|
+
# @api private
|
162
|
+
def load_winrm_transport!
|
163
|
+
silence_warnings { require 'winrm-fs' }
|
164
|
+
end
|
165
|
+
|
166
|
+
# Return the last saved WinRM connection instance.
|
167
|
+
#
|
168
|
+
# @return [Winrm::Connection] a WinRM Connection instance
|
169
|
+
# @api private
|
170
|
+
def reuse_connection
|
171
|
+
logger.debug("[WinRM] reusing existing connection #{@connection}")
|
172
|
+
yield @connection if block_given?
|
173
|
+
@connection
|
174
|
+
end
|
175
|
+
|
176
|
+
def silence_warnings
|
177
|
+
old_verbose = $VERBOSE
|
178
|
+
$VERBOSE = nil
|
179
|
+
yield
|
180
|
+
ensure
|
181
|
+
$VERBOSE = old_verbose
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,194 @@
|
|
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
|
31
|
+
def initialize(options)
|
32
|
+
super(options)
|
33
|
+
@endpoint = @options.delete(:endpoint)
|
34
|
+
@rdp_port = @options.delete(:rdp_port)
|
35
|
+
@winrm_transport = @options.delete(:winrm_transport)
|
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
|
+
@files = {}
|
40
|
+
end
|
41
|
+
|
42
|
+
# (see Base::Connection#close)
|
43
|
+
def close
|
44
|
+
return if @session.nil?
|
45
|
+
|
46
|
+
session.close
|
47
|
+
ensure
|
48
|
+
@session = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def os
|
52
|
+
@os ||= OS.new(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
def file(path)
|
56
|
+
@files[path] ||= WindowsFile.new(self, path)
|
57
|
+
end
|
58
|
+
|
59
|
+
def run_command(command)
|
60
|
+
return if command.nil?
|
61
|
+
logger.debug("[WinRM] #{self} (#{command})")
|
62
|
+
out = ''
|
63
|
+
|
64
|
+
response = session.run_powershell_script(command) do |stdout, _|
|
65
|
+
out << stdout if stdout
|
66
|
+
end
|
67
|
+
|
68
|
+
CommandResult.new(out, response.stderr, response[:exitcode])
|
69
|
+
end
|
70
|
+
|
71
|
+
# (see Base::Connection#login_command)
|
72
|
+
def login_command
|
73
|
+
case RbConfig::CONFIG['host_os']
|
74
|
+
when /darwin/
|
75
|
+
login_command_for_mac
|
76
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
77
|
+
login_command_for_windows
|
78
|
+
when /linux/
|
79
|
+
login_command_for_linux
|
80
|
+
else
|
81
|
+
fail ActionFailed,
|
82
|
+
"Remote login not supported in #{self.class} " \
|
83
|
+
"from host OS '#{RbConfig::CONFIG['host_os']}'."
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# (see Base::Connection#upload)
|
88
|
+
def upload(locals, remote)
|
89
|
+
file_transporter.upload(locals, remote)
|
90
|
+
end
|
91
|
+
|
92
|
+
# (see Base::Connection#wait_until_ready)
|
93
|
+
def wait_until_ready
|
94
|
+
delay = 3
|
95
|
+
session(
|
96
|
+
retry_limit: @max_wait_until_ready / delay,
|
97
|
+
retry_delay: delay,
|
98
|
+
)
|
99
|
+
execute(PING_COMMAND.dup)
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
PING_COMMAND = "Write-Host '[WinRM] Established\n'".freeze
|
105
|
+
|
106
|
+
# Create a local RDP document and return it
|
107
|
+
#
|
108
|
+
# @param opts [Hash] configuration options
|
109
|
+
# @option opts [true,false] :mac whether or not the document is for a
|
110
|
+
# Mac system
|
111
|
+
# @api private
|
112
|
+
def rdp_doc(opts = {})
|
113
|
+
host = URI.parse(@endpoint).host
|
114
|
+
content = [
|
115
|
+
"full address:s:#{host}:#{@rdp_port}",
|
116
|
+
'prompt for credentials:i:1',
|
117
|
+
"username:s:#{options[:user]}",
|
118
|
+
].join("\n")
|
119
|
+
|
120
|
+
content.prepend("drivestoredirect:s:*\n") if opts[:mac]
|
121
|
+
|
122
|
+
content
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return [Winrm::FileTransporter] a file transporter
|
126
|
+
# @api private
|
127
|
+
def file_transporter
|
128
|
+
@file_transporter ||= WinRM::FS::Core::FileTransporter.new(session)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Builds a `LoginCommand` for use by Linux-based platforms.
|
132
|
+
#
|
133
|
+
# TODO: determine whether or not `desktop` exists
|
134
|
+
#
|
135
|
+
# @return [LoginCommand] a login command
|
136
|
+
# @api private
|
137
|
+
def login_command_for_linux
|
138
|
+
args = %W( -u #{options[:user]} )
|
139
|
+
args += %W( -p #{options[:pass]} ) if options.key?(:pass)
|
140
|
+
args += %W( #{URI.parse(@endpoint).host}:#{@rdp_port} )
|
141
|
+
LoginCommand.new('rdesktop', args)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Builds a `LoginCommand` for use by Mac-based platforms.
|
145
|
+
#
|
146
|
+
# @return [LoginCommand] a login command
|
147
|
+
# @api private
|
148
|
+
def login_command_for_mac
|
149
|
+
LoginCommand.new('open', rdp_doc(mac: true))
|
150
|
+
end
|
151
|
+
|
152
|
+
# Builds a `LoginCommand` for use by Windows-based platforms.
|
153
|
+
#
|
154
|
+
# @return [LoginCommand] a login command
|
155
|
+
# @api private
|
156
|
+
def login_command_for_windows
|
157
|
+
LoginCommand.new('mstsc', rdp_doc)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Establishes a remote shell session, or establishes one when invoked
|
161
|
+
# the first time.
|
162
|
+
#
|
163
|
+
# @param retry_options [Hash] retry options for the initial connection
|
164
|
+
# @return [Winrm::CommandExecutor] the command executor session
|
165
|
+
# @api private
|
166
|
+
def session(retry_options = {})
|
167
|
+
@session ||= begin
|
168
|
+
opts = {
|
169
|
+
retry_limit: @connection_retries.to_i,
|
170
|
+
retry_delay: @connection_retry_sleep.to_i,
|
171
|
+
}.merge(retry_options)
|
172
|
+
|
173
|
+
service_args = [@endpoint, @winrm_transport, options.merge(opts)]
|
174
|
+
@service = ::WinRM::WinRMWebService.new(*service_args)
|
175
|
+
@service.logger = logger
|
176
|
+
@service.create_executor
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# String representation of object, reporting its connection details and
|
181
|
+
# configuration.
|
182
|
+
#
|
183
|
+
# @api private
|
184
|
+
def to_s
|
185
|
+
"#{@winrm_transport}::#{@endpoint}<#{options.inspect}>"
|
186
|
+
end
|
187
|
+
|
188
|
+
class OS < OSCommon
|
189
|
+
def initialize(backend)
|
190
|
+
super(backend, { family: 'windows' })
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|