test-kitchen 1.0.0.alpha.7 → 1.0.0.beta.1

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.
@@ -0,0 +1,69 @@
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 'kitchen/provisioner/chef_base'
20
+
21
+ module Kitchen
22
+
23
+ module Provisioner
24
+
25
+ # Chef Solo provisioner.
26
+ #
27
+ # @author Fletcher Nichol <fnichol@nichol.ca>
28
+ class ChefSolo < ChefBase
29
+
30
+ def create_sandbox
31
+ create_chef_sandbox { prepare_solo_rb }
32
+ end
33
+
34
+ def run_command
35
+ [
36
+ sudo('chef-solo'),
37
+ "--config #{home_path}/solo.rb",
38
+ "--json-attributes #{home_path}/dna.json",
39
+ "--log_level #{config[:log_level]}"
40
+ ].join(" ")
41
+ end
42
+
43
+ def home_path
44
+ "/tmp/kitchen-chef-solo".freeze
45
+ end
46
+
47
+ private
48
+
49
+ def prepare_solo_rb
50
+ solo = []
51
+ solo << %{node_name "#{instance.name}"}
52
+ solo << %{file_cache_path "#{home_path}/cache"}
53
+ solo << %{cookbook_path "#{home_path}/cookbooks"}
54
+ solo << %{role_path "#{home_path}/roles"}
55
+ if instance.suite.data_bags_path
56
+ solo << %{data_bag_path "#{home_path}/data_bags"}
57
+ end
58
+ if instance.suite.encrypted_data_bag_secret_key_path
59
+ secret = "#{home_path}/encrypted_data_bag_secret"
60
+ solo << %{encrypted_data_bag_secret "#{secret}"}
61
+ end
62
+
63
+ File.open(File.join(tmpdir, "solo.rb"), "wb") do |file|
64
+ file.write(solo.join("\n"))
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,92 @@
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 'kitchen/provisioner/chef_base'
20
+
21
+ module Kitchen
22
+
23
+ module Provisioner
24
+
25
+ # Chef Zero provisioner.
26
+ #
27
+ # @author Fletcher Nichol <fnichol@nichol.ca>
28
+ class ChefZero < ChefBase
29
+
30
+ def create_sandbox
31
+ create_chef_sandbox do
32
+ prepare_chef_client_zero_rb
33
+ prepare_client_rb
34
+ end
35
+ end
36
+
37
+ def prepare_command
38
+ ruby_bin = "/opt/chef/embedded/bin"
39
+
40
+ <<-PREPARE.gsub(/^ {10}/, '')
41
+ bash -c '
42
+ if [ ! -f "#{ruby_bin}/chef-zero" ] ; then
43
+ echo "-----> Installing chef-zero and knife-essentials gems"
44
+ #{sudo("#{ruby_bin}/gem")} install \
45
+ chef-zero knife-essentials --no-ri --no-rdoc
46
+ fi'
47
+ PREPARE
48
+ end
49
+
50
+ def run_command
51
+ [
52
+ sudo('/opt/chef/embedded/bin/ruby'),
53
+ "#{home_path}/chef-client-zero.rb",
54
+ "--config #{home_path}/client.rb",
55
+ "--json-attributes #{home_path}/dna.json",
56
+ "--log_level #{config[:log_level]}"
57
+ ].join(" ")
58
+ end
59
+
60
+ def home_path
61
+ "/tmp/kitchen-chef-zero".freeze
62
+ end
63
+
64
+ private
65
+
66
+ def prepare_chef_client_zero_rb
67
+ source = File.join(File.dirname(__FILE__),
68
+ %w{.. .. .. support chef-client-zero.rb})
69
+ FileUtils.cp(source, File.join(tmpdir, "chef-client-zero.rb"))
70
+ end
71
+
72
+ def prepare_client_rb
73
+ client = []
74
+ client << %{node_name "#{instance.name}"}
75
+ client << %{file_cache_path "#{home_path}/cache"}
76
+ client << %{cookbook_path "#{home_path}/cookbooks"}
77
+ client << %{node_path "#{home_path}/nodes"}
78
+ client << %{client_path "#{home_path}/clients"}
79
+ client << %{role_path "#{home_path}/roles"}
80
+ client << %{data_bag_path "#{home_path}/data_bags"}
81
+ if instance.suite.encrypted_data_bag_secret_key_path
82
+ secret = "#{home_path}/encrypted_data_bag_secret"
83
+ client << %{encrypted_data_bag_secret "#{secret}"}
84
+ end
85
+
86
+ File.open(File.join(tmpdir, "client.rb"), "wb") do |file|
87
+ file.write(client.join("\n"))
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,160 @@
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 'logger'
20
+ require 'net/ssh'
21
+ require 'net/scp'
22
+ require 'socket'
23
+
24
+ require 'kitchen/errors'
25
+ require 'kitchen/login_command'
26
+
27
+ module Kitchen
28
+
29
+ # Wrapped exception for any internally raised SSH-related errors.
30
+ #
31
+ # @author Fletcher Nichol <fnichol@nichol.ca>
32
+ class SSHFailed < TransientFailure ; end
33
+
34
+ # Class to help establish SSH connections, issue remote commands, and
35
+ # transfer files between a local system and remote node.
36
+ #
37
+ # @author Fletcher Nichol <fnichol@nichol.ca>
38
+ class SSH
39
+
40
+ def initialize(hostname, username, options = {})
41
+ @hostname = hostname
42
+ @username = username
43
+ @options = options.dup
44
+ @logger = @options.delete(:logger) || ::Logger.new(STDOUT)
45
+
46
+ if block_given?
47
+ yield self
48
+ shutdown
49
+ end
50
+ end
51
+
52
+ def exec(cmd)
53
+ logger.debug("[SSH] #{self} (#{cmd})")
54
+ exit_code = exec_with_exit(cmd)
55
+
56
+ if exit_code != 0
57
+ raise SSHFailed, "SSH exited (#{exit_code}) for command: [#{cmd}]"
58
+ end
59
+ end
60
+
61
+ def upload!(local, remote, options = {}, &progress)
62
+ if progress.nil?
63
+ progress = lambda { |ch, name, sent, total|
64
+ if sent == total
65
+ logger.info("Uploaded #{name} (#{total} bytes)")
66
+ end
67
+ }
68
+ end
69
+
70
+ session.scp.upload!(local, remote, options, &progress)
71
+ end
72
+
73
+ def upload_path!(local, remote, options = {}, &progress)
74
+ options = { :recursive => true }.merge(options)
75
+
76
+ upload!(local, remote, options, &progress)
77
+ end
78
+
79
+ def shutdown
80
+ return if @session.nil?
81
+
82
+ logger.debug("[SSH] closing connection to #{self}")
83
+ session.shutdown!
84
+ ensure
85
+ @session = nil
86
+ end
87
+
88
+ def wait
89
+ logger.log("Waiting for #{hostname}:#{port}...") until test_ssh
90
+ end
91
+
92
+ def login_command
93
+ args = %W{ -o UserKnownHostsFile=/dev/null }
94
+ args += %W{ -o StrictHostKeyChecking=no }
95
+ args += %W{ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} }
96
+ Array(options[:keys]).each { |ssh_key| args += %W{ -i #{ssh_key}} }
97
+ args += %W{ -p #{port}}
98
+ args += %W{ #{username}@#{hostname}}
99
+
100
+ LoginCommand.new(["ssh", *args])
101
+ end
102
+
103
+ private
104
+
105
+ attr_reader :hostname, :username, :options, :logger
106
+
107
+ def session
108
+ @session ||= begin
109
+ logger.debug("[SSH] opening connection to #{self}")
110
+ Net::SSH.start(hostname, username, options)
111
+ end
112
+ end
113
+
114
+ def to_s
115
+ "#{username}@#{hostname}:#{port}<#{options.inspect}>"
116
+ end
117
+
118
+ def port
119
+ options.fetch(:port, 22)
120
+ end
121
+
122
+ def exec_with_exit(cmd)
123
+ exit_code = nil
124
+ session.open_channel do |channel|
125
+
126
+ channel.request_pty
127
+
128
+ channel.exec(cmd) do |ch, success|
129
+
130
+ channel.on_data do |ch, data|
131
+ logger << data
132
+ end
133
+
134
+ channel.on_extended_data do |ch, type, data|
135
+ logger << data
136
+ end
137
+
138
+ channel.on_request("exit-status") do |ch, data|
139
+ exit_code = data.read_long
140
+ end
141
+ end
142
+ end
143
+ session.loop
144
+ exit_code
145
+ end
146
+
147
+ def test_ssh
148
+ socket = TCPSocket.new(hostname, port)
149
+ IO.select([socket], nil, nil, 5)
150
+ rescue SocketError, Errno::ECONNREFUSED,
151
+ Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
152
+ sleep 2
153
+ false
154
+ rescue Errno::EPERM, Errno::ETIMEDOUT
155
+ false
156
+ ensure
157
+ socket && socket.close
158
+ end
159
+ end
160
+ end
@@ -27,55 +27,75 @@ module Kitchen
27
27
  # @return [String] logical name of this suite
28
28
  attr_reader :name
29
29
 
30
- # @return [Array] Array of Chef run_list items
31
- attr_reader :run_list
32
-
33
- # @return [Hash] Hash of Chef node attributes
34
- attr_reader :attributes
35
-
36
30
  # @return [Array] Array of names of excluded platforms
37
31
  attr_reader :excludes
38
32
 
39
- # @return [String] local path to the suite's data bags, or nil if one does
40
- # not exist
41
- attr_reader :data_bags_path
42
-
43
- # @return [String] local path to the suite's encrypted data bag secret
44
- # key path, or nil if one does not exist
45
- attr_reader :encrypted_data_bag_secret_key_path
46
-
47
- # @return [String] local path to the suite's roles, or nil if one does
48
- # not exist
49
- attr_reader :roles_path
50
-
51
33
  # Constructs a new suite.
52
34
  #
53
35
  # @param [Hash] options configuration for a new suite
54
36
  # @option options [String] :name logical name of this suit (**Required**)
55
- # @option options [String] :run_list Array of Chef run_list items
56
- # (**Required**)
57
- # @option options [Hash] :attributes Hash of Chef node attributes
58
37
  # @option options [String] :excludes Array of names of excluded platforms
59
- # @option options [String] :data_bags_path path to data bags
60
- # @option options [String] :roles_path path to roles
61
- # @option options [String] :encrypted_data_bag_secret_key_path path to
62
- # secret key file
63
38
  def initialize(options = {})
39
+ options = options.dup
64
40
  validate_options(options)
65
41
 
66
- @name = options[:name]
67
- @run_list = options[:run_list]
68
- @attributes = options[:attributes] || Hash.new
69
- @excludes = options[:excludes] || Array.new
70
- @data_bags_path = options[:data_bags_path]
71
- @roles_path = options[:roles_path]
72
- @encrypted_data_bag_secret_key_path = options[:encrypted_data_bag_secret_key_path]
42
+ @name = options.delete(:name)
43
+ @excludes = Array(options[:excludes])
44
+ @data = options
45
+ end
46
+
47
+ # Extra suite methods used for accessing Chef data such as a run list,
48
+ # node attributes, etc.
49
+ module Cheflike
50
+
51
+ # @return [Array] Array of Chef run_list items
52
+ def run_list
53
+ Array(data[:run_list])
54
+ end
55
+
56
+ # @return [Hash] Hash of Chef node attributes
57
+ def attributes
58
+ data[:attributes] || Hash.new
59
+ end
60
+
61
+ # @return [String] local path to the suite's data bags, or nil if one
62
+ # does not exist
63
+ def data_bags_path
64
+ data[:data_bags_path]
65
+ end
66
+
67
+ # @return [String] local path to the suite's encrypted data bag secret
68
+ # key path, or nil if one does not exist
69
+ def encrypted_data_bag_secret_key_path
70
+ data[:encrypted_data_bag_secret_key_path]
71
+ end
72
+
73
+ # @return [String] local path to the suite's roles, or nil if one does
74
+ # not exist
75
+ def roles_path
76
+ data[:roles_path]
77
+ end
78
+
79
+ # @return [String] local path to the suite's nodes, or nil if one does
80
+ # not exist
81
+ def nodes_path
82
+ data[:nodes_path]
83
+ end
84
+ end
85
+
86
+ # Extra suite methods used for accessing Puppet data such as a manifest.
87
+ module Puppetlike
88
+
89
+ def manifest
90
+ end
73
91
  end
74
92
 
75
93
  private
76
94
 
95
+ attr_reader :data
96
+
77
97
  def validate_options(opts)
78
- [:name, :run_list].each do |k|
98
+ [:name].each do |k|
79
99
  raise ClientError, "Suite#new requires option :#{k}" if opts[k].nil?
80
100
  end
81
101
  end
@@ -18,5 +18,5 @@
18
18
 
19
19
  module Kitchen
20
20
 
21
- VERSION = "1.0.0.alpha.7"
21
+ VERSION = "1.0.0.beta.1"
22
22
  end
@@ -93,43 +93,75 @@ describe Kitchen::Config do
93
93
  config.suites.must_equal []
94
94
  end
95
95
 
96
+ def cheflike_suite(suite)
97
+ suite.extend(Kitchen::Suite::Cheflike)
98
+ end
99
+
96
100
  it "returns a suite with nil for data_bags_path by default" do
97
101
  stub_data!({ :suites => [{ :name => 'one', :run_list => [] }] })
98
- config.suites.first.data_bags_path.must_be_nil
102
+ cheflike_suite(config.suites.first).data_bags_path.must_be_nil
99
103
  end
100
104
 
101
- it "retuns a suite with a common data_bags_path set" do
105
+ it "returns a suite with a common data_bags_path set" do
102
106
  stub_data!({ :suites => [{ :name => 'one', :run_list => [] }] })
103
107
  config.test_base_path = "/tmp/base"
104
108
  FileUtils.mkdir_p "/tmp/base/data_bags"
105
109
 
106
- config.suites.first.data_bags_path.must_equal "/tmp/base/data_bags"
110
+ cheflike_suite(config.suites.first).data_bags_path.
111
+ must_equal "/tmp/base/data_bags"
107
112
  end
108
113
 
109
- it "retuns a suite with a suite-specific data_bags_path set" do
114
+ it "returns a suite with a suite-specific data_bags_path set" do
110
115
  stub_data!({ :suites => [{ :name => 'cool', :run_list => [] }] })
111
116
  config.test_base_path = "/tmp/base"
112
117
  FileUtils.mkdir_p "/tmp/base/cool/data_bags"
113
- config.suites.first.data_bags_path.must_equal "/tmp/base/cool/data_bags"
118
+
119
+ cheflike_suite(config.suites.first).data_bags_path.
120
+ must_equal "/tmp/base/cool/data_bags"
121
+ end
122
+
123
+ it "returns a suite with a custom data_bags_path set" do
124
+ stub_data!({ :suites => [{ :name => 'one', :run_list => [],
125
+ :data_bags_path => 'shared/data_bags' }] })
126
+ config.kitchen_root = "/tmp/base"
127
+ FileUtils.mkdir_p "/tmp/base/shared/data_bags"
128
+
129
+ cheflike_suite(config.suites.first).data_bags_path.
130
+ must_equal "/tmp/base/shared/data_bags"
114
131
  end
115
132
 
116
133
  it "returns a suite with nil for roles_path by default" do
117
134
  stub_data!({ :suites => [{ :name => 'one', :run_list => [] }] })
118
- config.suites.first.roles_path.must_be_nil
135
+
136
+ cheflike_suite(config.suites.first).roles_path.must_be_nil
119
137
  end
120
138
 
121
139
  it "returns a suite with a common roles_path set" do
122
140
  stub_data!({ :suites => [{ :name => 'one', :run_list => [] }] })
123
141
  config.test_base_path = "/tmp/base"
124
142
  FileUtils.mkdir_p "/tmp/base/roles"
125
- config.suites.first.roles_path.must_equal "/tmp/base/roles"
143
+
144
+ cheflike_suite(config.suites.first).roles_path.
145
+ must_equal "/tmp/base/roles"
126
146
  end
127
147
 
128
148
  it "returns a suite with a suite-specific roles_path set" do
129
149
  stub_data!({ :suites => [{ :name => 'mysuite', :run_list => [] }] })
130
150
  config.test_base_path = "/tmp/base"
131
151
  FileUtils.mkdir_p "/tmp/base/mysuite/roles"
132
- config.suites.first.roles_path.must_equal "/tmp/base/mysuite/roles"
152
+
153
+ cheflike_suite(config.suites.first).roles_path.
154
+ must_equal "/tmp/base/mysuite/roles"
155
+ end
156
+
157
+ it "returns a suite with a custom roles_path set" do
158
+ stub_data!({ :suites => [{ :name => 'one', :run_list => [],
159
+ :roles_path => 'shared/roles' }] })
160
+ config.kitchen_root = "/tmp/base"
161
+ FileUtils.mkdir_p "/tmp/base/shared/roles"
162
+
163
+ cheflike_suite(config.suites.first).roles_path.
164
+ must_equal "/tmp/base/shared/roles"
133
165
  end
134
166
  end
135
167