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