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