test-kitchen 1.6.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +8 -7
  3. data/.github/ISSUE_TEMPLATE.md +56 -0
  4. data/.gitignore +28 -27
  5. data/.kitchen.ci.yml +23 -0
  6. data/.kitchen.proxy.yml +27 -0
  7. data/.rubocop.yml +3 -3
  8. data/.travis.yml +70 -53
  9. data/.yardopts +3 -3
  10. data/Berksfile +3 -0
  11. data/CHANGELOG.md +1083 -1051
  12. data/CONTRIBUTING.md +14 -14
  13. data/Gemfile +19 -14
  14. data/Gemfile.proxy_tests +4 -5
  15. data/Guardfile +42 -42
  16. data/LICENSE +15 -15
  17. data/MAINTAINERS.md +23 -24
  18. data/README.md +135 -135
  19. data/Rakefile +61 -76
  20. data/appveyor.yml +44 -34
  21. data/features/kitchen_action_commands.feature +164 -164
  22. data/features/kitchen_command.feature +16 -16
  23. data/features/kitchen_console_command.feature +34 -34
  24. data/features/kitchen_defaults.feature +38 -38
  25. data/features/kitchen_diagnose_command.feature +96 -96
  26. data/features/kitchen_driver_create_command.feature +64 -64
  27. data/features/kitchen_driver_discover_command.feature +25 -25
  28. data/features/kitchen_help_command.feature +16 -16
  29. data/features/kitchen_init_command.feature +274 -274
  30. data/features/kitchen_list_command.feature +104 -104
  31. data/features/kitchen_login_command.feature +62 -62
  32. data/features/kitchen_sink_command.feature +30 -30
  33. data/features/kitchen_test_command.feature +88 -88
  34. data/features/step_definitions/gem_steps.rb +36 -36
  35. data/features/step_definitions/git_steps.rb +5 -5
  36. data/features/step_definitions/output_steps.rb +5 -5
  37. data/features/support/env.rb +75 -75
  38. data/lib/kitchen.rb +150 -150
  39. data/lib/kitchen/base64_stream.rb +55 -55
  40. data/lib/kitchen/cli.rb +419 -419
  41. data/lib/kitchen/collection.rb +55 -55
  42. data/lib/kitchen/color.rb +65 -65
  43. data/lib/kitchen/command.rb +185 -185
  44. data/lib/kitchen/command/action.rb +45 -45
  45. data/lib/kitchen/command/console.rb +58 -58
  46. data/lib/kitchen/command/diagnose.rb +92 -92
  47. data/lib/kitchen/command/driver_discover.rb +105 -105
  48. data/lib/kitchen/command/exec.rb +41 -41
  49. data/lib/kitchen/command/list.rb +119 -119
  50. data/lib/kitchen/command/login.rb +43 -43
  51. data/lib/kitchen/command/sink.rb +54 -54
  52. data/lib/kitchen/command/test.rb +51 -51
  53. data/lib/kitchen/config.rb +322 -322
  54. data/lib/kitchen/configurable.rb +529 -529
  55. data/lib/kitchen/data_munger.rb +959 -960
  56. data/lib/kitchen/diagnostic.rb +141 -141
  57. data/lib/kitchen/driver.rb +56 -56
  58. data/lib/kitchen/driver/base.rb +134 -134
  59. data/lib/kitchen/driver/dummy.rb +108 -108
  60. data/lib/kitchen/driver/proxy.rb +72 -72
  61. data/lib/kitchen/driver/ssh_base.rb +357 -357
  62. data/lib/kitchen/errors.rb +229 -229
  63. data/lib/kitchen/generator/driver_create.rb +177 -177
  64. data/lib/kitchen/generator/init.rb +296 -296
  65. data/lib/kitchen/instance.rb +662 -662
  66. data/lib/kitchen/lazy_hash.rb +142 -142
  67. data/lib/kitchen/loader/yaml.rb +349 -349
  68. data/lib/kitchen/logger.rb +423 -423
  69. data/lib/kitchen/logging.rb +56 -56
  70. data/lib/kitchen/login_command.rb +52 -52
  71. data/lib/kitchen/metadata_chopper.rb +52 -52
  72. data/lib/kitchen/platform.rb +67 -67
  73. data/lib/kitchen/provisioner.rb +54 -54
  74. data/lib/kitchen/provisioner/base.rb +236 -236
  75. data/lib/kitchen/provisioner/chef/berkshelf.rb +114 -114
  76. data/lib/kitchen/provisioner/chef/common_sandbox.rb +322 -322
  77. data/lib/kitchen/provisioner/chef/librarian.rb +112 -112
  78. data/lib/kitchen/provisioner/chef_apply.rb +124 -125
  79. data/lib/kitchen/provisioner/chef_base.rb +341 -294
  80. data/lib/kitchen/provisioner/chef_solo.rb +88 -89
  81. data/lib/kitchen/provisioner/chef_zero.rb +245 -245
  82. data/lib/kitchen/provisioner/dummy.rb +79 -79
  83. data/lib/kitchen/provisioner/shell.rb +138 -138
  84. data/lib/kitchen/rake_tasks.rb +63 -63
  85. data/lib/kitchen/shell_out.rb +93 -93
  86. data/lib/kitchen/ssh.rb +276 -276
  87. data/lib/kitchen/state_file.rb +120 -120
  88. data/lib/kitchen/suite.rb +51 -51
  89. data/lib/kitchen/thor_tasks.rb +66 -66
  90. data/lib/kitchen/transport.rb +54 -54
  91. data/lib/kitchen/transport/base.rb +176 -176
  92. data/lib/kitchen/transport/dummy.rb +79 -79
  93. data/lib/kitchen/transport/ssh.rb +364 -364
  94. data/lib/kitchen/transport/winrm.rb +486 -486
  95. data/lib/kitchen/util.rb +147 -147
  96. data/lib/kitchen/verifier.rb +55 -55
  97. data/lib/kitchen/verifier/base.rb +235 -235
  98. data/lib/kitchen/verifier/busser.rb +277 -277
  99. data/lib/kitchen/verifier/dummy.rb +79 -79
  100. data/lib/kitchen/verifier/shell.rb +101 -101
  101. data/lib/kitchen/version.rb +21 -21
  102. data/lib/vendor/hash_recursive_merge.rb +82 -82
  103. data/spec/kitchen/base64_stream_spec.rb +77 -77
  104. data/spec/kitchen/cli_spec.rb +56 -56
  105. data/spec/kitchen/collection_spec.rb +80 -80
  106. data/spec/kitchen/color_spec.rb +54 -54
  107. data/spec/kitchen/config_spec.rb +408 -408
  108. data/spec/kitchen/configurable_spec.rb +1095 -1062
  109. data/spec/kitchen/data_munger_spec.rb +2694 -2383
  110. data/spec/kitchen/diagnostic_spec.rb +129 -129
  111. data/spec/kitchen/driver/base_spec.rb +121 -121
  112. data/spec/kitchen/driver/dummy_spec.rb +199 -199
  113. data/spec/kitchen/driver/proxy_spec.rb +138 -138
  114. data/spec/kitchen/driver/ssh_base_spec.rb +1115 -1115
  115. data/spec/kitchen/driver_spec.rb +112 -112
  116. data/spec/kitchen/errors_spec.rb +309 -309
  117. data/spec/kitchen/instance_spec.rb +1419 -1419
  118. data/spec/kitchen/lazy_hash_spec.rb +117 -117
  119. data/spec/kitchen/loader/yaml_spec.rb +774 -774
  120. data/spec/kitchen/logger_spec.rb +429 -429
  121. data/spec/kitchen/logging_spec.rb +59 -59
  122. data/spec/kitchen/login_command_spec.rb +68 -68
  123. data/spec/kitchen/metadata_chopper_spec.rb +82 -82
  124. data/spec/kitchen/platform_spec.rb +89 -89
  125. data/spec/kitchen/provisioner/base_spec.rb +386 -386
  126. data/spec/kitchen/provisioner/chef_apply_spec.rb +136 -136
  127. data/spec/kitchen/provisioner/chef_base_spec.rb +1161 -1067
  128. data/spec/kitchen/provisioner/chef_solo_spec.rb +557 -557
  129. data/spec/kitchen/provisioner/chef_zero_spec.rb +1001 -1001
  130. data/spec/kitchen/provisioner/dummy_spec.rb +99 -99
  131. data/spec/kitchen/provisioner/shell_spec.rb +566 -566
  132. data/spec/kitchen/provisioner_spec.rb +107 -107
  133. data/spec/kitchen/shell_out_spec.rb +150 -150
  134. data/spec/kitchen/ssh_spec.rb +693 -693
  135. data/spec/kitchen/state_file_spec.rb +129 -129
  136. data/spec/kitchen/suite_spec.rb +62 -62
  137. data/spec/kitchen/transport/base_spec.rb +89 -89
  138. data/spec/kitchen/transport/ssh_spec.rb +1255 -1255
  139. data/spec/kitchen/transport/winrm_spec.rb +1143 -1143
  140. data/spec/kitchen/transport_spec.rb +112 -112
  141. data/spec/kitchen/util_spec.rb +165 -165
  142. data/spec/kitchen/verifier/base_spec.rb +362 -362
  143. data/spec/kitchen/verifier/busser_spec.rb +610 -610
  144. data/spec/kitchen/verifier/dummy_spec.rb +99 -99
  145. data/spec/kitchen/verifier/shell_spec.rb +160 -158
  146. data/spec/kitchen/verifier_spec.rb +120 -120
  147. data/spec/kitchen_spec.rb +114 -114
  148. data/spec/spec_helper.rb +85 -85
  149. data/spec/support/powershell_max_size_spec.rb +40 -40
  150. data/support/busser_install_command.ps1 +14 -14
  151. data/support/busser_install_command.sh +14 -14
  152. data/support/chef-client-zero.rb +77 -77
  153. data/support/chef_base_init_command.ps1 +18 -18
  154. data/support/chef_base_init_command.sh +2 -2
  155. data/support/chef_base_install_command.ps1 +85 -85
  156. data/support/chef_base_install_command.sh +229 -229
  157. data/support/chef_zero_prepare_command_legacy.ps1 +9 -9
  158. data/support/chef_zero_prepare_command_legacy.sh +10 -10
  159. data/support/download_helpers.sh +109 -109
  160. data/support/dummy-validation.pem +27 -27
  161. data/templates/driver/CHANGELOG.md.erb +3 -3
  162. data/templates/driver/Gemfile.erb +3 -3
  163. data/templates/driver/README.md.erb +64 -64
  164. data/templates/driver/Rakefile.erb +21 -21
  165. data/templates/driver/driver.rb.erb +23 -23
  166. data/templates/driver/gemspec.erb +29 -29
  167. data/templates/driver/gitignore.erb +17 -17
  168. data/templates/driver/license_apachev2.erb +15 -15
  169. data/templates/driver/license_lgplv3.erb +16 -16
  170. data/templates/driver/license_mit.erb +22 -22
  171. data/templates/driver/license_reserved.erb +5 -5
  172. data/templates/driver/tailor.erb +4 -4
  173. data/templates/driver/travis.yml.erb +11 -11
  174. data/templates/driver/version.rb.erb +12 -12
  175. data/templates/init/chefignore.erb +1 -1
  176. data/templates/init/kitchen.yml.erb +18 -18
  177. data/test-kitchen.gemspec +62 -62
  178. data/test/integration/default/default_spec.rb +3 -0
  179. data/testing_windows.md +37 -37
  180. metadata +23 -11
@@ -1,693 +1,693 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
- #
5
- # Copyright (C) 2014, 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/ssh"
22
- require "tmpdir"
23
-
24
- # Hack to sort results in `Dir.entries` only within the yielded block, to limit
25
- # the "behavior pollution" to other code. This was needed for Net::SCP, as
26
- # recursive directory upload doesn't sort the file and directory upload
27
- # candidates which leads to different results based on the underlying
28
- # filesystem (i.e. lexically sorted, inode insertion, mtime/atime, total
29
- # randomness, etc.)
30
- #
31
- # See: https://github.com/net-ssh/net-scp/blob/a24948/lib/net/scp/upload.rb#L52
32
-
33
- def with_sorted_dir_entries
34
- Dir.class_exec do
35
- class << self
36
- alias_method :__entries__, :entries unless method_defined?(:__entries__)
37
-
38
- def entries(*args) # rubocop:disable Lint/NestedMethodDefinition
39
- send(:__entries__, *args).sort
40
- end
41
- end
42
- end
43
-
44
- yield
45
-
46
- Dir.class_exec do
47
- class << self
48
- alias_method :entries, :__entries__
49
- end
50
- end
51
- end
52
-
53
- # Terrible hack to deal with Net::SSH:Test::Extensions which monkey patches
54
- # `IO.select` with a version for testing Net::SSH code. Unfortunetly this
55
- # impacts other code, so we'll "un-patch" this after each spec and "re-patch"
56
- # it before the next one.
57
- require "net/ssh/test"
58
- def depatch_io
59
- IO.class_exec do
60
- class << self
61
- alias_method :select, :select_for_real
62
- end
63
- end
64
- end
65
- # We need to immediately call depatch so that `IO.select` is in a good state
66
- # _right now_. The require immediately monkeypatches it and we only want
67
- # it monkey patched inside each ssh test
68
- depatch_io
69
-
70
- def repatch_io
71
- IO.class_exec do
72
- class << self
73
- alias_method :select, :select_for_test
74
- end
75
- end
76
- end
77
-
78
- # Major hack-and-a-half to add basic `Channel#request_pty` support to
79
- # Net::SSH's testing framework. The `Net::SSH::Test::LocalPacket` does not
80
- # recognize the `"pty-req"` request type, so bombs out whenever this channel
81
- # request is sent.
82
- #
83
- # This "make-work" fix adds a method (`#sends_request_pty`) which works just
84
- # like `#sends_exec` expcept that it enqueues a patched subclass of
85
- # `LocalPacket` which can deal with the `"pty-req"` type.
86
- #
87
- # An upstream patch to Net::SSH will be required to retire this yak shave ;)
88
- require "net/ssh/test/channel"
89
- module Net
90
-
91
- module SSH
92
-
93
- module Test
94
-
95
- class Channel
96
-
97
- def sends_request_pty
98
- pty_data = ["xterm", 80, 24, 640, 480, "\0"]
99
-
100
- script.events << Class.new(Net::SSH::Test::LocalPacket) do
101
- def types # rubocop:disable Lint/NestedMethodDefinition
102
- if @type == 98 && @data[1] == "pty-req"
103
- @types ||= [
104
- :long, :string, :bool, :string,
105
- :long, :long, :long, :long, :string
106
- ]
107
- else
108
- super
109
- end
110
- end
111
- end.new(:channel_request, remote_id, "pty-req", false, *pty_data)
112
- end
113
- end
114
- end
115
- end
116
- end
117
-
118
- describe Kitchen::SSH do
119
-
120
- include Net::SSH::Test
121
-
122
- let(:logged_output) { StringIO.new }
123
- let(:logger) { Logger.new(logged_output) }
124
- let(:opts) { Hash.new }
125
- let(:ssh) { Kitchen::SSH.new("foo", "me", opts) }
126
- let(:conn) { connection }
127
-
128
- before do
129
- repatch_io
130
- logger.level = Logger::DEBUG
131
- opts[:logger] = logger
132
- Net::SSH.stubs(:start).returns(conn)
133
- end
134
-
135
- after do
136
- depatch_io
137
- end
138
-
139
- describe "establishing a connection" do
140
-
141
- [
142
- Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
143
- Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
144
- Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout
145
- ].each do |klass|
146
- describe "raising #{klass}" do
147
-
148
- before do
149
- Net::SSH.stubs(:start).raises(klass)
150
- opts[:ssh_retries] = 3
151
- ssh.stubs(:sleep)
152
- end
153
-
154
- it "reraises the #{klass} exception" do
155
- proc { ssh.exec("nope") }.must_raise klass
156
- end
157
-
158
- it "attempts to connect ':ssh_retries' times" do
159
- begin
160
- ssh.exec("nope")
161
- rescue # rubocop:disable Lint/HandleExceptions
162
- end
163
-
164
- logged_output.string.lines.count { |l|
165
- l =~ debug_line("[SSH] opening connection to me@foo:22<{:ssh_retries=>3}>")
166
- }.must_equal opts[:ssh_retries]
167
- end
168
-
169
- it "sleeps for 1 second between retries" do
170
- ssh.unstub(:sleep)
171
- ssh.expects(:sleep).with(1).twice
172
-
173
- begin
174
- ssh.exec("nope")
175
- rescue # rubocop:disable Lint/HandleExceptions
176
- end
177
- end
178
-
179
- it "logs the first 2 retry failures on info" do
180
- begin
181
- ssh.exec("nope")
182
- rescue # rubocop:disable Lint/HandleExceptions
183
- end
184
-
185
- logged_output.string.lines.count { |l|
186
- l =~ info_line_with("[SSH] connection failed, retrying ")
187
- }.must_equal 2
188
- end
189
-
190
- it "logs the last retry failures on warn" do
191
- begin
192
- ssh.exec("nope")
193
- rescue # rubocop:disable Lint/HandleExceptions
194
- end
195
-
196
- logged_output.string.lines.count { |l|
197
- l =~ warn_line_with("[SSH] connection failed, terminating ")
198
- }.must_equal 1
199
- end
200
- end
201
- end
202
- end
203
-
204
- describe "#exec" do
205
-
206
- describe "for a successful command" do
207
-
208
- before do
209
- story do |script|
210
- channel = script.opens_channel
211
- channel.sends_request_pty
212
- channel.sends_exec("doit")
213
- channel.gets_data("ok\n")
214
- channel.gets_extended_data("some stderr stuffs\n")
215
- channel.gets_exit_status(0)
216
- channel.gets_close
217
- channel.sends_close
218
- end
219
- end
220
-
221
- it "logger displays command on debug" do
222
- assert_scripted { ssh.exec("doit") }
223
-
224
- logged_output.string.must_match debug_line(
225
- "[SSH] me@foo:22<{}> (doit)"
226
- )
227
- end
228
-
229
- it "logger displays establishing connection on debug" do
230
- assert_scripted { ssh.exec("doit") }
231
-
232
- logged_output.string.must_match debug_line(
233
- "[SSH] opening connection to me@foo:22<{}>"
234
- )
235
- end
236
-
237
- it "logger captures stdout" do
238
- assert_scripted { ssh.exec("doit") }
239
-
240
- logged_output.string.must_match(/^ok$/)
241
- end
242
-
243
- it "logger captures stderr" do
244
- assert_scripted { ssh.exec("doit") }
245
-
246
- logged_output.string.must_match(/^some stderr stuffs$/)
247
- end
248
- end
249
-
250
- describe "for a failed command" do
251
-
252
- before do
253
- story do |script|
254
- channel = script.opens_channel
255
- channel.sends_request_pty
256
- channel.sends_exec("doit")
257
- channel.gets_data("nope\n")
258
- channel.gets_extended_data("youdead\n")
259
- channel.gets_exit_status(42)
260
- channel.gets_close
261
- channel.sends_close
262
- end
263
- end
264
-
265
- it "logger displays command on debug" do
266
- begin
267
- assert_scripted { ssh.exec("doit") }
268
- rescue # rubocop:disable Lint/HandleExceptions
269
- end
270
-
271
- logged_output.string.must_match debug_line(
272
- "[SSH] me@foo:22<{}> (doit)"
273
- )
274
- end
275
-
276
- it "logger displays establishing connection on debug" do
277
- begin
278
- assert_scripted { ssh.exec("doit") }
279
- rescue # rubocop:disable Lint/HandleExceptions
280
- end
281
-
282
- logged_output.string.must_match debug_line(
283
- "[SSH] opening connection to me@foo:22<{}>"
284
- )
285
- end
286
-
287
- it "logger captures stdout" do
288
- begin
289
- assert_scripted { ssh.exec("doit") }
290
- rescue # rubocop:disable Lint/HandleExceptions
291
- end
292
-
293
- logged_output.string.must_match(/^nope$/)
294
- end
295
-
296
- it "logger captures stderr" do
297
- begin
298
- assert_scripted { ssh.exec("doit") }
299
- rescue # rubocop:disable Lint/HandleExceptions
300
- end
301
-
302
- logged_output.string.must_match(/^youdead$/)
303
- end
304
-
305
- it "raises an SSHFailed exception" do
306
- err = proc { ssh.exec("doit") }.must_raise Kitchen::SSHFailed
307
- err.message.must_equal "SSH exited (42) for command: [doit]"
308
- end
309
- end
310
- end
311
-
312
- describe "#upload!" do
313
-
314
- let(:content) { "a" * 1234 }
315
-
316
- let(:src) do
317
- file = Tempfile.new("file")
318
- file.write("a" * 1234)
319
- file.close
320
- FileUtils.chmod(0755, file.path)
321
- file
322
- end
323
-
324
- before do
325
- expect_scp_session("-t /tmp/remote") do |channel|
326
- file_mode = running_tests_on_windows? ? 0644 : 0755
327
- channel.gets_data("\0")
328
- channel.sends_data("C#{padded_octal_string(file_mode)} 1234 #{File.basename(src.path)}\n")
329
- channel.gets_data("\0")
330
- channel.sends_data("a" * 1234)
331
- channel.sends_data("\0")
332
- channel.gets_data("\0")
333
- end
334
- end
335
-
336
- after do
337
- src.unlink
338
- end
339
-
340
- it "uploads a file to remote over scp" do
341
- assert_scripted do
342
- ssh.upload!(src.path, "/tmp/remote")
343
- end
344
- end
345
-
346
- it "logs upload progress to debug" do
347
- assert_scripted do
348
- ssh.upload!(src.path, "/tmp/remote")
349
- end
350
-
351
- logged_output.string.must_match debug_line(
352
- "[SSH] opening connection to me@foo:22<{}>"
353
- )
354
- logged_output.string.must_match debug_line(
355
- "Uploaded #{src.path} (1234 bytes)"
356
- )
357
- end
358
- end
359
-
360
- describe "#upload_path!" do
361
-
362
- before do
363
- @dir = Dir.mktmpdir("local")
364
-
365
- # Since File.chmod is a NOOP on Windows
366
- @tmp_dir_mode = running_tests_on_windows? ? 0755 : 0700
367
- @alpha_file_mode = running_tests_on_windows? ? 0644 : 0644
368
- @beta_file_mode = running_tests_on_windows? ? 0444 : 0555
369
-
370
- FileUtils.chmod(0700, @dir)
371
- File.open("#{@dir}/alpha", "wb") { |f| f.write("alpha-contents\n") }
372
- FileUtils.chmod(0644, "#{@dir}/alpha")
373
- FileUtils.mkdir_p("#{@dir}/subdir")
374
- FileUtils.chmod(0755, "#{@dir}/subdir")
375
- File.open("#{@dir}/subdir/beta", "wb") { |f| f.write("beta-contents\n") }
376
- FileUtils.chmod(0555, "#{@dir}/subdir/beta")
377
- File.open("#{@dir}/zulu", "wb") { |f| f.write("zulu-contents\n") }
378
- FileUtils.chmod(0444, "#{@dir}/zulu")
379
-
380
- expect_scp_session("-t -r /tmp/remote") do |channel|
381
- channel.gets_data("\0")
382
- channel.sends_data("D#{padded_octal_string(@tmp_dir_mode)} 0 #{File.basename(@dir)}\n")
383
- channel.gets_data("\0")
384
- channel.sends_data("C#{padded_octal_string(@alpha_file_mode)} 15 alpha\n")
385
- channel.gets_data("\0")
386
- channel.sends_data("alpha-contents\n")
387
- channel.sends_data("\0")
388
- channel.gets_data("\0")
389
- channel.sends_data("D0755 0 subdir\n")
390
- channel.gets_data("\0")
391
- channel.sends_data("C#{padded_octal_string(@beta_file_mode)} 14 beta\n")
392
- channel.gets_data("\0")
393
- channel.sends_data("beta-contents\n")
394
- channel.sends_data("\0")
395
- channel.gets_data("\0")
396
- channel.sends_data("E\n")
397
- channel.gets_data("\0")
398
- channel.sends_data("C0444 14 zulu\n")
399
- channel.gets_data("\0")
400
- channel.sends_data("zulu-contents\n")
401
- channel.sends_data("\0")
402
- channel.gets_data("\0")
403
- channel.sends_data("E\n")
404
- channel.gets_data("\0")
405
- end
406
- end
407
-
408
- after do
409
- FileUtils.remove_entry_secure(@dir)
410
- end
411
-
412
- it "uploads a file to remote over scp" do
413
- with_sorted_dir_entries do
414
- assert_scripted { ssh.upload_path!(@dir, "/tmp/remote") }
415
- end
416
- end
417
-
418
- it "logs upload progress to debug" do
419
- remote_base = "#{Dir.tmpdir}/#{File.basename(@dir)}"
420
-
421
- with_sorted_dir_entries do
422
- assert_scripted { ssh.upload_path!(@dir, "/tmp/remote") }
423
- end
424
-
425
- logged_output.string.must_match debug_line(
426
- "[SSH] opening connection to me@foo:22<{}>"
427
- )
428
- logged_output.string.must_match debug_line(
429
- "Uploaded #{remote_base}/alpha (15 bytes)"
430
- )
431
- logged_output.string.must_match debug_line(
432
- "Uploaded #{remote_base}/subdir/beta (14 bytes)"
433
- )
434
- logged_output.string.must_match debug_line(
435
- "Uploaded #{remote_base}/zulu (14 bytes)"
436
- )
437
- end
438
- end
439
-
440
- describe "#shutdown" do
441
-
442
- before do
443
- story do |script|
444
- channel = script.opens_channel
445
- channel.sends_request_pty
446
- channel.sends_exec("doit")
447
- channel.gets_data("ok\n")
448
- channel.gets_exit_status(0)
449
- channel.gets_close
450
- channel.sends_close
451
- end
452
- end
453
-
454
- it "logger displays closing connection on debug" do
455
- conn.expects(:shutdown!)
456
-
457
- assert_scripted do
458
- ssh.exec("doit")
459
- ssh.shutdown
460
- end
461
-
462
- logged_output.string.must_match debug_line(
463
- "[SSH] closing connection to me@foo:22<{}>"
464
- )
465
- end
466
-
467
- it "only closes the connection once for multiple calls" do
468
- conn.expects(:shutdown!).once
469
-
470
- assert_scripted do
471
- ssh.exec("doit")
472
- ssh.shutdown
473
- ssh.shutdown
474
- ssh.shutdown
475
- end
476
- end
477
- end
478
-
479
- describe "block form" do
480
-
481
- before do
482
- story do |script|
483
- channel = script.opens_channel
484
- channel.sends_request_pty
485
- channel.sends_exec("doit")
486
- channel.gets_data("ok\n")
487
- channel.gets_exit_status(0)
488
- channel.gets_close
489
- channel.sends_close
490
- end
491
- end
492
-
493
- it "shuts down the connection when block closes" do
494
- conn.expects(:shutdown!)
495
-
496
- Kitchen::SSH.new("foo", "me", opts) do |ssh|
497
- ssh.exec("doit")
498
- end
499
- end
500
- end
501
-
502
- describe "#login_command" do
503
-
504
- let(:login_command) { ssh.login_command }
505
- let(:args) { login_command.arguments.join(" ") }
506
-
507
- it "returns a LoginCommand" do
508
- login_command.must_be_instance_of Kitchen::LoginCommand
509
- end
510
-
511
- it "is an SSH command" do
512
- login_command.command.must_equal "ssh"
513
- args.must_match %r{ me@foo$}
514
- end
515
-
516
- it "sets the UserKnownHostsFile option" do
517
- args.must_match regexify("-o UserKnownHostsFile=/dev/null ")
518
- end
519
-
520
- it "sets the StrictHostKeyChecking option" do
521
- args.must_match regexify(" -o StrictHostKeyChecking=no ")
522
- end
523
-
524
- it "won't set IdentitiesOnly option by default" do
525
- args.wont_match regexify(" -o IdentitiesOnly=")
526
- end
527
-
528
- it "sets the IdentiesOnly option if :keys option is given" do
529
- opts[:keys] = ["yep"]
530
-
531
- args.must_match regexify(" -o IdentitiesOnly=yes ")
532
- end
533
-
534
- it "sets the LogLevel option to VERBOSE if logger is set to debug" do
535
- logger.level = ::Logger::DEBUG
536
- opts[:logger] = logger
537
-
538
- args.must_match regexify(" -o LogLevel=VERBOSE ")
539
- end
540
-
541
- it "sets the LogLevel option to ERROR if logger is not set to debug" do
542
- logger.level = ::Logger::INFO
543
- opts[:logger] = logger
544
-
545
- args.must_match regexify(" -o LogLevel=ERROR ")
546
- end
547
-
548
- it "won't set the ForwardAgent option by default" do
549
- args.wont_match regexify(" -o ForwardAgent=")
550
- end
551
-
552
- it "sets the ForwardAgent option to yes if truthy" do
553
- opts[:forward_agent] = "yep"
554
-
555
- args.must_match regexify(" -o ForwardAgent=yes")
556
- end
557
-
558
- it "sets the ForwardAgent option to no if falsey" do
559
- opts[:forward_agent] = false
560
-
561
- args.must_match regexify(" -o ForwardAgent=no")
562
- end
563
-
564
- it "won't add any SSH keys by default" do
565
- args.wont_match regexify(" -i ")
566
- end
567
-
568
- it "sets SSH keys options if given" do
569
- opts[:keys] = %w[one two]
570
-
571
- args.must_match regexify(" -i one ")
572
- args.must_match regexify(" -i two ")
573
- end
574
-
575
- it "sets the port option to 22 by default" do
576
- args.must_match regexify(" -p 22 ")
577
- end
578
-
579
- it "sets the port option" do
580
- opts[:port] = 1234
581
-
582
- args.must_match regexify(" -p 1234 ")
583
- end
584
- end
585
-
586
- describe "#test_ssh" do
587
-
588
- let(:tcp_socket) { stub(:select_for_read? => true, :close => true) }
589
-
590
- before { ssh.stubs(:sleep) }
591
-
592
- it "returns a truthy value" do
593
- TCPSocket.stubs(:new).returns(tcp_socket)
594
-
595
- result = ssh.send(:test_ssh)
596
- result.wont_equal nil
597
- result.wont_equal false
598
- end
599
-
600
- it "closes socket when finished" do
601
- TCPSocket.stubs(:new).returns(tcp_socket)
602
- tcp_socket.expects(:close)
603
-
604
- ssh.send(:test_ssh)
605
- end
606
-
607
- [
608
- SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
609
- Errno::ENETUNREACH, IOError
610
- ].each do |klass|
611
- describe "when #{klass} is raised" do
612
-
613
- before { TCPSocket.stubs(:new).raises(klass) }
614
-
615
- it "returns false" do
616
- ssh.send(:test_ssh).must_equal false
617
- end
618
-
619
- it "sleeps for 2 seconds" do
620
- ssh.expects(:sleep).with(2)
621
-
622
- ssh.send(:test_ssh)
623
- end
624
- end
625
- end
626
-
627
- [
628
- Errno::EPERM, Errno::ETIMEDOUT
629
- ].each do |klass|
630
- describe "when #{klass} is raised" do
631
-
632
- it "returns false when #{klass} is raised" do
633
- TCPSocket.stubs(:new).raises(klass)
634
-
635
- ssh.send(:test_ssh).must_equal false
636
- end
637
- end
638
- end
639
- end
640
-
641
- describe "#wait" do
642
-
643
- let(:not_ready) do
644
- stub(:select_for_read? => false, :idle! => true, :close => true)
645
- end
646
-
647
- let(:ready) do
648
- stub(:select_for_read? => true, :close => true)
649
- end
650
-
651
- it "logs to info for each retry" do
652
- TCPSocket.stubs(:new).returns(not_ready, not_ready, ready)
653
- ssh.wait
654
-
655
- logged_output.string.lines.count { |l|
656
- l =~ info_line_with("Waiting for foo:22...")
657
- }.must_equal 2
658
- end
659
- end
660
-
661
- def expect_scp_session(args)
662
- story do |script|
663
- channel = script.opens_channel
664
- channel.sends_exec("scp #{args}")
665
- yield channel if block_given?
666
- channel.sends_eof
667
- channel.gets_exit_status(0)
668
- channel.gets_eof
669
- channel.gets_close
670
- channel.sends_close
671
- end
672
- end
673
-
674
- def regexify(string)
675
- Regexp.new(Regexp.escape(string))
676
- end
677
-
678
- def debug_line(msg)
679
- %r{^D, .* : #{Regexp.escape(msg)}$}
680
- end
681
-
682
- def debug_line_with(msg)
683
- %r{^D, .* : #{Regexp.escape(msg)}}
684
- end
685
-
686
- def info_line_with(msg)
687
- %r{^I, .* : #{Regexp.escape(msg)}}
688
- end
689
-
690
- def warn_line_with(msg)
691
- %r{^W, .* : #{Regexp.escape(msg)}}
692
- end
693
- end
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2014, 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/ssh"
22
+ require "tmpdir"
23
+
24
+ # Hack to sort results in `Dir.entries` only within the yielded block, to limit
25
+ # the "behavior pollution" to other code. This was needed for Net::SCP, as
26
+ # recursive directory upload doesn't sort the file and directory upload
27
+ # candidates which leads to different results based on the underlying
28
+ # filesystem (i.e. lexically sorted, inode insertion, mtime/atime, total
29
+ # randomness, etc.)
30
+ #
31
+ # See: https://github.com/net-ssh/net-scp/blob/a24948/lib/net/scp/upload.rb#L52
32
+
33
+ def with_sorted_dir_entries
34
+ Dir.class_exec do
35
+ class << self
36
+ alias_method :__entries__, :entries unless method_defined?(:__entries__)
37
+
38
+ def entries(*args) # rubocop:disable Lint/NestedMethodDefinition
39
+ send(:__entries__, *args).sort
40
+ end
41
+ end
42
+ end
43
+
44
+ yield
45
+
46
+ Dir.class_exec do
47
+ class << self
48
+ alias_method :entries, :__entries__
49
+ end
50
+ end
51
+ end
52
+
53
+ # Terrible hack to deal with Net::SSH:Test::Extensions which monkey patches
54
+ # `IO.select` with a version for testing Net::SSH code. Unfortunetly this
55
+ # impacts other code, so we'll "un-patch" this after each spec and "re-patch"
56
+ # it before the next one.
57
+ require "net/ssh/test"
58
+ def depatch_io
59
+ IO.class_exec do
60
+ class << self
61
+ alias_method :select, :select_for_real
62
+ end
63
+ end
64
+ end
65
+ # We need to immediately call depatch so that `IO.select` is in a good state
66
+ # _right now_. The require immediately monkeypatches it and we only want
67
+ # it monkey patched inside each ssh test
68
+ depatch_io
69
+
70
+ def repatch_io
71
+ IO.class_exec do
72
+ class << self
73
+ alias_method :select, :select_for_test
74
+ end
75
+ end
76
+ end
77
+
78
+ # Major hack-and-a-half to add basic `Channel#request_pty` support to
79
+ # Net::SSH's testing framework. The `Net::SSH::Test::LocalPacket` does not
80
+ # recognize the `"pty-req"` request type, so bombs out whenever this channel
81
+ # request is sent.
82
+ #
83
+ # This "make-work" fix adds a method (`#sends_request_pty`) which works just
84
+ # like `#sends_exec` expcept that it enqueues a patched subclass of
85
+ # `LocalPacket` which can deal with the `"pty-req"` type.
86
+ #
87
+ # An upstream patch to Net::SSH will be required to retire this yak shave ;)
88
+ require "net/ssh/test/channel"
89
+ module Net
90
+
91
+ module SSH
92
+
93
+ module Test
94
+
95
+ class Channel
96
+
97
+ def sends_request_pty
98
+ pty_data = ["xterm", 80, 24, 640, 480, "\0"]
99
+
100
+ script.events << Class.new(Net::SSH::Test::LocalPacket) do
101
+ def types # rubocop:disable Lint/NestedMethodDefinition
102
+ if @type == 98 && @data[1] == "pty-req"
103
+ @types ||= [
104
+ :long, :string, :bool, :string,
105
+ :long, :long, :long, :long, :string
106
+ ]
107
+ else
108
+ super
109
+ end
110
+ end
111
+ end.new(:channel_request, remote_id, "pty-req", false, *pty_data)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ describe Kitchen::SSH do
119
+
120
+ include Net::SSH::Test
121
+
122
+ let(:logged_output) { StringIO.new }
123
+ let(:logger) { Logger.new(logged_output) }
124
+ let(:opts) { Hash.new }
125
+ let(:ssh) { Kitchen::SSH.new("foo", "me", opts) }
126
+ let(:conn) { connection }
127
+
128
+ before do
129
+ repatch_io
130
+ logger.level = Logger::DEBUG
131
+ opts[:logger] = logger
132
+ Net::SSH.stubs(:start).returns(conn)
133
+ end
134
+
135
+ after do
136
+ depatch_io
137
+ end
138
+
139
+ describe "establishing a connection" do
140
+
141
+ [
142
+ Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
143
+ Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
144
+ Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout
145
+ ].each do |klass|
146
+ describe "raising #{klass}" do
147
+
148
+ before do
149
+ Net::SSH.stubs(:start).raises(klass)
150
+ opts[:ssh_retries] = 3
151
+ ssh.stubs(:sleep)
152
+ end
153
+
154
+ it "reraises the #{klass} exception" do
155
+ proc { ssh.exec("nope") }.must_raise klass
156
+ end
157
+
158
+ it "attempts to connect ':ssh_retries' times" do
159
+ begin
160
+ ssh.exec("nope")
161
+ rescue # rubocop:disable Lint/HandleExceptions
162
+ end
163
+
164
+ logged_output.string.lines.count { |l|
165
+ l =~ debug_line("[SSH] opening connection to me@foo:22<{:ssh_retries=>3}>")
166
+ }.must_equal opts[:ssh_retries]
167
+ end
168
+
169
+ it "sleeps for 1 second between retries" do
170
+ ssh.unstub(:sleep)
171
+ ssh.expects(:sleep).with(1).twice
172
+
173
+ begin
174
+ ssh.exec("nope")
175
+ rescue # rubocop:disable Lint/HandleExceptions
176
+ end
177
+ end
178
+
179
+ it "logs the first 2 retry failures on info" do
180
+ begin
181
+ ssh.exec("nope")
182
+ rescue # rubocop:disable Lint/HandleExceptions
183
+ end
184
+
185
+ logged_output.string.lines.count { |l|
186
+ l =~ info_line_with("[SSH] connection failed, retrying ")
187
+ }.must_equal 2
188
+ end
189
+
190
+ it "logs the last retry failures on warn" do
191
+ begin
192
+ ssh.exec("nope")
193
+ rescue # rubocop:disable Lint/HandleExceptions
194
+ end
195
+
196
+ logged_output.string.lines.count { |l|
197
+ l =~ warn_line_with("[SSH] connection failed, terminating ")
198
+ }.must_equal 1
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ describe "#exec" do
205
+
206
+ describe "for a successful command" do
207
+
208
+ before do
209
+ story do |script|
210
+ channel = script.opens_channel
211
+ channel.sends_request_pty
212
+ channel.sends_exec("doit")
213
+ channel.gets_data("ok\n")
214
+ channel.gets_extended_data("some stderr stuffs\n")
215
+ channel.gets_exit_status(0)
216
+ channel.gets_close
217
+ channel.sends_close
218
+ end
219
+ end
220
+
221
+ it "logger displays command on debug" do
222
+ assert_scripted { ssh.exec("doit") }
223
+
224
+ logged_output.string.must_match debug_line(
225
+ "[SSH] me@foo:22<{}> (doit)"
226
+ )
227
+ end
228
+
229
+ it "logger displays establishing connection on debug" do
230
+ assert_scripted { ssh.exec("doit") }
231
+
232
+ logged_output.string.must_match debug_line(
233
+ "[SSH] opening connection to me@foo:22<{}>"
234
+ )
235
+ end
236
+
237
+ it "logger captures stdout" do
238
+ assert_scripted { ssh.exec("doit") }
239
+
240
+ logged_output.string.must_match(/^ok$/)
241
+ end
242
+
243
+ it "logger captures stderr" do
244
+ assert_scripted { ssh.exec("doit") }
245
+
246
+ logged_output.string.must_match(/^some stderr stuffs$/)
247
+ end
248
+ end
249
+
250
+ describe "for a failed command" do
251
+
252
+ before do
253
+ story do |script|
254
+ channel = script.opens_channel
255
+ channel.sends_request_pty
256
+ channel.sends_exec("doit")
257
+ channel.gets_data("nope\n")
258
+ channel.gets_extended_data("youdead\n")
259
+ channel.gets_exit_status(42)
260
+ channel.gets_close
261
+ channel.sends_close
262
+ end
263
+ end
264
+
265
+ it "logger displays command on debug" do
266
+ begin
267
+ assert_scripted { ssh.exec("doit") }
268
+ rescue # rubocop:disable Lint/HandleExceptions
269
+ end
270
+
271
+ logged_output.string.must_match debug_line(
272
+ "[SSH] me@foo:22<{}> (doit)"
273
+ )
274
+ end
275
+
276
+ it "logger displays establishing connection on debug" do
277
+ begin
278
+ assert_scripted { ssh.exec("doit") }
279
+ rescue # rubocop:disable Lint/HandleExceptions
280
+ end
281
+
282
+ logged_output.string.must_match debug_line(
283
+ "[SSH] opening connection to me@foo:22<{}>"
284
+ )
285
+ end
286
+
287
+ it "logger captures stdout" do
288
+ begin
289
+ assert_scripted { ssh.exec("doit") }
290
+ rescue # rubocop:disable Lint/HandleExceptions
291
+ end
292
+
293
+ logged_output.string.must_match(/^nope$/)
294
+ end
295
+
296
+ it "logger captures stderr" do
297
+ begin
298
+ assert_scripted { ssh.exec("doit") }
299
+ rescue # rubocop:disable Lint/HandleExceptions
300
+ end
301
+
302
+ logged_output.string.must_match(/^youdead$/)
303
+ end
304
+
305
+ it "raises an SSHFailed exception" do
306
+ err = proc { ssh.exec("doit") }.must_raise Kitchen::SSHFailed
307
+ err.message.must_equal "SSH exited (42) for command: [doit]"
308
+ end
309
+ end
310
+ end
311
+
312
+ describe "#upload!" do
313
+
314
+ let(:content) { "a" * 1234 }
315
+
316
+ let(:src) do
317
+ file = Tempfile.new("file")
318
+ file.write("a" * 1234)
319
+ file.close
320
+ FileUtils.chmod(0755, file.path)
321
+ file
322
+ end
323
+
324
+ before do
325
+ expect_scp_session("-t /tmp/remote") do |channel|
326
+ file_mode = running_tests_on_windows? ? 0644 : 0755
327
+ channel.gets_data("\0")
328
+ channel.sends_data("C#{padded_octal_string(file_mode)} 1234 #{File.basename(src.path)}\n")
329
+ channel.gets_data("\0")
330
+ channel.sends_data("a" * 1234)
331
+ channel.sends_data("\0")
332
+ channel.gets_data("\0")
333
+ end
334
+ end
335
+
336
+ after do
337
+ src.unlink
338
+ end
339
+
340
+ it "uploads a file to remote over scp" do
341
+ assert_scripted do
342
+ ssh.upload!(src.path, "/tmp/remote")
343
+ end
344
+ end
345
+
346
+ it "logs upload progress to debug" do
347
+ assert_scripted do
348
+ ssh.upload!(src.path, "/tmp/remote")
349
+ end
350
+
351
+ logged_output.string.must_match debug_line(
352
+ "[SSH] opening connection to me@foo:22<{}>"
353
+ )
354
+ logged_output.string.must_match debug_line(
355
+ "Uploaded #{src.path} (1234 bytes)"
356
+ )
357
+ end
358
+ end
359
+
360
+ describe "#upload_path!" do
361
+
362
+ before do
363
+ @dir = Dir.mktmpdir("local")
364
+
365
+ # Since File.chmod is a NOOP on Windows
366
+ @tmp_dir_mode = running_tests_on_windows? ? 0755 : 0700
367
+ @alpha_file_mode = running_tests_on_windows? ? 0644 : 0644
368
+ @beta_file_mode = running_tests_on_windows? ? 0444 : 0555
369
+
370
+ FileUtils.chmod(0700, @dir)
371
+ File.open("#{@dir}/alpha", "wb") { |f| f.write("alpha-contents\n") }
372
+ FileUtils.chmod(0644, "#{@dir}/alpha")
373
+ FileUtils.mkdir_p("#{@dir}/subdir")
374
+ FileUtils.chmod(0755, "#{@dir}/subdir")
375
+ File.open("#{@dir}/subdir/beta", "wb") { |f| f.write("beta-contents\n") }
376
+ FileUtils.chmod(0555, "#{@dir}/subdir/beta")
377
+ File.open("#{@dir}/zulu", "wb") { |f| f.write("zulu-contents\n") }
378
+ FileUtils.chmod(0444, "#{@dir}/zulu")
379
+
380
+ expect_scp_session("-t -r /tmp/remote") do |channel|
381
+ channel.gets_data("\0")
382
+ channel.sends_data("D#{padded_octal_string(@tmp_dir_mode)} 0 #{File.basename(@dir)}\n")
383
+ channel.gets_data("\0")
384
+ channel.sends_data("C#{padded_octal_string(@alpha_file_mode)} 15 alpha\n")
385
+ channel.gets_data("\0")
386
+ channel.sends_data("alpha-contents\n")
387
+ channel.sends_data("\0")
388
+ channel.gets_data("\0")
389
+ channel.sends_data("D0755 0 subdir\n")
390
+ channel.gets_data("\0")
391
+ channel.sends_data("C#{padded_octal_string(@beta_file_mode)} 14 beta\n")
392
+ channel.gets_data("\0")
393
+ channel.sends_data("beta-contents\n")
394
+ channel.sends_data("\0")
395
+ channel.gets_data("\0")
396
+ channel.sends_data("E\n")
397
+ channel.gets_data("\0")
398
+ channel.sends_data("C0444 14 zulu\n")
399
+ channel.gets_data("\0")
400
+ channel.sends_data("zulu-contents\n")
401
+ channel.sends_data("\0")
402
+ channel.gets_data("\0")
403
+ channel.sends_data("E\n")
404
+ channel.gets_data("\0")
405
+ end
406
+ end
407
+
408
+ after do
409
+ FileUtils.remove_entry_secure(@dir)
410
+ end
411
+
412
+ it "uploads a file to remote over scp" do
413
+ with_sorted_dir_entries do
414
+ assert_scripted { ssh.upload_path!(@dir, "/tmp/remote") }
415
+ end
416
+ end
417
+
418
+ it "logs upload progress to debug" do
419
+ remote_base = "#{Dir.tmpdir}/#{File.basename(@dir)}"
420
+
421
+ with_sorted_dir_entries do
422
+ assert_scripted { ssh.upload_path!(@dir, "/tmp/remote") }
423
+ end
424
+
425
+ logged_output.string.must_match debug_line(
426
+ "[SSH] opening connection to me@foo:22<{}>"
427
+ )
428
+ logged_output.string.must_match debug_line(
429
+ "Uploaded #{remote_base}/alpha (15 bytes)"
430
+ )
431
+ logged_output.string.must_match debug_line(
432
+ "Uploaded #{remote_base}/subdir/beta (14 bytes)"
433
+ )
434
+ logged_output.string.must_match debug_line(
435
+ "Uploaded #{remote_base}/zulu (14 bytes)"
436
+ )
437
+ end
438
+ end
439
+
440
+ describe "#shutdown" do
441
+
442
+ before do
443
+ story do |script|
444
+ channel = script.opens_channel
445
+ channel.sends_request_pty
446
+ channel.sends_exec("doit")
447
+ channel.gets_data("ok\n")
448
+ channel.gets_exit_status(0)
449
+ channel.gets_close
450
+ channel.sends_close
451
+ end
452
+ end
453
+
454
+ it "logger displays closing connection on debug" do
455
+ conn.expects(:shutdown!)
456
+
457
+ assert_scripted do
458
+ ssh.exec("doit")
459
+ ssh.shutdown
460
+ end
461
+
462
+ logged_output.string.must_match debug_line(
463
+ "[SSH] closing connection to me@foo:22<{}>"
464
+ )
465
+ end
466
+
467
+ it "only closes the connection once for multiple calls" do
468
+ conn.expects(:shutdown!).once
469
+
470
+ assert_scripted do
471
+ ssh.exec("doit")
472
+ ssh.shutdown
473
+ ssh.shutdown
474
+ ssh.shutdown
475
+ end
476
+ end
477
+ end
478
+
479
+ describe "block form" do
480
+
481
+ before do
482
+ story do |script|
483
+ channel = script.opens_channel
484
+ channel.sends_request_pty
485
+ channel.sends_exec("doit")
486
+ channel.gets_data("ok\n")
487
+ channel.gets_exit_status(0)
488
+ channel.gets_close
489
+ channel.sends_close
490
+ end
491
+ end
492
+
493
+ it "shuts down the connection when block closes" do
494
+ conn.expects(:shutdown!)
495
+
496
+ Kitchen::SSH.new("foo", "me", opts) do |ssh|
497
+ ssh.exec("doit")
498
+ end
499
+ end
500
+ end
501
+
502
+ describe "#login_command" do
503
+
504
+ let(:login_command) { ssh.login_command }
505
+ let(:args) { login_command.arguments.join(" ") }
506
+
507
+ it "returns a LoginCommand" do
508
+ login_command.must_be_instance_of Kitchen::LoginCommand
509
+ end
510
+
511
+ it "is an SSH command" do
512
+ login_command.command.must_equal "ssh"
513
+ args.must_match %r{ me@foo$}
514
+ end
515
+
516
+ it "sets the UserKnownHostsFile option" do
517
+ args.must_match regexify("-o UserKnownHostsFile=/dev/null ")
518
+ end
519
+
520
+ it "sets the StrictHostKeyChecking option" do
521
+ args.must_match regexify(" -o StrictHostKeyChecking=no ")
522
+ end
523
+
524
+ it "won't set IdentitiesOnly option by default" do
525
+ args.wont_match regexify(" -o IdentitiesOnly=")
526
+ end
527
+
528
+ it "sets the IdentiesOnly option if :keys option is given" do
529
+ opts[:keys] = ["yep"]
530
+
531
+ args.must_match regexify(" -o IdentitiesOnly=yes ")
532
+ end
533
+
534
+ it "sets the LogLevel option to VERBOSE if logger is set to debug" do
535
+ logger.level = ::Logger::DEBUG
536
+ opts[:logger] = logger
537
+
538
+ args.must_match regexify(" -o LogLevel=VERBOSE ")
539
+ end
540
+
541
+ it "sets the LogLevel option to ERROR if logger is not set to debug" do
542
+ logger.level = ::Logger::INFO
543
+ opts[:logger] = logger
544
+
545
+ args.must_match regexify(" -o LogLevel=ERROR ")
546
+ end
547
+
548
+ it "won't set the ForwardAgent option by default" do
549
+ args.wont_match regexify(" -o ForwardAgent=")
550
+ end
551
+
552
+ it "sets the ForwardAgent option to yes if truthy" do
553
+ opts[:forward_agent] = "yep"
554
+
555
+ args.must_match regexify(" -o ForwardAgent=yes")
556
+ end
557
+
558
+ it "sets the ForwardAgent option to no if falsey" do
559
+ opts[:forward_agent] = false
560
+
561
+ args.must_match regexify(" -o ForwardAgent=no")
562
+ end
563
+
564
+ it "won't add any SSH keys by default" do
565
+ args.wont_match regexify(" -i ")
566
+ end
567
+
568
+ it "sets SSH keys options if given" do
569
+ opts[:keys] = %w[one two]
570
+
571
+ args.must_match regexify(" -i one ")
572
+ args.must_match regexify(" -i two ")
573
+ end
574
+
575
+ it "sets the port option to 22 by default" do
576
+ args.must_match regexify(" -p 22 ")
577
+ end
578
+
579
+ it "sets the port option" do
580
+ opts[:port] = 1234
581
+
582
+ args.must_match regexify(" -p 1234 ")
583
+ end
584
+ end
585
+
586
+ describe "#test_ssh" do
587
+
588
+ let(:tcp_socket) { stub(:select_for_read? => true, :close => true) }
589
+
590
+ before { ssh.stubs(:sleep) }
591
+
592
+ it "returns a truthy value" do
593
+ TCPSocket.stubs(:new).returns(tcp_socket)
594
+
595
+ result = ssh.send(:test_ssh)
596
+ result.wont_equal nil
597
+ result.wont_equal false
598
+ end
599
+
600
+ it "closes socket when finished" do
601
+ TCPSocket.stubs(:new).returns(tcp_socket)
602
+ tcp_socket.expects(:close)
603
+
604
+ ssh.send(:test_ssh)
605
+ end
606
+
607
+ [
608
+ SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
609
+ Errno::ENETUNREACH, IOError
610
+ ].each do |klass|
611
+ describe "when #{klass} is raised" do
612
+
613
+ before { TCPSocket.stubs(:new).raises(klass) }
614
+
615
+ it "returns false" do
616
+ ssh.send(:test_ssh).must_equal false
617
+ end
618
+
619
+ it "sleeps for 2 seconds" do
620
+ ssh.expects(:sleep).with(2)
621
+
622
+ ssh.send(:test_ssh)
623
+ end
624
+ end
625
+ end
626
+
627
+ [
628
+ Errno::EPERM, Errno::ETIMEDOUT
629
+ ].each do |klass|
630
+ describe "when #{klass} is raised" do
631
+
632
+ it "returns false when #{klass} is raised" do
633
+ TCPSocket.stubs(:new).raises(klass)
634
+
635
+ ssh.send(:test_ssh).must_equal false
636
+ end
637
+ end
638
+ end
639
+ end
640
+
641
+ describe "#wait" do
642
+
643
+ let(:not_ready) do
644
+ stub(:select_for_read? => false, :idle! => true, :close => true)
645
+ end
646
+
647
+ let(:ready) do
648
+ stub(:select_for_read? => true, :close => true)
649
+ end
650
+
651
+ it "logs to info for each retry" do
652
+ TCPSocket.stubs(:new).returns(not_ready, not_ready, ready)
653
+ ssh.wait
654
+
655
+ logged_output.string.lines.count { |l|
656
+ l =~ info_line_with("Waiting for foo:22...")
657
+ }.must_equal 2
658
+ end
659
+ end
660
+
661
+ def expect_scp_session(args)
662
+ story do |script|
663
+ channel = script.opens_channel
664
+ channel.sends_exec("scp #{args}")
665
+ yield channel if block_given?
666
+ channel.sends_eof
667
+ channel.gets_exit_status(0)
668
+ channel.gets_eof
669
+ channel.gets_close
670
+ channel.sends_close
671
+ end
672
+ end
673
+
674
+ def regexify(string)
675
+ Regexp.new(Regexp.escape(string))
676
+ end
677
+
678
+ def debug_line(msg)
679
+ %r{^D, .* : #{Regexp.escape(msg)}$}
680
+ end
681
+
682
+ def debug_line_with(msg)
683
+ %r{^D, .* : #{Regexp.escape(msg)}}
684
+ end
685
+
686
+ def info_line_with(msg)
687
+ %r{^I, .* : #{Regexp.escape(msg)}}
688
+ end
689
+
690
+ def warn_line_with(msg)
691
+ %r{^W, .* : #{Regexp.escape(msg)}}
692
+ end
693
+ end