test-kitchen 0.7.0 → 1.0.0.alpha.0

Sign up to get free protection for your applications and to get access to all the features.
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