test-kitchen 1.3.1 → 1.4.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +2 -0
  3. data/.gitignore +4 -0
  4. data/CHANGELOG.md +45 -0
  5. data/Rakefile +15 -0
  6. data/features/kitchen_action_commands.feature +12 -9
  7. data/features/kitchen_defaults.feature +38 -0
  8. data/features/kitchen_init_command.feature +0 -1
  9. data/features/kitchen_list_command.feature +2 -2
  10. data/features/kitchen_login_command.feature +7 -1
  11. data/features/kitchen_test_command.feature +4 -4
  12. data/lib/kitchen.rb +40 -11
  13. data/lib/kitchen/cli.rb +38 -22
  14. data/lib/kitchen/command/list.rb +5 -2
  15. data/lib/kitchen/config.rb +45 -18
  16. data/lib/kitchen/configurable.rb +137 -1
  17. data/lib/kitchen/data_munger.rb +248 -17
  18. data/lib/kitchen/driver.rb +1 -1
  19. data/lib/kitchen/driver/base.rb +1 -83
  20. data/lib/kitchen/driver/dummy.rb +0 -5
  21. data/lib/kitchen/driver/ssh_base.rb +177 -22
  22. data/lib/kitchen/instance.rb +140 -20
  23. data/lib/kitchen/logger.rb +43 -8
  24. data/lib/kitchen/login_command.rb +14 -5
  25. data/lib/kitchen/platform.rb +19 -0
  26. data/lib/kitchen/provisioner.rb +5 -3
  27. data/lib/kitchen/provisioner/base.rb +46 -48
  28. data/lib/kitchen/provisioner/chef/common_sandbox.rb +322 -0
  29. data/lib/kitchen/provisioner/chef_base.rb +179 -286
  30. data/lib/kitchen/provisioner/chef_solo.rb +11 -5
  31. data/lib/kitchen/provisioner/chef_zero.rb +108 -94
  32. data/lib/kitchen/provisioner/dummy.rb +47 -0
  33. data/lib/kitchen/provisioner/shell.rb +45 -12
  34. data/lib/kitchen/rake_tasks.rb +1 -1
  35. data/lib/kitchen/ssh.rb +1 -1
  36. data/lib/kitchen/thor_tasks.rb +1 -1
  37. data/lib/kitchen/transport.rb +54 -0
  38. data/lib/kitchen/transport/base.rb +146 -0
  39. data/lib/kitchen/transport/dummy.rb +75 -0
  40. data/lib/kitchen/transport/ssh.rb +325 -0
  41. data/lib/kitchen/transport/winrm.rb +508 -0
  42. data/lib/kitchen/transport/winrm/command_executor.rb +188 -0
  43. data/lib/kitchen/transport/winrm/file_transporter.rb +454 -0
  44. data/lib/kitchen/transport/winrm/logging.rb +50 -0
  45. data/lib/kitchen/transport/winrm/template.rb +74 -0
  46. data/lib/kitchen/transport/winrm/tmp_zip.rb +187 -0
  47. data/lib/kitchen/verifier.rb +55 -0
  48. data/lib/kitchen/verifier/base.rb +191 -0
  49. data/lib/kitchen/verifier/busser.rb +266 -0
  50. data/lib/kitchen/verifier/dummy.rb +75 -0
  51. data/lib/kitchen/version.rb +1 -1
  52. data/spec/kitchen/cli_spec.rb +56 -0
  53. data/spec/kitchen/config_spec.rb +61 -20
  54. data/spec/kitchen/configurable_spec.rb +327 -1
  55. data/spec/kitchen/data_munger_spec.rb +777 -14
  56. data/spec/kitchen/driver/base_spec.rb +7 -38
  57. data/spec/kitchen/driver/dummy_spec.rb +0 -29
  58. data/spec/kitchen/driver/ssh_base_spec.rb +580 -236
  59. data/spec/kitchen/driver_spec.rb +1 -0
  60. data/spec/kitchen/instance_spec.rb +383 -83
  61. data/spec/kitchen/login_command_spec.rb +29 -10
  62. data/spec/kitchen/platform_spec.rb +58 -2
  63. data/spec/kitchen/provisioner/base_spec.rb +170 -18
  64. data/spec/kitchen/provisioner/chef_base_spec.rb +454 -104
  65. data/spec/kitchen/provisioner/chef_solo_spec.rb +307 -104
  66. data/spec/kitchen/provisioner/chef_zero_spec.rb +561 -230
  67. data/spec/kitchen/provisioner/dummy_spec.rb +91 -0
  68. data/spec/kitchen/provisioner/shell_spec.rb +158 -56
  69. data/spec/kitchen/provisioner_spec.rb +37 -0
  70. data/spec/kitchen/ssh_spec.rb +19 -19
  71. data/spec/kitchen/transport/base_spec.rb +89 -0
  72. data/spec/kitchen/transport/ssh_spec.rb +1147 -0
  73. data/spec/kitchen/transport/winrm/command_executor_spec.rb +400 -0
  74. data/spec/kitchen/transport/winrm/file_transporter_spec.rb +876 -0
  75. data/spec/kitchen/transport/winrm/logging_spec.rb +92 -0
  76. data/spec/kitchen/transport/winrm/template_spec.rb +51 -0
  77. data/spec/kitchen/transport/winrm/tmp_zip_spec.rb +132 -0
  78. data/spec/kitchen/transport/winrm_spec.rb +1069 -0
  79. data/spec/kitchen/transport_spec.rb +112 -0
  80. data/spec/kitchen/verifier/base_spec.rb +310 -0
  81. data/spec/kitchen/verifier/busser_spec.rb +540 -0
  82. data/spec/kitchen/verifier/dummy_spec.rb +91 -0
  83. data/spec/kitchen/verifier_spec.rb +120 -0
  84. data/spec/kitchen_spec.rb +7 -0
  85. data/spec/spec_helper.rb +8 -0
  86. data/spec/support/powershell_max_size_spec.rb +40 -0
  87. data/support/busser_install_command.ps1 +14 -0
  88. data/support/busser_install_command.sh +15 -0
  89. data/support/check_files.ps1.erb +48 -0
  90. data/support/chef_base_init_command.ps1 +18 -0
  91. data/support/chef_base_init_command.sh +2 -0
  92. data/support/chef_base_install_command.ps1 +76 -0
  93. data/support/chef_base_install_command.sh +137 -0
  94. data/support/chef_zero_prepare_command_legacy.ps1 +9 -0
  95. data/support/chef_zero_prepare_command_legacy.sh +10 -0
  96. data/support/decode_files.ps1.erb +61 -0
  97. data/test-kitchen.gemspec +2 -0
  98. metadata +97 -8
  99. data/lib/kitchen/busser.rb +0 -316
  100. data/spec/kitchen/busser_spec.rb +0 -490
  101. data/support/chef_helpers.sh +0 -16
@@ -0,0 +1,89 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2015, 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_relative "../../spec_helper"
20
+
21
+ require "kitchen"
22
+
23
+ describe Kitchen::Transport::Base do
24
+
25
+ let(:logged_output) { StringIO.new }
26
+ let(:logger) { Logger.new(logged_output) }
27
+ let(:config) { Hash.new }
28
+
29
+ let(:instance) do
30
+ stub(:name => "coolbeans", :logger => logger)
31
+ end
32
+
33
+ let(:transport) do
34
+ Kitchen::Transport::Base.new(config).finalize_config!(instance)
35
+ end
36
+
37
+ it "has an #connection method which raises a ClientError" do
38
+ proc { transport.connection({}) }.must_raise Kitchen::ClientError
39
+ end
40
+
41
+ describe "#logger" do
42
+
43
+ before { @klog = Kitchen.logger }
44
+ after { Kitchen.logger = @klog }
45
+
46
+ it "returns the instance's logger" do
47
+ transport.send(:logger).must_equal logger
48
+ end
49
+
50
+ it "returns the default logger if instance's logger is not set" do
51
+ transport = Kitchen::Transport::Base.new(config)
52
+ Kitchen.logger = "yep"
53
+
54
+ transport.send(:logger).must_equal Kitchen.logger
55
+ end
56
+ end
57
+ end
58
+
59
+ describe Kitchen::Transport::Base::Connection do
60
+
61
+ let(:logged_output) { StringIO.new }
62
+ let(:logger) { Logger.new(logged_output) }
63
+ let(:options) { { :logger => logger } }
64
+
65
+ let(:connection) do
66
+ Kitchen::Transport::Base::Connection.new(options)
67
+ end
68
+
69
+ it "has a #close method that does nothing" do
70
+ connection.close.must_be_nil
71
+ end
72
+
73
+ it "has an #execute method which raises a ClientError" do
74
+ proc { connection.execute("boo") }.must_raise Kitchen::ClientError
75
+ end
76
+
77
+ it "has a #login_command method which raises an ActionFailed" do
78
+ proc { connection.login_command }.must_raise Kitchen::ActionFailed
79
+ end
80
+
81
+ it "has an #upload method which raises a ClientError" do
82
+ proc { connection.upload(["file"], "/path/to") }.
83
+ must_raise Kitchen::ClientError
84
+ end
85
+
86
+ it "has a #wait_until_ready method that does nothing" do
87
+ connection.wait_until_ready.must_be_nil
88
+ end
89
+ end
@@ -0,0 +1,1147 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2015, 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_relative "../../spec_helper"
20
+
21
+ require "net/ssh/test"
22
+
23
+ require "kitchen/transport/ssh"
24
+
25
+ # Hack to sort results in `Dir.entries` only within the yielded block, to limit
26
+ # the "behavior pollution" to other code. This was needed for Net::SCP, as
27
+ # recursive directory upload doesn't sort the file and directory upload
28
+ # candidates which leads to different results based on the underlying
29
+ # filesystem (i.e. lexically sorted, inode insertion, mtime/atime, total
30
+ # randomness, etc.)
31
+ #
32
+ # See: https://github.com/net-ssh/net-scp/blob/a24948/lib/net/scp/upload.rb#L52
33
+
34
+ def with_sorted_dir_entries
35
+ Dir.class_exec do
36
+ class << self
37
+ alias_method :__entries__, :entries unless method_defined?(:__entries__)
38
+
39
+ def entries(*args)
40
+ send(:__entries__, *args).sort
41
+ end
42
+ end
43
+ end
44
+
45
+ yield
46
+
47
+ Dir.class_exec do
48
+ class << self
49
+ alias_method :entries, :__entries__
50
+ end
51
+ end
52
+ end
53
+
54
+ # Terrible hack to deal with Net::SSH:Test::Extensions which monkey patches
55
+ # `IO.select` with a version for testing Net::SSH code. Unfortunetly this
56
+ # impacts other code, so we'll "un-patch" this after each spec and "re-patch"
57
+ # it before the next one.
58
+
59
+ def depatch_io
60
+ IO.class_exec do
61
+ class << self
62
+ alias_method :select, :select_for_real
63
+ end
64
+ end
65
+ end
66
+
67
+ def repatch_io
68
+ IO.class_exec do
69
+ class << self
70
+ alias_method :select, :select_for_test
71
+ end
72
+ end
73
+ end
74
+
75
+ # Major hack-and-a-half to add basic `Channel#request_pty` support to
76
+ # Net::SSH's testing framework. The `Net::SSH::Test::LocalPacket` does not
77
+ # recognize the `"pty-req"` request type, so bombs out whenever this channel
78
+ # request is sent.
79
+ #
80
+ # This "make-work" fix adds a method (`#sends_request_pty`) which works just
81
+ # like `#sends_exec` expcept that it enqueues a patched subclass of
82
+ # `LocalPacket` which can deal with the `"pty-req"` type.
83
+ #
84
+ # An upstream patch to Net::SSH will be required to retire this yak shave ;)
85
+
86
+ module Net
87
+
88
+ module SSH
89
+
90
+ module Test
91
+
92
+ class Channel
93
+
94
+ def sends_request_pty
95
+ pty_data = ["xterm", 80, 24, 640, 480, "\0"]
96
+
97
+ script.events << Class.new(Net::SSH::Test::LocalPacket) do
98
+ def types
99
+ if @type == 98 && @data[1] == "pty-req"
100
+ @types ||= [
101
+ :long, :string, :bool, :string,
102
+ :long, :long, :long, :long, :string
103
+ ]
104
+ else
105
+ super
106
+ end
107
+ end
108
+ end.new(:channel_request, remote_id, "pty-req", false, *pty_data)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ describe Kitchen::Transport::Ssh do
116
+
117
+ let(:logged_output) { StringIO.new }
118
+ let(:logger) { Logger.new(logged_output) }
119
+ let(:config) { Hash.new }
120
+ let(:state) { Hash.new }
121
+
122
+ let(:instance) do
123
+ stub(:name => "coolbeans", :logger => logger, :to_str => "instance")
124
+ end
125
+
126
+ let(:transport) do
127
+ Kitchen::Transport::Ssh.new(config).finalize_config!(instance)
128
+ end
129
+
130
+ describe "default_config" do
131
+
132
+ it "sets :port to 22 by default" do
133
+ transport[:port].must_equal 22
134
+ end
135
+
136
+ it "sets :username to root by default" do
137
+ transport[:username].must_equal "root"
138
+ end
139
+
140
+ it "sets :keepalive to true by default" do
141
+ transport[:keepalive].must_equal true
142
+ end
143
+
144
+ it "sets :keepalive_interval to 60 by default" do
145
+ transport[:keepalive_interval].must_equal 60
146
+ end
147
+
148
+ it "sets :connection_timeout to 15 by default" do
149
+ transport[:connection_timeout].must_equal 15
150
+ end
151
+
152
+ it "sets :connection_retries to 5 by default" do
153
+ transport[:connection_retries].must_equal 5
154
+ end
155
+
156
+ it "sets :connection_retry_sleep to 1 by default" do
157
+ transport[:connection_retry_sleep].must_equal 1
158
+ end
159
+
160
+ it "sets :max_wait_until_ready to 600 by default" do
161
+ transport[:max_wait_until_ready].must_equal 600
162
+ end
163
+ end
164
+
165
+ describe "#connection" do
166
+
167
+ let(:klass) { Kitchen::Transport::Ssh::Connection }
168
+
169
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
170
+ def self.common_connection_specs
171
+ it "returns a Kitchen::Transport::Ssh::Connection object" do
172
+ transport.connection(state).must_be_kind_of klass
173
+ end
174
+
175
+ it "sets the :logger to the transport's logger" do
176
+ klass.expects(:new).with do |hash|
177
+ hash[:logger] == logger
178
+ end
179
+
180
+ make_connection
181
+ end
182
+
183
+ it "sets the :user_known_hosts_file to /dev/null" do
184
+ klass.expects(:new).with do |hash|
185
+ hash[:user_known_hosts_file] == "/dev/null"
186
+ end
187
+
188
+ make_connection
189
+ end
190
+
191
+ it "sets the :paranoid flag to false" do
192
+ klass.expects(:new).with do |hash|
193
+ hash[:paranoid] == false
194
+ end
195
+
196
+ make_connection
197
+ end
198
+
199
+ it "sets :hostname from config" do
200
+ config[:hostname] = "host_from_config"
201
+
202
+ klass.expects(:new).with do |hash|
203
+ hash[:hostname] == "host_from_config"
204
+ end
205
+
206
+ make_connection
207
+ end
208
+
209
+ it "sets :hostname from state over config data" do
210
+ state[:hostname] = "host_from_state"
211
+ config[:hostname] = "host_from_config"
212
+
213
+ klass.expects(:new).with do |hash|
214
+ hash[:hostname] == "host_from_state"
215
+ end
216
+
217
+ make_connection
218
+ end
219
+
220
+ it "sets :port from config" do
221
+ config[:port] = "port_from_config"
222
+
223
+ klass.expects(:new).with do |hash|
224
+ hash[:port] == "port_from_config"
225
+ end
226
+
227
+ make_connection
228
+ end
229
+
230
+ it "sets :port from state over config data" do
231
+ state[:port] = "port_from_state"
232
+ config[:port] = "port_from_config"
233
+
234
+ klass.expects(:new).with do |hash|
235
+ hash[:port] == "port_from_state"
236
+ end
237
+
238
+ make_connection
239
+ end
240
+
241
+ it "sets :username from config" do
242
+ config[:username] = "user_from_config"
243
+
244
+ klass.expects(:new).with do |hash|
245
+ hash[:username] == "user_from_config"
246
+ end
247
+
248
+ make_connection
249
+ end
250
+
251
+ it "sets :username from state over config data" do
252
+ state[:username] = "user_from_state"
253
+ config[:username] = "user_from_config"
254
+
255
+ klass.expects(:new).with do |hash|
256
+ hash[:username] == "user_from_state"
257
+ end
258
+
259
+ make_connection
260
+ end
261
+
262
+ it "sets :timeout from :connection_timeout in config" do
263
+ config[:connection_timeout] = "timeout_from_config"
264
+
265
+ klass.expects(:new).with do |hash|
266
+ hash[:timeout] == "timeout_from_config"
267
+ end
268
+
269
+ make_connection
270
+ end
271
+
272
+ it "sets :timeout from :connection_timeout in state over config data" do
273
+ state[:connection_timeout] = "timeout_from_state"
274
+ config[:connection_timeout] = "timeout_from_config"
275
+
276
+ klass.expects(:new).with do |hash|
277
+ hash[:timeout] == "timeout_from_state"
278
+ end
279
+
280
+ make_connection
281
+ end
282
+
283
+ it "sets :keepalive from config" do
284
+ config[:keepalive] = "keepalive_from_config"
285
+
286
+ klass.expects(:new).with do |hash|
287
+ hash[:keepalive] == "keepalive_from_config"
288
+ end
289
+
290
+ make_connection
291
+ end
292
+
293
+ it "sets :keepalive from state over config data" do
294
+ state[:keepalive] = "keepalive_from_state"
295
+ config[:keepalive] = "keepalive_from_config"
296
+
297
+ klass.expects(:new).with do |hash|
298
+ hash[:keepalive] == "keepalive_from_state"
299
+ end
300
+
301
+ make_connection
302
+ end
303
+
304
+ it "sets :keepalive_interval from config" do
305
+ config[:keepalive_interval] = "interval_from_config"
306
+
307
+ klass.expects(:new).with do |hash|
308
+ hash[:keepalive_interval] == "interval_from_config"
309
+ end
310
+
311
+ make_connection
312
+ end
313
+
314
+ it "sets :keepalive_interval from state over config data" do
315
+ state[:keepalive_interval] = "interval_from_state"
316
+ config[:keepalive_interval] = "interval_from_config"
317
+
318
+ klass.expects(:new).with do |hash|
319
+ hash[:keepalive_interval] == "interval_from_state"
320
+ end
321
+
322
+ make_connection
323
+ end
324
+
325
+ it "sets :connection_retries from config" do
326
+ config[:connection_retries] = "retries_from_config"
327
+
328
+ klass.expects(:new).with do |hash|
329
+ hash[:connection_retries] == "retries_from_config"
330
+ end
331
+
332
+ make_connection
333
+ end
334
+
335
+ it "sets :connection_retries from state over config data" do
336
+ state[:connection_retries] = "retries_from_state"
337
+ config[:connection_retries] = "retries_from_config"
338
+
339
+ klass.expects(:new).with do |hash|
340
+ hash[:connection_retries] == "retries_from_state"
341
+ end
342
+
343
+ make_connection
344
+ end
345
+
346
+ it "sets :connection_retry_sleep from config" do
347
+ config[:connection_retry_sleep] = "sleep_from_config"
348
+
349
+ klass.expects(:new).with do |hash|
350
+ hash[:connection_retry_sleep] == "sleep_from_config"
351
+ end
352
+
353
+ make_connection
354
+ end
355
+
356
+ it "sets :connection_retry_sleep from state over config data" do
357
+ state[:connection_retry_sleep] = "sleep_from_state"
358
+ config[:connection_retry_sleep] = "sleep_from_config"
359
+
360
+ klass.expects(:new).with do |hash|
361
+ hash[:connection_retry_sleep] == "sleep_from_state"
362
+ end
363
+
364
+ make_connection
365
+ end
366
+
367
+ it "sets :max_wait_until_ready from config" do
368
+ config[:max_wait_until_ready] = "max_from_config"
369
+
370
+ klass.expects(:new).with do |hash|
371
+ hash[:max_wait_until_ready] == "max_from_config"
372
+ end
373
+
374
+ make_connection
375
+ end
376
+
377
+ it "sets :max_wait_until_ready from state over config data" do
378
+ state[:max_wait_until_ready] = "max_from_state"
379
+ config[:max_wait_until_ready] = "max_from_config"
380
+
381
+ klass.expects(:new).with do |hash|
382
+ hash[:max_wait_until_ready] == "max_from_state"
383
+ end
384
+
385
+ make_connection
386
+ end
387
+
388
+ it "sets :keys_only to true if :ssh_key is set in config" do
389
+ config[:ssh_key] = "ssh_key_from_config"
390
+
391
+ klass.expects(:new).with do |hash|
392
+ hash[:keys_only] == true
393
+ end
394
+
395
+ make_connection
396
+ end
397
+
398
+ it "sets :keys_only to true if :ssh_key is set in state" do
399
+ state[:ssh_key] = "ssh_key_from_config"
400
+ config[:ssh_key] = false
401
+
402
+ klass.expects(:new).with do |hash|
403
+ hash[:keys_only] == true
404
+ end
405
+
406
+ make_connection
407
+ end
408
+
409
+ it "sets :keys to an array if :ssh_key is set in config" do
410
+ config[:ssh_key] = "ssh_key_from_config"
411
+
412
+ klass.expects(:new).with do |hash|
413
+ hash[:keys] == ["ssh_key_from_config"]
414
+ end
415
+
416
+ make_connection
417
+ end
418
+
419
+ it "sets :keys to an array if :ssh_key is set in state" do
420
+ state[:ssh_key] = "ssh_key_from_state"
421
+ config[:ssh_key] = "ssh_key_from_config"
422
+
423
+ klass.expects(:new).with do |hash|
424
+ hash[:keys] == ["ssh_key_from_state"]
425
+ end
426
+
427
+ make_connection
428
+ end
429
+
430
+ it "passes in :password if set in config" do
431
+ config[:password] = "password_from_config"
432
+
433
+ klass.expects(:new).with do |hash|
434
+ hash[:password] == "password_from_config"
435
+ end
436
+
437
+ make_connection
438
+ end
439
+
440
+ it "passes in :password from state over config data" do
441
+ state[:password] = "password_from_state"
442
+ config[:password] = "password_from_config"
443
+
444
+ klass.expects(:new).with do |hash|
445
+ hash[:password] == "password_from_state"
446
+ end
447
+
448
+ make_connection
449
+ end
450
+
451
+ it "passes in :forward_agent if set in config" do
452
+ config[:forward_agent] = "forward_agent_from_config"
453
+
454
+ klass.expects(:new).with do |hash|
455
+ hash[:forward_agent] == "forward_agent_from_config"
456
+ end
457
+
458
+ make_connection
459
+ end
460
+
461
+ it "passes in :forward_agent from state over config data" do
462
+ state[:forward_agent] = "forward_agent_from_state"
463
+ config[:forward_agent] = "forward_agent_from_config"
464
+
465
+ klass.expects(:new).with do |hash|
466
+ hash[:forward_agent] == "forward_agent_from_state"
467
+ end
468
+
469
+ make_connection
470
+ end
471
+
472
+ it "returns the same connection when called again with same state" do
473
+ first_connection = make_connection(state)
474
+ second_connection = make_connection(state)
475
+
476
+ first_connection.object_id.must_equal second_connection.object_id
477
+ end
478
+
479
+ it "logs a debug message when the connection is reused" do
480
+ make_connection(state)
481
+ make_connection(state)
482
+
483
+ logged_output.string.lines.select { |l|
484
+ l =~ debug_line_with("[SSH] reusing existing connection ")
485
+ }.size.must_equal 1
486
+ end
487
+
488
+ it "returns a new connection when called again if state differs" do
489
+ first_connection = make_connection(state)
490
+ second_connection = make_connection(state.merge(:port => 9000))
491
+
492
+ first_connection.object_id.wont_equal second_connection.object_id
493
+ end
494
+
495
+ it "closes first connection when a second is created" do
496
+ first_connection = make_connection(state)
497
+ first_connection.expects(:close)
498
+
499
+ make_connection(state.merge(:port => 9000))
500
+ end
501
+
502
+ it "logs a debug message a second connection is created" do
503
+ make_connection(state)
504
+ make_connection(state.merge(:port => 9000))
505
+
506
+ logged_output.string.lines.select { |l|
507
+ l =~ debug_line_with("[SSH] shutting previous connection ")
508
+ }.size.must_equal 1
509
+ end
510
+ end
511
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
512
+
513
+ describe "called without a block" do
514
+
515
+ def make_connection(s = state)
516
+ transport.connection(s)
517
+ end
518
+
519
+ common_connection_specs
520
+ end
521
+
522
+ describe "called with a block" do
523
+
524
+ def make_connection(s = state)
525
+ transport.connection(s) do |conn|
526
+ conn
527
+ end
528
+ end
529
+
530
+ common_connection_specs
531
+ end
532
+ end
533
+
534
+ def debug_line_with(msg)
535
+ %r{^D, .* : #{Regexp.escape(msg)}}
536
+ end
537
+ end
538
+
539
+ describe Kitchen::Transport::Ssh::Connection do
540
+
541
+ include Net::SSH::Test
542
+ # sadly, Net:SSH::Test includes a #connection method so we'll alias this one
543
+ # before redefining it
544
+ alias_method :net_ssh_connection, :connection
545
+
546
+ let(:logged_output) { StringIO.new }
547
+ let(:logger) { Logger.new(logged_output) }
548
+ let(:conn) { net_ssh_connection }
549
+
550
+ let(:options) do
551
+ { :logger => logger, :username => "me", :hostname => "foo", :port => 22 }
552
+ end
553
+
554
+ let(:connection) do
555
+ Kitchen::Transport::Ssh::Connection.new(options)
556
+ end
557
+
558
+ before do
559
+ repatch_io
560
+ logger.level = Logger::DEBUG
561
+ Net::SSH.stubs(:start).returns(conn)
562
+ end
563
+
564
+ after do
565
+ depatch_io
566
+ end
567
+
568
+ describe "establishing a connection" do
569
+
570
+ [
571
+ Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
572
+ Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
573
+ Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Timeout::Error
574
+ ].each do |klass|
575
+ describe "raising #{klass}" do
576
+
577
+ before do
578
+ Net::SSH.stubs(:start).raises(klass)
579
+ options[:connection_retries] = 3
580
+ options[:connection_retry_sleep] = 7
581
+ connection.stubs(:sleep)
582
+ end
583
+
584
+ it "raises an SshFailed exception" do
585
+ e = proc {
586
+ connection.execute("nope")
587
+ }.must_raise Kitchen::Transport::SshFailed
588
+ e.message.must_match regexify("SSH session could not be established")
589
+ end
590
+
591
+ it "attempts to connect :connection_retries times" do
592
+ begin
593
+ connection.execute("nope")
594
+ rescue # rubocop:disable Lint/HandleExceptions
595
+ # the raise is not what is being tested here, rather its side-effect
596
+ end
597
+
598
+ logged_output.string.lines.select { |l|
599
+ l =~ debug_line("[SSH] opening connection to me@foo<{:port=>22}>")
600
+ }.size.must_equal 3
601
+ end
602
+
603
+ it "sleeps for :connection_retry_sleep seconds between retries" do
604
+ connection.unstub(:sleep)
605
+ connection.expects(:sleep).with(7).twice
606
+
607
+ begin
608
+ connection.execute("nope")
609
+ rescue # rubocop:disable Lint/HandleExceptions
610
+ # the raise is not what is being tested here, rather its side-effect
611
+ end
612
+ end
613
+
614
+ it "logs the first 2 retry failures on info" do
615
+ begin
616
+ connection.execute("nope")
617
+ rescue # rubocop:disable Lint/HandleExceptions
618
+ # the raise is not what is being tested here, rather its side-effect
619
+ end
620
+
621
+ logged_output.string.lines.select { |l|
622
+ l =~ info_line_with(
623
+ "[SSH] connection failed, retrying in 7 seconds")
624
+ }.size.must_equal 2
625
+ end
626
+
627
+ it "logs the last retry failures on warn" do
628
+ begin
629
+ connection.execute("nope")
630
+ rescue # rubocop:disable Lint/HandleExceptions
631
+ # the raise is not what is being tested here, rather its side-effect
632
+ end
633
+
634
+ logged_output.string.lines.select { |l|
635
+ l =~ warn_line_with("[SSH] connection failed, terminating ")
636
+ }.size.must_equal 1
637
+ end
638
+ end
639
+ end
640
+ end
641
+
642
+ describe "#close" do
643
+
644
+ before do
645
+ story do |script|
646
+ channel = script.opens_channel
647
+ channel.sends_request_pty
648
+ channel.sends_exec("doit")
649
+ channel.gets_data("ok\n")
650
+ channel.gets_exit_status(0)
651
+ channel.gets_close
652
+ channel.sends_close
653
+ end
654
+ end
655
+
656
+ it "logger displays closing connection on debug" do
657
+ conn.expects(:close)
658
+
659
+ assert_scripted do
660
+ connection.execute("doit")
661
+ connection.close
662
+ end
663
+
664
+ logged_output.string.must_match debug_line(
665
+ "[SSH] closing connection to me@foo<{:port=>22}>"
666
+ )
667
+ end
668
+
669
+ it "only closes the connection once for multiple calls" do
670
+ conn.expects(:close).once
671
+
672
+ assert_scripted do
673
+ connection.execute("doit")
674
+ connection.close
675
+ connection.close
676
+ connection.close
677
+ end
678
+ end
679
+ end
680
+
681
+ describe "#execute" do
682
+
683
+ describe "for a successful command" do
684
+
685
+ before do
686
+ story do |script|
687
+ channel = script.opens_channel
688
+ channel.sends_request_pty
689
+ channel.sends_exec("doit")
690
+ channel.gets_data("ok\n")
691
+ channel.gets_extended_data("some stderr stuffs\n")
692
+ channel.gets_exit_status(0)
693
+ channel.gets_close
694
+ channel.sends_close
695
+ end
696
+ end
697
+
698
+ it "logger displays command on debug" do
699
+ assert_scripted { connection.execute("doit") }
700
+
701
+ logged_output.string.must_match debug_line(
702
+ "[SSH] me@foo<{:port=>22}> (doit)"
703
+ )
704
+ end
705
+
706
+ it "logger displays establishing connection on debug" do
707
+ assert_scripted { connection.execute("doit") }
708
+
709
+ logged_output.string.must_match debug_line(
710
+ "[SSH] opening connection to me@foo<{:port=>22}>"
711
+ )
712
+ end
713
+
714
+ it "logger captures stdout" do
715
+ assert_scripted { connection.execute("doit") }
716
+
717
+ logged_output.string.must_match(/^ok$/)
718
+ end
719
+
720
+ it "logger captures stderr" do
721
+ assert_scripted { connection.execute("doit") }
722
+
723
+ logged_output.string.must_match(/^some stderr stuffs$/)
724
+ end
725
+ end
726
+
727
+ describe "for a failed command" do
728
+
729
+ before do
730
+ story do |script|
731
+ channel = script.opens_channel
732
+ channel.sends_request_pty
733
+ channel.sends_exec("doit")
734
+ channel.gets_data("nope\n")
735
+ channel.gets_extended_data("youdead\n")
736
+ channel.gets_exit_status(42)
737
+ channel.gets_close
738
+ channel.sends_close
739
+ end
740
+ end
741
+
742
+ it "logger displays command on debug" do
743
+ begin
744
+ assert_scripted { connection.execute("doit") }
745
+ rescue # rubocop:disable Lint/HandleExceptions
746
+ # the raise is not what is being tested here, rather its side-effect
747
+ end
748
+
749
+ logged_output.string.must_match debug_line(
750
+ "[SSH] me@foo<{:port=>22}> (doit)"
751
+ )
752
+ end
753
+
754
+ it "logger displays establishing connection on debug" do
755
+ begin
756
+ assert_scripted { connection.execute("doit") }
757
+ rescue # rubocop:disable Lint/HandleExceptions
758
+ # the raise is not what is being tested here, rather its side-effect
759
+ end
760
+
761
+ logged_output.string.must_match debug_line(
762
+ "[SSH] opening connection to me@foo<{:port=>22}>"
763
+ )
764
+ end
765
+
766
+ it "logger captures stdout" do
767
+ begin
768
+ assert_scripted { connection.execute("doit") }
769
+ rescue # rubocop:disable Lint/HandleExceptions
770
+ # the raise is not what is being tested here, rather its side-effect
771
+ end
772
+
773
+ logged_output.string.must_match(/^nope$/)
774
+ end
775
+
776
+ it "logger captures stderr" do
777
+ begin
778
+ assert_scripted { connection.execute("doit") }
779
+ rescue # rubocop:disable Lint/HandleExceptions
780
+ # the raise is not what is being tested here, rather its side-effect
781
+ end
782
+
783
+ logged_output.string.must_match(/^youdead$/)
784
+ end
785
+
786
+ it "raises an SshFailed exception" do
787
+ err = proc {
788
+ connection.execute("doit")
789
+ }.must_raise Kitchen::Transport::SshFailed
790
+ err.message.must_equal "SSH exited (42) for command: [doit]"
791
+ end
792
+ end
793
+
794
+ describe "for an interrupted command" do
795
+
796
+ let(:conn) { mock("session") }
797
+
798
+ before do
799
+ Net::SSH.stubs(:start).returns(conn)
800
+ end
801
+
802
+ it "raises SshFailed when an SSH exception is raised" do
803
+ conn.stubs(:open_channel).raises(Net::SSH::Exception)
804
+
805
+ e = proc {
806
+ connection.execute("nope")
807
+ }.must_raise Kitchen::Transport::SshFailed
808
+ e.message.must_match regexify("SSH command failed")
809
+ end
810
+ end
811
+
812
+ describe "for a nil command" do
813
+
814
+ it "does not log on debug" do
815
+ connection.execute(nil)
816
+
817
+ logged_output.string.must_equal ""
818
+ end
819
+ end
820
+ end
821
+
822
+ describe "#login_command" do
823
+
824
+ let(:login_command) { connection.login_command }
825
+ let(:args) { login_command.arguments.join(" ") }
826
+
827
+ it "returns a LoginCommand" do
828
+ login_command.must_be_instance_of Kitchen::LoginCommand
829
+ end
830
+
831
+ it "is an SSH command" do
832
+ login_command.command.must_equal "ssh"
833
+ args.must_match %r{ me@foo$}
834
+ end
835
+
836
+ it "sets the UserKnownHostsFile option" do
837
+ args.must_match regexify("-o UserKnownHostsFile=/dev/null ")
838
+ end
839
+
840
+ it "sets the StrictHostKeyChecking option" do
841
+ args.must_match regexify(" -o StrictHostKeyChecking=no ")
842
+ end
843
+
844
+ it "won't set IdentitiesOnly option by default" do
845
+ args.wont_match regexify(" -o IdentitiesOnly=")
846
+ end
847
+
848
+ it "sets the IdentiesOnly option if :keys option is given" do
849
+ options[:keys] = ["yep"]
850
+
851
+ args.must_match regexify(" -o IdentitiesOnly=yes ")
852
+ end
853
+
854
+ it "sets the LogLevel option to VERBOSE if logger is set to debug" do
855
+ logger.level = ::Logger::DEBUG
856
+ options[:logger] = logger
857
+
858
+ args.must_match regexify(" -o LogLevel=VERBOSE ")
859
+ end
860
+
861
+ it "sets the LogLevel option to ERROR if logger is not set to debug" do
862
+ logger.level = ::Logger::INFO
863
+ options[:logger] = logger
864
+
865
+ args.must_match regexify(" -o LogLevel=ERROR ")
866
+ end
867
+
868
+ it "won't set the ForwardAgent option by default" do
869
+ args.wont_match regexify(" -o ForwardAgent=")
870
+ end
871
+
872
+ it "sets the ForwardAgent option to yes if truthy" do
873
+ options[:forward_agent] = "yep"
874
+
875
+ args.must_match regexify(" -o ForwardAgent=yes")
876
+ end
877
+
878
+ it "sets the ForwardAgent option to no if falsey" do
879
+ options[:forward_agent] = false
880
+
881
+ args.must_match regexify(" -o ForwardAgent=no")
882
+ end
883
+
884
+ it "won't add any SSH keys by default" do
885
+ args.wont_match regexify(" -i ")
886
+ end
887
+
888
+ it "sets SSH keys options if given" do
889
+ options[:keys] = %w[one two]
890
+
891
+ args.must_match regexify(" -i one ")
892
+ args.must_match regexify(" -i two ")
893
+ end
894
+
895
+ it "sets the port option to 22 by default" do
896
+ args.must_match regexify(" -p 22 ")
897
+ end
898
+
899
+ it "sets the port option" do
900
+ options[:port] = 1234
901
+
902
+ args.must_match regexify(" -p 1234 ")
903
+ end
904
+ end
905
+
906
+ describe "#upload" do
907
+
908
+ describe "for a file" do
909
+
910
+ let(:content) { "a" * 1234 }
911
+
912
+ let(:src) do
913
+ file = Tempfile.new("file")
914
+ file.write("a" * 1234)
915
+ file.close
916
+ FileUtils.chmod(0755, file.path)
917
+ file
918
+ end
919
+
920
+ before do
921
+ expect_scp_session("-t /tmp/remote") do |channel|
922
+ channel.gets_data("\0")
923
+ channel.sends_data("C0755 1234 #{File.basename(src.path)}\n")
924
+ channel.gets_data("\0")
925
+ channel.sends_data("a" * 1234)
926
+ channel.sends_data("\0")
927
+ channel.gets_data("\0")
928
+ end
929
+ end
930
+
931
+ after do
932
+ src.unlink
933
+ end
934
+
935
+ it "uploads a file to remote over scp" do
936
+ assert_scripted do
937
+ connection.upload(src.path, "/tmp/remote")
938
+ end
939
+ end
940
+
941
+ it "logs upload progress to debug" do
942
+ assert_scripted do
943
+ connection.upload(src.path, "/tmp/remote")
944
+ end
945
+
946
+ logged_output.string.must_match debug_line(
947
+ "[SSH] opening connection to me@foo<{:port=>22}>"
948
+ )
949
+ logged_output.string.must_match debug_line(
950
+ "Uploaded #{src.path} (1234 bytes)"
951
+ )
952
+ end
953
+ end
954
+
955
+ describe "for a path" do
956
+ before do
957
+ @dir = Dir.mktmpdir("local")
958
+ FileUtils.chmod(0700, @dir)
959
+ File.open("#{@dir}/alpha", "wb") { |f| f.write("alpha-contents\n") }
960
+ FileUtils.chmod(0644, "#{@dir}/alpha")
961
+ FileUtils.mkdir_p("#{@dir}/subdir")
962
+ FileUtils.chmod(0755, "#{@dir}/subdir")
963
+ File.open("#{@dir}/subdir/beta", "wb") { |f| f.write("beta-contents\n") }
964
+ FileUtils.chmod(0555, "#{@dir}/subdir/beta")
965
+ File.open("#{@dir}/zulu", "wb") { |f| f.write("zulu-contents\n") }
966
+ FileUtils.chmod(0444, "#{@dir}/zulu")
967
+
968
+ expect_scp_session("-t -r /tmp/remote") do |channel|
969
+ channel.gets_data("\0")
970
+ channel.sends_data("D0700 0 #{File.basename(@dir)}\n")
971
+ channel.gets_data("\0")
972
+ channel.sends_data("C0644 15 alpha\n")
973
+ channel.gets_data("\0")
974
+ channel.sends_data("alpha-contents\n")
975
+ channel.sends_data("\0")
976
+ channel.gets_data("\0")
977
+ channel.sends_data("D0755 0 subdir\n")
978
+ channel.gets_data("\0")
979
+ channel.sends_data("C0555 14 beta\n")
980
+ channel.gets_data("\0")
981
+ channel.sends_data("beta-contents\n")
982
+ channel.sends_data("\0")
983
+ channel.gets_data("\0")
984
+ channel.sends_data("E\n")
985
+ channel.gets_data("\0")
986
+ channel.sends_data("C0444 14 zulu\n")
987
+ channel.gets_data("\0")
988
+ channel.sends_data("zulu-contents\n")
989
+ channel.sends_data("\0")
990
+ channel.gets_data("\0")
991
+ channel.sends_data("E\n")
992
+ channel.gets_data("\0")
993
+ end
994
+ end
995
+
996
+ after do
997
+ FileUtils.remove_entry_secure(@dir)
998
+ end
999
+
1000
+ it "uploads a file to remote over scp" do
1001
+ with_sorted_dir_entries do
1002
+ assert_scripted { connection.upload(@dir, "/tmp/remote") }
1003
+ end
1004
+ end
1005
+
1006
+ it "logs upload progress to debug" do
1007
+ with_sorted_dir_entries do
1008
+ assert_scripted { connection.upload(@dir, "/tmp/remote") }
1009
+ end
1010
+
1011
+ logged_output.string.must_match debug_line(
1012
+ "[SSH] opening connection to me@foo<{:port=>22}>"
1013
+ )
1014
+ logged_output.string.must_match debug_line(
1015
+ "Uploaded #{@dir}/alpha (15 bytes)"
1016
+ )
1017
+ logged_output.string.must_match debug_line(
1018
+ "Uploaded #{@dir}/subdir/beta (14 bytes)"
1019
+ )
1020
+ logged_output.string.must_match debug_line(
1021
+ "Uploaded #{@dir}/zulu (14 bytes)"
1022
+ )
1023
+ end
1024
+ end
1025
+
1026
+ describe "for a failed upload" do
1027
+
1028
+ let(:conn) { mock("session") }
1029
+
1030
+ before do
1031
+ Net::SSH.stubs(:start).returns(conn)
1032
+ end
1033
+
1034
+ it "raises SshFailed when an SSH exception is raised" do
1035
+ conn.stubs(:scp).raises(Net::SSH::Exception)
1036
+
1037
+ e = proc {
1038
+ connection.upload("nope", "fail")
1039
+ }.must_raise Kitchen::Transport::SshFailed
1040
+ e.message.must_match regexify("SCP upload failed")
1041
+ end
1042
+ end
1043
+ end
1044
+
1045
+ describe "#wait_until_ready" do
1046
+
1047
+ before do
1048
+ options[:max_wait_until_ready] = 300
1049
+ connection.stubs(:sleep)
1050
+ end
1051
+
1052
+ describe "when failing to connect" do
1053
+
1054
+ before do
1055
+ Net::SSH.stubs(:start).raises(Errno::ECONNREFUSED)
1056
+ end
1057
+
1058
+ it "attempts to connect :max_wait_until_ready / 3 times if failing" do
1059
+ begin
1060
+ connection.wait_until_ready
1061
+ rescue # rubocop:disable Lint/HandleExceptions
1062
+ # the raise is not what is being tested here, rather its side-effect
1063
+ end
1064
+
1065
+ logged_output.string.lines.select { |l|
1066
+ l =~ info_line_with(
1067
+ "Waiting for SSH service on foo:22, retrying in 3 seconds")
1068
+ }.size.must_equal((300 / 3) - 1)
1069
+ logged_output.string.lines.select { |l|
1070
+ l =~ debug_line_with("[SSH] connection failed ")
1071
+ }.size.must_equal((300 / 3) - 1)
1072
+ logged_output.string.lines.select { |l|
1073
+ l =~ warn_line_with("[SSH] connection failed, terminating ")
1074
+ }.size.must_equal 1
1075
+ end
1076
+
1077
+ it "sleeps for 3 seconds between retries" do
1078
+ connection.unstub(:sleep)
1079
+ connection.expects(:sleep).with(3).times((300 / 3) - 1)
1080
+
1081
+ begin
1082
+ connection.wait_until_ready
1083
+ rescue # rubocop:disable Lint/HandleExceptions
1084
+ # the raise is not what is being tested here, rather its side-effect
1085
+ end
1086
+ end
1087
+ end
1088
+
1089
+ describe "when connection is successful" do
1090
+
1091
+ before do
1092
+ story do |script|
1093
+ channel = script.opens_channel
1094
+ channel.sends_request_pty
1095
+ channel.sends_exec("echo '[SSH] Established'")
1096
+ channel.gets_data("[SSH] Established\n")
1097
+ channel.gets_exit_status(0)
1098
+ channel.gets_close
1099
+ channel.sends_close
1100
+ end
1101
+ end
1102
+
1103
+ it "executes an ping command string to ensure working" do
1104
+ assert_scripted { connection.wait_until_ready }
1105
+ end
1106
+
1107
+ it "logger captures stdout" do
1108
+ assert_scripted { connection.wait_until_ready }
1109
+
1110
+ logged_output.string.must_match(/^\[SSH\] Established$/)
1111
+ end
1112
+ end
1113
+ end
1114
+
1115
+ def expect_scp_session(args)
1116
+ story do |script|
1117
+ channel = script.opens_channel
1118
+ channel.sends_exec("scp #{args}")
1119
+ yield channel if block_given?
1120
+ channel.sends_eof
1121
+ channel.gets_exit_status(0)
1122
+ channel.gets_eof
1123
+ channel.gets_close
1124
+ channel.sends_close
1125
+ end
1126
+ end
1127
+
1128
+ def debug_line(msg)
1129
+ %r{^D, .* : #{Regexp.escape(msg)}$}
1130
+ end
1131
+
1132
+ def debug_line_with(msg)
1133
+ %r{^D, .* : #{Regexp.escape(msg)}}
1134
+ end
1135
+
1136
+ def info_line_with(msg)
1137
+ %r{^I, .* : #{Regexp.escape(msg)}}
1138
+ end
1139
+
1140
+ def regexify(string)
1141
+ Regexp.new(Regexp.escape(string))
1142
+ end
1143
+
1144
+ def warn_line_with(msg)
1145
+ %r{^W, .* : #{Regexp.escape(msg)}}
1146
+ end
1147
+ end