test-kitchen 0.7.0 → 1.0.0.alpha.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 (95) hide show
  1. data/.gitignore +20 -0
  2. data/.travis.yml +11 -0
  3. data/.yardopts +3 -0
  4. data/Gemfile +13 -0
  5. data/Guardfile +11 -0
  6. data/LICENSE +15 -0
  7. data/README.md +131 -0
  8. data/Rakefile +69 -0
  9. data/bin/kitchen +9 -4
  10. data/features/cli.feature +17 -0
  11. data/features/cli_init.feature +156 -0
  12. data/features/support/env.rb +14 -0
  13. data/lib/kitchen/busser.rb +166 -0
  14. data/lib/kitchen/chef_data_uploader.rb +156 -0
  15. data/lib/kitchen/cli.rb +540 -0
  16. data/lib/kitchen/collection.rb +55 -0
  17. data/lib/kitchen/color.rb +46 -0
  18. data/lib/kitchen/config.rb +223 -0
  19. data/lib/kitchen/driver/base.rb +180 -0
  20. data/lib/kitchen/driver/dummy.rb +81 -0
  21. data/lib/kitchen/driver/ssh_base.rb +192 -0
  22. data/lib/kitchen/driver.rb +42 -0
  23. data/lib/kitchen/errors.rb +52 -0
  24. data/lib/kitchen/instance.rb +327 -0
  25. data/lib/kitchen/instance_actor.rb +42 -0
  26. data/lib/kitchen/loader/yaml.rb +105 -0
  27. data/lib/kitchen/logger.rb +145 -0
  28. data/{cookbooks/test-kitchen/libraries/helpers.rb → lib/kitchen/logging.rb} +13 -9
  29. data/lib/kitchen/manager.rb +45 -0
  30. data/lib/kitchen/metadata_chopper.rb +52 -0
  31. data/lib/kitchen/platform.rb +61 -0
  32. data/lib/kitchen/rake_tasks.rb +59 -0
  33. data/lib/kitchen/shell_out.rb +65 -0
  34. data/lib/kitchen/state_file.rb +88 -0
  35. data/lib/kitchen/suite.rb +76 -0
  36. data/lib/kitchen/thor_tasks.rb +62 -0
  37. data/lib/kitchen/util.rb +79 -0
  38. data/{cookbooks/test-kitchen/recipes/erlang.rb → lib/kitchen/version.rb} +9 -6
  39. data/lib/kitchen.rb +98 -0
  40. data/lib/vendor/hash_recursive_merge.rb +74 -0
  41. data/spec/kitchen/collection_spec.rb +80 -0
  42. data/spec/kitchen/color_spec.rb +54 -0
  43. data/spec/kitchen/config_spec.rb +201 -0
  44. data/spec/kitchen/driver/dummy_spec.rb +191 -0
  45. data/spec/kitchen/instance_spec.rb +162 -0
  46. data/spec/kitchen/loader/yaml_spec.rb +243 -0
  47. data/spec/kitchen/platform_spec.rb +48 -0
  48. data/spec/kitchen/state_file_spec.rb +122 -0
  49. data/spec/kitchen/suite_spec.rb +64 -0
  50. data/spec/spec_helper.rb +47 -0
  51. data/templates/plugin/driver.rb.erb +23 -0
  52. data/templates/plugin/license_apachev2.erb +15 -0
  53. data/templates/plugin/license_gplv2.erb +18 -0
  54. data/templates/plugin/license_gplv3.erb +16 -0
  55. data/templates/plugin/license_mit.erb +22 -0
  56. data/templates/plugin/license_reserved.erb +5 -0
  57. data/templates/plugin/version.rb.erb +12 -0
  58. data/test-kitchen.gemspec +44 -0
  59. metadata +290 -82
  60. data/config/Cheffile +0 -47
  61. data/config/Kitchenfile +0 -39
  62. data/config/Vagrantfile +0 -114
  63. data/cookbooks/test-kitchen/attributes/default.rb +0 -25
  64. data/cookbooks/test-kitchen/metadata.rb +0 -27
  65. data/cookbooks/test-kitchen/recipes/chef.rb +0 -19
  66. data/cookbooks/test-kitchen/recipes/compat.rb +0 -39
  67. data/cookbooks/test-kitchen/recipes/default.rb +0 -51
  68. data/cookbooks/test-kitchen/recipes/ruby.rb +0 -29
  69. data/lib/test-kitchen/cli/destroy.rb +0 -36
  70. data/lib/test-kitchen/cli/init.rb +0 -37
  71. data/lib/test-kitchen/cli/platform_list.rb +0 -37
  72. data/lib/test-kitchen/cli/project_info.rb +0 -44
  73. data/lib/test-kitchen/cli/ssh.rb +0 -36
  74. data/lib/test-kitchen/cli/status.rb +0 -36
  75. data/lib/test-kitchen/cli/test.rb +0 -68
  76. data/lib/test-kitchen/cli.rb +0 -282
  77. data/lib/test-kitchen/dsl.rb +0 -63
  78. data/lib/test-kitchen/environment.rb +0 -166
  79. data/lib/test-kitchen/platform.rb +0 -79
  80. data/lib/test-kitchen/project/base.rb +0 -159
  81. data/lib/test-kitchen/project/cookbook.rb +0 -97
  82. data/lib/test-kitchen/project/cookbook_copy.rb +0 -58
  83. data/lib/test-kitchen/project/ruby.rb +0 -37
  84. data/lib/test-kitchen/project/supported_platforms.rb +0 -75
  85. data/lib/test-kitchen/project.rb +0 -23
  86. data/lib/test-kitchen/runner/base.rb +0 -154
  87. data/lib/test-kitchen/runner/openstack/dsl.rb +0 -39
  88. data/lib/test-kitchen/runner/openstack/environment.rb +0 -141
  89. data/lib/test-kitchen/runner/openstack.rb +0 -147
  90. data/lib/test-kitchen/runner/vagrant.rb +0 -95
  91. data/lib/test-kitchen/runner.rb +0 -21
  92. data/lib/test-kitchen/scaffold.rb +0 -88
  93. data/lib/test-kitchen/ui.rb +0 -73
  94. data/lib/test-kitchen/version.rb +0 -21
  95. data/lib/test-kitchen.rb +0 -34
@@ -0,0 +1,192 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2012, 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 'net/ssh'
20
+ require 'socket'
21
+
22
+ module Kitchen
23
+
24
+ module Driver
25
+
26
+ # Base class for a driver that uses SSH to communication with an instance.
27
+ # A subclass must implement the following methods:
28
+ # * #create(state)
29
+ # * #destroy(state)
30
+ #
31
+ # @author Fletcher Nichol <fnichol@nichol.ca>
32
+ class SSHBase < Base
33
+
34
+ def create(state)
35
+ raise ClientError, "#{self.class}#create must be implemented"
36
+ end
37
+
38
+ def converge(state)
39
+ ssh_args = build_ssh_args(state)
40
+
41
+ install_omnibus(ssh_args) if config[:require_chef_omnibus]
42
+ prepare_chef_home(ssh_args)
43
+ upload_chef_data(ssh_args)
44
+ run_chef_solo(ssh_args)
45
+ end
46
+
47
+ def setup(state)
48
+ ssh_args = build_ssh_args(state)
49
+
50
+ if kb_setup_cmd
51
+ ssh(ssh_args, kb_setup_cmd)
52
+ end
53
+ end
54
+
55
+ def verify(state)
56
+ ssh_args = build_ssh_args(state)
57
+
58
+ if kb_run_cmd
59
+ ssh(ssh_args, kb_sync_cmd)
60
+ ssh(ssh_args, kb_run_cmd)
61
+ end
62
+ end
63
+
64
+ def destroy(state)
65
+ raise ClientError, "#{self.class}#destroy must be implemented"
66
+ end
67
+
68
+ def login_command(state)
69
+ args = %W{ -o UserKnownHostsFile=/dev/null }
70
+ args += %W{ -o StrictHostKeyChecking=no }
71
+ args += %W{ -i #{config[:ssh_key]}} if config[:ssh_key]
72
+ args += %W{ #{config[:username]}@#{state[:hostname]}}
73
+
74
+ ["ssh", *args]
75
+ end
76
+
77
+ protected
78
+
79
+ def build_ssh_args(state)
80
+ opts = Hash.new
81
+ opts[:user_known_hosts_file] = "/dev/null"
82
+ opts[:paranoid] = false
83
+ opts[:password] = config[:password] if config[:password]
84
+ opts[:keys] = Array(config[:ssh_key]) if config[:ssh_key]
85
+
86
+ [state[:hostname], config[:username], opts]
87
+ end
88
+
89
+ def chef_home
90
+ "/tmp/kitchen-chef-solo".freeze
91
+ end
92
+
93
+ def install_omnibus(ssh_args)
94
+ flag = config[:require_chef_omnibus].downcase
95
+ version = if flag.is_a?(String) && flag != "latest"
96
+ "-s -- -v #{flag}"
97
+ else
98
+ ""
99
+ end
100
+
101
+ ssh(ssh_args, <<-INSTALL.gsub(/^ {10}/, ''))
102
+ should_update_chef() {
103
+ case "#{flag}" in
104
+ $(chef-solo --v | awk "{print \$2}")) return 1 ;;
105
+ latest|*) return 0 ;;
106
+ esac
107
+ }
108
+
109
+ if [ ! -d "/opt/chef" ] || should_update_chef ; then
110
+ echo "-----> Installing Chef Omnibus (#{flag})"
111
+ curl -sSL https://www.opscode.com/chef/install.sh \
112
+ | sudo bash #{version}
113
+ fi
114
+ INSTALL
115
+ end
116
+
117
+ def prepare_chef_home(ssh_args)
118
+ ssh(ssh_args, "sudo rm -rf #{chef_home} && mkdir -p #{chef_home}/cache")
119
+ end
120
+
121
+ def upload_chef_data(ssh_args)
122
+ Kitchen::ChefDataUploader.new(
123
+ instance, ssh_args, config[:kitchen_root], chef_home
124
+ ).upload
125
+ end
126
+
127
+ def run_chef_solo(ssh_args)
128
+ ssh(ssh_args, <<-RUN_SOLO)
129
+ sudo chef-solo -c #{chef_home}/solo.rb -j #{chef_home}/dna.json \
130
+ --log_level #{Util.from_logger_level(logger.level)}
131
+ RUN_SOLO
132
+ end
133
+
134
+ def ssh(ssh_args, cmd)
135
+ debug("[SSH] #{ssh_args[1]}@#{ssh_args[0]} (#{cmd})")
136
+ Net::SSH.start(*ssh_args) do |ssh|
137
+ exit_code = ssh_exec_with_exit!(ssh, cmd)
138
+
139
+ if exit_code != 0
140
+ shorter_cmd = cmd.squeeze(" ").strip
141
+ raise ActionFailed,
142
+ "SSH exited (#{exit_code}) for command: [#{shorter_cmd}]"
143
+ end
144
+ end
145
+ rescue Net::SSH::Exception => ex
146
+ raise ActionFailed, ex.message
147
+ end
148
+
149
+ def ssh_exec_with_exit!(ssh, cmd)
150
+ exit_code = nil
151
+ ssh.open_channel do |channel|
152
+
153
+ channel.request_pty
154
+
155
+ channel.exec(cmd) do |ch, success|
156
+
157
+ channel.on_data do |ch, data|
158
+ logger << data
159
+ end
160
+
161
+ channel.on_extended_data do |ch, type, data|
162
+ logger << data
163
+ end
164
+
165
+ channel.on_request("exit-status") do |ch, data|
166
+ exit_code = data.read_long
167
+ end
168
+ end
169
+ end
170
+ ssh.loop
171
+ exit_code
172
+ end
173
+
174
+ def wait_for_sshd(hostname)
175
+ logger << "." until test_ssh(hostname)
176
+ end
177
+
178
+ def test_ssh(hostname)
179
+ socket = TCPSocket.new(hostname, config[:port])
180
+ IO.select([socket], nil, nil, 5)
181
+ rescue SocketError, Errno::ECONNREFUSED,
182
+ Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
183
+ sleep 2
184
+ false
185
+ rescue Errno::EPERM, Errno::ETIMEDOUT
186
+ false
187
+ ensure
188
+ socket && socket.close
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,42 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2012, 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
+ module Kitchen
20
+
21
+ module Driver
22
+
23
+ # Returns an instance of a driver given a plugin type string.
24
+ #
25
+ # @param plugin [String] a driver plugin type, which will be constantized
26
+ # @return [Driver::Base] a driver instance
27
+ # @raise [ClientError] if a driver instance could not be created
28
+ def self.for_plugin(plugin, config)
29
+ require "kitchen/driver/#{plugin}"
30
+
31
+ str_const = Util.to_camel_case(plugin)
32
+ klass = self.const_get(str_const)
33
+ klass.new(config)
34
+ rescue UserError
35
+ raise
36
+ rescue LoadError
37
+ raise ClientError, "Could not require '#{plugin}' plugin from load path"
38
+ rescue
39
+ raise ClientError, "Failed to create a driver for '#{plugin}' plugin"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,52 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2013, 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
+ module Kitchen
20
+
21
+ module Error ; end
22
+
23
+ # Base exception class from which all Kitchen exceptions derive. This class
24
+ # nests an exception when this class is re-raised from a rescue block.
25
+ class StandardError < ::StandardError
26
+
27
+ include Error
28
+
29
+ attr_reader :original
30
+
31
+ def initialize(msg, original = $!)
32
+ super(msg)
33
+ @original = original
34
+ end
35
+ end
36
+
37
+ # Base exception class for all exceptions that are caused by user input
38
+ # errors.
39
+ class UserError < StandardError ; end
40
+
41
+ # Base exception class for all exceptions that are caused by incorrect use
42
+ # of an API.
43
+ class ClientError < StandardError ; end
44
+
45
+ # Base exception class for exceptions that are caused by external library
46
+ # failures which may be temporary.
47
+ class TransientFailure < StandardError ; end
48
+
49
+ # Exception class for any exceptions raised when performing an instance
50
+ # action.
51
+ class ActionFailed < TransientFailure ; end
52
+ end
@@ -0,0 +1,327 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2012, 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
+ require 'thread'
22
+ require 'vendor/hash_recursive_merge'
23
+
24
+ module Kitchen
25
+
26
+ # An instance of a suite running on a platform. A created instance may be a
27
+ # local virtual machine, cloud instance, container, or even a bare metal
28
+ # server, which is determined by the platform's driver.
29
+ #
30
+ # @author Fletcher Nichol <fnichol@nichol.ca>
31
+ class Instance
32
+
33
+ include Logging
34
+
35
+ class << self
36
+ attr_accessor :mutexes
37
+ end
38
+
39
+ # @return [Suite] the test suite configuration
40
+ attr_reader :suite
41
+
42
+ # @return [Platform] the target platform configuration
43
+ attr_reader :platform
44
+
45
+ # @return [Driver::Base] driver object which will manage this instance's
46
+ # lifecycle actions
47
+ attr_reader :driver
48
+
49
+ # @return [Logger] the logger for this instance
50
+ attr_reader :logger
51
+
52
+ # Creates a new instance, given a suite and a platform.
53
+ #
54
+ # @param [Hash] options configuration for a new suite
55
+ # @option options [Suite] :suite the suite
56
+ # @option options [Platform] :platform the platform
57
+ # @option options [Driver::Base] :driver the driver
58
+ # @option options [Logger] :logger the instance logger
59
+ def initialize(options = {})
60
+ options = { :logger => Kitchen.logger }.merge(options)
61
+ validate_options(options)
62
+ logger = options[:logger]
63
+
64
+ @suite = options[:suite]
65
+ @platform = options[:platform]
66
+ @driver = options[:driver]
67
+ @logger = logger.is_a?(Proc) ? logger.call(name) : logger
68
+
69
+ @driver.instance = self
70
+ setup_driver_mutex
71
+ end
72
+
73
+ # @return [String] name of this instance
74
+ def name
75
+ "#{suite.name}-#{platform.name}".gsub(/_/, '-').gsub(/\./, '')
76
+ end
77
+
78
+ def to_str
79
+ "<#{name}>"
80
+ end
81
+
82
+ # Returns a combined run_list starting with the platform's run_list
83
+ # followed by the suite's run_list.
84
+ #
85
+ # @return [Array] combined run_list from suite and platform
86
+ def run_list
87
+ Array(platform.run_list) + Array(suite.run_list)
88
+ end
89
+
90
+ # Returns a merged hash of Chef node attributes with values from the
91
+ # suite overriding values from the platform.
92
+ #
93
+ # @return [Hash] merged hash of Chef node attributes
94
+ def attributes
95
+ platform.attributes.rmerge(suite.attributes)
96
+ end
97
+
98
+ def dna
99
+ attributes.rmerge({ :run_list => run_list })
100
+ end
101
+
102
+ # Creates this instance.
103
+ #
104
+ # @see Driver::Base#create
105
+ # @return [self] this instance, used to chain actions
106
+ #
107
+ # @todo rescue Driver::ActionFailed and return some kind of null object
108
+ # to gracfully stop action chaining
109
+ def create
110
+ transition_to(:create)
111
+ end
112
+
113
+ # Converges this running instance.
114
+ #
115
+ # @see Driver::Base#converge
116
+ # @return [self] this instance, used to chain actions
117
+ #
118
+ # @todo rescue Driver::ActionFailed and return some kind of null object
119
+ # to gracfully stop action chaining
120
+ def converge
121
+ transition_to(:converge)
122
+ end
123
+
124
+ # Sets up this converged instance for suite tests.
125
+ #
126
+ # @see Driver::Base#setup
127
+ # @return [self] this instance, used to chain actions
128
+ #
129
+ # @todo rescue Driver::ActionFailed and return some kind of null object
130
+ # to gracfully stop action chaining
131
+ def setup
132
+ transition_to(:setup)
133
+ end
134
+
135
+ # Verifies this set up instance by executing suite tests.
136
+ #
137
+ # @see Driver::Base#verify
138
+ # @return [self] this instance, used to chain actions
139
+ #
140
+ # @todo rescue Driver::ActionFailed and return some kind of null object
141
+ # to gracfully stop action chaining
142
+ def verify
143
+ transition_to(:verify)
144
+ end
145
+
146
+ # Destroys this instance.
147
+ #
148
+ # @see Driver::Base#destroy
149
+ # @return [self] this instance, used to chain actions
150
+ #
151
+ # @todo rescue Driver::ActionFailed and return some kind of null object
152
+ # to gracfully stop action chaining
153
+ def destroy
154
+ transition_to(:destroy)
155
+ end
156
+
157
+ # Tests this instance by creating, converging and verifying. If this
158
+ # instance is running, it will be pre-emptively destroyed to ensure a
159
+ # clean slate. The instance will be left post-verify in a running state.
160
+ #
161
+ # @param destroy_mode [Symbol] strategy used to cleanup after instance
162
+ # has finished verifying (default: `:passing`)
163
+ # @return [self] this instance, used to chain actions
164
+ #
165
+ # @todo rescue Driver::ActionFailed and return some kind of null object
166
+ # to gracfully stop action chaining
167
+ def test(destroy_mode = :passing)
168
+ elapsed = Benchmark.measure do
169
+ banner "Cleaning up any prior instances of #{to_str}"
170
+ destroy
171
+ banner "Testing #{to_str}"
172
+ verify
173
+ destroy if destroy_mode == :passing
174
+ end
175
+ info "Finished testing #{to_str} #{Util.duration(elapsed.real)}."
176
+ self
177
+ ensure
178
+ destroy if destroy_mode == :always
179
+ end
180
+
181
+ # Logs in to this instance by invoking a system command, provided by the
182
+ # instance's driver. This could be an SSH command, telnet, or serial
183
+ # console session.
184
+ #
185
+ # **Note** This method calls exec and will not return.
186
+ #
187
+ # @see Driver::Base#login_command
188
+ def login
189
+ command, *args = driver.login_command(state_file.read)
190
+
191
+ debug("Login command: #{command} #{args.join(' ')}")
192
+ Kernel.exec(command, *args)
193
+ end
194
+
195
+ def last_action
196
+ state_file.read[:last_action]
197
+ end
198
+
199
+ private
200
+
201
+ def validate_options(opts)
202
+ [:suite, :platform, :driver, :logger].each do |k|
203
+ raise ClientError, "Instance#new requires option :#{k}" if opts[k].nil?
204
+ end
205
+ end
206
+
207
+ def setup_driver_mutex
208
+ if driver.class.serial_actions
209
+ Kitchen.mutex.synchronize do
210
+ self.class.mutexes ||= Hash.new
211
+ self.class.mutexes[driver.class] = Mutex.new
212
+ end
213
+ end
214
+ end
215
+
216
+ def transition_to(desired)
217
+ result = nil
218
+ FSM.actions(last_action, desired).each do |transition|
219
+ result = send("#{transition}_action")
220
+ end
221
+ result
222
+ end
223
+
224
+ def create_action
225
+ perform_action(:create, "Creating")
226
+ end
227
+
228
+ def converge_action
229
+ perform_action(:converge, "Converging")
230
+ end
231
+
232
+ def setup_action
233
+ perform_action(:setup, "Setting up")
234
+ end
235
+
236
+ def verify_action
237
+ perform_action(:verify, "Verifying")
238
+ end
239
+
240
+ def destroy_action
241
+ perform_action(:destroy, "Destroying") { state_file.destroy }
242
+ end
243
+
244
+ def perform_action(verb, output_verb)
245
+ banner "#{output_verb} #{to_str}"
246
+ elapsed = action(verb) { |state| driver.public_send(verb, state) }
247
+ info("Finished #{output_verb.downcase} #{to_str}" +
248
+ " #{Util.duration(elapsed.real)}.")
249
+ yield if block_given?
250
+ self
251
+ end
252
+
253
+ def action(what, &block)
254
+ state = state_file.read
255
+ elapsed = Benchmark.measure do
256
+ synchronize_or_call(what, state, &block)
257
+ end
258
+ state[:last_action] = what.to_s
259
+ elapsed
260
+ rescue ActionFailed
261
+ raise
262
+ rescue Exception => e
263
+ raise ActionFailed, "Failed to complete ##{what} action: [#{e.message}]"
264
+ ensure
265
+ state_file.write(state)
266
+ end
267
+
268
+ def synchronize_or_call(what, state, &block)
269
+ if Array(driver.class.serial_actions).include?(what)
270
+ debug("#{to_str} is synchronizing on #{driver.class}##{what}")
271
+ self.class.mutexes[driver.class].synchronize do
272
+ debug("#{to_str} is messaging #{driver.class}##{what}")
273
+ block.call(state)
274
+ end
275
+ else
276
+ block.call(state)
277
+ end
278
+ end
279
+
280
+ def state_file
281
+ @state_file ||= StateFile.new(driver[:kitchen_root], name)
282
+ end
283
+
284
+ def banner(*args)
285
+ Kitchen.logger.logdev && Kitchen.logger.logdev.banner(*args)
286
+ super
287
+ end
288
+
289
+ # The simplest finite state machine pseudo-implementation needed to manage
290
+ # an Instance.
291
+ #
292
+ # @author Fletcher Nichol <fnichol@nichol.ca>
293
+ class FSM
294
+
295
+ # Returns an Array of all transitions to bring an Instance from its last
296
+ # reported transistioned state into the desired transitioned state.
297
+ #
298
+ # @param last [String,Symbol,nil] the last known transitioned state of
299
+ # the Instance, defaulting to `nil` (for unknown or no history)
300
+ # @param desired [String,Symbol] the desired transitioned state for the
301
+ # Instance
302
+ # @return [Array<Symbol>] an Array of transition actions to perform
303
+ def self.actions(last = nil, desired)
304
+ last_index = index(last)
305
+ desired_index = index(desired)
306
+
307
+ if last_index == desired_index || last_index > desired_index
308
+ Array(TRANSITIONS[desired_index])
309
+ else
310
+ TRANSITIONS.slice(last_index + 1, desired_index - last_index)
311
+ end
312
+ end
313
+
314
+ private
315
+
316
+ TRANSITIONS = [:destroy, :create, :converge, :setup, :verify]
317
+
318
+ def self.index(transition)
319
+ if transition.nil?
320
+ 0
321
+ else
322
+ TRANSITIONS.find_index { |t| t == transition.to_sym }
323
+ end
324
+ end
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,42 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2013, 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 'delegate'
20
+
21
+ module Kitchen
22
+
23
+ # A delegator for Instance which adds Celluloid actor support, boom.
24
+ #
25
+ # @author Fletcher Nichol <fnichol@nichol.ca>
26
+ class InstanceActor < SimpleDelegator
27
+
28
+ include Celluloid
29
+
30
+ # @!method actor_name()
31
+ # Returns the name of the Celluloid actor.
32
+ # @return [String] the actor name
33
+ alias_method :actor_name, :name
34
+
35
+ # Returns the name of the underlying instance.
36
+ #
37
+ # @return [String] the instance name
38
+ def name
39
+ __getobj__.name
40
+ end
41
+ end
42
+ end