test-kitchen 1.6.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
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,662 +1,662 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
- #
5
- # Copyright (C) 2012, 2013, 2014, Fletcher Nichol
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
-
19
- require "benchmark"
20
- require "fileutils"
21
-
22
- module Kitchen
23
-
24
- # An instance of a suite running on a platform. A created instance may be a
25
- # local virtual machine, cloud instance, container, or even a bare metal
26
- # server, which is determined by the platform's driver.
27
- #
28
- # @author Fletcher Nichol <fnichol@nichol.ca>
29
- class Instance
30
-
31
- include Logging
32
-
33
- class << self
34
-
35
- # @return [Hash] a hash of mutxes, arranged by Driver class names
36
- # @api private
37
- attr_accessor :mutexes
38
-
39
- # Generates a name for an instance given a suite and platform.
40
- #
41
- # @param suite [Suite,#name] a Suite
42
- # @param platform [Platform,#name] a Platform
43
- # @return [String] a normalized, consistent name for an instance
44
- def name_for(suite, platform)
45
- "#{suite.name}-#{platform.name}".gsub(%r{[_,/]}, "-").gsub(/\./, "")
46
- end
47
- end
48
-
49
- # @return [Suite] the test suite configuration
50
- attr_reader :suite
51
-
52
- # @return [Platform] the target platform configuration
53
- attr_reader :platform
54
-
55
- # @return [String] name of this instance
56
- attr_reader :name
57
-
58
- # @return [Driver::Base] driver object which will manage this instance's
59
- # lifecycle actions
60
- attr_reader :driver
61
-
62
- # @return [Provisioner::Base] provisioner object which will the setup
63
- # and invocation instructions for configuration management and other
64
- # automation tools
65
- attr_reader :provisioner
66
-
67
- # @return [Transport::Base] transport object which will communicate with
68
- # an instance.
69
- attr_reader :transport
70
-
71
- # @return [Verifier] verifier object for instance to manage the verifier
72
- # installation on this instance
73
- attr_reader :verifier
74
-
75
- # @return [Logger] the logger for this instance
76
- attr_reader :logger
77
-
78
- # Creates a new instance, given a suite and a platform.
79
- #
80
- # @param [Hash] options configuration for a new suite
81
- # @option options [Suite] :suite the suite (**Required**)
82
- # @option options [Platform] :platform the platform (**Required**)
83
- # @option options [Driver::Base] :driver the driver (**Required**)
84
- # @option options [Provisioner::Base] :provisioner the provisioner
85
- # @option options [Transport::Base] :transport the transport
86
- # (**Required**)
87
- # @option options [Verifier] :verifier the verifier logger (**Required**)
88
- # @option options [Logger] :logger the instance logger
89
- # (default: Kitchen.logger)
90
- # @option options [StateFile] :state_file the state file object to use
91
- # when tracking instance state (**Required**)
92
- # @raise [ClientError] if one or more required options are omitted
93
- def initialize(options = {})
94
- validate_options(options)
95
-
96
- @suite = options.fetch(:suite)
97
- @platform = options.fetch(:platform)
98
- @name = self.class.name_for(@suite, @platform)
99
- @driver = options.fetch(:driver)
100
- @provisioner = options.fetch(:provisioner)
101
- @transport = options.fetch(:transport)
102
- @verifier = options.fetch(:verifier)
103
- @logger = options.fetch(:logger) { Kitchen.logger }
104
- @state_file = options.fetch(:state_file)
105
-
106
- setup_driver
107
- setup_provisioner
108
- setup_transport
109
- setup_verifier
110
- end
111
-
112
- # Returns a displayable representation of the instance.
113
- #
114
- # @return [String] an instance display string
115
- def to_str
116
- "<#{name}>"
117
- end
118
-
119
- # Creates this instance.
120
- #
121
- # @see Driver::Base#create
122
- # @return [self] this instance, used to chain actions
123
- #
124
- # @todo rescue Driver::ActionFailed and return some kind of null object
125
- # to gracfully stop action chaining
126
- def create
127
- transition_to(:create)
128
- end
129
-
130
- # Converges this running instance.
131
- #
132
- # @see Provisioner::Base#call
133
- # @return [self] this instance, used to chain actions
134
- #
135
- # @todo rescue Driver::ActionFailed and return some kind of null object
136
- # to gracfully stop action chaining
137
- def converge
138
- transition_to(:converge)
139
- end
140
-
141
- # Sets up this converged instance for suite tests.
142
- #
143
- # @see Driver::Base#setup
144
- # @return [self] this instance, used to chain actions
145
- #
146
- # @todo rescue Driver::ActionFailed and return some kind of null object
147
- # to gracfully stop action chaining
148
- def setup
149
- transition_to(:setup)
150
- end
151
-
152
- # Verifies this set up instance by executing suite tests.
153
- #
154
- # @see Driver::Base#verify
155
- # @return [self] this instance, used to chain actions
156
- #
157
- # @todo rescue Driver::ActionFailed and return some kind of null object
158
- # to gracfully stop action chaining
159
- def verify
160
- transition_to(:verify)
161
- end
162
-
163
- # Destroys this instance.
164
- #
165
- # @see Driver::Base#destroy
166
- # @return [self] this instance, used to chain actions
167
- #
168
- # @todo rescue Driver::ActionFailed and return some kind of null object
169
- # to gracfully stop action chaining
170
- def destroy
171
- transition_to(:destroy)
172
- end
173
-
174
- # Tests this instance by creating, converging and verifying. If this
175
- # instance is running, it will be pre-emptively destroyed to ensure a
176
- # clean slate. The instance will be left post-verify in a running state.
177
- #
178
- # @param destroy_mode [Symbol] strategy used to cleanup after instance
179
- # has finished verifying (default: `:passing`)
180
- # @return [self] this instance, used to chain actions
181
- #
182
- # @todo rescue Driver::ActionFailed and return some kind of null object
183
- # to gracfully stop action chaining
184
- def test(destroy_mode = :passing)
185
- elapsed = Benchmark.measure do
186
- banner "Cleaning up any prior instances of #{to_str}"
187
- destroy
188
- banner "Testing #{to_str}"
189
- verify
190
- destroy if destroy_mode == :passing
191
- end
192
- info "Finished testing #{to_str} #{Util.duration(elapsed.real)}."
193
- self
194
- ensure
195
- destroy if destroy_mode == :always
196
- end
197
-
198
- # Logs in to this instance by invoking a system command, provided by the
199
- # instance's transport. This could be an SSH command, telnet, or serial
200
- # console session.
201
- #
202
- # **Note** This method calls exec and will not return.
203
- #
204
- # @see Kitchen::LoginCommand
205
- # @see Transport::Base::Connection#login_command
206
- def login
207
- state = state_file.read
208
- if state[:last_action].nil?
209
- raise UserError, "Instance #{to_str} has not yet been created"
210
- end
211
-
212
- lc = if legacy_ssh_base_driver?
213
- legacy_ssh_base_login(state)
214
- else
215
- transport.connection(state).login_command
216
- end
217
-
218
- debug(%{Login command: #{lc.command} #{lc.arguments.join(" ")} } \
219
- "(Options: #{lc.options})")
220
- Kernel.exec(*lc.exec_args)
221
- end
222
-
223
- # Executes an arbitrary command on this instance.
224
- #
225
- # @param command [String] a command string to execute
226
- def remote_exec(command)
227
- transport.connection(state_file.read) do |conn|
228
- conn.execute(command)
229
- end
230
- end
231
-
232
- # Returns a Hash of configuration and other useful diagnostic information.
233
- #
234
- # @return [Hash] a diagnostic hash
235
- def diagnose
236
- result = Hash.new
237
- [
238
- :platform, :state_file, :driver, :provisioner, :transport, :verifier
239
- ].each do |sym|
240
- obj = send(sym)
241
- result[sym] = obj.respond_to?(:diagnose) ? obj.diagnose : :unknown
242
- end
243
- result
244
- end
245
-
246
- # Returns a Hash of configuration and other useful diagnostic information
247
- # associated with plugins (such as loaded version, class name, etc.).
248
- #
249
- # @return [Hash] a diagnostic hash
250
- def diagnose_plugins
251
- result = Hash.new
252
- [:driver, :provisioner, :verifier, :transport].each do |sym|
253
- obj = send(sym)
254
- result[sym] = if obj.respond_to?(:diagnose_plugin)
255
- obj.diagnose_plugin
256
- else
257
- :unknown
258
- end
259
- end
260
- result
261
- end
262
-
263
- # Returns the last successfully completed action state of the instance.
264
- #
265
- # @return [String] a named action which was last successfully completed
266
- def last_action
267
- state_file.read[:last_action]
268
- end
269
-
270
- # Clean up any per-instance resources before exiting.
271
- #
272
- # @return [void]
273
- def cleanup!
274
- @transport.cleanup! if @transport
275
- end
276
-
277
- private
278
-
279
- # @return [StateFile] a state file object that can be read from or written
280
- # to
281
- # @api private
282
- attr_reader :state_file
283
-
284
- # Validate the initial internal state of this object and raising an
285
- # exception if any preconditions are not met.
286
- #
287
- # @param options[Hash] options hash passed into the constructor
288
- # @raise [ClientError] if any validations fail
289
- # @api private
290
- def validate_options(options)
291
- [
292
- :suite, :platform, :driver, :provisioner,
293
- :transport, :verifier, :state_file
294
- ].each do |k|
295
- next if options.key?(k)
296
-
297
- raise ClientError, "Instance#new requires option :#{k}"
298
- end
299
- end
300
-
301
- # Perform any final configuration or preparation needed for the driver
302
- # object carry out its duties.
303
- #
304
- # @api private
305
- def setup_driver
306
- @driver.finalize_config!(self)
307
-
308
- if driver.class.serial_actions
309
- Kitchen.mutex.synchronize do
310
- self.class.mutexes ||= Hash.new
311
- self.class.mutexes[driver.class] = Mutex.new
312
- end
313
- end
314
- end
315
-
316
- # Perform any final configuration or preparation needed for the provisioner
317
- # object carry out its duties.
318
- #
319
- # @api private
320
- def setup_provisioner
321
- @provisioner.finalize_config!(self)
322
- end
323
-
324
- # Perform any final configuration or preparation needed for the transport
325
- # object carry out its duties.
326
- #
327
- # @api private
328
- def setup_transport
329
- transport.finalize_config!(self)
330
- end
331
-
332
- # Perform any final configuration or preparation needed for the verifier
333
- # object carry out its duties.
334
- #
335
- # @api private
336
- def setup_verifier
337
- verifier.finalize_config!(self)
338
- end
339
-
340
- # Perform all actions in order from last state to desired state.
341
- #
342
- # @param desired [Symbol] a symbol representing the desired action state
343
- # @return [self] this instance, used to chain actions
344
- # @api private
345
- def transition_to(desired)
346
- result = nil
347
- FSM.actions(last_action, desired).each do |transition|
348
- result = send("#{transition}_action")
349
- end
350
- result
351
- end
352
-
353
- # Perform the create action.
354
- #
355
- # @see Driver::Base#create
356
- # @return [self] this instance, used to chain actions
357
- # @api private
358
- def create_action
359
- perform_action(:create, "Creating")
360
- end
361
-
362
- # Perform the converge action.
363
- #
364
- # @see Provisioner::Base#call
365
- # @return [self] this instance, used to chain actions
366
- # @api private
367
- def converge_action
368
- banner "Converging #{to_str}..."
369
- elapsed = action(:converge) do |state|
370
- if legacy_ssh_base_driver?
371
- legacy_ssh_base_converge(state)
372
- else
373
- provisioner.call(state)
374
- end
375
- end
376
- info("Finished converging #{to_str} #{Util.duration(elapsed.real)}.")
377
- self
378
- end
379
-
380
- # Perform the setup action.
381
- #
382
- # @see Driver::Base#setup
383
- # @return [self] this instance, used to chain actions
384
- # @api private
385
- def setup_action
386
- banner "Setting up #{to_str}..."
387
- elapsed = action(:setup) do |state|
388
- legacy_ssh_base_setup(state) if legacy_ssh_base_driver?
389
- end
390
- info("Finished setting up #{to_str} #{Util.duration(elapsed.real)}.")
391
- self
392
- end
393
-
394
- # returns true, if the verifier is busser
395
- def verifier_busser?(verifier)
396
- !defined?(Kitchen::Verifier::Busser).nil? && verifier.is_a?(Kitchen::Verifier::Busser)
397
- end
398
-
399
- # returns true, if the verifier is dummy
400
- def verifier_dummy?(verifier)
401
- !defined?(Kitchen::Verifier::Dummy).nil? && verifier.is_a?(Kitchen::Verifier::Dummy)
402
- end
403
-
404
- def use_legacy_ssh_verifier?(verifier)
405
- verifier_busser?(verifier) || verifier_dummy?(verifier)
406
- end
407
-
408
- # Perform the verify action.
409
- #
410
- # @see Driver::Base#verify
411
- # @return [self] this instance, used to chain actions
412
- # @api private
413
- def verify_action
414
- banner "Verifying #{to_str}..."
415
- elapsed = action(:verify) do |state|
416
- # use special handling for legacy driver
417
- if legacy_ssh_base_driver? && use_legacy_ssh_verifier?(verifier)
418
- legacy_ssh_base_verify(state)
419
- elsif legacy_ssh_base_driver?
420
- # read ssh options from legacy driver
421
- verifier.call(driver.legacy_state(state))
422
- else
423
- verifier.call(state)
424
- end
425
- end
426
- info("Finished verifying #{to_str} #{Util.duration(elapsed.real)}.")
427
- self
428
- end
429
-
430
- # Perform the destroy action.
431
- #
432
- # @see Driver::Base#destroy
433
- # @return [self] this instance, used to chain actions
434
- # @api private
435
- def destroy_action
436
- perform_action(:destroy, "Destroying") { state_file.destroy }
437
- end
438
-
439
- # Perform an arbitrary action and provide useful logging.
440
- #
441
- # @param verb [Symbol] the action to be performed
442
- # @param output_verb [String] a verb representing the action, suitable for
443
- # use in output logging
444
- # @yield perform optional work just after action has complted
445
- # @return [self] this instance, used to chain actions
446
- # @api private
447
- def perform_action(verb, output_verb)
448
- banner "#{output_verb} #{to_str}..."
449
- elapsed = action(verb) { |state| driver.public_send(verb, state) }
450
- info("Finished #{output_verb.downcase} #{to_str}" \
451
- " #{Util.duration(elapsed.real)}.")
452
- yield if block_given?
453
- self
454
- end
455
-
456
- # Times a call to an action block and handles any raised exceptions. This
457
- # method ensures that the last successfully completed action is persisted
458
- # to the state file. The last action state will either be the desired
459
- # action that is passed in or the previous action that was persisted to the
460
- # state file.
461
- #
462
- # @param what [Symbol] the action to be performed
463
- # @param block [Proc] a block to be called
464
- # @return [Benchmark::Tms] timing information for the given action
465
- # @raise [InstanceFailed] if a driver action fails to complete, signaled
466
- # by a driver raising an ActionFailed exception. Typical reasons for this
467
- # would be a driver create action failing, a chef convergence crashing
468
- # in normal course of development, failing acceptance tests in the
469
- # verify action, etc.
470
- # @raise [ActionFailed] if an unforseen or unplanned exception is raised.
471
- # This would usually indicate that a race condition was triggered, a
472
- # bug exists in a driver, provisioner, or core, a transient IO error
473
- # occured, etc.
474
- # @api private
475
- def action(what, &block)
476
- state = state_file.read
477
- elapsed = Benchmark.measure do
478
- synchronize_or_call(what, state, &block)
479
- end
480
- state[:last_action] = what.to_s
481
- elapsed
482
- rescue ActionFailed => e
483
- log_failure(what, e)
484
- raise(InstanceFailure, failure_message(what) +
485
- " Please see .kitchen/logs/#{name}.log for more details",
486
- e.backtrace)
487
- rescue Exception => e # rubocop:disable Lint/RescueException
488
- log_failure(what, e)
489
- raise ActionFailed,
490
- "Failed to complete ##{what} action: [#{e.message}]", e.backtrace
491
- ensure
492
- state_file.write(state)
493
- end
494
-
495
- # Runs a given action block through a common driver mutex if required or
496
- # runs it directly otherwise. If a driver class' `.serial_actions` array
497
- # includes the desired action, then the action must be run with a muxtex
498
- # lock. Otherwise, it is assumed that the action can happen concurrently,
499
- # or fully in parallel.
500
- #
501
- # @param what [Symbol] the action to be performed
502
- # @param state [Hash] a mutable state hash for this instance
503
- # @param block [Proc] a block to be called
504
- # @api private
505
- def synchronize_or_call(what, state, &block)
506
- if Array(driver.class.serial_actions).include?(what)
507
- debug("#{to_str} is synchronizing on #{driver.class}##{what}")
508
- self.class.mutexes[driver.class].synchronize do
509
- debug("#{to_str} is messaging #{driver.class}##{what}")
510
- block.call(state)
511
- end
512
- else
513
- block.call(state)
514
- end
515
- end
516
-
517
- # Writes a high level message for logging and/or output.
518
- #
519
- # In this case, all instance banner messages will be written to the common
520
- # Kitchen logger so that the high level flow of a run can be followed in
521
- # the kitchen.log file.
522
- #
523
- # @api private
524
- def banner(*args)
525
- Kitchen.logger.logdev && Kitchen.logger.logdev.banner(*args)
526
- super
527
- end
528
-
529
- # Logs a failure (message and backtrace) to the instance's file logger
530
- # to help with debugging and diagnosing issues without overwhelming the
531
- # console output in the default case (i.e. running kitchen with :info
532
- # level debugging).
533
- #
534
- # @param what [String] an action
535
- # @param e [Exception] an exception
536
- # @api private
537
- def log_failure(what, e)
538
- return if logger.logdev.nil?
539
-
540
- logger.logdev.error(failure_message(what))
541
- Error.formatted_trace(e).each { |line| logger.logdev.error(line) }
542
- end
543
-
544
- # Returns a string explaining what action failed, at a high level. Used
545
- # for displaying to end user.
546
- #
547
- # @param what [String] an action
548
- # @return [String] a failure message
549
- # @api private
550
- def failure_message(what)
551
- "#{what.capitalize} failed on instance #{to_str}."
552
- end
553
-
554
- # Invokes `Driver#converge` on a legacy Driver, which inherits from
555
- # `Kitchen::Driver::SSHBase`.
556
- #
557
- # @param state [Hash] mutable instance state
558
- # @deprecated When legacy Driver::SSHBase support is removed, the
559
- # `#converge` method will no longer be called on the Driver.
560
- # @api private
561
- def legacy_ssh_base_converge(state)
562
- warn("Running legacy converge for '#{driver.name}' Driver")
563
- # TODO: Document upgrade path and provide link
564
- # warn("Driver authors: please read http://example.com for more details.")
565
- driver.converge(state)
566
- end
567
-
568
- # @return [TrueClass,FalseClass] whether or not the Driver inherits from
569
- # `Kitchen::Driver::SSHBase`
570
- # @deprecated When legacy Driver::SSHBase support is removed, the
571
- # `#converge` method will no longer be called on the Driver.
572
- # @api private
573
- def legacy_ssh_base_driver?
574
- driver.class < Kitchen::Driver::SSHBase
575
- end
576
-
577
- # Invokes `Driver#login_command` on a legacy Driver, which inherits from
578
- # `Kitchen::Driver::SSHBase`.
579
- #
580
- # @param state [Hash] mutable instance state
581
- # @deprecated When legacy Driver::SSHBase support is removed, the
582
- # `#login_command` method will no longer be called on the Driver.
583
- # @api private
584
- def legacy_ssh_base_login(state)
585
- warn("Running legacy login for '#{driver.name}' Driver")
586
- # TODO: Document upgrade path and provide link
587
- # warn("Driver authors: please read http://example.com for more details.")
588
- driver.login_command(state)
589
- end
590
-
591
- # Invokes `Driver#setup` on a legacy Driver, which inherits from
592
- # `Kitchen::Driver::SSHBase`.
593
- #
594
- # @param state [Hash] mutable instance state
595
- # @deprecated When legacy Driver::SSHBase support is removed, the
596
- # `#setup` method will no longer be called on the Driver.
597
- # @api private
598
- def legacy_ssh_base_setup(state)
599
- warn("Running legacy setup for '#{driver.name}' Driver")
600
- # TODO: Document upgrade path and provide link
601
- # warn("Driver authors: please read http://example.com for more details.")
602
- driver.setup(state)
603
- end
604
-
605
- # Invokes `Driver#verify` on a legacy Driver, which inherits from
606
- # `Kitchen::Driver::SSHBase`.
607
- #
608
- # @param state [Hash] mutable instance state
609
- # @deprecated When legacy Driver::SSHBase support is removed, the
610
- # `#verify` method will no longer be called on the Driver.
611
- # @api private
612
- def legacy_ssh_base_verify(state)
613
- warn("Running legacy verify for '#{driver.name}' Driver")
614
- # TODO: Document upgrade path and provide link
615
- # warn("Driver authors: please read http://example.com for more details.")
616
- driver.verify(state)
617
- end
618
-
619
- # The simplest finite state machine pseudo-implementation needed to manage
620
- # an Instance.
621
- #
622
- # @api private
623
- # @author Fletcher Nichol <fnichol@nichol.ca>
624
- class FSM
625
-
626
- # Returns an Array of all transitions to bring an Instance from its last
627
- # reported transistioned state into the desired transitioned state.
628
- #
629
- # @param last [String,Symbol,nil] the last known transitioned state of
630
- # the Instance, defaulting to `nil` (for unknown or no history)
631
- # @param desired [String,Symbol] the desired transitioned state for the
632
- # Instance
633
- # @return [Array<Symbol>] an Array of transition actions to perform
634
- # @api private
635
- def self.actions(last = nil, desired)
636
- last_index = index(last)
637
- desired_index = index(desired)
638
-
639
- if last_index == desired_index || last_index > desired_index
640
- Array(TRANSITIONS[desired_index])
641
- else
642
- TRANSITIONS.slice(last_index + 1, desired_index - last_index)
643
- end
644
- end
645
-
646
- TRANSITIONS = [:destroy, :create, :converge, :setup, :verify]
647
-
648
- # Determines the index of a state in the state lifecycle vector. Woah.
649
- #
650
- # @param transition [Symbol,#to_sym] a state
651
- # @param [Integer] the index position
652
- # @api private
653
- def self.index(transition)
654
- if transition.nil?
655
- 0
656
- else
657
- TRANSITIONS.find_index { |t| t == transition.to_sym }
658
- end
659
- end
660
- end
661
- end
662
- end
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2012, 2013, 2014, Fletcher Nichol
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require "benchmark"
20
+ require "fileutils"
21
+
22
+ module Kitchen
23
+
24
+ # An instance of a suite running on a platform. A created instance may be a
25
+ # local virtual machine, cloud instance, container, or even a bare metal
26
+ # server, which is determined by the platform's driver.
27
+ #
28
+ # @author Fletcher Nichol <fnichol@nichol.ca>
29
+ class Instance
30
+
31
+ include Logging
32
+
33
+ class << self
34
+
35
+ # @return [Hash] a hash of mutxes, arranged by Driver class names
36
+ # @api private
37
+ attr_accessor :mutexes
38
+
39
+ # Generates a name for an instance given a suite and platform.
40
+ #
41
+ # @param suite [Suite,#name] a Suite
42
+ # @param platform [Platform,#name] a Platform
43
+ # @return [String] a normalized, consistent name for an instance
44
+ def name_for(suite, platform)
45
+ "#{suite.name}-#{platform.name}".gsub(%r{[_,/]}, "-").gsub(/\./, "")
46
+ end
47
+ end
48
+
49
+ # @return [Suite] the test suite configuration
50
+ attr_reader :suite
51
+
52
+ # @return [Platform] the target platform configuration
53
+ attr_reader :platform
54
+
55
+ # @return [String] name of this instance
56
+ attr_reader :name
57
+
58
+ # @return [Driver::Base] driver object which will manage this instance's
59
+ # lifecycle actions
60
+ attr_reader :driver
61
+
62
+ # @return [Provisioner::Base] provisioner object which will the setup
63
+ # and invocation instructions for configuration management and other
64
+ # automation tools
65
+ attr_reader :provisioner
66
+
67
+ # @return [Transport::Base] transport object which will communicate with
68
+ # an instance.
69
+ attr_reader :transport
70
+
71
+ # @return [Verifier] verifier object for instance to manage the verifier
72
+ # installation on this instance
73
+ attr_reader :verifier
74
+
75
+ # @return [Logger] the logger for this instance
76
+ attr_reader :logger
77
+
78
+ # Creates a new instance, given a suite and a platform.
79
+ #
80
+ # @param [Hash] options configuration for a new suite
81
+ # @option options [Suite] :suite the suite (**Required**)
82
+ # @option options [Platform] :platform the platform (**Required**)
83
+ # @option options [Driver::Base] :driver the driver (**Required**)
84
+ # @option options [Provisioner::Base] :provisioner the provisioner
85
+ # @option options [Transport::Base] :transport the transport
86
+ # (**Required**)
87
+ # @option options [Verifier] :verifier the verifier logger (**Required**)
88
+ # @option options [Logger] :logger the instance logger
89
+ # (default: Kitchen.logger)
90
+ # @option options [StateFile] :state_file the state file object to use
91
+ # when tracking instance state (**Required**)
92
+ # @raise [ClientError] if one or more required options are omitted
93
+ def initialize(options = {})
94
+ validate_options(options)
95
+
96
+ @suite = options.fetch(:suite)
97
+ @platform = options.fetch(:platform)
98
+ @name = self.class.name_for(@suite, @platform)
99
+ @driver = options.fetch(:driver)
100
+ @provisioner = options.fetch(:provisioner)
101
+ @transport = options.fetch(:transport)
102
+ @verifier = options.fetch(:verifier)
103
+ @logger = options.fetch(:logger) { Kitchen.logger }
104
+ @state_file = options.fetch(:state_file)
105
+
106
+ setup_driver
107
+ setup_provisioner
108
+ setup_transport
109
+ setup_verifier
110
+ end
111
+
112
+ # Returns a displayable representation of the instance.
113
+ #
114
+ # @return [String] an instance display string
115
+ def to_str
116
+ "<#{name}>"
117
+ end
118
+
119
+ # Creates this instance.
120
+ #
121
+ # @see Driver::Base#create
122
+ # @return [self] this instance, used to chain actions
123
+ #
124
+ # @todo rescue Driver::ActionFailed and return some kind of null object
125
+ # to gracfully stop action chaining
126
+ def create
127
+ transition_to(:create)
128
+ end
129
+
130
+ # Converges this running instance.
131
+ #
132
+ # @see Provisioner::Base#call
133
+ # @return [self] this instance, used to chain actions
134
+ #
135
+ # @todo rescue Driver::ActionFailed and return some kind of null object
136
+ # to gracfully stop action chaining
137
+ def converge
138
+ transition_to(:converge)
139
+ end
140
+
141
+ # Sets up this converged instance for suite tests.
142
+ #
143
+ # @see Driver::Base#setup
144
+ # @return [self] this instance, used to chain actions
145
+ #
146
+ # @todo rescue Driver::ActionFailed and return some kind of null object
147
+ # to gracfully stop action chaining
148
+ def setup
149
+ transition_to(:setup)
150
+ end
151
+
152
+ # Verifies this set up instance by executing suite tests.
153
+ #
154
+ # @see Driver::Base#verify
155
+ # @return [self] this instance, used to chain actions
156
+ #
157
+ # @todo rescue Driver::ActionFailed and return some kind of null object
158
+ # to gracfully stop action chaining
159
+ def verify
160
+ transition_to(:verify)
161
+ end
162
+
163
+ # Destroys this instance.
164
+ #
165
+ # @see Driver::Base#destroy
166
+ # @return [self] this instance, used to chain actions
167
+ #
168
+ # @todo rescue Driver::ActionFailed and return some kind of null object
169
+ # to gracfully stop action chaining
170
+ def destroy
171
+ transition_to(:destroy)
172
+ end
173
+
174
+ # Tests this instance by creating, converging and verifying. If this
175
+ # instance is running, it will be pre-emptively destroyed to ensure a
176
+ # clean slate. The instance will be left post-verify in a running state.
177
+ #
178
+ # @param destroy_mode [Symbol] strategy used to cleanup after instance
179
+ # has finished verifying (default: `:passing`)
180
+ # @return [self] this instance, used to chain actions
181
+ #
182
+ # @todo rescue Driver::ActionFailed and return some kind of null object
183
+ # to gracfully stop action chaining
184
+ def test(destroy_mode = :passing)
185
+ elapsed = Benchmark.measure do
186
+ banner "Cleaning up any prior instances of #{to_str}"
187
+ destroy
188
+ banner "Testing #{to_str}"
189
+ verify
190
+ destroy if destroy_mode == :passing
191
+ end
192
+ info "Finished testing #{to_str} #{Util.duration(elapsed.real)}."
193
+ self
194
+ ensure
195
+ destroy if destroy_mode == :always
196
+ end
197
+
198
+ # Logs in to this instance by invoking a system command, provided by the
199
+ # instance's transport. This could be an SSH command, telnet, or serial
200
+ # console session.
201
+ #
202
+ # **Note** This method calls exec and will not return.
203
+ #
204
+ # @see Kitchen::LoginCommand
205
+ # @see Transport::Base::Connection#login_command
206
+ def login
207
+ state = state_file.read
208
+ if state[:last_action].nil?
209
+ raise UserError, "Instance #{to_str} has not yet been created"
210
+ end
211
+
212
+ lc = if legacy_ssh_base_driver?
213
+ legacy_ssh_base_login(state)
214
+ else
215
+ transport.connection(state).login_command
216
+ end
217
+
218
+ debug(%{Login command: #{lc.command} #{lc.arguments.join(" ")} } \
219
+ "(Options: #{lc.options})")
220
+ Kernel.exec(*lc.exec_args)
221
+ end
222
+
223
+ # Executes an arbitrary command on this instance.
224
+ #
225
+ # @param command [String] a command string to execute
226
+ def remote_exec(command)
227
+ transport.connection(state_file.read) do |conn|
228
+ conn.execute(command)
229
+ end
230
+ end
231
+
232
+ # Returns a Hash of configuration and other useful diagnostic information.
233
+ #
234
+ # @return [Hash] a diagnostic hash
235
+ def diagnose
236
+ result = Hash.new
237
+ [
238
+ :platform, :state_file, :driver, :provisioner, :transport, :verifier
239
+ ].each do |sym|
240
+ obj = send(sym)
241
+ result[sym] = obj.respond_to?(:diagnose) ? obj.diagnose : :unknown
242
+ end
243
+ result
244
+ end
245
+
246
+ # Returns a Hash of configuration and other useful diagnostic information
247
+ # associated with plugins (such as loaded version, class name, etc.).
248
+ #
249
+ # @return [Hash] a diagnostic hash
250
+ def diagnose_plugins
251
+ result = Hash.new
252
+ [:driver, :provisioner, :verifier, :transport].each do |sym|
253
+ obj = send(sym)
254
+ result[sym] = if obj.respond_to?(:diagnose_plugin)
255
+ obj.diagnose_plugin
256
+ else
257
+ :unknown
258
+ end
259
+ end
260
+ result
261
+ end
262
+
263
+ # Returns the last successfully completed action state of the instance.
264
+ #
265
+ # @return [String] a named action which was last successfully completed
266
+ def last_action
267
+ state_file.read[:last_action]
268
+ end
269
+
270
+ # Clean up any per-instance resources before exiting.
271
+ #
272
+ # @return [void]
273
+ def cleanup!
274
+ @transport.cleanup! if @transport
275
+ end
276
+
277
+ private
278
+
279
+ # @return [StateFile] a state file object that can be read from or written
280
+ # to
281
+ # @api private
282
+ attr_reader :state_file
283
+
284
+ # Validate the initial internal state of this object and raising an
285
+ # exception if any preconditions are not met.
286
+ #
287
+ # @param options[Hash] options hash passed into the constructor
288
+ # @raise [ClientError] if any validations fail
289
+ # @api private
290
+ def validate_options(options)
291
+ [
292
+ :suite, :platform, :driver, :provisioner,
293
+ :transport, :verifier, :state_file
294
+ ].each do |k|
295
+ next if options.key?(k)
296
+
297
+ raise ClientError, "Instance#new requires option :#{k}"
298
+ end
299
+ end
300
+
301
+ # Perform any final configuration or preparation needed for the driver
302
+ # object carry out its duties.
303
+ #
304
+ # @api private
305
+ def setup_driver
306
+ @driver.finalize_config!(self)
307
+
308
+ if driver.class.serial_actions
309
+ Kitchen.mutex.synchronize do
310
+ self.class.mutexes ||= Hash.new
311
+ self.class.mutexes[driver.class] = Mutex.new
312
+ end
313
+ end
314
+ end
315
+
316
+ # Perform any final configuration or preparation needed for the provisioner
317
+ # object carry out its duties.
318
+ #
319
+ # @api private
320
+ def setup_provisioner
321
+ @provisioner.finalize_config!(self)
322
+ end
323
+
324
+ # Perform any final configuration or preparation needed for the transport
325
+ # object carry out its duties.
326
+ #
327
+ # @api private
328
+ def setup_transport
329
+ transport.finalize_config!(self)
330
+ end
331
+
332
+ # Perform any final configuration or preparation needed for the verifier
333
+ # object carry out its duties.
334
+ #
335
+ # @api private
336
+ def setup_verifier
337
+ verifier.finalize_config!(self)
338
+ end
339
+
340
+ # Perform all actions in order from last state to desired state.
341
+ #
342
+ # @param desired [Symbol] a symbol representing the desired action state
343
+ # @return [self] this instance, used to chain actions
344
+ # @api private
345
+ def transition_to(desired)
346
+ result = nil
347
+ FSM.actions(last_action, desired).each do |transition|
348
+ result = send("#{transition}_action")
349
+ end
350
+ result
351
+ end
352
+
353
+ # Perform the create action.
354
+ #
355
+ # @see Driver::Base#create
356
+ # @return [self] this instance, used to chain actions
357
+ # @api private
358
+ def create_action
359
+ perform_action(:create, "Creating")
360
+ end
361
+
362
+ # Perform the converge action.
363
+ #
364
+ # @see Provisioner::Base#call
365
+ # @return [self] this instance, used to chain actions
366
+ # @api private
367
+ def converge_action
368
+ banner "Converging #{to_str}..."
369
+ elapsed = action(:converge) do |state|
370
+ if legacy_ssh_base_driver?
371
+ legacy_ssh_base_converge(state)
372
+ else
373
+ provisioner.call(state)
374
+ end
375
+ end
376
+ info("Finished converging #{to_str} #{Util.duration(elapsed.real)}.")
377
+ self
378
+ end
379
+
380
+ # Perform the setup action.
381
+ #
382
+ # @see Driver::Base#setup
383
+ # @return [self] this instance, used to chain actions
384
+ # @api private
385
+ def setup_action
386
+ banner "Setting up #{to_str}..."
387
+ elapsed = action(:setup) do |state|
388
+ legacy_ssh_base_setup(state) if legacy_ssh_base_driver?
389
+ end
390
+ info("Finished setting up #{to_str} #{Util.duration(elapsed.real)}.")
391
+ self
392
+ end
393
+
394
+ # returns true, if the verifier is busser
395
+ def verifier_busser?(verifier)
396
+ !defined?(Kitchen::Verifier::Busser).nil? && verifier.is_a?(Kitchen::Verifier::Busser)
397
+ end
398
+
399
+ # returns true, if the verifier is dummy
400
+ def verifier_dummy?(verifier)
401
+ !defined?(Kitchen::Verifier::Dummy).nil? && verifier.is_a?(Kitchen::Verifier::Dummy)
402
+ end
403
+
404
+ def use_legacy_ssh_verifier?(verifier)
405
+ verifier_busser?(verifier) || verifier_dummy?(verifier)
406
+ end
407
+
408
+ # Perform the verify action.
409
+ #
410
+ # @see Driver::Base#verify
411
+ # @return [self] this instance, used to chain actions
412
+ # @api private
413
+ def verify_action
414
+ banner "Verifying #{to_str}..."
415
+ elapsed = action(:verify) do |state|
416
+ # use special handling for legacy driver
417
+ if legacy_ssh_base_driver? && use_legacy_ssh_verifier?(verifier)
418
+ legacy_ssh_base_verify(state)
419
+ elsif legacy_ssh_base_driver?
420
+ # read ssh options from legacy driver
421
+ verifier.call(driver.legacy_state(state))
422
+ else
423
+ verifier.call(state)
424
+ end
425
+ end
426
+ info("Finished verifying #{to_str} #{Util.duration(elapsed.real)}.")
427
+ self
428
+ end
429
+
430
+ # Perform the destroy action.
431
+ #
432
+ # @see Driver::Base#destroy
433
+ # @return [self] this instance, used to chain actions
434
+ # @api private
435
+ def destroy_action
436
+ perform_action(:destroy, "Destroying") { state_file.destroy }
437
+ end
438
+
439
+ # Perform an arbitrary action and provide useful logging.
440
+ #
441
+ # @param verb [Symbol] the action to be performed
442
+ # @param output_verb [String] a verb representing the action, suitable for
443
+ # use in output logging
444
+ # @yield perform optional work just after action has complted
445
+ # @return [self] this instance, used to chain actions
446
+ # @api private
447
+ def perform_action(verb, output_verb)
448
+ banner "#{output_verb} #{to_str}..."
449
+ elapsed = action(verb) { |state| driver.public_send(verb, state) }
450
+ info("Finished #{output_verb.downcase} #{to_str}" \
451
+ " #{Util.duration(elapsed.real)}.")
452
+ yield if block_given?
453
+ self
454
+ end
455
+
456
+ # Times a call to an action block and handles any raised exceptions. This
457
+ # method ensures that the last successfully completed action is persisted
458
+ # to the state file. The last action state will either be the desired
459
+ # action that is passed in or the previous action that was persisted to the
460
+ # state file.
461
+ #
462
+ # @param what [Symbol] the action to be performed
463
+ # @param block [Proc] a block to be called
464
+ # @return [Benchmark::Tms] timing information for the given action
465
+ # @raise [InstanceFailed] if a driver action fails to complete, signaled
466
+ # by a driver raising an ActionFailed exception. Typical reasons for this
467
+ # would be a driver create action failing, a chef convergence crashing
468
+ # in normal course of development, failing acceptance tests in the
469
+ # verify action, etc.
470
+ # @raise [ActionFailed] if an unforseen or unplanned exception is raised.
471
+ # This would usually indicate that a race condition was triggered, a
472
+ # bug exists in a driver, provisioner, or core, a transient IO error
473
+ # occured, etc.
474
+ # @api private
475
+ def action(what, &block)
476
+ state = state_file.read
477
+ elapsed = Benchmark.measure do
478
+ synchronize_or_call(what, state, &block)
479
+ end
480
+ state[:last_action] = what.to_s
481
+ elapsed
482
+ rescue ActionFailed => e
483
+ log_failure(what, e)
484
+ raise(InstanceFailure, failure_message(what) +
485
+ " Please see .kitchen/logs/#{name}.log for more details",
486
+ e.backtrace)
487
+ rescue Exception => e # rubocop:disable Lint/RescueException
488
+ log_failure(what, e)
489
+ raise ActionFailed,
490
+ "Failed to complete ##{what} action: [#{e.message}]", e.backtrace
491
+ ensure
492
+ state_file.write(state)
493
+ end
494
+
495
+ # Runs a given action block through a common driver mutex if required or
496
+ # runs it directly otherwise. If a driver class' `.serial_actions` array
497
+ # includes the desired action, then the action must be run with a muxtex
498
+ # lock. Otherwise, it is assumed that the action can happen concurrently,
499
+ # or fully in parallel.
500
+ #
501
+ # @param what [Symbol] the action to be performed
502
+ # @param state [Hash] a mutable state hash for this instance
503
+ # @param block [Proc] a block to be called
504
+ # @api private
505
+ def synchronize_or_call(what, state, &block)
506
+ if Array(driver.class.serial_actions).include?(what)
507
+ debug("#{to_str} is synchronizing on #{driver.class}##{what}")
508
+ self.class.mutexes[driver.class].synchronize do
509
+ debug("#{to_str} is messaging #{driver.class}##{what}")
510
+ block.call(state)
511
+ end
512
+ else
513
+ block.call(state)
514
+ end
515
+ end
516
+
517
+ # Writes a high level message for logging and/or output.
518
+ #
519
+ # In this case, all instance banner messages will be written to the common
520
+ # Kitchen logger so that the high level flow of a run can be followed in
521
+ # the kitchen.log file.
522
+ #
523
+ # @api private
524
+ def banner(*args)
525
+ Kitchen.logger.logdev && Kitchen.logger.logdev.banner(*args)
526
+ super
527
+ end
528
+
529
+ # Logs a failure (message and backtrace) to the instance's file logger
530
+ # to help with debugging and diagnosing issues without overwhelming the
531
+ # console output in the default case (i.e. running kitchen with :info
532
+ # level debugging).
533
+ #
534
+ # @param what [String] an action
535
+ # @param e [Exception] an exception
536
+ # @api private
537
+ def log_failure(what, e)
538
+ return if logger.logdev.nil?
539
+
540
+ logger.logdev.error(failure_message(what))
541
+ Error.formatted_trace(e).each { |line| logger.logdev.error(line) }
542
+ end
543
+
544
+ # Returns a string explaining what action failed, at a high level. Used
545
+ # for displaying to end user.
546
+ #
547
+ # @param what [String] an action
548
+ # @return [String] a failure message
549
+ # @api private
550
+ def failure_message(what)
551
+ "#{what.capitalize} failed on instance #{to_str}."
552
+ end
553
+
554
+ # Invokes `Driver#converge` on a legacy Driver, which inherits from
555
+ # `Kitchen::Driver::SSHBase`.
556
+ #
557
+ # @param state [Hash] mutable instance state
558
+ # @deprecated When legacy Driver::SSHBase support is removed, the
559
+ # `#converge` method will no longer be called on the Driver.
560
+ # @api private
561
+ def legacy_ssh_base_converge(state)
562
+ warn("Running legacy converge for '#{driver.name}' Driver")
563
+ # TODO: Document upgrade path and provide link
564
+ # warn("Driver authors: please read http://example.com for more details.")
565
+ driver.converge(state)
566
+ end
567
+
568
+ # @return [TrueClass,FalseClass] whether or not the Driver inherits from
569
+ # `Kitchen::Driver::SSHBase`
570
+ # @deprecated When legacy Driver::SSHBase support is removed, the
571
+ # `#converge` method will no longer be called on the Driver.
572
+ # @api private
573
+ def legacy_ssh_base_driver?
574
+ driver.class < Kitchen::Driver::SSHBase
575
+ end
576
+
577
+ # Invokes `Driver#login_command` on a legacy Driver, which inherits from
578
+ # `Kitchen::Driver::SSHBase`.
579
+ #
580
+ # @param state [Hash] mutable instance state
581
+ # @deprecated When legacy Driver::SSHBase support is removed, the
582
+ # `#login_command` method will no longer be called on the Driver.
583
+ # @api private
584
+ def legacy_ssh_base_login(state)
585
+ warn("Running legacy login for '#{driver.name}' Driver")
586
+ # TODO: Document upgrade path and provide link
587
+ # warn("Driver authors: please read http://example.com for more details.")
588
+ driver.login_command(state)
589
+ end
590
+
591
+ # Invokes `Driver#setup` on a legacy Driver, which inherits from
592
+ # `Kitchen::Driver::SSHBase`.
593
+ #
594
+ # @param state [Hash] mutable instance state
595
+ # @deprecated When legacy Driver::SSHBase support is removed, the
596
+ # `#setup` method will no longer be called on the Driver.
597
+ # @api private
598
+ def legacy_ssh_base_setup(state)
599
+ warn("Running legacy setup for '#{driver.name}' Driver")
600
+ # TODO: Document upgrade path and provide link
601
+ # warn("Driver authors: please read http://example.com for more details.")
602
+ driver.setup(state)
603
+ end
604
+
605
+ # Invokes `Driver#verify` on a legacy Driver, which inherits from
606
+ # `Kitchen::Driver::SSHBase`.
607
+ #
608
+ # @param state [Hash] mutable instance state
609
+ # @deprecated When legacy Driver::SSHBase support is removed, the
610
+ # `#verify` method will no longer be called on the Driver.
611
+ # @api private
612
+ def legacy_ssh_base_verify(state)
613
+ warn("Running legacy verify for '#{driver.name}' Driver")
614
+ # TODO: Document upgrade path and provide link
615
+ # warn("Driver authors: please read http://example.com for more details.")
616
+ driver.verify(state)
617
+ end
618
+
619
+ # The simplest finite state machine pseudo-implementation needed to manage
620
+ # an Instance.
621
+ #
622
+ # @api private
623
+ # @author Fletcher Nichol <fnichol@nichol.ca>
624
+ class FSM
625
+
626
+ # Returns an Array of all transitions to bring an Instance from its last
627
+ # reported transistioned state into the desired transitioned state.
628
+ #
629
+ # @param last [String,Symbol,nil] the last known transitioned state of
630
+ # the Instance, defaulting to `nil` (for unknown or no history)
631
+ # @param desired [String,Symbol] the desired transitioned state for the
632
+ # Instance
633
+ # @return [Array<Symbol>] an Array of transition actions to perform
634
+ # @api private
635
+ def self.actions(last = nil, desired)
636
+ last_index = index(last)
637
+ desired_index = index(desired)
638
+
639
+ if last_index == desired_index || last_index > desired_index
640
+ Array(TRANSITIONS[desired_index])
641
+ else
642
+ TRANSITIONS.slice(last_index + 1, desired_index - last_index)
643
+ end
644
+ end
645
+
646
+ TRANSITIONS = [:destroy, :create, :converge, :setup, :verify]
647
+
648
+ # Determines the index of a state in the state lifecycle vector. Woah.
649
+ #
650
+ # @param transition [Symbol,#to_sym] a state
651
+ # @param [Integer] the index position
652
+ # @api private
653
+ def self.index(transition)
654
+ if transition.nil?
655
+ 0
656
+ else
657
+ TRANSITIONS.find_index { |t| t == transition.to_sym }
658
+ end
659
+ end
660
+ end
661
+ end
662
+ end