test-kitchen 1.7.0 → 1.7.1.dev

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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +8 -8
  3. data/.gitattributes +3 -0
  4. data/.github/ISSUE_TEMPLATE.md +55 -55
  5. data/.gitignore +28 -28
  6. data/.kitchen.ci.yml +23 -23
  7. data/.kitchen.proxy.yml +27 -27
  8. data/.rubocop.yml +3 -3
  9. data/.travis.yml +70 -70
  10. data/.yardopts +3 -3
  11. data/Berksfile +3 -3
  12. data/CHANGELOG.md +1090 -1083
  13. data/CONTRIBUTING.md +14 -14
  14. data/Gemfile +19 -19
  15. data/Gemfile.proxy_tests +4 -4
  16. data/Guardfile +42 -42
  17. data/LICENSE +15 -15
  18. data/MAINTAINERS.md +23 -23
  19. data/README.md +135 -135
  20. data/Rakefile +61 -61
  21. data/appveyor.yml +44 -44
  22. data/features/kitchen_action_commands.feature +164 -164
  23. data/features/kitchen_command.feature +16 -16
  24. data/features/kitchen_console_command.feature +34 -34
  25. data/features/kitchen_defaults.feature +38 -38
  26. data/features/kitchen_diagnose_command.feature +96 -96
  27. data/features/kitchen_driver_create_command.feature +64 -64
  28. data/features/kitchen_driver_discover_command.feature +25 -25
  29. data/features/kitchen_help_command.feature +16 -16
  30. data/features/kitchen_init_command.feature +274 -274
  31. data/features/kitchen_list_command.feature +104 -104
  32. data/features/kitchen_login_command.feature +62 -62
  33. data/features/kitchen_sink_command.feature +30 -30
  34. data/features/kitchen_test_command.feature +88 -88
  35. data/features/step_definitions/gem_steps.rb +36 -36
  36. data/features/step_definitions/git_steps.rb +5 -5
  37. data/features/step_definitions/output_steps.rb +5 -5
  38. data/features/support/env.rb +75 -75
  39. data/lib/kitchen.rb +150 -150
  40. data/lib/kitchen/base64_stream.rb +55 -55
  41. data/lib/kitchen/cli.rb +419 -419
  42. data/lib/kitchen/collection.rb +55 -55
  43. data/lib/kitchen/color.rb +65 -65
  44. data/lib/kitchen/command.rb +185 -185
  45. data/lib/kitchen/command/action.rb +45 -45
  46. data/lib/kitchen/command/console.rb +58 -58
  47. data/lib/kitchen/command/diagnose.rb +92 -92
  48. data/lib/kitchen/command/driver_discover.rb +105 -105
  49. data/lib/kitchen/command/exec.rb +41 -41
  50. data/lib/kitchen/command/list.rb +119 -119
  51. data/lib/kitchen/command/login.rb +43 -43
  52. data/lib/kitchen/command/sink.rb +54 -54
  53. data/lib/kitchen/command/test.rb +51 -51
  54. data/lib/kitchen/config.rb +322 -322
  55. data/lib/kitchen/configurable.rb +529 -529
  56. data/lib/kitchen/data_munger.rb +959 -959
  57. data/lib/kitchen/diagnostic.rb +141 -141
  58. data/lib/kitchen/driver.rb +56 -56
  59. data/lib/kitchen/driver/base.rb +134 -134
  60. data/lib/kitchen/driver/dummy.rb +108 -108
  61. data/lib/kitchen/driver/proxy.rb +72 -72
  62. data/lib/kitchen/driver/ssh_base.rb +357 -357
  63. data/lib/kitchen/errors.rb +229 -229
  64. data/lib/kitchen/generator/driver_create.rb +177 -177
  65. data/lib/kitchen/generator/init.rb +296 -296
  66. data/lib/kitchen/instance.rb +662 -662
  67. data/lib/kitchen/lazy_hash.rb +142 -142
  68. data/lib/kitchen/loader/yaml.rb +349 -349
  69. data/lib/kitchen/logger.rb +423 -423
  70. data/lib/kitchen/logging.rb +56 -56
  71. data/lib/kitchen/login_command.rb +52 -52
  72. data/lib/kitchen/metadata_chopper.rb +52 -52
  73. data/lib/kitchen/platform.rb +67 -67
  74. data/lib/kitchen/provisioner.rb +54 -54
  75. data/lib/kitchen/provisioner/base.rb +236 -236
  76. data/lib/kitchen/provisioner/chef/berkshelf.rb +114 -114
  77. data/lib/kitchen/provisioner/chef/common_sandbox.rb +322 -322
  78. data/lib/kitchen/provisioner/chef/librarian.rb +112 -112
  79. data/lib/kitchen/provisioner/chef_apply.rb +124 -124
  80. data/lib/kitchen/provisioner/chef_base.rb +341 -341
  81. data/lib/kitchen/provisioner/chef_solo.rb +88 -88
  82. data/lib/kitchen/provisioner/chef_zero.rb +245 -245
  83. data/lib/kitchen/provisioner/dummy.rb +79 -79
  84. data/lib/kitchen/provisioner/shell.rb +138 -138
  85. data/lib/kitchen/rake_tasks.rb +63 -63
  86. data/lib/kitchen/shell_out.rb +93 -93
  87. data/lib/kitchen/ssh.rb +276 -276
  88. data/lib/kitchen/state_file.rb +120 -120
  89. data/lib/kitchen/suite.rb +51 -51
  90. data/lib/kitchen/thor_tasks.rb +66 -66
  91. data/lib/kitchen/transport.rb +54 -54
  92. data/lib/kitchen/transport/base.rb +176 -176
  93. data/lib/kitchen/transport/dummy.rb +79 -79
  94. data/lib/kitchen/transport/ssh.rb +364 -364
  95. data/lib/kitchen/transport/winrm.rb +486 -486
  96. data/lib/kitchen/util.rb +147 -147
  97. data/lib/kitchen/verifier.rb +55 -55
  98. data/lib/kitchen/verifier/base.rb +235 -235
  99. data/lib/kitchen/verifier/busser.rb +277 -277
  100. data/lib/kitchen/verifier/dummy.rb +79 -79
  101. data/lib/kitchen/verifier/shell.rb +101 -101
  102. data/lib/kitchen/version.rb +21 -21
  103. data/lib/vendor/hash_recursive_merge.rb +82 -82
  104. data/spec/kitchen/base64_stream_spec.rb +77 -77
  105. data/spec/kitchen/cli_spec.rb +56 -56
  106. data/spec/kitchen/collection_spec.rb +80 -80
  107. data/spec/kitchen/color_spec.rb +54 -54
  108. data/spec/kitchen/config_spec.rb +408 -408
  109. data/spec/kitchen/configurable_spec.rb +1095 -1095
  110. data/spec/kitchen/data_munger_spec.rb +2694 -2694
  111. data/spec/kitchen/diagnostic_spec.rb +129 -129
  112. data/spec/kitchen/driver/base_spec.rb +121 -121
  113. data/spec/kitchen/driver/dummy_spec.rb +199 -199
  114. data/spec/kitchen/driver/proxy_spec.rb +138 -138
  115. data/spec/kitchen/driver/ssh_base_spec.rb +1115 -1115
  116. data/spec/kitchen/driver_spec.rb +112 -112
  117. data/spec/kitchen/errors_spec.rb +309 -309
  118. data/spec/kitchen/instance_spec.rb +1419 -1419
  119. data/spec/kitchen/lazy_hash_spec.rb +117 -117
  120. data/spec/kitchen/loader/yaml_spec.rb +774 -774
  121. data/spec/kitchen/logger_spec.rb +429 -429
  122. data/spec/kitchen/logging_spec.rb +59 -59
  123. data/spec/kitchen/login_command_spec.rb +68 -68
  124. data/spec/kitchen/metadata_chopper_spec.rb +82 -82
  125. data/spec/kitchen/platform_spec.rb +89 -89
  126. data/spec/kitchen/provisioner/base_spec.rb +386 -386
  127. data/spec/kitchen/provisioner/chef_apply_spec.rb +136 -136
  128. data/spec/kitchen/provisioner/chef_base_spec.rb +1161 -1161
  129. data/spec/kitchen/provisioner/chef_solo_spec.rb +557 -557
  130. data/spec/kitchen/provisioner/chef_zero_spec.rb +1001 -1001
  131. data/spec/kitchen/provisioner/dummy_spec.rb +99 -99
  132. data/spec/kitchen/provisioner/shell_spec.rb +566 -566
  133. data/spec/kitchen/provisioner_spec.rb +107 -107
  134. data/spec/kitchen/shell_out_spec.rb +150 -150
  135. data/spec/kitchen/ssh_spec.rb +693 -693
  136. data/spec/kitchen/state_file_spec.rb +129 -129
  137. data/spec/kitchen/suite_spec.rb +62 -62
  138. data/spec/kitchen/transport/base_spec.rb +89 -89
  139. data/spec/kitchen/transport/ssh_spec.rb +1255 -1255
  140. data/spec/kitchen/transport/winrm_spec.rb +1143 -1143
  141. data/spec/kitchen/transport_spec.rb +112 -112
  142. data/spec/kitchen/util_spec.rb +165 -165
  143. data/spec/kitchen/verifier/base_spec.rb +362 -362
  144. data/spec/kitchen/verifier/busser_spec.rb +610 -610
  145. data/spec/kitchen/verifier/dummy_spec.rb +99 -99
  146. data/spec/kitchen/verifier/shell_spec.rb +160 -160
  147. data/spec/kitchen/verifier_spec.rb +120 -120
  148. data/spec/kitchen_spec.rb +114 -114
  149. data/spec/spec_helper.rb +85 -85
  150. data/spec/support/powershell_max_size_spec.rb +40 -40
  151. data/support/busser_install_command.ps1 +14 -14
  152. data/support/busser_install_command.sh +14 -14
  153. data/support/chef-client-zero.rb +77 -77
  154. data/support/chef_base_init_command.ps1 +18 -18
  155. data/support/chef_base_init_command.sh +2 -2
  156. data/support/chef_base_install_command.ps1 +85 -85
  157. data/support/chef_base_install_command.sh +229 -229
  158. data/support/chef_zero_prepare_command_legacy.ps1 +9 -9
  159. data/support/chef_zero_prepare_command_legacy.sh +10 -10
  160. data/support/download_helpers.sh +109 -109
  161. data/support/dummy-validation.pem +27 -27
  162. data/templates/driver/CHANGELOG.md.erb +3 -3
  163. data/templates/driver/Gemfile.erb +3 -3
  164. data/templates/driver/README.md.erb +64 -64
  165. data/templates/driver/Rakefile.erb +21 -21
  166. data/templates/driver/driver.rb.erb +23 -23
  167. data/templates/driver/gemspec.erb +29 -29
  168. data/templates/driver/gitignore.erb +17 -17
  169. data/templates/driver/license_apachev2.erb +15 -15
  170. data/templates/driver/license_lgplv3.erb +16 -16
  171. data/templates/driver/license_mit.erb +22 -22
  172. data/templates/driver/license_reserved.erb +5 -5
  173. data/templates/driver/tailor.erb +4 -4
  174. data/templates/driver/travis.yml.erb +11 -11
  175. data/templates/driver/version.rb.erb +12 -12
  176. data/templates/init/chefignore.erb +1 -1
  177. data/templates/init/kitchen.yml.erb +18 -18
  178. data/test-kitchen.gemspec +62 -62
  179. data/test/integration/default/default_spec.rb +3 -3
  180. data/testing_windows.md +37 -37
  181. metadata +5 -4
@@ -1,486 +1,486 @@
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
- #
7
- # Copyright (C) 2014, Salim Afiune
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 "rbconfig"
22
- require "uri"
23
-
24
- require "kitchen"
25
-
26
- module Kitchen
27
- module Transport
28
- # Wrapped exception for any internally raised WinRM-related errors.
29
- #
30
- # @author Fletcher Nichol <fnichol@nichol.ca>
31
- class WinrmFailed < TransportFailed; 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 < Kitchen::Transport::Base
39
- kitchen_transport_api_version 1
40
-
41
- plugin_version Kitchen::VERSION
42
-
43
- default_config :username, "administrator"
44
- default_config :password, nil
45
- default_config :rdp_port, 3389
46
- default_config :connection_retries, 5
47
- default_config :connection_retry_sleep, 1
48
- default_config :max_wait_until_ready, 600
49
- default_config :winrm_transport, :negotiate
50
- default_config :port do |transport|
51
- transport[:winrm_transport] == :ssl ? 5986 : 5985
52
- end
53
- default_config :endpoint_template do |transport|
54
- scheme = transport[:winrm_transport] == :ssl ? "https" : "http"
55
- "#{scheme}://%{hostname}:%{port}/wsman"
56
- end
57
-
58
- def finalize_config!(instance)
59
- super
60
-
61
- config[:winrm_transport] = config[:winrm_transport].to_sym
62
-
63
- self
64
- end
65
-
66
- # (see Base#connection)
67
- def connection(state, &block)
68
- options = connection_options(config.to_hash.merge(state))
69
-
70
- if @connection && @connection_options == options
71
- reuse_connection(&block)
72
- else
73
- create_new_connection(options, &block)
74
- end
75
- end
76
-
77
- # A Connection instance can be generated and re-generated, given new
78
- # connection details such as connection port, hostname, credentials, etc.
79
- # This object is responsible for carrying out the actions on the remote
80
- # host such as executing commands, transferring files, etc.
81
- #
82
- # @author Fletcher Nichol <fnichol@nichol.ca>
83
- class Connection < Kitchen::Transport::Base::Connection
84
- # (see Base::Connection#close)
85
- def close
86
- return if @session.nil?
87
-
88
- session.close
89
- ensure
90
- @session = nil
91
- end
92
-
93
- # (see Base::Connection#execute)
94
- def execute(command)
95
- return if command.nil?
96
- logger.debug("[WinRM] #{self} (#{command})")
97
-
98
- if command.length > MAX_COMMAND_SIZE
99
- command = run_from_file_command(command)
100
- end
101
- exit_code, stderr = execute_with_exit_code(command)
102
-
103
- if logger.debug? && exit_code == 0
104
- log_stderr_on_warn(stderr)
105
- elsif exit_code != 0
106
- log_stderr_on_warn(stderr)
107
- raise Transport::WinrmFailed,
108
- "WinRM exited (#{exit_code}) for command: [#{command}]"
109
- end
110
- end
111
-
112
- # (see Base::Connection#login_command)
113
- def login_command
114
- case RbConfig::CONFIG["host_os"]
115
- when /darwin/
116
- login_command_for_mac
117
- when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
118
- login_command_for_windows
119
- when /linux/
120
- login_command_for_linux
121
- else
122
- fail ActionFailed, "Remote login not supported in #{self.class} " \
123
- "from host OS '#{RbConfig::CONFIG["host_os"]}'."
124
- end
125
- end
126
-
127
- # (see Base::Connection#upload)
128
- def upload(locals, remote)
129
- file_transporter.upload(locals, remote)
130
- end
131
-
132
- # (see Base::Connection#wait_until_ready)
133
- def wait_until_ready
134
- delay = 3
135
- session(
136
- :retry_limit => max_wait_until_ready / delay,
137
- :retry_delay => delay
138
- )
139
- execute(PING_COMMAND.dup)
140
- end
141
-
142
- private
143
-
144
- PING_COMMAND = "Write-Host '[WinRM] Established\n'".freeze
145
-
146
- # Maximum string to send to the transport to execute. WinRM has an 8000 character
147
- # command line limit. The original command string is coverted to a base 64 encoded
148
- # UTF-16 string which will double the string size.
149
- MAX_COMMAND_SIZE = 3000
150
-
151
- # @return [Integer] how many times to retry when failing to execute
152
- # a command or transfer files
153
- # @api private
154
- attr_reader :connection_retries
155
-
156
- # @return [Float] how many seconds to wait before attempting a retry
157
- # when failing to execute a command or transfer files
158
- # @api private
159
- attr_reader :connection_retry_sleep
160
-
161
- # @return [String] the endpoint URL of the remote WinRM host
162
- # @api private
163
- attr_reader :endpoint
164
-
165
- # @return [String] display name for the associated instance
166
- # @api private
167
- attr_reader :instance_name
168
-
169
- # @return [String] local path to the root of the project
170
- # @api private
171
- attr_reader :kitchen_root
172
-
173
- # @return [Integer] how many times to retry when invoking
174
- # `#wait_until_ready` before failing
175
- # @api private
176
- attr_reader :max_wait_until_ready
177
-
178
- # @return [Integer] the TCP port number to use when connection to the
179
- # remote WinRM host
180
- # @api private
181
- attr_reader :rdp_port
182
-
183
- # @return [Symbol] the transport strategy to use when constructing a
184
- # `WinRM::WinRMWebService`
185
- # @api private
186
- attr_reader :winrm_transport
187
-
188
- # Writes an RDP document to the local file system.
189
- #
190
- # @param opts [Hash] file options
191
- # @option opts [true,false] :mac whether or not the document is for a
192
- # Mac system
193
- # @api private
194
- def create_rdp_doc(opts = {})
195
- content = Util.outdent!(<<-RDP)
196
- full address:s:#{URI.parse(endpoint).host}:#{rdp_port}
197
- prompt for credentials:i:1
198
- username:s:#{options[:user]}
199
- RDP
200
- content.prepend("drivestoredirect:s:*\n") if opts[:mac]
201
-
202
- File.open(rdp_doc_path, "wb") { |f| f.write(content) }
203
-
204
- if logger.debug?
205
- debug("Creating RDP document for #{instance_name} (#{rdp_doc_path})")
206
- debug("------------")
207
- IO.read(rdp_doc_path).each_line { |l| debug("#{l.chomp}") }
208
- debug("------------")
209
- end
210
- end
211
-
212
- # Execute a Powershell script over WinRM and return the command's
213
- # exit code and standard error.
214
- #
215
- # @param command [String] Powershell script to execute
216
- # @return [[Integer,String]] an array containing the exit code of the
217
- # script and the standard error stream
218
- # @api private
219
- def execute_with_exit_code(command)
220
- response = session.run_powershell_script(command) do |stdout, _|
221
- logger << stdout if stdout
222
- end
223
-
224
- [response[:exitcode], response.stderr]
225
- end
226
-
227
- # @return [Winrm::FileTransporter] a file transporter
228
- # @api private
229
- def file_transporter
230
- @file_transporter ||= WinRM::FS::Core::FileTransporter.new(session)
231
- end
232
-
233
- # (see Base#init_options)
234
- def init_options(options)
235
- super
236
- @instance_name = @options.delete(:instance_name)
237
- @kitchen_root = @options.delete(:kitchen_root)
238
- @endpoint = @options.delete(:endpoint)
239
- @rdp_port = @options.delete(:rdp_port)
240
- @winrm_transport = @options.delete(:winrm_transport)
241
- @connection_retries = @options.delete(:connection_retries)
242
- @connection_retry_sleep = @options.delete(:connection_retry_sleep)
243
- @max_wait_until_ready = @options.delete(:max_wait_until_ready)
244
- end
245
-
246
- # Logs formatted standard error output at the warning level.
247
- #
248
- # @param stderr [String] standard error output
249
- # @api private
250
- def log_stderr_on_warn(stderr)
251
- error_regexp = /<S S=\"Error\">/
252
-
253
- if error_regexp.match(stderr)
254
- stderr.
255
- split(error_regexp)[1..-2].
256
- map! { |line| line.sub(/_x000D__x000A_<\/S>/, "").rstrip }.
257
- each { |line| logger.warn(line) }
258
- else
259
- stderr.
260
- split("\r\n").
261
- each { |line| logger.warn(line) }
262
- end
263
- end
264
-
265
- # Builds a `LoginCommand` for use by Linux-based platforms.
266
- #
267
- # TODO: determine whether or not `desktop` exists
268
- #
269
- # @return [LoginCommand] a login command
270
- # @api private
271
- def login_command_for_linux
272
- args = %W[-u #{options[:user]}]
273
- args += %W[-p #{options[:pass]}] if options.key?(:pass)
274
- args += %W[#{URI.parse(endpoint).host}:#{rdp_port}]
275
-
276
- LoginCommand.new("rdesktop", args)
277
- end
278
-
279
- # Builds a `LoginCommand` for use by Mac-based platforms.
280
- #
281
- # @return [LoginCommand] a login command
282
- # @api private
283
- def login_command_for_mac
284
- create_rdp_doc(:mac => true)
285
-
286
- LoginCommand.new("open", rdp_doc_path)
287
- end
288
-
289
- # Builds a `LoginCommand` for use by Windows-based platforms.
290
- #
291
- # @return [LoginCommand] a login command
292
- # @api private
293
- def login_command_for_windows
294
- create_rdp_doc
295
-
296
- LoginCommand.new("mstsc", rdp_doc_path)
297
- end
298
-
299
- # @return [String] path to the local RDP document
300
- # @api private
301
- def rdp_doc_path
302
- File.join(kitchen_root, ".kitchen", "#{instance_name}.rdp")
303
- end
304
-
305
- # Establishes a remote shell session, or establishes one when invoked
306
- # the first time.
307
- #
308
- # @param retry_options [Hash] retry options for the initial connection
309
- # @return [Winrm::CommandExecutor] the command executor session
310
- # @api private
311
- def session(retry_options = {})
312
- @session ||= begin
313
- opts = {
314
- :retry_limit => connection_retries.to_i,
315
- :retry_delay => connection_retry_sleep.to_i
316
- }.merge(retry_options)
317
-
318
- service_args = [endpoint, winrm_transport, options.merge(opts)]
319
- @service = ::WinRM::WinRMWebService.new(*service_args)
320
- @service.logger = logger
321
- @service.create_executor
322
- end
323
- end
324
-
325
- # String representation of object, reporting its connection details and
326
- # configuration.
327
- #
328
- # @api private
329
- def to_s
330
- "#{winrm_transport}::#{endpoint}<#{options.inspect}>"
331
- end
332
-
333
- # takes a long (greater than 3000 characters) command and saves it to a
334
- # file and uploads it to the test instance.
335
- #
336
- # @param command [String] a long command to be saved and uploaded
337
- # @return [String] a command that executes the uploaded script
338
- # @api private
339
- def run_from_file_command(command)
340
- temp_dir = Dir.mktmpdir("kitchen-long-script")
341
- begin
342
- script_name = "#{instance_name}-long_script.ps1"
343
- script_path = File.join(temp_dir, script_name)
344
-
345
- File.open(script_path, "wb") do |file|
346
- file.write(command)
347
- end
348
-
349
- target_path = File.join("$env:TEMP", script_name)
350
- upload(script_path, target_path)
351
-
352
- %{powershell -ExecutionPolicy Bypass -File "#{target_path}"}
353
- ensure
354
- FileUtils.rmtree(temp_dir)
355
- end
356
- end
357
- end
358
-
359
- private
360
-
361
- WINRM_SPEC_VERSION = ["~> 1.6"].freeze
362
- WINRM_FS_SPEC_VERSION = ["~> 0.4.0"].freeze
363
-
364
- # Builds the hash of options needed by the Connection object on
365
- # construction.
366
- #
367
- # @param data [Hash] merged configuration and mutable state data
368
- # @return [Hash] hash of connection options
369
- # @api private
370
- def connection_options(data)
371
- opts = {
372
- :instance_name => instance.name,
373
- :kitchen_root => data[:kitchen_root],
374
- :logger => logger,
375
- :endpoint => data[:endpoint_template] % data,
376
- :user => data[:username],
377
- :pass => data[:password],
378
- :rdp_port => data[:rdp_port],
379
- :connection_retries => data[:connection_retries],
380
- :connection_retry_sleep => data[:connection_retry_sleep],
381
- :max_wait_until_ready => data[:max_wait_until_ready],
382
- :winrm_transport => data[:winrm_transport]
383
- }
384
- opts.merge!(additional_transport_args(opts[:winrm_transport]))
385
- opts
386
- end
387
-
388
- def additional_transport_args(transport_type)
389
- case transport_type.to_sym
390
- when :ssl, :negotiate
391
- {
392
- :no_ssl_peer_verification => true,
393
- :disable_sspi => false,
394
- :basic_auth_only => false
395
- }
396
- when :plaintext
397
- {
398
- :disable_sspi => true,
399
- :basic_auth_only => true
400
- }
401
- else
402
- {}
403
- end
404
- end
405
-
406
- # Creates a new WinRM Connection instance and save it for potential
407
- # future reuse.
408
- #
409
- # @param options [Hash] conneciton options
410
- # @return [Ssh::Connection] a WinRM Connection instance
411
- # @api private
412
- def create_new_connection(options, &block)
413
- if @connection
414
- logger.debug("[WinRM] shutting previous connection #{@connection}")
415
- @connection.close
416
- end
417
-
418
- @connection_options = options
419
- @connection = Kitchen::Transport::Winrm::Connection.new(options, &block)
420
- end
421
-
422
- # (see Base#load_needed_dependencies!)
423
- def load_needed_dependencies!
424
- super
425
- load_with_rescue!("winrm", WINRM_SPEC_VERSION.dup)
426
- load_with_rescue!("winrm-fs", WINRM_FS_SPEC_VERSION.dup)
427
- end
428
-
429
- def load_with_rescue!(gem_name, spec_version)
430
- logger.debug("#{gem_name} requested," \
431
- " loading #{gem_name} gem (#{spec_version})")
432
- attempt_load = false
433
- gem gem_name, spec_version
434
- silence_warnings { attempt_load = require gem_name }
435
- if attempt_load
436
- logger.debug("#{gem_name} is loaded.")
437
- else
438
- logger.debug("#{gem_name} was already loaded.")
439
- end
440
- rescue LoadError => e
441
- message = fail_to_load_gem_message(gem_name,
442
- spec_version)
443
- logger.fatal(message)
444
- raise UserError,
445
- "Could not load or activate #{gem_name}. (#{e.message})"
446
- end
447
-
448
- def fail_to_load_gem_message(name, version = nil)
449
- version_cmd = "--version '#{version}'" if version
450
- version_file = "', '#{version}"
451
-
452
- "The `#{name}` gem is missing and must" \
453
- " be installed or cannot be properly activated. Run" \
454
- " `gem install #{name} #{version_cmd}`" \
455
- " or add the following to your Gemfile if you are using Bundler:" \
456
- " `gem '#{name} #{version_file}'`."
457
- end
458
-
459
- def host_os_windows?
460
- case RbConfig::CONFIG["host_os"]
461
- when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
462
- true
463
- else
464
- false
465
- end
466
- end
467
-
468
- # Return the last saved WinRM connection instance.
469
- #
470
- # @return [Winrm::Connection] a WinRM Connection instance
471
- # @api private
472
- def reuse_connection
473
- logger.debug("[WinRM] reusing existing connection #{@connection}")
474
- yield @connection if block_given?
475
- @connection
476
- end
477
-
478
- def silence_warnings
479
- old_verbose, $VERBOSE = $VERBOSE, nil
480
- yield
481
- ensure
482
- $VERBOSE = old_verbose
483
- end
484
- end
485
- end
486
- end
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
+ #
7
+ # Copyright (C) 2014, Salim Afiune
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 "rbconfig"
22
+ require "uri"
23
+
24
+ require "kitchen"
25
+
26
+ module Kitchen
27
+ module Transport
28
+ # Wrapped exception for any internally raised WinRM-related errors.
29
+ #
30
+ # @author Fletcher Nichol <fnichol@nichol.ca>
31
+ class WinrmFailed < TransportFailed; 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 < Kitchen::Transport::Base
39
+ kitchen_transport_api_version 1
40
+
41
+ plugin_version Kitchen::VERSION
42
+
43
+ default_config :username, "administrator"
44
+ default_config :password, nil
45
+ default_config :rdp_port, 3389
46
+ default_config :connection_retries, 5
47
+ default_config :connection_retry_sleep, 1
48
+ default_config :max_wait_until_ready, 600
49
+ default_config :winrm_transport, :negotiate
50
+ default_config :port do |transport|
51
+ transport[:winrm_transport] == :ssl ? 5986 : 5985
52
+ end
53
+ default_config :endpoint_template do |transport|
54
+ scheme = transport[:winrm_transport] == :ssl ? "https" : "http"
55
+ "#{scheme}://%{hostname}:%{port}/wsman"
56
+ end
57
+
58
+ def finalize_config!(instance)
59
+ super
60
+
61
+ config[:winrm_transport] = config[:winrm_transport].to_sym
62
+
63
+ self
64
+ end
65
+
66
+ # (see Base#connection)
67
+ def connection(state, &block)
68
+ options = connection_options(config.to_hash.merge(state))
69
+
70
+ if @connection && @connection_options == options
71
+ reuse_connection(&block)
72
+ else
73
+ create_new_connection(options, &block)
74
+ end
75
+ end
76
+
77
+ # A Connection instance can be generated and re-generated, given new
78
+ # connection details such as connection port, hostname, credentials, etc.
79
+ # This object is responsible for carrying out the actions on the remote
80
+ # host such as executing commands, transferring files, etc.
81
+ #
82
+ # @author Fletcher Nichol <fnichol@nichol.ca>
83
+ class Connection < Kitchen::Transport::Base::Connection
84
+ # (see Base::Connection#close)
85
+ def close
86
+ return if @session.nil?
87
+
88
+ session.close
89
+ ensure
90
+ @session = nil
91
+ end
92
+
93
+ # (see Base::Connection#execute)
94
+ def execute(command)
95
+ return if command.nil?
96
+ logger.debug("[WinRM] #{self} (#{command})")
97
+
98
+ if command.length > MAX_COMMAND_SIZE
99
+ command = run_from_file_command(command)
100
+ end
101
+ exit_code, stderr = execute_with_exit_code(command)
102
+
103
+ if logger.debug? && exit_code == 0
104
+ log_stderr_on_warn(stderr)
105
+ elsif exit_code != 0
106
+ log_stderr_on_warn(stderr)
107
+ raise Transport::WinrmFailed,
108
+ "WinRM exited (#{exit_code}) for command: [#{command}]"
109
+ end
110
+ end
111
+
112
+ # (see Base::Connection#login_command)
113
+ def login_command
114
+ case RbConfig::CONFIG["host_os"]
115
+ when /darwin/
116
+ login_command_for_mac
117
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
118
+ login_command_for_windows
119
+ when /linux/
120
+ login_command_for_linux
121
+ else
122
+ fail ActionFailed, "Remote login not supported in #{self.class} " \
123
+ "from host OS '#{RbConfig::CONFIG["host_os"]}'."
124
+ end
125
+ end
126
+
127
+ # (see Base::Connection#upload)
128
+ def upload(locals, remote)
129
+ file_transporter.upload(locals, remote)
130
+ end
131
+
132
+ # (see Base::Connection#wait_until_ready)
133
+ def wait_until_ready
134
+ delay = 3
135
+ session(
136
+ :retry_limit => max_wait_until_ready / delay,
137
+ :retry_delay => delay
138
+ )
139
+ execute(PING_COMMAND.dup)
140
+ end
141
+
142
+ private
143
+
144
+ PING_COMMAND = "Write-Host '[WinRM] Established\n'".freeze
145
+
146
+ # Maximum string to send to the transport to execute. WinRM has an 8000 character
147
+ # command line limit. The original command string is coverted to a base 64 encoded
148
+ # UTF-16 string which will double the string size.
149
+ MAX_COMMAND_SIZE = 3000
150
+
151
+ # @return [Integer] how many times to retry when failing to execute
152
+ # a command or transfer files
153
+ # @api private
154
+ attr_reader :connection_retries
155
+
156
+ # @return [Float] how many seconds to wait before attempting a retry
157
+ # when failing to execute a command or transfer files
158
+ # @api private
159
+ attr_reader :connection_retry_sleep
160
+
161
+ # @return [String] the endpoint URL of the remote WinRM host
162
+ # @api private
163
+ attr_reader :endpoint
164
+
165
+ # @return [String] display name for the associated instance
166
+ # @api private
167
+ attr_reader :instance_name
168
+
169
+ # @return [String] local path to the root of the project
170
+ # @api private
171
+ attr_reader :kitchen_root
172
+
173
+ # @return [Integer] how many times to retry when invoking
174
+ # `#wait_until_ready` before failing
175
+ # @api private
176
+ attr_reader :max_wait_until_ready
177
+
178
+ # @return [Integer] the TCP port number to use when connection to the
179
+ # remote WinRM host
180
+ # @api private
181
+ attr_reader :rdp_port
182
+
183
+ # @return [Symbol] the transport strategy to use when constructing a
184
+ # `WinRM::WinRMWebService`
185
+ # @api private
186
+ attr_reader :winrm_transport
187
+
188
+ # Writes an RDP document to the local file system.
189
+ #
190
+ # @param opts [Hash] file options
191
+ # @option opts [true,false] :mac whether or not the document is for a
192
+ # Mac system
193
+ # @api private
194
+ def create_rdp_doc(opts = {})
195
+ content = Util.outdent!(<<-RDP)
196
+ full address:s:#{URI.parse(endpoint).host}:#{rdp_port}
197
+ prompt for credentials:i:1
198
+ username:s:#{options[:user]}
199
+ RDP
200
+ content.prepend("drivestoredirect:s:*\n") if opts[:mac]
201
+
202
+ File.open(rdp_doc_path, "wb") { |f| f.write(content) }
203
+
204
+ if logger.debug?
205
+ debug("Creating RDP document for #{instance_name} (#{rdp_doc_path})")
206
+ debug("------------")
207
+ IO.read(rdp_doc_path).each_line { |l| debug("#{l.chomp}") }
208
+ debug("------------")
209
+ end
210
+ end
211
+
212
+ # Execute a Powershell script over WinRM and return the command's
213
+ # exit code and standard error.
214
+ #
215
+ # @param command [String] Powershell script to execute
216
+ # @return [[Integer,String]] an array containing the exit code of the
217
+ # script and the standard error stream
218
+ # @api private
219
+ def execute_with_exit_code(command)
220
+ response = session.run_powershell_script(command) do |stdout, _|
221
+ logger << stdout if stdout
222
+ end
223
+
224
+ [response[:exitcode], response.stderr]
225
+ end
226
+
227
+ # @return [Winrm::FileTransporter] a file transporter
228
+ # @api private
229
+ def file_transporter
230
+ @file_transporter ||= WinRM::FS::Core::FileTransporter.new(session)
231
+ end
232
+
233
+ # (see Base#init_options)
234
+ def init_options(options)
235
+ super
236
+ @instance_name = @options.delete(:instance_name)
237
+ @kitchen_root = @options.delete(:kitchen_root)
238
+ @endpoint = @options.delete(:endpoint)
239
+ @rdp_port = @options.delete(:rdp_port)
240
+ @winrm_transport = @options.delete(:winrm_transport)
241
+ @connection_retries = @options.delete(:connection_retries)
242
+ @connection_retry_sleep = @options.delete(:connection_retry_sleep)
243
+ @max_wait_until_ready = @options.delete(:max_wait_until_ready)
244
+ end
245
+
246
+ # Logs formatted standard error output at the warning level.
247
+ #
248
+ # @param stderr [String] standard error output
249
+ # @api private
250
+ def log_stderr_on_warn(stderr)
251
+ error_regexp = /<S S=\"Error\">/
252
+
253
+ if error_regexp.match(stderr)
254
+ stderr.
255
+ split(error_regexp)[1..-2].
256
+ map! { |line| line.sub(/_x000D__x000A_<\/S>/, "").rstrip }.
257
+ each { |line| logger.warn(line) }
258
+ else
259
+ stderr.
260
+ split("\r\n").
261
+ each { |line| logger.warn(line) }
262
+ end
263
+ end
264
+
265
+ # Builds a `LoginCommand` for use by Linux-based platforms.
266
+ #
267
+ # TODO: determine whether or not `desktop` exists
268
+ #
269
+ # @return [LoginCommand] a login command
270
+ # @api private
271
+ def login_command_for_linux
272
+ args = %W[-u #{options[:user]}]
273
+ args += %W[-p #{options[:pass]}] if options.key?(:pass)
274
+ args += %W[#{URI.parse(endpoint).host}:#{rdp_port}]
275
+
276
+ LoginCommand.new("rdesktop", args)
277
+ end
278
+
279
+ # Builds a `LoginCommand` for use by Mac-based platforms.
280
+ #
281
+ # @return [LoginCommand] a login command
282
+ # @api private
283
+ def login_command_for_mac
284
+ create_rdp_doc(:mac => true)
285
+
286
+ LoginCommand.new("open", rdp_doc_path)
287
+ end
288
+
289
+ # Builds a `LoginCommand` for use by Windows-based platforms.
290
+ #
291
+ # @return [LoginCommand] a login command
292
+ # @api private
293
+ def login_command_for_windows
294
+ create_rdp_doc
295
+
296
+ LoginCommand.new("mstsc", rdp_doc_path)
297
+ end
298
+
299
+ # @return [String] path to the local RDP document
300
+ # @api private
301
+ def rdp_doc_path
302
+ File.join(kitchen_root, ".kitchen", "#{instance_name}.rdp")
303
+ end
304
+
305
+ # Establishes a remote shell session, or establishes one when invoked
306
+ # the first time.
307
+ #
308
+ # @param retry_options [Hash] retry options for the initial connection
309
+ # @return [Winrm::CommandExecutor] the command executor session
310
+ # @api private
311
+ def session(retry_options = {})
312
+ @session ||= begin
313
+ opts = {
314
+ :retry_limit => connection_retries.to_i,
315
+ :retry_delay => connection_retry_sleep.to_i
316
+ }.merge(retry_options)
317
+
318
+ service_args = [endpoint, winrm_transport, options.merge(opts)]
319
+ @service = ::WinRM::WinRMWebService.new(*service_args)
320
+ @service.logger = logger
321
+ @service.create_executor
322
+ end
323
+ end
324
+
325
+ # String representation of object, reporting its connection details and
326
+ # configuration.
327
+ #
328
+ # @api private
329
+ def to_s
330
+ "#{winrm_transport}::#{endpoint}<#{options.inspect}>"
331
+ end
332
+
333
+ # takes a long (greater than 3000 characters) command and saves it to a
334
+ # file and uploads it to the test instance.
335
+ #
336
+ # @param command [String] a long command to be saved and uploaded
337
+ # @return [String] a command that executes the uploaded script
338
+ # @api private
339
+ def run_from_file_command(command)
340
+ temp_dir = Dir.mktmpdir("kitchen-long-script")
341
+ begin
342
+ script_name = "#{instance_name}-long_script.ps1"
343
+ script_path = File.join(temp_dir, script_name)
344
+
345
+ File.open(script_path, "wb") do |file|
346
+ file.write(command)
347
+ end
348
+
349
+ target_path = File.join("$env:TEMP", script_name)
350
+ upload(script_path, target_path)
351
+
352
+ %{powershell -ExecutionPolicy Bypass -File "#{target_path}"}
353
+ ensure
354
+ FileUtils.rmtree(temp_dir)
355
+ end
356
+ end
357
+ end
358
+
359
+ private
360
+
361
+ WINRM_SPEC_VERSION = ["~> 1.6"].freeze
362
+ WINRM_FS_SPEC_VERSION = ["~> 0.4.0"].freeze
363
+
364
+ # Builds the hash of options needed by the Connection object on
365
+ # construction.
366
+ #
367
+ # @param data [Hash] merged configuration and mutable state data
368
+ # @return [Hash] hash of connection options
369
+ # @api private
370
+ def connection_options(data)
371
+ opts = {
372
+ :instance_name => instance.name,
373
+ :kitchen_root => data[:kitchen_root],
374
+ :logger => logger,
375
+ :endpoint => data[:endpoint_template] % data,
376
+ :user => data[:username],
377
+ :pass => data[:password],
378
+ :rdp_port => data[:rdp_port],
379
+ :connection_retries => data[:connection_retries],
380
+ :connection_retry_sleep => data[:connection_retry_sleep],
381
+ :max_wait_until_ready => data[:max_wait_until_ready],
382
+ :winrm_transport => data[:winrm_transport]
383
+ }
384
+ opts.merge!(additional_transport_args(opts[:winrm_transport]))
385
+ opts
386
+ end
387
+
388
+ def additional_transport_args(transport_type)
389
+ case transport_type.to_sym
390
+ when :ssl, :negotiate
391
+ {
392
+ :no_ssl_peer_verification => true,
393
+ :disable_sspi => false,
394
+ :basic_auth_only => false
395
+ }
396
+ when :plaintext
397
+ {
398
+ :disable_sspi => true,
399
+ :basic_auth_only => true
400
+ }
401
+ else
402
+ {}
403
+ end
404
+ end
405
+
406
+ # Creates a new WinRM Connection instance and save it for potential
407
+ # future reuse.
408
+ #
409
+ # @param options [Hash] conneciton options
410
+ # @return [Ssh::Connection] a WinRM Connection instance
411
+ # @api private
412
+ def create_new_connection(options, &block)
413
+ if @connection
414
+ logger.debug("[WinRM] shutting previous connection #{@connection}")
415
+ @connection.close
416
+ end
417
+
418
+ @connection_options = options
419
+ @connection = Kitchen::Transport::Winrm::Connection.new(options, &block)
420
+ end
421
+
422
+ # (see Base#load_needed_dependencies!)
423
+ def load_needed_dependencies!
424
+ super
425
+ load_with_rescue!("winrm", WINRM_SPEC_VERSION.dup)
426
+ load_with_rescue!("winrm-fs", WINRM_FS_SPEC_VERSION.dup)
427
+ end
428
+
429
+ def load_with_rescue!(gem_name, spec_version)
430
+ logger.debug("#{gem_name} requested," \
431
+ " loading #{gem_name} gem (#{spec_version})")
432
+ attempt_load = false
433
+ gem gem_name, spec_version
434
+ silence_warnings { attempt_load = require gem_name }
435
+ if attempt_load
436
+ logger.debug("#{gem_name} is loaded.")
437
+ else
438
+ logger.debug("#{gem_name} was already loaded.")
439
+ end
440
+ rescue LoadError => e
441
+ message = fail_to_load_gem_message(gem_name,
442
+ spec_version)
443
+ logger.fatal(message)
444
+ raise UserError,
445
+ "Could not load or activate #{gem_name}. (#{e.message})"
446
+ end
447
+
448
+ def fail_to_load_gem_message(name, version = nil)
449
+ version_cmd = "--version '#{version}'" if version
450
+ version_file = "', '#{version}"
451
+
452
+ "The `#{name}` gem is missing and must" \
453
+ " be installed or cannot be properly activated. Run" \
454
+ " `gem install #{name} #{version_cmd}`" \
455
+ " or add the following to your Gemfile if you are using Bundler:" \
456
+ " `gem '#{name} #{version_file}'`."
457
+ end
458
+
459
+ def host_os_windows?
460
+ case RbConfig::CONFIG["host_os"]
461
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
462
+ true
463
+ else
464
+ false
465
+ end
466
+ end
467
+
468
+ # Return the last saved WinRM connection instance.
469
+ #
470
+ # @return [Winrm::Connection] a WinRM Connection instance
471
+ # @api private
472
+ def reuse_connection
473
+ logger.debug("[WinRM] reusing existing connection #{@connection}")
474
+ yield @connection if block_given?
475
+ @connection
476
+ end
477
+
478
+ def silence_warnings
479
+ old_verbose, $VERBOSE = $VERBOSE, nil
480
+ yield
481
+ ensure
482
+ $VERBOSE = old_verbose
483
+ end
484
+ end
485
+ end
486
+ end