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.
- data/.gitignore +20 -0
- data/.travis.yml +11 -0
- data/.yardopts +3 -0
- data/Gemfile +13 -0
- data/Guardfile +11 -0
- data/LICENSE +15 -0
- data/README.md +131 -0
- data/Rakefile +69 -0
- data/bin/kitchen +9 -4
- data/features/cli.feature +17 -0
- data/features/cli_init.feature +156 -0
- data/features/support/env.rb +14 -0
- data/lib/kitchen/busser.rb +166 -0
- data/lib/kitchen/chef_data_uploader.rb +156 -0
- data/lib/kitchen/cli.rb +540 -0
- data/lib/kitchen/collection.rb +55 -0
- data/lib/kitchen/color.rb +46 -0
- data/lib/kitchen/config.rb +223 -0
- data/lib/kitchen/driver/base.rb +180 -0
- data/lib/kitchen/driver/dummy.rb +81 -0
- data/lib/kitchen/driver/ssh_base.rb +192 -0
- data/lib/kitchen/driver.rb +42 -0
- data/lib/kitchen/errors.rb +52 -0
- data/lib/kitchen/instance.rb +327 -0
- data/lib/kitchen/instance_actor.rb +42 -0
- data/lib/kitchen/loader/yaml.rb +105 -0
- data/lib/kitchen/logger.rb +145 -0
- data/{cookbooks/test-kitchen/libraries/helpers.rb → lib/kitchen/logging.rb} +13 -9
- data/lib/kitchen/manager.rb +45 -0
- data/lib/kitchen/metadata_chopper.rb +52 -0
- data/lib/kitchen/platform.rb +61 -0
- data/lib/kitchen/rake_tasks.rb +59 -0
- data/lib/kitchen/shell_out.rb +65 -0
- data/lib/kitchen/state_file.rb +88 -0
- data/lib/kitchen/suite.rb +76 -0
- data/lib/kitchen/thor_tasks.rb +62 -0
- data/lib/kitchen/util.rb +79 -0
- data/{cookbooks/test-kitchen/recipes/erlang.rb → lib/kitchen/version.rb} +9 -6
- data/lib/kitchen.rb +98 -0
- data/lib/vendor/hash_recursive_merge.rb +74 -0
- data/spec/kitchen/collection_spec.rb +80 -0
- data/spec/kitchen/color_spec.rb +54 -0
- data/spec/kitchen/config_spec.rb +201 -0
- data/spec/kitchen/driver/dummy_spec.rb +191 -0
- data/spec/kitchen/instance_spec.rb +162 -0
- data/spec/kitchen/loader/yaml_spec.rb +243 -0
- data/spec/kitchen/platform_spec.rb +48 -0
- data/spec/kitchen/state_file_spec.rb +122 -0
- data/spec/kitchen/suite_spec.rb +64 -0
- data/spec/spec_helper.rb +47 -0
- data/templates/plugin/driver.rb.erb +23 -0
- data/templates/plugin/license_apachev2.erb +15 -0
- data/templates/plugin/license_gplv2.erb +18 -0
- data/templates/plugin/license_gplv3.erb +16 -0
- data/templates/plugin/license_mit.erb +22 -0
- data/templates/plugin/license_reserved.erb +5 -0
- data/templates/plugin/version.rb.erb +12 -0
- data/test-kitchen.gemspec +44 -0
- metadata +290 -82
- data/config/Cheffile +0 -47
- data/config/Kitchenfile +0 -39
- data/config/Vagrantfile +0 -114
- data/cookbooks/test-kitchen/attributes/default.rb +0 -25
- data/cookbooks/test-kitchen/metadata.rb +0 -27
- data/cookbooks/test-kitchen/recipes/chef.rb +0 -19
- data/cookbooks/test-kitchen/recipes/compat.rb +0 -39
- data/cookbooks/test-kitchen/recipes/default.rb +0 -51
- data/cookbooks/test-kitchen/recipes/ruby.rb +0 -29
- data/lib/test-kitchen/cli/destroy.rb +0 -36
- data/lib/test-kitchen/cli/init.rb +0 -37
- data/lib/test-kitchen/cli/platform_list.rb +0 -37
- data/lib/test-kitchen/cli/project_info.rb +0 -44
- data/lib/test-kitchen/cli/ssh.rb +0 -36
- data/lib/test-kitchen/cli/status.rb +0 -36
- data/lib/test-kitchen/cli/test.rb +0 -68
- data/lib/test-kitchen/cli.rb +0 -282
- data/lib/test-kitchen/dsl.rb +0 -63
- data/lib/test-kitchen/environment.rb +0 -166
- data/lib/test-kitchen/platform.rb +0 -79
- data/lib/test-kitchen/project/base.rb +0 -159
- data/lib/test-kitchen/project/cookbook.rb +0 -97
- data/lib/test-kitchen/project/cookbook_copy.rb +0 -58
- data/lib/test-kitchen/project/ruby.rb +0 -37
- data/lib/test-kitchen/project/supported_platforms.rb +0 -75
- data/lib/test-kitchen/project.rb +0 -23
- data/lib/test-kitchen/runner/base.rb +0 -154
- data/lib/test-kitchen/runner/openstack/dsl.rb +0 -39
- data/lib/test-kitchen/runner/openstack/environment.rb +0 -141
- data/lib/test-kitchen/runner/openstack.rb +0 -147
- data/lib/test-kitchen/runner/vagrant.rb +0 -95
- data/lib/test-kitchen/runner.rb +0 -21
- data/lib/test-kitchen/scaffold.rb +0 -88
- data/lib/test-kitchen/ui.rb +0 -73
- data/lib/test-kitchen/version.rb +0 -21
- 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
|