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

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