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.
- 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
|