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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +71 -0
  3. data/CHANGELOG.md +308 -0
  4. data/Gemfile +30 -0
  5. data/LICENSE +201 -0
  6. data/README.md +156 -0
  7. data/Rakefile +148 -0
  8. data/lib/train.rb +117 -0
  9. data/lib/train/errors.rb +23 -0
  10. data/lib/train/extras.rb +17 -0
  11. data/lib/train/extras/command_wrapper.rb +148 -0
  12. data/lib/train/extras/file_aix.rb +20 -0
  13. data/lib/train/extras/file_common.rb +161 -0
  14. data/lib/train/extras/file_linux.rb +16 -0
  15. data/lib/train/extras/file_unix.rb +79 -0
  16. data/lib/train/extras/file_windows.rb +91 -0
  17. data/lib/train/extras/linux_lsb.rb +60 -0
  18. data/lib/train/extras/os_common.rb +136 -0
  19. data/lib/train/extras/os_detect_darwin.rb +32 -0
  20. data/lib/train/extras/os_detect_linux.rb +148 -0
  21. data/lib/train/extras/os_detect_unix.rb +99 -0
  22. data/lib/train/extras/os_detect_windows.rb +57 -0
  23. data/lib/train/extras/stat.rb +133 -0
  24. data/lib/train/options.rb +80 -0
  25. data/lib/train/plugins.rb +40 -0
  26. data/lib/train/plugins/base_connection.rb +86 -0
  27. data/lib/train/plugins/transport.rb +49 -0
  28. data/lib/train/transports/docker.rb +103 -0
  29. data/lib/train/transports/local.rb +52 -0
  30. data/lib/train/transports/local_file.rb +90 -0
  31. data/lib/train/transports/local_os.rb +51 -0
  32. data/lib/train/transports/mock.rb +147 -0
  33. data/lib/train/transports/ssh.rb +163 -0
  34. data/lib/train/transports/ssh_connection.rb +225 -0
  35. data/lib/train/transports/winrm.rb +184 -0
  36. data/lib/train/transports/winrm_connection.rb +194 -0
  37. data/lib/train/version.rb +7 -0
  38. data/test/integration/.kitchen.yml +43 -0
  39. data/test/integration/Berksfile +3 -0
  40. data/test/integration/bootstrap.sh +17 -0
  41. data/test/integration/chefignore +1 -0
  42. data/test/integration/cookbooks/test/metadata.rb +1 -0
  43. data/test/integration/cookbooks/test/recipes/default.rb +100 -0
  44. data/test/integration/cookbooks/test/recipes/prep_files.rb +47 -0
  45. data/test/integration/docker_run.rb +153 -0
  46. data/test/integration/docker_test.rb +24 -0
  47. data/test/integration/docker_test_container.rb +24 -0
  48. data/test/integration/helper.rb +61 -0
  49. data/test/integration/sudo/customcommand.rb +15 -0
  50. data/test/integration/sudo/nopasswd.rb +16 -0
  51. data/test/integration/sudo/passwd.rb +21 -0
  52. data/test/integration/sudo/reqtty.rb +17 -0
  53. data/test/integration/sudo/run_as.rb +12 -0
  54. data/test/integration/test-travis-1.yaml +13 -0
  55. data/test/integration/test-travis-2.yaml +13 -0
  56. data/test/integration/test_local.rb +19 -0
  57. data/test/integration/test_ssh.rb +39 -0
  58. data/test/integration/tests/path_block_device_test.rb +74 -0
  59. data/test/integration/tests/path_character_device_test.rb +74 -0
  60. data/test/integration/tests/path_file_test.rb +79 -0
  61. data/test/integration/tests/path_folder_test.rb +90 -0
  62. data/test/integration/tests/path_missing_test.rb +77 -0
  63. data/test/integration/tests/path_pipe_test.rb +78 -0
  64. data/test/integration/tests/path_symlink_test.rb +95 -0
  65. data/test/integration/tests/run_command_test.rb +28 -0
  66. data/test/unit/extras/command_wrapper_test.rb +78 -0
  67. data/test/unit/extras/file_common_test.rb +180 -0
  68. data/test/unit/extras/linux_file_test.rb +167 -0
  69. data/test/unit/extras/os_common_test.rb +269 -0
  70. data/test/unit/extras/os_detect_linux_test.rb +189 -0
  71. data/test/unit/extras/os_detect_windows_test.rb +99 -0
  72. data/test/unit/extras/stat_test.rb +148 -0
  73. data/test/unit/extras/windows_file_test.rb +44 -0
  74. data/test/unit/helper.rb +7 -0
  75. data/test/unit/plugins/connection_test.rb +44 -0
  76. data/test/unit/plugins/transport_test.rb +111 -0
  77. data/test/unit/plugins_test.rb +22 -0
  78. data/test/unit/train_test.rb +156 -0
  79. data/test/unit/transports/local_file_test.rb +184 -0
  80. data/test/unit/transports/local_test.rb +87 -0
  81. data/test/unit/transports/mock_test.rb +87 -0
  82. data/test/unit/transports/ssh_test.rb +109 -0
  83. data/test/unit/version_test.rb +8 -0
  84. data/test/windows/local_test.rb +46 -0
  85. data/test/windows/winrm_test.rb +52 -0
  86. data/train.gemspec +38 -0
  87. 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