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.
@@ -20,6 +20,10 @@ require 'thor/util'
20
20
 
21
21
  module Kitchen
22
22
 
23
+ # A driver is responsible for carrying out the lifecycle activities of an
24
+ # instance, such as creating, converging, and destroying an instance.
25
+ #
26
+ # @author Fletcher Nichol <fnichol@nichol.ca>
23
27
  module Driver
24
28
 
25
29
  # Returns an instance of a driver given a plugin type string.
@@ -22,23 +22,7 @@ module Kitchen
22
22
 
23
23
  module Driver
24
24
 
25
- # Value object to track a shell command that will be passed to Kernel.exec
26
- # for execution.
27
- #
28
- # @author Fletcher Nichol <fnichol@nichol.ca>
29
- class LoginCommand
30
-
31
- attr_reader :cmd_array, :options
32
-
33
- def initialize(cmd_array, options = {})
34
- @cmd_array = cmd_array
35
- @options = options
36
- end
37
- end
38
-
39
- # Base class for a driver. A driver is responsible for carrying out the
40
- # lifecycle activities of an instance, such as creating, converging, and
41
- # destroying an instance.
25
+ # Base class for a driver.
42
26
  #
43
27
  # @author Fletcher Nichol <fnichol@nichol.ca>
44
28
  class Base
@@ -46,22 +30,32 @@ module Kitchen
46
30
  include ShellOut
47
31
  include Logging
48
32
 
49
- attr_writer :instance
33
+ attr_accessor :instance
50
34
 
51
35
  class << self
52
36
  attr_reader :serial_actions
53
37
  end
54
38
 
55
39
  def initialize(config = {})
56
- @config = config
40
+ @config = LazyDriverHash.new(config, self)
57
41
  self.class.defaults.each do |attr, value|
58
42
  @config[attr] = value unless @config[attr]
59
43
  end
44
+ end
45
+
46
+ def validate_config!
60
47
  Array(self.class.validations).each do |tuple|
61
- tuple.last.call(tuple.first, config[tuple.first])
48
+ tuple.last.call(tuple.first, config[tuple.first], self)
62
49
  end
63
50
  end
64
51
 
52
+ # Returns the name of this driver, suitable for display in a CLI.
53
+ #
54
+ # @return [String] name of this driver
55
+ def name
56
+ self.class.name.split('::').last
57
+ end
58
+
65
59
  # Provides hash-like access to configuration keys.
66
60
  #
67
61
  # @param attr [Object] configuration key
@@ -121,7 +115,7 @@ module Kitchen
121
115
 
122
116
  protected
123
117
 
124
- attr_reader :config, :instance
118
+ attr_reader :config
125
119
 
126
120
  ACTION_METHODS = %w{create converge setup verify destroy}.
127
121
  map(&:to_sym).freeze
@@ -180,8 +174,8 @@ module Kitchen
180
174
  end
181
175
  end
182
176
 
183
- def self.default_config(attr, value)
184
- defaults[attr] = value
177
+ def self.default_config(attr, value = nil, &block)
178
+ defaults[attr] = block_given? ? block : value
185
179
  end
186
180
 
187
181
  def self.validations
@@ -192,9 +186,10 @@ module Kitchen
192
186
  @validations = [] if @validations.nil?
193
187
  if ! block_given?
194
188
  klass = self
195
- block = lambda do |attr, value|
189
+ block = lambda do |attr, value, driver|
196
190
  if value.nil? || value.to_s.empty?
197
- raise UserError, "#{klass}#config[:#{attr}] cannot be blank"
191
+ attribute = "#{klass}#{driver.instance.to_str}#config[:#{attr}]"
192
+ raise UserError, "#{attribute} cannot be blank"
198
193
  end
199
194
  end
200
195
  end
@@ -211,6 +206,28 @@ module Kitchen
211
206
  @serial_actions ||= []
212
207
  @serial_actions += methods
213
208
  end
209
+
210
+ # A modifed Hash object that may contain procs as a value which must be
211
+ # executed in the context of a Driver object.
212
+ #
213
+ # @author Fletcher Nichol <fnichol@nichol.ca>
214
+ class LazyDriverHash < SimpleDelegator
215
+
216
+ def initialize(obj, driver)
217
+ @driver = driver
218
+ super(obj)
219
+ end
220
+
221
+ def [](key)
222
+ proc_or_val = __getobj__[key]
223
+
224
+ if proc_or_val.respond_to?(:call)
225
+ proc_or_val.call(@driver)
226
+ else
227
+ proc_or_val
228
+ end
229
+ end
230
+ end
214
231
  end
215
232
  end
216
233
  end
@@ -16,9 +16,6 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'net/ssh'
20
- require 'socket'
21
-
22
19
  module Kitchen
23
20
 
24
21
  module Driver
@@ -39,28 +36,29 @@ module Kitchen
39
36
  end
40
37
 
41
38
  def converge(state)
42
- ssh_args = build_ssh_args(state)
43
-
44
- install_omnibus(ssh_args) if config[:require_chef_omnibus]
45
- prepare_chef_home(ssh_args)
46
- upload_chef_data(ssh_args)
47
- run_chef_solo(ssh_args)
39
+ provisioner = new_provisioner
40
+
41
+ Kitchen::SSH.new(*build_ssh_args(state)) do |conn|
42
+ run_remote(provisioner.install_command, conn)
43
+ run_remote(provisioner.init_command, conn)
44
+ transfer_path(provisioner.create_sandbox, provisioner.home_path, conn)
45
+ run_remote(provisioner.prepare_command, conn)
46
+ run_remote(provisioner.run_command, conn)
47
+ end
48
+ ensure
49
+ provisioner && provisioner.cleanup_sandbox
48
50
  end
49
51
 
50
52
  def setup(state)
51
- ssh_args = build_ssh_args(state)
52
-
53
- if busser_setup_cmd
54
- ssh(ssh_args, busser_setup_cmd)
53
+ Kitchen::SSH.new(*build_ssh_args(state)) do |conn|
54
+ run_remote(busser_setup_cmd, conn)
55
55
  end
56
56
  end
57
57
 
58
58
  def verify(state)
59
- ssh_args = build_ssh_args(state)
60
-
61
- if busser_run_cmd
62
- ssh(ssh_args, busser_sync_cmd)
63
- ssh(ssh_args, busser_run_cmd)
59
+ Kitchen::SSH.new(*build_ssh_args(state)) do |conn|
60
+ run_remote(busser_sync_cmd, conn)
61
+ run_remote(busser_run_cmd, conn)
64
62
  end
65
63
  end
66
64
 
@@ -69,20 +67,23 @@ module Kitchen
69
67
  end
70
68
 
71
69
  def login_command(state)
72
- combined = config.merge(state)
73
-
74
- args = %W{ -o UserKnownHostsFile=/dev/null }
75
- args += %W{ -o StrictHostKeyChecking=no }
76
- args += %W{ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} }
77
- args += %W{ -i #{combined[:ssh_key]}} if combined[:ssh_key]
78
- args += %W{ -p #{combined[:port]}} if combined[:port]
79
- args += %W{ #{combined[:username]}@#{combined[:hostname]}}
70
+ SSH.new(*build_ssh_args(state)).login_command
71
+ end
80
72
 
81
- Driver::LoginCommand.new(["ssh", *args])
73
+ def ssh(ssh_args, command)
74
+ Kitchen::SSH.new(*ssh_args) do |conn|
75
+ run_remote(command, conn)
76
+ end
82
77
  end
83
78
 
84
79
  protected
85
80
 
81
+ def new_provisioner
82
+ combined = config.dup
83
+ combined[:log_level] = Util.from_logger_level(logger.level)
84
+ Provisioner.for_plugin(combined[:provisioner], instance, combined)
85
+ end
86
+
86
87
  def build_ssh_args(state)
87
88
  combined = config.merge(state)
88
89
 
@@ -92,132 +93,37 @@ module Kitchen
92
93
  opts[:password] = combined[:password] if combined[:password]
93
94
  opts[:port] = combined[:port] if combined[:port]
94
95
  opts[:keys] = Array(combined[:ssh_key]) if combined[:ssh_key]
96
+ opts[:logger] = logger
95
97
 
96
98
  [combined[:hostname], combined[:username], opts]
97
99
  end
98
100
 
99
- def chef_home
100
- "/tmp/kitchen-chef-solo".freeze
101
- end
102
-
103
- def install_omnibus(ssh_args)
104
- url = "https://www.opscode.com/chef/install.sh"
105
- flag = config[:require_chef_omnibus]
106
- version = if flag.is_a?(String) && flag != "latest"
107
- "-s -- -v #{flag.downcase}"
108
- else
109
- ""
110
- end
111
-
112
- ssh(ssh_args, <<-INSTALL.gsub(/^ {10}/, ''))
113
- should_update_chef() {
114
- case "#{flag}" in
115
- true|$(chef-solo -v | cut -d " " -f 2)) return 1 ;;
116
- latest|*) return 0 ;;
117
- esac
118
- }
119
-
120
- if [ ! -d "/opt/chef" ] || should_update_chef ; then
121
- echo "-----> Installing Chef Omnibus (#{flag})"
122
- if command -v wget >/dev/null ; then
123
- wget #{url} -O - | #{cmd('bash')} #{version}
124
- elif command -v curl >/dev/null ; then
125
- curl -sSL #{url} | #{cmd('bash')} #{version}
126
- else
127
- echo ">>>>>> Neither wget nor curl found on this instance."
128
- exit 1
129
- fi
130
- fi
131
- INSTALL
132
- end
133
-
134
- def prepare_chef_home(ssh_args)
135
- ssh(ssh_args, "#{cmd('rm')} -rf #{chef_home} && mkdir -p #{chef_home}/cache")
136
- end
137
-
138
- def upload_chef_data(ssh_args)
139
- Kitchen::ChefDataUploader.new(
140
- instance, ssh_args, config[:kitchen_root], chef_home
141
- ).upload
142
- end
143
-
144
- def run_chef_solo(ssh_args)
145
- ssh(ssh_args, <<-RUN_SOLO)
146
- #{cmd('chef-solo')} -c #{chef_home}/solo.rb -j #{chef_home}/dna.json \
147
- --log_level #{Util.from_logger_level(logger.level)}
148
- RUN_SOLO
149
- end
150
-
151
- def ssh(ssh_args, cmd)
101
+ def env_cmd(cmd)
152
102
  env = "env"
153
- if config[:http_proxy]
154
- env << " http_proxy=#{config[:http_proxy]}"
155
- end
156
- if config[:https_proxy]
157
- env << " https_proxy=#{config[:https_proxy]}"
158
- end
159
- if env != "env"
160
- cmd = "#{env} #{cmd}"
161
- end
162
-
163
- debug("[SSH] #{ssh_args[1]}@#{ssh_args[0]} (#{cmd})")
164
- Net::SSH.start(*ssh_args) do |ssh|
165
- exit_code = ssh_exec_with_exit!(ssh, cmd)
103
+ env << " http_proxy=#{config[:http_proxy]}" if config[:http_proxy]
104
+ env << " https_proxy=#{config[:https_proxy]}" if config[:https_proxy]
166
105
 
167
- if exit_code != 0
168
- shorter_cmd = cmd.squeeze(" ").strip
169
- raise ActionFailed,
170
- "SSH exited (#{exit_code}) for command: [#{shorter_cmd}]"
171
- end
172
- end
173
- rescue Net::SSH::Exception => ex
174
- raise ActionFailed, ex.message
106
+ env == "env" ? cmd : "#{env} #{cmd}"
175
107
  end
176
108
 
177
- def ssh_exec_with_exit!(ssh, cmd)
178
- exit_code = nil
179
- ssh.open_channel do |channel|
109
+ def run_remote(command, connection)
110
+ return if command.nil?
180
111
 
181
- channel.request_pty
182
-
183
- channel.exec(cmd) do |ch, success|
184
-
185
- channel.on_data do |ch, data|
186
- logger << data
187
- end
188
-
189
- channel.on_extended_data do |ch, type, data|
190
- logger << data
191
- end
192
-
193
- channel.on_request("exit-status") do |ch, data|
194
- exit_code = data.read_long
195
- end
196
- end
197
- end
198
- ssh.loop
199
- exit_code
112
+ connection.exec(env_cmd(command))
113
+ rescue SSHFailed, Net::SSH::Exception => ex
114
+ raise ActionFailed, ex.message
200
115
  end
201
116
 
202
- def wait_for_sshd(hostname)
203
- logger << "." until test_ssh(hostname)
204
- end
117
+ def transfer_path(local, remote, connection)
118
+ return if local.nil?
205
119
 
206
- def test_ssh(hostname)
207
- socket = TCPSocket.new(hostname, config[:port])
208
- IO.select([socket], nil, nil, 5)
209
- rescue SocketError, Errno::ECONNREFUSED,
210
- Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
211
- sleep 2
212
- false
213
- rescue Errno::EPERM, Errno::ETIMEDOUT
214
- false
215
- ensure
216
- socket && socket.close
120
+ connection.upload_path!(local, remote)
121
+ rescue SSHFailed, Net::SSH::Exception => ex
122
+ raise ActionFailed, ex.message
217
123
  end
218
124
 
219
- def cmd(script)
220
- config[:sudo] ? "sudo -E #{script}" : script
125
+ def wait_for_sshd(*ssh_args)
126
+ SSH.new(*ssh_args).wait
221
127
  end
222
128
  end
223
129
  end
@@ -18,6 +18,9 @@
18
18
 
19
19
  module Kitchen
20
20
 
21
+ # All Kitchen errors and exceptions.
22
+ #
23
+ # @author Fletcher Nichol <fnichol@nichol.ca>
21
24
  module Error
22
25
 
23
26
  def self.formatted_trace(exception)
@@ -47,33 +47,12 @@ module Kitchen
47
47
  self.class.source_root(Kitchen.source_root.join("templates", "init"))
48
48
 
49
49
  create_kitchen_yaml
50
-
51
- rakedoc = <<-RAKE.gsub(/^ {10}/, '')
52
-
53
- begin
54
- require 'kitchen/rake_tasks'
55
- Kitchen::RakeTasks.new
56
- rescue LoadError
57
- puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
58
- end
59
- RAKE
60
- append_to_file("Rakefile", rakedoc) if init_rakefile?
61
-
62
- thordoc = <<-THOR.gsub(/^ {10}/, '')
63
-
64
- begin
65
- require 'kitchen/thor_tasks'
66
- Kitchen::ThorTasks.new
67
- rescue LoadError
68
- puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
69
- end
70
- THOR
71
- append_to_file("Thorfile", thordoc) if init_thorfile?
72
-
50
+ prepare_rakefile if init_rakefile?
51
+ prepare_thorfile if init_thorfile?
73
52
  empty_directory "test/integration/default" if init_test_dir?
74
53
  append_to_gitignore(".kitchen/")
75
54
  append_to_gitignore(".kitchen.local.yml")
76
- prepare_gemfile if File.exists?("Gemfile") || options[:create_gemfile]
55
+ prepare_gemfile if init_gemfile?
77
56
  add_drivers
78
57
 
79
58
  if @display_bundle_msg
@@ -98,13 +77,18 @@ module Kitchen
98
77
  })
99
78
  end
100
79
 
80
+ def init_gemfile?
81
+ File.exists?(File.join(destination_root, "Gemfile")) ||
82
+ options[:create_gemfile]
83
+ end
84
+
101
85
  def init_rakefile?
102
- File.exists?("Rakefile") &&
86
+ File.exists?(File.join(destination_root, "Rakefile")) &&
103
87
  not_in_file?("Rakefile", %r{require 'kitchen/rake_tasks'})
104
88
  end
105
89
 
106
90
  def init_thorfile?
107
- File.exists?("Thorfile") &&
91
+ File.exists?(File.join(destination_root, "Thorfile")) &&
108
92
  not_in_file?("Thorfile", %r{require 'kitchen/thor_tasks'})
109
93
  end
110
94
 
@@ -112,10 +96,36 @@ module Kitchen
112
96
  Dir.glob("test/integration/*").select { |d| File.directory?(d) }.empty?
113
97
  end
114
98
 
99
+ def prepare_rakefile
100
+ rakedoc = <<-RAKE.gsub(/^ {10}/, '')
101
+
102
+ begin
103
+ require 'kitchen/rake_tasks'
104
+ Kitchen::RakeTasks.new
105
+ rescue LoadError
106
+ puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
107
+ end
108
+ RAKE
109
+ append_to_file(File.join(destination_root, "Rakefile"), rakedoc)
110
+ end
111
+
112
+ def prepare_thorfile
113
+ thordoc = <<-THOR.gsub(/^ {10}/, '')
114
+
115
+ begin
116
+ require 'kitchen/thor_tasks'
117
+ Kitchen::ThorTasks.new
118
+ rescue LoadError
119
+ puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI']
120
+ end
121
+ THOR
122
+ append_to_file(File.join(destination_root, "Thorfile"), thordoc)
123
+ end
124
+
115
125
  def append_to_gitignore(line)
116
- create_file(".gitignore") unless File.exists?(".gitignore")
126
+ create_file(".gitignore") unless File.exists?(File.join(destination_root, ".gitignore"))
117
127
 
118
- if IO.readlines(".gitignore").grep(%r{^#{line}}).empty?
128
+ if IO.readlines(File.join(destination_root, ".gitignore")).grep(%r{^#{line}}).empty?
119
129
  append_to_file(".gitignore", "#{line}\n")
120
130
  end
121
131
  end
@@ -126,7 +136,7 @@ module Kitchen
126
136
  end
127
137
 
128
138
  def create_gemfile_if_missing
129
- unless File.exists?("Gemfile")
139
+ unless File.exists?(File.join(destination_root, "Gemfile"))
130
140
  create_file("Gemfile", %{source 'https://rubygems.org'\n\n})
131
141
  end
132
142
  end
@@ -144,7 +154,7 @@ module Kitchen
144
154
  display_warning = false
145
155
 
146
156
  Array(options[:driver]).each do |driver_gem|
147
- if File.exists?("Gemfile") || options[:create_gemfile]
157
+ if File.exists?(File.join(destination_root, "Gemfile")) || options[:create_gemfile]
148
158
  add_driver_to_gemfile(driver_gem)
149
159
  else
150
160
  install_gem(driver_gem)
@@ -167,7 +177,7 @@ module Kitchen
167
177
  end
168
178
 
169
179
  def not_in_file?(filename, regexp)
170
- IO.readlines(filename).grep(regexp).empty?
180
+ IO.readlines(File.join(destination_root, filename)).grep(regexp).empty?
171
181
  end
172
182
 
173
183
  def unbundlerize