r-train 0.9.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 (77) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +45 -0
  4. data/.travis.yml +12 -0
  5. data/Gemfile +22 -0
  6. data/LICENSE +201 -0
  7. data/README.md +137 -0
  8. data/Rakefile +39 -0
  9. data/lib/train.rb +100 -0
  10. data/lib/train/errors.rb +23 -0
  11. data/lib/train/extras.rb +15 -0
  12. data/lib/train/extras/command_wrapper.rb +105 -0
  13. data/lib/train/extras/file_common.rb +131 -0
  14. data/lib/train/extras/linux_file.rb +74 -0
  15. data/lib/train/extras/linux_lsb.rb +60 -0
  16. data/lib/train/extras/os_common.rb +131 -0
  17. data/lib/train/extras/os_detect_darwin.rb +32 -0
  18. data/lib/train/extras/os_detect_linux.rb +126 -0
  19. data/lib/train/extras/os_detect_unix.rb +77 -0
  20. data/lib/train/extras/os_detect_windows.rb +73 -0
  21. data/lib/train/extras/stat.rb +92 -0
  22. data/lib/train/extras/windows_file.rb +85 -0
  23. data/lib/train/options.rb +80 -0
  24. data/lib/train/plugins.rb +40 -0
  25. data/lib/train/plugins/base_connection.rb +86 -0
  26. data/lib/train/plugins/transport.rb +49 -0
  27. data/lib/train/transports/docker.rb +102 -0
  28. data/lib/train/transports/local.rb +52 -0
  29. data/lib/train/transports/local_file.rb +77 -0
  30. data/lib/train/transports/local_os.rb +51 -0
  31. data/lib/train/transports/mock.rb +125 -0
  32. data/lib/train/transports/ssh.rb +163 -0
  33. data/lib/train/transports/ssh_connection.rb +216 -0
  34. data/lib/train/transports/winrm.rb +187 -0
  35. data/lib/train/transports/winrm_connection.rb +258 -0
  36. data/lib/train/version.rb +7 -0
  37. data/test/integration/.kitchen.yml +43 -0
  38. data/test/integration/Berksfile +3 -0
  39. data/test/integration/bootstrap.sh +17 -0
  40. data/test/integration/chefignore +1 -0
  41. data/test/integration/cookbooks/test/metadata.rb +1 -0
  42. data/test/integration/cookbooks/test/recipes/default.rb +101 -0
  43. data/test/integration/docker_run.rb +153 -0
  44. data/test/integration/docker_test.rb +24 -0
  45. data/test/integration/docker_test_container.rb +24 -0
  46. data/test/integration/helper.rb +58 -0
  47. data/test/integration/sudo/nopasswd.rb +16 -0
  48. data/test/integration/sudo/passwd.rb +21 -0
  49. data/test/integration/sudo/run_as.rb +12 -0
  50. data/test/integration/test-runner.yaml +24 -0
  51. data/test/integration/test_local.rb +19 -0
  52. data/test/integration/test_ssh.rb +24 -0
  53. data/test/integration/tests/path_block_device_test.rb +74 -0
  54. data/test/integration/tests/path_character_device_test.rb +74 -0
  55. data/test/integration/tests/path_file_test.rb +79 -0
  56. data/test/integration/tests/path_folder_test.rb +88 -0
  57. data/test/integration/tests/path_missing_test.rb +77 -0
  58. data/test/integration/tests/path_pipe_test.rb +78 -0
  59. data/test/integration/tests/path_symlink_test.rb +83 -0
  60. data/test/integration/tests/run_command_test.rb +28 -0
  61. data/test/unit/extras/command_wrapper_test.rb +41 -0
  62. data/test/unit/extras/file_common_test.rb +133 -0
  63. data/test/unit/extras/linux_file_test.rb +98 -0
  64. data/test/unit/extras/os_common_test.rb +258 -0
  65. data/test/unit/extras/stat_test.rb +105 -0
  66. data/test/unit/helper.rb +6 -0
  67. data/test/unit/plugins/connection_test.rb +44 -0
  68. data/test/unit/plugins/transport_test.rb +111 -0
  69. data/test/unit/plugins_test.rb +22 -0
  70. data/test/unit/train_test.rb +132 -0
  71. data/test/unit/transports/local_file_test.rb +112 -0
  72. data/test/unit/transports/local_test.rb +73 -0
  73. data/test/unit/transports/mock_test.rb +76 -0
  74. data/test/unit/transports/ssh_test.rb +95 -0
  75. data/test/unit/version_test.rb +8 -0
  76. data/train.gemspec +32 -0
  77. metadata +299 -0
@@ -0,0 +1,216 @@
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
+
24
+ class Train::Transports::SSH
25
+ # A Connection instance can be generated and re-generated, given new
26
+ # connection details such as connection port, hostname, credentials, etc.
27
+ # This object is responsible for carrying out the actions on the remote
28
+ # host such as executing commands, transferring files, etc.
29
+ #
30
+ # @author Fletcher Nichol <fnichol@nichol.ca>
31
+ class Connection < BaseConnection # rubocop:disable Metrics/ClassLength
32
+ def initialize(options)
33
+ super(options)
34
+ @username = @options.delete(:username)
35
+ @hostname = @options.delete(:hostname)
36
+ @port = @options[:port] # don't delete from options
37
+ @connection_retries = @options.delete(:connection_retries)
38
+ @connection_retry_sleep = @options.delete(:connection_retry_sleep)
39
+ @max_wait_until_ready = @options.delete(:max_wait_until_ready)
40
+ @files = {}
41
+ @session = nil
42
+ @transport_options = @options.delete(:transport_options)
43
+ @cmd_wrapper = nil
44
+ @cmd_wrapper = CommandWrapper.load(self, @transport_options)
45
+ end
46
+
47
+ # (see Base::Connection#close)
48
+ def close
49
+ return if @session.nil?
50
+ logger.debug("[SSH] closing connection to #{self}")
51
+ session.close
52
+ ensure
53
+ @session = nil
54
+ end
55
+
56
+ def os
57
+ @os ||= OS.new(self)
58
+ end
59
+
60
+ def file(path)
61
+ @files[path] ||= LinuxFile.new(self, path)
62
+ end
63
+
64
+ # (see Base::Connection#run_command)
65
+ def run_command(cmd)
66
+ stdout = stderr = ''
67
+ exit_status = nil
68
+ cmd.force_encoding('binary') if cmd.respond_to?(:force_encoding)
69
+ logger.debug("[SSH] #{self} (#{cmd})")
70
+
71
+ session.open_channel do |channel|
72
+ # wrap commands if that is configured
73
+ cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
74
+
75
+ channel.exec(cmd) do |_, success|
76
+ abort 'Couldn\'t execute command on SSH.' unless success
77
+
78
+ channel.on_data do |_, data|
79
+ stdout += data
80
+ end
81
+
82
+ channel.on_extended_data do |_, _type, data|
83
+ stderr += data
84
+ end
85
+
86
+ channel.on_request('exit-status') do |_, data|
87
+ exit_status = data.read_long
88
+ end
89
+
90
+ channel.on_request('exit-signal') do |_, data|
91
+ exit_status = data.read_long
92
+ end
93
+ end
94
+ end
95
+ @session.loop
96
+
97
+ CommandResult.new(stdout, stderr, exit_status)
98
+ rescue Net::SSH::Exception => ex
99
+ raise Train::Transports::SSHFailed, "SSH command failed (#{ex.message})"
100
+ end
101
+
102
+ # (see Base::Connection#login_command)
103
+ def login_command
104
+ level = logger.debug? ? 'VERBOSE' : 'ERROR'
105
+ fwd_agent = options[:forward_agent] ? 'yes' : 'no'
106
+
107
+ args = %w{ -o UserKnownHostsFile=/dev/null }
108
+ args += %w{ -o StrictHostKeyChecking=no }
109
+ args += %w{ -o IdentitiesOnly=yes } if options[:keys]
110
+ args += %W( -o LogLevel=#{level} )
111
+ args += %W( -o ForwardAgent=#{fwd_agent} ) if options.key?(:forward_agent)
112
+ Array(options[:keys]).each do |ssh_key|
113
+ args += %W( -i #{ssh_key} )
114
+ end
115
+ args += %W( -p #{@port} )
116
+ args += %W( #{@username}@#{@hostname} )
117
+
118
+ LoginCommand.new('ssh', args)
119
+ end
120
+
121
+ # (see Base::Connection#upload)
122
+ def upload(locals, remote)
123
+ Array(locals).each do |local|
124
+ opts = File.directory?(local) ? { recursive: true } : {}
125
+
126
+ session.scp.upload!(local, remote, opts) do |_ch, name, sent, total|
127
+ logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total
128
+ end
129
+ end
130
+ rescue Net::SSH::Exception => ex
131
+ raise Train::Transports::SSHFailed, "SCP upload 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
+ execute(PING_COMMAND.dup)
144
+ end
145
+
146
+ private
147
+
148
+ PING_COMMAND = "echo '[SSH] Established'".freeze
149
+
150
+ RESCUE_EXCEPTIONS_ON_ESTABLISH = [
151
+ Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
152
+ Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
153
+ Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Timeout::Error
154
+ ].freeze
155
+
156
+ # Establish an SSH session on the remote host.
157
+ #
158
+ # @param opts [Hash] retry options
159
+ # @option opts [Integer] :retries the number of times to retry before
160
+ # failing
161
+ # @option opts [Float] :delay the number of seconds to wait until
162
+ # attempting a retry
163
+ # @option opts [String] :message an optional message to be logged on
164
+ # debug (overriding the default) when a rescuable exception is raised
165
+ # @return [Net::SSH::Connection::Session] the SSH connection session
166
+ # @api private
167
+ def establish_connection(opts)
168
+ logger.debug("[SSH] opening connection to #{self}")
169
+ Net::SSH.start(@hostname, @username, @options)
170
+ rescue *RESCUE_EXCEPTIONS_ON_ESTABLISH => e
171
+ if (opts[:retries] -= 1) <= 0
172
+ logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
173
+ raise Train::Transports::SSHFailed, 'SSH session could not be established'
174
+ end
175
+
176
+ if opts[:message]
177
+ logger.debug("[SSH] connection failed (#{e.inspect})")
178
+ message = opts[:message]
179
+ else
180
+ message = "[SSH] connection failed, retrying in #{opts[:delay]}"\
181
+ " seconds (#{e.inspect})"
182
+ end
183
+ logger.info(message)
184
+
185
+ sleep(opts[:delay])
186
+ retry
187
+ end
188
+
189
+ # Returns a connection session, or establishes one when invoked the
190
+ # first time.
191
+ #
192
+ # @param retry_options [Hash] retry options for the initial connection
193
+ # @return [Net::SSH::Connection::Session] the SSH connection session
194
+ # @api private
195
+ def session(retry_options = {})
196
+ @session ||= establish_connection({
197
+ retries: @connection_retries.to_i,
198
+ delay: @connection_retry_sleep.to_i,
199
+ }.merge(retry_options))
200
+ end
201
+
202
+ # String representation of object, reporting its connection details and
203
+ # configuration.
204
+ #
205
+ # @api private
206
+ def to_s
207
+ "#{@username}@#{@hostname}<#{@options.inspect}>"
208
+ end
209
+
210
+ class OS < OSCommon
211
+ def initialize(backend)
212
+ super(backend, { family: 'unix' })
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,187 @@
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 Metrics/ClassLength
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_TRANSPORT_SPEC_VERSION = '~> 1.0'.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: :plaintext,
104
+ disable_sspi: true,
105
+ basic_auth_only: true,
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_TRANSPORT_SPEC_VERSION.dup
136
+ logger.debug('Winrm Transport requested,' \
137
+ " loading WinRM::Transport gem (#{spec_version})")
138
+ gem 'winrm-transport', spec_version
139
+ first_load = require 'winrm/transport/version'
140
+ load_winrm_transport!
141
+
142
+ version = ::WinRM::Transport::VERSION
143
+ if first_load
144
+ logger.debug("WinRM::Transport #{version} library loaded")
145
+ else
146
+ logger.debug("WinRM::Transport #{version} previously loaded")
147
+ end
148
+ rescue LoadError => e
149
+ logger.fatal(
150
+ "The `winrm-transport' gem is missing and must" \
151
+ ' be installed or cannot be properly activated. Run' \
152
+ " `gem install winrm-transport --version '#{spec_version}'`" \
153
+ ' or add the following to your Gemfile if you are using Bundler:' \
154
+ " `gem 'winrm-transport', '#{spec_version}'`.",
155
+ )
156
+ raise Train::UserError,
157
+ "Could not load or activate WinRM::Transport (#{e.message})"
158
+ end
159
+
160
+ # Load WinRM::Transport code.
161
+ #
162
+ # @api private
163
+ def load_winrm_transport!
164
+ silence_warnings { require 'winrm' }
165
+ require 'winrm/transport/shell_closer'
166
+ require 'winrm/transport/command_executor'
167
+ require 'winrm/transport/file_transporter'
168
+ end
169
+
170
+ # Return the last saved WinRM connection instance.
171
+ #
172
+ # @return [Winrm::Connection] a WinRM Connection instance
173
+ # @api private
174
+ def reuse_connection
175
+ logger.debug("[WinRM] reusing existing connection #{@connection}")
176
+ yield @connection if block_given?
177
+ @connection
178
+ end
179
+
180
+ def silence_warnings
181
+ old_verbose, $VERBOSE = $VERBOSE, nil
182
+ yield
183
+ ensure
184
+ $VERBOSE = old_verbose
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,258 @@
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
+ 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
+ shell_id = session.shell
46
+ logger.debug("[WinRM] closing remote shell #{shell_id} on #{self}")
47
+ session.close
48
+ logger.debug("[WinRM] remote shell #{shell_id} closed")
49
+ ensure
50
+ @session = nil
51
+ end
52
+
53
+ def os
54
+ @os ||= OS.new(self)
55
+ end
56
+
57
+ def file(path)
58
+ @files[path] ||= WindowsFile.new(self, path)
59
+ end
60
+
61
+ def run_command(command)
62
+ return if command.nil?
63
+ logger.debug("[WinRM] #{self} (#{command})")
64
+ out = ''
65
+
66
+ response = session.run_powershell_script(command) do |stdout, _|
67
+ out << stdout if stdout
68
+ end
69
+
70
+ CommandResult.new(out, response.stderr, response[:exitcode])
71
+ end
72
+
73
+ # (see Base::Connection#login_command)
74
+ def login_command
75
+ case RbConfig::CONFIG['host_os']
76
+ when /darwin/
77
+ login_command_for_mac
78
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
79
+ login_command_for_windows
80
+ when /linux/
81
+ login_command_for_linux
82
+ else
83
+ fail ActionFailed,
84
+ "Remote login not supported in #{self.class} " \
85
+ "from host OS '#{RbConfig::CONFIG['host_os']}'."
86
+ end
87
+ end
88
+
89
+ # (see Base::Connection#upload)
90
+ def upload(locals, remote)
91
+ file_transporter.upload(locals, remote)
92
+ end
93
+
94
+ # (see Base::Connection#wait_until_ready)
95
+ def wait_until_ready
96
+ delay = 3
97
+ session(
98
+ retries: @max_wait_until_ready / delay,
99
+ delay: delay,
100
+ message: "Waiting for WinRM service on #{endpoint}, "\
101
+ "retrying in #{delay} seconds",
102
+ )
103
+ execute(PING_COMMAND.dup)
104
+ end
105
+
106
+ private
107
+
108
+ PING_COMMAND = "Write-Host '[WinRM] Established\n'".freeze
109
+
110
+ RESCUE_EXCEPTIONS_ON_ESTABLISH = lambda do
111
+ [
112
+ Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
113
+ Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
114
+ ::WinRM::WinRMHTTPTransportError, ::WinRM::WinRMAuthorizationError,
115
+ HTTPClient::KeepAliveDisconnected,
116
+ HTTPClient::ConnectTimeoutError
117
+ ].freeze
118
+ end
119
+
120
+ # Create a local RDP document and return it
121
+ #
122
+ # @param opts [Hash] configuration options
123
+ # @option opts [true,false] :mac whether or not the document is for a
124
+ # Mac system
125
+ # @api private
126
+ def rdp_doc(opts = {})
127
+ host = URI.parse(@endpoint).host
128
+ content = [
129
+ "full address:s:#{host}:#{@rdp_port}",
130
+ 'prompt for credentials:i:1',
131
+ "username:s:#{options[:user]}",
132
+ ].join("\n")
133
+
134
+ content.prepend("drivestoredirect:s:*\n") if opts[:mac]
135
+
136
+ content
137
+ end
138
+
139
+ # Establish a remote shell session on the remote host.
140
+ #
141
+ # @param opts [Hash] retry options
142
+ # @option opts [Integer] :retries the number of times to retry before
143
+ # failing
144
+ # @option opts [Float] :delay the number of seconds to wait until
145
+ # attempting a retry
146
+ # @option opts [String] :message an optional message to be logged on
147
+ # debug (overriding the default) when a rescuable exception is raised
148
+ # @return [Winrm::CommandExecutor] the command executor session
149
+ # @api private
150
+ def establish_shell(opts)
151
+ service_args = [@endpoint, @winrm_transport, options]
152
+ @service = ::WinRM::WinRMWebService.new(*service_args)
153
+ closer = WinRM::Transport::ShellCloser.new("#{self}", false, service_args)
154
+
155
+ executor = WinRM::Transport::CommandExecutor.new(@service, logger, closer)
156
+ retryable(opts) do
157
+ logger.debug("[WinRM] opening remote shell on #{self}")
158
+ shell_id = executor.open
159
+ logger.debug("[WinRM] remote shell #{shell_id} is open on #{self}")
160
+ end
161
+ executor
162
+ end
163
+
164
+ # @return [Winrm::FileTransporter] a file transporter
165
+ # @api private
166
+ def file_transporter
167
+ @file_transporter ||= WinRM::Transport::FileTransporter.new(session, logger)
168
+ end
169
+
170
+ # Builds a `LoginCommand` for use by Linux-based platforms.
171
+ #
172
+ # TODO: determine whether or not `desktop` exists
173
+ #
174
+ # @return [LoginCommand] a login command
175
+ # @api private
176
+ def login_command_for_linux
177
+ args = %W( -u #{options[:user]} )
178
+ args += %W( -p #{options[:pass]} ) if options.key?(:pass)
179
+ args += %W( #{URI.parse(@endpoint).host}:#{@rdp_port} )
180
+ LoginCommand.new('rdesktop', args)
181
+ end
182
+
183
+ # Builds a `LoginCommand` for use by Mac-based platforms.
184
+ #
185
+ # @return [LoginCommand] a login command
186
+ # @api private
187
+ def login_command_for_mac
188
+ LoginCommand.new('open', rdp_doc(mac: true))
189
+ end
190
+
191
+ # Builds a `LoginCommand` for use by Windows-based platforms.
192
+ #
193
+ # @return [LoginCommand] a login command
194
+ # @api private
195
+ def login_command_for_windows
196
+ LoginCommand.new('mstsc', rdp_doc)
197
+ end
198
+
199
+ # Yields to a block and reties the block if certain rescuable
200
+ # exceptions are raised.
201
+ #
202
+ # @param opts [Hash] retry options
203
+ # @option opts [Integer] :retries the number of times to retry before
204
+ # failing
205
+ # @option opts [Float] :delay the number of seconds to wait until
206
+ # attempting a retry
207
+ # @option opts [String] :message an optional message to be logged on
208
+ # debug (overriding the default) when a rescuable exception is raised
209
+ # @return [Winrm::CommandExecutor] the command executor session
210
+ # @api private
211
+ def retryable(opts)
212
+ yield
213
+ rescue *RESCUE_EXCEPTIONS_ON_ESTABLISH.call => e
214
+ if (opts[:retries] -= 1) <= 0
215
+ logger.warn("[WinRM] connection failed, terminating (#{e.inspect})")
216
+ raise
217
+ end
218
+
219
+ if opts[:message]
220
+ logger.debug("[WinRM] connection failed (#{e.inspect})")
221
+ message = opts[:message]
222
+ else
223
+ message = '[WinRM] connection failed, '\
224
+ "retrying in #{opts[:delay]} seconds (#{e.inspect})"
225
+ end
226
+ logger.info(message)
227
+ sleep(opts[:delay])
228
+ retry
229
+ end
230
+
231
+ # Establishes a remote shell session, or establishes one when invoked
232
+ # the first time.
233
+ #
234
+ # @param retry_options [Hash] retry options for the initial connection
235
+ # @return [Winrm::CommandExecutor] the command executor session
236
+ # @api private
237
+ def session(retry_options = {})
238
+ @session ||= establish_shell({
239
+ retries: @connection_retries.to_i,
240
+ delay: @connection_retry_sleep.to_i,
241
+ }.merge(retry_options))
242
+ end
243
+
244
+ # String representation of object, reporting its connection details and
245
+ # configuration.
246
+ #
247
+ # @api private
248
+ def to_s
249
+ "#{@winrm_transport}::#{@endpoint}<#{options.inspect}>"
250
+ end
251
+
252
+ class OS < OSCommon
253
+ def initialize(backend)
254
+ super(backend, { family: 'windows' })
255
+ end
256
+ end
257
+ end
258
+ end