r-train 0.9.1

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