test-kitchen-rsync 3.0.0.pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +21 -0
  3. data/LICENSE +15 -0
  4. data/Rakefile +53 -0
  5. data/bin/zl-kitchen +11 -0
  6. data/lib/kitchen/base64_stream.rb +48 -0
  7. data/lib/kitchen/chef_utils_wiring.rb +40 -0
  8. data/lib/kitchen/cli.rb +413 -0
  9. data/lib/kitchen/collection.rb +52 -0
  10. data/lib/kitchen/color.rb +63 -0
  11. data/lib/kitchen/command/action.rb +41 -0
  12. data/lib/kitchen/command/console.rb +54 -0
  13. data/lib/kitchen/command/diagnose.rb +84 -0
  14. data/lib/kitchen/command/doctor.rb +39 -0
  15. data/lib/kitchen/command/exec.rb +37 -0
  16. data/lib/kitchen/command/list.rb +148 -0
  17. data/lib/kitchen/command/login.rb +39 -0
  18. data/lib/kitchen/command/package.rb +32 -0
  19. data/lib/kitchen/command/sink.rb +50 -0
  20. data/lib/kitchen/command/test.rb +47 -0
  21. data/lib/kitchen/command.rb +207 -0
  22. data/lib/kitchen/config.rb +344 -0
  23. data/lib/kitchen/configurable.rb +616 -0
  24. data/lib/kitchen/data_munger.rb +1024 -0
  25. data/lib/kitchen/diagnostic.rb +138 -0
  26. data/lib/kitchen/driver/base.rb +133 -0
  27. data/lib/kitchen/driver/dummy.rb +105 -0
  28. data/lib/kitchen/driver/exec.rb +70 -0
  29. data/lib/kitchen/driver/proxy.rb +70 -0
  30. data/lib/kitchen/driver/ssh_base.rb +351 -0
  31. data/lib/kitchen/driver.rb +40 -0
  32. data/lib/kitchen/errors.rb +243 -0
  33. data/lib/kitchen/generator/init.rb +254 -0
  34. data/lib/kitchen/instance.rb +726 -0
  35. data/lib/kitchen/lazy_hash.rb +148 -0
  36. data/lib/kitchen/lifecycle_hook/base.rb +78 -0
  37. data/lib/kitchen/lifecycle_hook/local.rb +53 -0
  38. data/lib/kitchen/lifecycle_hook/remote.rb +39 -0
  39. data/lib/kitchen/lifecycle_hooks.rb +92 -0
  40. data/lib/kitchen/loader/yaml.rb +377 -0
  41. data/lib/kitchen/logger.rb +422 -0
  42. data/lib/kitchen/logging.rb +52 -0
  43. data/lib/kitchen/login_command.rb +49 -0
  44. data/lib/kitchen/metadata_chopper.rb +49 -0
  45. data/lib/kitchen/platform.rb +64 -0
  46. data/lib/kitchen/plugin.rb +76 -0
  47. data/lib/kitchen/plugin_base.rb +60 -0
  48. data/lib/kitchen/provisioner/base.rb +269 -0
  49. data/lib/kitchen/provisioner/chef/berkshelf.rb +116 -0
  50. data/lib/kitchen/provisioner/chef/common_sandbox.rb +350 -0
  51. data/lib/kitchen/provisioner/chef/policyfile.rb +163 -0
  52. data/lib/kitchen/provisioner/chef_apply.rb +121 -0
  53. data/lib/kitchen/provisioner/chef_base.rb +705 -0
  54. data/lib/kitchen/provisioner/chef_infra.rb +167 -0
  55. data/lib/kitchen/provisioner/chef_solo.rb +82 -0
  56. data/lib/kitchen/provisioner/chef_zero.rb +12 -0
  57. data/lib/kitchen/provisioner/dummy.rb +75 -0
  58. data/lib/kitchen/provisioner/shell.rb +157 -0
  59. data/lib/kitchen/provisioner.rb +42 -0
  60. data/lib/kitchen/rake_tasks.rb +80 -0
  61. data/lib/kitchen/shell_out.rb +90 -0
  62. data/lib/kitchen/ssh.rb +289 -0
  63. data/lib/kitchen/state_file.rb +112 -0
  64. data/lib/kitchen/suite.rb +48 -0
  65. data/lib/kitchen/thor_tasks.rb +63 -0
  66. data/lib/kitchen/transport/base.rb +236 -0
  67. data/lib/kitchen/transport/dummy.rb +78 -0
  68. data/lib/kitchen/transport/exec.rb +145 -0
  69. data/lib/kitchen/transport/ssh.rb +579 -0
  70. data/lib/kitchen/transport/winrm.rb +546 -0
  71. data/lib/kitchen/transport.rb +40 -0
  72. data/lib/kitchen/util.rb +229 -0
  73. data/lib/kitchen/verifier/base.rb +243 -0
  74. data/lib/kitchen/verifier/busser.rb +275 -0
  75. data/lib/kitchen/verifier/dummy.rb +75 -0
  76. data/lib/kitchen/verifier/shell.rb +99 -0
  77. data/lib/kitchen/verifier.rb +39 -0
  78. data/lib/kitchen/version.rb +20 -0
  79. data/lib/kitchen/which.rb +26 -0
  80. data/lib/kitchen.rb +152 -0
  81. data/lib/vendor/hash_recursive_merge.rb +79 -0
  82. data/support/busser_install_command.ps1 +14 -0
  83. data/support/busser_install_command.sh +21 -0
  84. data/support/chef-client-fail-if-update-handler.rb +15 -0
  85. data/support/chef_base_init_command.ps1 +18 -0
  86. data/support/chef_base_init_command.sh +1 -0
  87. data/support/chef_base_install_command.ps1 +85 -0
  88. data/support/chef_base_install_command.sh +229 -0
  89. data/support/download_helpers.sh +109 -0
  90. data/support/dummy-validation.pem +27 -0
  91. data/templates/driver/CHANGELOG.md.erb +3 -0
  92. data/templates/driver/Gemfile.erb +3 -0
  93. data/templates/driver/README.md.erb +64 -0
  94. data/templates/driver/Rakefile.erb +21 -0
  95. data/templates/driver/driver.rb.erb +23 -0
  96. data/templates/driver/gemspec.erb +29 -0
  97. data/templates/driver/gitignore.erb +17 -0
  98. data/templates/driver/license_apachev2.erb +15 -0
  99. data/templates/driver/license_lgplv3.erb +16 -0
  100. data/templates/driver/license_mit.erb +22 -0
  101. data/templates/driver/license_reserved.erb +5 -0
  102. data/templates/driver/tailor.erb +4 -0
  103. data/templates/driver/travis.yml.erb +11 -0
  104. data/templates/driver/version.rb.erb +12 -0
  105. data/templates/init/chefignore.erb +2 -0
  106. data/templates/init/kitchen.yml.erb +18 -0
  107. data/test-kitchen.gemspec +52 -0
  108. metadata +528 -0
@@ -0,0 +1,351 @@
1
+ #
2
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
+ #
4
+ # Copyright (C) 2012, Fletcher Nichol
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require "thor/util"
19
+
20
+ require_relative "../lazy_hash"
21
+ require_relative "../plugin_base"
22
+ require "benchmark" unless defined?(Benchmark)
23
+
24
+ module Kitchen
25
+ module Driver
26
+ # Legacy base class for a driver that uses SSH to communication with an
27
+ # instance. This class has been updated to use the Instance's Transport to
28
+ # issue commands and transfer files and no longer uses the `Kitchen:SSH`
29
+ # class directly.
30
+ #
31
+ # **NOTE:** Authors of new Drivers are encouraged to inherit from
32
+ # `Kitchen::Driver::Base` instead and existing Driver authors are
33
+ # encouraged to update their Driver class to inherit from
34
+ # `Kitchen::Driver::SSHBase`.
35
+ #
36
+ # A subclass must implement the following methods:
37
+ # * #create(state)
38
+ # * #destroy(state)
39
+ #
40
+ # @author Fletcher Nichol <fnichol@nichol.ca>
41
+ # @deprecated While all possible effort has been made to preserve the
42
+ # original behavior of this class, future improvements to the Driver,
43
+ # Transport, and Verifier subsystems may not be picked up in these
44
+ # Drivers. When legacy Driver::SSHBase support is removed, this class
45
+ # will no longer be available.
46
+ class SSHBase < Kitchen::Plugin::Base
47
+ include ShellOut
48
+ include Configurable
49
+ include Logging
50
+
51
+ default_config :sudo, true
52
+ default_config :port, 22
53
+ # needs to be one less than the configured sshd_config MaxSessions
54
+ default_config :max_ssh_sessions, 9
55
+
56
+ # Creates a new Driver object using the provided configuration data
57
+ # which will be merged with any default configuration.
58
+ #
59
+ # @param config [Hash] provided driver configuration
60
+ def initialize(config = {})
61
+ init_config(config)
62
+ end
63
+
64
+ # (see Base#create)
65
+ def create(state) # rubocop:disable Lint/UnusedMethodArgument
66
+ raise ClientError, "#{self.class}#create must be implemented"
67
+ end
68
+
69
+ # (see Base#converge)
70
+ def converge(state) # rubocop:disable Metrics/AbcSize
71
+ provisioner = instance.provisioner
72
+ provisioner.create_sandbox
73
+ sandbox_dirs = Util.list_directory(provisioner.sandbox_path)
74
+
75
+ instance.transport.connection(backcompat_merged_state(state)) do |conn|
76
+ conn.execute(env_cmd(provisioner.install_command))
77
+ conn.execute(env_cmd(provisioner.init_command))
78
+ info("Transferring files to #{instance.to_str}")
79
+ conn.upload(sandbox_dirs, provisioner[:root_path])
80
+ debug("Transfer complete")
81
+ conn.execute(env_cmd(provisioner.prepare_command))
82
+ conn.execute(env_cmd(provisioner.run_command))
83
+ info("Downloading files from #{instance.to_str}")
84
+ provisioner[:downloads].to_h.each do |remotes, local|
85
+ debug("Downloading #{Array(remotes).join(", ")} to #{local}")
86
+ conn.download(remotes, local)
87
+ end
88
+ debug("Download complete")
89
+ end
90
+ rescue Kitchen::Transport::TransportFailed => ex
91
+ raise ActionFailed, ex.message
92
+ ensure
93
+ instance.provisioner.cleanup_sandbox
94
+ end
95
+
96
+ # (see Base#setup)
97
+ def setup(state)
98
+ verifier = instance.verifier
99
+
100
+ instance.transport.connection(backcompat_merged_state(state)) do |conn|
101
+ conn.execute(env_cmd(verifier.install_command))
102
+ end
103
+ rescue Kitchen::Transport::TransportFailed => ex
104
+ raise ActionFailed, ex.message
105
+ end
106
+
107
+ # (see Base#verify)
108
+ def verify(state) # rubocop:disable Metrics/AbcSize
109
+ verifier = instance.verifier
110
+ verifier.create_sandbox
111
+ sandbox_dirs = Util.list_directory(verifier.sandbox_path)
112
+
113
+ instance.transport.connection(backcompat_merged_state(state)) do |conn|
114
+ conn.execute(env_cmd(verifier.init_command))
115
+ info("Transferring files to #{instance.to_str}")
116
+ conn.upload(sandbox_dirs, verifier[:root_path])
117
+ debug("Transfer complete")
118
+ conn.execute(env_cmd(verifier.prepare_command))
119
+ conn.execute(env_cmd(verifier.run_command))
120
+ end
121
+ rescue Kitchen::Transport::TransportFailed => ex
122
+ raise ActionFailed, ex.message
123
+ ensure
124
+ instance.verifier.cleanup_sandbox
125
+ end
126
+
127
+ # (see Base#destroy)
128
+ def destroy(state) # rubocop:disable Lint/UnusedMethodArgument
129
+ raise ClientError, "#{self.class}#destroy must be implemented"
130
+ end
131
+
132
+ def legacy_state(state)
133
+ backcompat_merged_state(state)
134
+ end
135
+
136
+ # Package an instance.
137
+ #
138
+ # (see Base#package)
139
+ def package(state); end
140
+
141
+ # (see Base#login_command)
142
+ def login_command(state)
143
+ instance.transport.connection(backcompat_merged_state(state))
144
+ .login_command
145
+ end
146
+
147
+ # Executes an arbitrary command on an instance over an SSH connection.
148
+ #
149
+ # @param state [Hash] mutable instance and driver state
150
+ # @param command [String] the command to be executed
151
+ # @raise [ActionFailed] if the command could not be successfully completed
152
+ def remote_command(state, command)
153
+ instance.transport.connection(backcompat_merged_state(state)) do |conn|
154
+ conn.execute(env_cmd(command))
155
+ end
156
+ end
157
+
158
+ # **(Deprecated)** Executes a remote command over SSH.
159
+ #
160
+ # @param ssh_args [Array] ssh arguments
161
+ # @param command [String] remote command to invoke
162
+ # @deprecated This method should no longer be called directly and exists
163
+ # to support very old drivers. This will be removed in the future.
164
+ def ssh(ssh_args, command)
165
+ pseudo_state = { hostname: ssh_args[0], username: ssh_args[1] }
166
+ pseudo_state.merge!(ssh_args[2])
167
+ connection_state = backcompat_merged_state(pseudo_state)
168
+
169
+ instance.transport.connection(connection_state) do |conn|
170
+ conn.execute(env_cmd(command))
171
+ end
172
+ end
173
+
174
+ # Performs whatever tests that may be required to ensure that this driver
175
+ # will be able to function in the current environment. This may involve
176
+ # checking for the presence of certain directories, software installed,
177
+ # etc.
178
+ #
179
+ # @raise [UserError] if the driver will not be able to perform or if a
180
+ # documented dependency is missing from the system
181
+ def verify_dependencies; end
182
+
183
+ # Cache directory that a driver could implement to inform the provisioner
184
+ # that it can leverage it internally
185
+ #
186
+ # @return path [String] a path of the cache directory
187
+ def cache_directory; end
188
+
189
+ private
190
+
191
+ def backcompat_merged_state(state)
192
+ driver_ssh_keys = %w{
193
+ forward_agent hostname password port ssh_key username
194
+ }.map(&:to_sym)
195
+ config.select { |key, _| driver_ssh_keys.include?(key) }.rmerge(state)
196
+ end
197
+
198
+ # Builds arguments for constructing a `Kitchen::SSH` instance.
199
+ #
200
+ # @param state [Hash] state hash
201
+ # @return [Array] SSH constructor arguments
202
+ # @api private
203
+ def build_ssh_args(state)
204
+ combined = config.to_hash.merge(state)
205
+
206
+ opts = {}
207
+ opts[:user_known_hosts_file] = "/dev/null"
208
+ opts[:verify_host_key] = false
209
+ opts[:keys_only] = true if combined[:ssh_key]
210
+ opts[:password] = combined[:password] if combined[:password]
211
+ opts[:forward_agent] = combined[:forward_agent] if combined.key? :forward_agent
212
+ opts[:port] = combined[:port] if combined[:port]
213
+ opts[:keys] = Array(combined[:ssh_key]) if combined[:ssh_key]
214
+ opts[:logger] = logger
215
+
216
+ [combined[:hostname], combined[:username], opts]
217
+ end
218
+
219
+ # Adds http, https and ftp proxy environment variables to a command, if
220
+ # set in configuration data or on local workstation.
221
+ #
222
+ # @param cmd [String] command string
223
+ # @return [String] command string
224
+ # @api private
225
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize
226
+ def env_cmd(cmd)
227
+ return if cmd.nil?
228
+
229
+ env = "env"
230
+ http_proxy = config[:http_proxy] || ENV["http_proxy"] ||
231
+ ENV["HTTP_PROXY"]
232
+ https_proxy = config[:https_proxy] || ENV["https_proxy"] ||
233
+ ENV["HTTPS_PROXY"]
234
+ ftp_proxy = config[:ftp_proxy] || ENV["ftp_proxy"] ||
235
+ ENV["FTP_PROXY"]
236
+ no_proxy = if (!config[:http_proxy] && http_proxy) ||
237
+ (!config[:https_proxy] && https_proxy) ||
238
+ (!config[:ftp_proxy] && ftp_proxy)
239
+ ENV["no_proxy"] || ENV["NO_PROXY"]
240
+ end
241
+ env << " http_proxy=#{http_proxy}" if http_proxy
242
+ env << " https_proxy=#{https_proxy}" if https_proxy
243
+ env << " ftp_proxy=#{ftp_proxy}" if ftp_proxy
244
+ env << " no_proxy=#{no_proxy}" if no_proxy
245
+
246
+ env == "env" ? cmd : "#{env} #{cmd}"
247
+ end
248
+
249
+ # Executes a remote command over SSH.
250
+ #
251
+ # @param command [String] remove command to run
252
+ # @param connection [Kitchen::SSH] an SSH connection
253
+ # @raise [ActionFailed] if an exception occurs
254
+ # @api private
255
+ def run_remote(command, connection)
256
+ return if command.nil?
257
+
258
+ connection.exec(env_cmd(command))
259
+ rescue SSHFailed, Net::SSH::Exception => ex
260
+ raise ActionFailed, ex.message
261
+ end
262
+
263
+ # Transfers one or more local paths over SSH.
264
+ #
265
+ # @param locals [Array<String>] array of local paths
266
+ # @param remote [String] remote destination path
267
+ # @param connection [Kitchen::SSH] an SSH connection
268
+ # @raise [ActionFailed] if an exception occurs
269
+ # @api private
270
+ def transfer_path(locals, remote, connection)
271
+ return if locals.nil? || Array(locals).empty?
272
+
273
+ info("Transferring files to #{instance.to_str}")
274
+ debug("TIMING: scp asynch upload (Kitchen::Driver::SSHBase)")
275
+ elapsed = Benchmark.measure do
276
+ transfer_path_async(locals, remote, connection)
277
+ end
278
+ delta = Util.duration(elapsed.real)
279
+ debug("TIMING: scp async upload (Kitchen::Driver::SSHBase) took #{delta}")
280
+ debug("Transfer complete")
281
+ rescue SSHFailed, Net::SSH::Exception => ex
282
+ raise ActionFailed, ex.message
283
+ end
284
+
285
+ def transfer_path_async(locals, remote, connection)
286
+ waits = []
287
+ locals.map do |local|
288
+ waits.push connection.upload_path(local, remote)
289
+ waits.shift.wait while waits.length >= config[:max_ssh_sessions]
290
+ end
291
+ waits.each(&:wait)
292
+ end
293
+
294
+ # Blocks until a TCP socket is available where a remote SSH server
295
+ # should be listening.
296
+ #
297
+ # @param hostname [String] remote SSH server host
298
+ # @param username [String] SSH username (default: `nil`)
299
+ # @param options [Hash] configuration hash (default: `{}`)
300
+ # @api private
301
+ def wait_for_sshd(hostname, username = nil, options = {})
302
+ pseudo_state = { hostname: hostname }
303
+ pseudo_state[:username] = username if username
304
+ pseudo_state.merge!(options)
305
+
306
+ instance.transport.connection(backcompat_merged_state(pseudo_state))
307
+ .wait_until_ready
308
+ end
309
+
310
+ # Intercepts any bare #puts calls in subclasses and issues an INFO log
311
+ # event instead.
312
+ #
313
+ # @param msg [String] message string
314
+ def puts(msg)
315
+ info(msg)
316
+ end
317
+
318
+ # Intercepts any bare #print calls in subclasses and issues an INFO log
319
+ # event instead.
320
+ #
321
+ # @param msg [String] message string
322
+ def print(msg)
323
+ info(msg)
324
+ end
325
+
326
+ # Delegates to Kitchen::ShellOut.run_command, overriding some default
327
+ # options:
328
+ #
329
+ # * `:use_sudo` defaults to the value of `config[:use_sudo]` in the
330
+ # Driver object
331
+ # * `:log_subject` defaults to a String representation of the Driver's
332
+ # class name
333
+ #
334
+ # @see ShellOut#run_command
335
+ def run_command(cmd, options = {})
336
+ base_options = {
337
+ use_sudo: config[:use_sudo],
338
+ log_subject: Thor::Util.snake_case(self.class.to_s),
339
+ }.merge(options)
340
+ super(cmd, base_options)
341
+ end
342
+
343
+ # Returns the Busser object associated with the driver.
344
+ #
345
+ # @return [Busser] a busser
346
+ def busser
347
+ instance.verifier
348
+ end
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,40 @@
1
+ #
2
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
+ #
4
+ # Copyright (C) 2012, Fletcher Nichol
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require_relative "plugin"
19
+
20
+ module Kitchen
21
+ # A driver is responsible for carrying out the lifecycle activities of an
22
+ # instance, such as creating and destroying an instance.
23
+ #
24
+ # @author Fletcher Nichol <fnichol@nichol.ca>
25
+ module Driver
26
+ # Default driver plugin to use
27
+ DEFAULT_PLUGIN = "dummy".freeze
28
+
29
+ # Returns an instance of a driver given a plugin type string.
30
+ #
31
+ # @param plugin [String] a driver plugin type, which will be constantized
32
+ # @param config [Hash] a configuration hash to initialize the driver
33
+ # @return [Driver::Base] a driver instance
34
+ # @raise [ClientError] if a driver instance could not be created
35
+ # @raise [UserError] if the driver's dependencies could not be met
36
+ def self.for_plugin(plugin, config)
37
+ Kitchen::Plugin.load(self, plugin, config)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,243 @@
1
+ #
2
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
+ #
4
+ # Copyright (C) 2013, Fletcher Nichol
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require "English"
19
+
20
+ module Kitchen
21
+ # All Kitchen errors and exceptions.
22
+ #
23
+ # @author Fletcher Nichol <fnichol@nichol.ca>
24
+ module Error
25
+ # Creates an array of strings, representing a formatted exception,
26
+ # containing backtrace and nested exception info as necessary, that can
27
+ # be viewed by a human.
28
+ #
29
+ # For example:
30
+ #
31
+ # ------Exception-------
32
+ # Class: Kitchen::StandardError
33
+ # Message: Failure starting the party
34
+ # ---Nested Exception---
35
+ # Class: IOError
36
+ # Message: not enough directories for a party
37
+ # ------Backtrace-------
38
+ # nil
39
+ # ----------------------
40
+ #
41
+ # @param exception [::StandardError] an exception
42
+ # @return [Array<String>] a formatted message
43
+ def self.formatted_trace(exception, title = "Exception")
44
+ arr = formatted_exception(exception, title).dup
45
+ arr += formatted_backtrace(exception)
46
+
47
+ if exception.respond_to?(:original) && exception.original
48
+ arr += if exception.original.is_a? Array
49
+ exception.original.map do |composite_exception|
50
+ formatted_trace(composite_exception, "Composite Exception").flatten
51
+ end
52
+ else
53
+ [
54
+ formatted_exception(exception.original, "Nested Exception"),
55
+ formatted_backtrace(exception),
56
+ ].flatten
57
+ end
58
+ end
59
+ arr.flatten
60
+ end
61
+
62
+ def self.formatted_backtrace(exception)
63
+ if exception.backtrace.nil?
64
+ []
65
+ else
66
+ [
67
+ "Backtrace".center(22, "-"),
68
+ exception.backtrace,
69
+ "End Backtrace".center(22, "-"),
70
+ ]
71
+ end
72
+ end
73
+
74
+ # Creates an array of strings, representing a formatted exception that
75
+ # can be viewed by a human. Thanks to MiniTest for the inspiration
76
+ # upon which this output has been designed.
77
+ #
78
+ # For example:
79
+ #
80
+ # ------Exception-------
81
+ # Class: Kitchen::StandardError
82
+ # Message: I have failed you
83
+ # ----------------------
84
+ #
85
+ # @param exception [::StandardError] an exception
86
+ # @param title [String] a custom title for the message
87
+ # (default: `"Exception"`)
88
+ # @return [Array<String>] a formatted message
89
+ def self.formatted_exception(exception, title = "Exception")
90
+ [
91
+ title.center(22, "-"),
92
+ "Class: #{exception.class}",
93
+ "Message: #{exception.message}",
94
+ "".center(22, "-"),
95
+ ]
96
+ end
97
+ end
98
+
99
+ # Base exception class from which all Kitchen exceptions derive. This class
100
+ # nests an exception when this class is re-raised from a rescue block.
101
+ class StandardError < ::StandardError
102
+ include Error
103
+
104
+ # @return [::StandardError] the original (wrapped) exception
105
+ attr_reader :original
106
+
107
+ # Creates a new StandardError exception which optionally wraps an original
108
+ # exception if given or detected by checking the `$!` global variable.
109
+ #
110
+ # @param msg [String] exception message
111
+ # @param original [::StandardError] an original exception which will be
112
+ # wrapped (default: `$ERROR_INFO`)
113
+ def initialize(msg, original = $ERROR_INFO)
114
+ super(msg)
115
+ @original = original
116
+ end
117
+ end
118
+
119
+ # Base exception class for all exceptions that are caused by user input
120
+ # errors.
121
+ class UserError < StandardError; end
122
+
123
+ # Base exception class for all exceptions that are caused by incorrect use
124
+ # of an API.
125
+ class ClientError < StandardError; end
126
+
127
+ # Base exception class for exceptions that are caused by external library
128
+ # failures which may be temporary.
129
+ class TransientFailure < StandardError; end
130
+
131
+ # Exception class for any exceptions raised when performing an instance
132
+ # action.
133
+ class ActionFailed < TransientFailure; end
134
+
135
+ # Exception class capturing what caused an instance to die.
136
+ class InstanceFailure < TransientFailure; end
137
+
138
+ # Yields to a code block in order to consistently emit a useful crash/error
139
+ # message and exit appropriately. There are two primary failure conditions:
140
+ # an expected instance failure, and any other unexpected failures.
141
+ #
142
+ # **Note** This method may call `Kernel.exit` so may not return if the
143
+ # yielded code block raises an exception.
144
+ #
145
+ # ## Instance Failure
146
+ #
147
+ # This is an expected failure scenario which could happen if an instance
148
+ # couldn't be created, a Chef run didn't successfully converge, a
149
+ # post-convergence test suite failed, etc. In other words, you can count on
150
+ # encountering these failures all the time--this is Kitchen's worldview:
151
+ # crash early and often. In this case a cleanly formatted exception is
152
+ # written to `STDERR` and the exception message is written to
153
+ # the common Kitchen file logger.
154
+ #
155
+ # ## Unexpected Failure
156
+ #
157
+ # All other forms of `Kitchen::Error` exceptions are considered unexpected
158
+ # or unplanned exceptions, typically from user configuration errors, driver
159
+ # or provisioner coding issues or bugs, or internal code issues. Given
160
+ # a stable release of Kitchen and a solid set of drivers and provisioners,
161
+ # the most likely cause of this is user configuration error originating in
162
+ # the `.kitchen.yml` setup. For this reason, the exception is written to
163
+ # `STDERR`, a full formatted exception trace is written to the common
164
+ # Kitchen file logger, and a message is displayed on `STDERR` to the user
165
+ # informing them to check the log files and check their configuration with
166
+ # the `kitchen diagnose` subcommand.
167
+ #
168
+ # @raise [SystemExit] if an exception is raised in the yielded block
169
+ def self.with_friendly_errors
170
+ yield
171
+ rescue Kitchen::InstanceFailure => e
172
+ Kitchen.mutex.synchronize do
173
+ handle_instance_failure(e)
174
+ end
175
+ exit 10
176
+ rescue Kitchen::Error => e
177
+ Kitchen.mutex.synchronize do
178
+ handle_error(e)
179
+ end
180
+ exit 20
181
+ end
182
+
183
+ # Writes an array of lines to the common Kitchen logger's file device at the
184
+ # given severity level. If the Kitchen logger is set to debug severity, then
185
+ # the array of lines will also be written to the console output.
186
+ #
187
+ # @param level [Symbol,String] the desired log level
188
+ # @param lines [Array<String>] an array of strings to log
189
+ # @api private
190
+ def self.file_log(level, lines)
191
+ Array(lines).each do |line|
192
+ if Kitchen.logger.debug?
193
+ Kitchen.logger.debug(line)
194
+ else
195
+ Kitchen.logger.logdev && Kitchen.logger.logdev.public_send(level, line)
196
+ end
197
+ end
198
+ end
199
+
200
+ # Writes an array of lines to the `STDERR` device.
201
+ #
202
+ # @param lines [Array<String>] an array of strings to log
203
+ # @api private
204
+ def self.stderr_log(lines)
205
+ Array(lines).map { |line| ">>>>>> #{line}" }.each do |line|
206
+ line = Color.colorize(line, :red) if Kitchen.tty?
207
+ $stderr.puts(line)
208
+ end
209
+ end
210
+
211
+ # Writes an array of lines to the common Kitchen debugger with debug
212
+ # severity.
213
+ #
214
+ # @param lines [Array<String>] an array of strings to log
215
+ # @api private
216
+ def self.debug_log(lines)
217
+ Array(lines).each { |line| Kitchen.logger.debug(line) }
218
+ end
219
+
220
+ # Handles an instance failure exception.
221
+ #
222
+ # @param e [StandardError] an exception to handle
223
+ # @see Kitchen.with_friendly_errors
224
+ # @api private
225
+ def self.handle_instance_failure(e)
226
+ stderr_log(e.message.split(/\s{2,}/))
227
+ stderr_log(Error.formatted_exception(e.original))
228
+ file_log(:error, e.message.split(/\s{2,}/).first)
229
+ debug_log(Error.formatted_trace(e))
230
+ end
231
+
232
+ # Handles an unexpected failure exception.
233
+ #
234
+ # @param e [StandardError] an exception to handle
235
+ # @see Kitchen.with_friendly_errors
236
+ # @api private
237
+ def self.handle_error(e)
238
+ stderr_log(Error.formatted_exception(e))
239
+ stderr_log("Please see .kitchen/logs/kitchen.log for more details")
240
+ stderr_log("Also try running `kitchen diagnose --all` for configuration\n")
241
+ file_log(:error, Error.formatted_trace(e))
242
+ end
243
+ end