test-kitchen 1.6.0 → 1.7.0

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +8 -7
  3. data/.github/ISSUE_TEMPLATE.md +56 -0
  4. data/.gitignore +28 -27
  5. data/.kitchen.ci.yml +23 -0
  6. data/.kitchen.proxy.yml +27 -0
  7. data/.rubocop.yml +3 -3
  8. data/.travis.yml +70 -53
  9. data/.yardopts +3 -3
  10. data/Berksfile +3 -0
  11. data/CHANGELOG.md +1083 -1051
  12. data/CONTRIBUTING.md +14 -14
  13. data/Gemfile +19 -14
  14. data/Gemfile.proxy_tests +4 -5
  15. data/Guardfile +42 -42
  16. data/LICENSE +15 -15
  17. data/MAINTAINERS.md +23 -24
  18. data/README.md +135 -135
  19. data/Rakefile +61 -76
  20. data/appveyor.yml +44 -34
  21. data/features/kitchen_action_commands.feature +164 -164
  22. data/features/kitchen_command.feature +16 -16
  23. data/features/kitchen_console_command.feature +34 -34
  24. data/features/kitchen_defaults.feature +38 -38
  25. data/features/kitchen_diagnose_command.feature +96 -96
  26. data/features/kitchen_driver_create_command.feature +64 -64
  27. data/features/kitchen_driver_discover_command.feature +25 -25
  28. data/features/kitchen_help_command.feature +16 -16
  29. data/features/kitchen_init_command.feature +274 -274
  30. data/features/kitchen_list_command.feature +104 -104
  31. data/features/kitchen_login_command.feature +62 -62
  32. data/features/kitchen_sink_command.feature +30 -30
  33. data/features/kitchen_test_command.feature +88 -88
  34. data/features/step_definitions/gem_steps.rb +36 -36
  35. data/features/step_definitions/git_steps.rb +5 -5
  36. data/features/step_definitions/output_steps.rb +5 -5
  37. data/features/support/env.rb +75 -75
  38. data/lib/kitchen.rb +150 -150
  39. data/lib/kitchen/base64_stream.rb +55 -55
  40. data/lib/kitchen/cli.rb +419 -419
  41. data/lib/kitchen/collection.rb +55 -55
  42. data/lib/kitchen/color.rb +65 -65
  43. data/lib/kitchen/command.rb +185 -185
  44. data/lib/kitchen/command/action.rb +45 -45
  45. data/lib/kitchen/command/console.rb +58 -58
  46. data/lib/kitchen/command/diagnose.rb +92 -92
  47. data/lib/kitchen/command/driver_discover.rb +105 -105
  48. data/lib/kitchen/command/exec.rb +41 -41
  49. data/lib/kitchen/command/list.rb +119 -119
  50. data/lib/kitchen/command/login.rb +43 -43
  51. data/lib/kitchen/command/sink.rb +54 -54
  52. data/lib/kitchen/command/test.rb +51 -51
  53. data/lib/kitchen/config.rb +322 -322
  54. data/lib/kitchen/configurable.rb +529 -529
  55. data/lib/kitchen/data_munger.rb +959 -960
  56. data/lib/kitchen/diagnostic.rb +141 -141
  57. data/lib/kitchen/driver.rb +56 -56
  58. data/lib/kitchen/driver/base.rb +134 -134
  59. data/lib/kitchen/driver/dummy.rb +108 -108
  60. data/lib/kitchen/driver/proxy.rb +72 -72
  61. data/lib/kitchen/driver/ssh_base.rb +357 -357
  62. data/lib/kitchen/errors.rb +229 -229
  63. data/lib/kitchen/generator/driver_create.rb +177 -177
  64. data/lib/kitchen/generator/init.rb +296 -296
  65. data/lib/kitchen/instance.rb +662 -662
  66. data/lib/kitchen/lazy_hash.rb +142 -142
  67. data/lib/kitchen/loader/yaml.rb +349 -349
  68. data/lib/kitchen/logger.rb +423 -423
  69. data/lib/kitchen/logging.rb +56 -56
  70. data/lib/kitchen/login_command.rb +52 -52
  71. data/lib/kitchen/metadata_chopper.rb +52 -52
  72. data/lib/kitchen/platform.rb +67 -67
  73. data/lib/kitchen/provisioner.rb +54 -54
  74. data/lib/kitchen/provisioner/base.rb +236 -236
  75. data/lib/kitchen/provisioner/chef/berkshelf.rb +114 -114
  76. data/lib/kitchen/provisioner/chef/common_sandbox.rb +322 -322
  77. data/lib/kitchen/provisioner/chef/librarian.rb +112 -112
  78. data/lib/kitchen/provisioner/chef_apply.rb +124 -125
  79. data/lib/kitchen/provisioner/chef_base.rb +341 -294
  80. data/lib/kitchen/provisioner/chef_solo.rb +88 -89
  81. data/lib/kitchen/provisioner/chef_zero.rb +245 -245
  82. data/lib/kitchen/provisioner/dummy.rb +79 -79
  83. data/lib/kitchen/provisioner/shell.rb +138 -138
  84. data/lib/kitchen/rake_tasks.rb +63 -63
  85. data/lib/kitchen/shell_out.rb +93 -93
  86. data/lib/kitchen/ssh.rb +276 -276
  87. data/lib/kitchen/state_file.rb +120 -120
  88. data/lib/kitchen/suite.rb +51 -51
  89. data/lib/kitchen/thor_tasks.rb +66 -66
  90. data/lib/kitchen/transport.rb +54 -54
  91. data/lib/kitchen/transport/base.rb +176 -176
  92. data/lib/kitchen/transport/dummy.rb +79 -79
  93. data/lib/kitchen/transport/ssh.rb +364 -364
  94. data/lib/kitchen/transport/winrm.rb +486 -486
  95. data/lib/kitchen/util.rb +147 -147
  96. data/lib/kitchen/verifier.rb +55 -55
  97. data/lib/kitchen/verifier/base.rb +235 -235
  98. data/lib/kitchen/verifier/busser.rb +277 -277
  99. data/lib/kitchen/verifier/dummy.rb +79 -79
  100. data/lib/kitchen/verifier/shell.rb +101 -101
  101. data/lib/kitchen/version.rb +21 -21
  102. data/lib/vendor/hash_recursive_merge.rb +82 -82
  103. data/spec/kitchen/base64_stream_spec.rb +77 -77
  104. data/spec/kitchen/cli_spec.rb +56 -56
  105. data/spec/kitchen/collection_spec.rb +80 -80
  106. data/spec/kitchen/color_spec.rb +54 -54
  107. data/spec/kitchen/config_spec.rb +408 -408
  108. data/spec/kitchen/configurable_spec.rb +1095 -1062
  109. data/spec/kitchen/data_munger_spec.rb +2694 -2383
  110. data/spec/kitchen/diagnostic_spec.rb +129 -129
  111. data/spec/kitchen/driver/base_spec.rb +121 -121
  112. data/spec/kitchen/driver/dummy_spec.rb +199 -199
  113. data/spec/kitchen/driver/proxy_spec.rb +138 -138
  114. data/spec/kitchen/driver/ssh_base_spec.rb +1115 -1115
  115. data/spec/kitchen/driver_spec.rb +112 -112
  116. data/spec/kitchen/errors_spec.rb +309 -309
  117. data/spec/kitchen/instance_spec.rb +1419 -1419
  118. data/spec/kitchen/lazy_hash_spec.rb +117 -117
  119. data/spec/kitchen/loader/yaml_spec.rb +774 -774
  120. data/spec/kitchen/logger_spec.rb +429 -429
  121. data/spec/kitchen/logging_spec.rb +59 -59
  122. data/spec/kitchen/login_command_spec.rb +68 -68
  123. data/spec/kitchen/metadata_chopper_spec.rb +82 -82
  124. data/spec/kitchen/platform_spec.rb +89 -89
  125. data/spec/kitchen/provisioner/base_spec.rb +386 -386
  126. data/spec/kitchen/provisioner/chef_apply_spec.rb +136 -136
  127. data/spec/kitchen/provisioner/chef_base_spec.rb +1161 -1067
  128. data/spec/kitchen/provisioner/chef_solo_spec.rb +557 -557
  129. data/spec/kitchen/provisioner/chef_zero_spec.rb +1001 -1001
  130. data/spec/kitchen/provisioner/dummy_spec.rb +99 -99
  131. data/spec/kitchen/provisioner/shell_spec.rb +566 -566
  132. data/spec/kitchen/provisioner_spec.rb +107 -107
  133. data/spec/kitchen/shell_out_spec.rb +150 -150
  134. data/spec/kitchen/ssh_spec.rb +693 -693
  135. data/spec/kitchen/state_file_spec.rb +129 -129
  136. data/spec/kitchen/suite_spec.rb +62 -62
  137. data/spec/kitchen/transport/base_spec.rb +89 -89
  138. data/spec/kitchen/transport/ssh_spec.rb +1255 -1255
  139. data/spec/kitchen/transport/winrm_spec.rb +1143 -1143
  140. data/spec/kitchen/transport_spec.rb +112 -112
  141. data/spec/kitchen/util_spec.rb +165 -165
  142. data/spec/kitchen/verifier/base_spec.rb +362 -362
  143. data/spec/kitchen/verifier/busser_spec.rb +610 -610
  144. data/spec/kitchen/verifier/dummy_spec.rb +99 -99
  145. data/spec/kitchen/verifier/shell_spec.rb +160 -158
  146. data/spec/kitchen/verifier_spec.rb +120 -120
  147. data/spec/kitchen_spec.rb +114 -114
  148. data/spec/spec_helper.rb +85 -85
  149. data/spec/support/powershell_max_size_spec.rb +40 -40
  150. data/support/busser_install_command.ps1 +14 -14
  151. data/support/busser_install_command.sh +14 -14
  152. data/support/chef-client-zero.rb +77 -77
  153. data/support/chef_base_init_command.ps1 +18 -18
  154. data/support/chef_base_init_command.sh +2 -2
  155. data/support/chef_base_install_command.ps1 +85 -85
  156. data/support/chef_base_install_command.sh +229 -229
  157. data/support/chef_zero_prepare_command_legacy.ps1 +9 -9
  158. data/support/chef_zero_prepare_command_legacy.sh +10 -10
  159. data/support/download_helpers.sh +109 -109
  160. data/support/dummy-validation.pem +27 -27
  161. data/templates/driver/CHANGELOG.md.erb +3 -3
  162. data/templates/driver/Gemfile.erb +3 -3
  163. data/templates/driver/README.md.erb +64 -64
  164. data/templates/driver/Rakefile.erb +21 -21
  165. data/templates/driver/driver.rb.erb +23 -23
  166. data/templates/driver/gemspec.erb +29 -29
  167. data/templates/driver/gitignore.erb +17 -17
  168. data/templates/driver/license_apachev2.erb +15 -15
  169. data/templates/driver/license_lgplv3.erb +16 -16
  170. data/templates/driver/license_mit.erb +22 -22
  171. data/templates/driver/license_reserved.erb +5 -5
  172. data/templates/driver/tailor.erb +4 -4
  173. data/templates/driver/travis.yml.erb +11 -11
  174. data/templates/driver/version.rb.erb +12 -12
  175. data/templates/init/chefignore.erb +1 -1
  176. data/templates/init/kitchen.yml.erb +18 -18
  177. data/test-kitchen.gemspec +62 -62
  178. data/test/integration/default/default_spec.rb +3 -0
  179. data/testing_windows.md +37 -37
  180. metadata +23 -11
@@ -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", "kitchen")
350
- upload(script_path, target_path)
351
-
352
- %{powershell -ExecutionPolicy Bypass -File "#{File.join(target_path, script_name)}"}
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.3"].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