train 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
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