test-kitchen 1.7.0 → 1.7.1.dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +8 -8
  3. data/.gitattributes +3 -0
  4. data/.github/ISSUE_TEMPLATE.md +55 -55
  5. data/.gitignore +28 -28
  6. data/.kitchen.ci.yml +23 -23
  7. data/.kitchen.proxy.yml +27 -27
  8. data/.rubocop.yml +3 -3
  9. data/.travis.yml +70 -70
  10. data/.yardopts +3 -3
  11. data/Berksfile +3 -3
  12. data/CHANGELOG.md +1090 -1083
  13. data/CONTRIBUTING.md +14 -14
  14. data/Gemfile +19 -19
  15. data/Gemfile.proxy_tests +4 -4
  16. data/Guardfile +42 -42
  17. data/LICENSE +15 -15
  18. data/MAINTAINERS.md +23 -23
  19. data/README.md +135 -135
  20. data/Rakefile +61 -61
  21. data/appveyor.yml +44 -44
  22. data/features/kitchen_action_commands.feature +164 -164
  23. data/features/kitchen_command.feature +16 -16
  24. data/features/kitchen_console_command.feature +34 -34
  25. data/features/kitchen_defaults.feature +38 -38
  26. data/features/kitchen_diagnose_command.feature +96 -96
  27. data/features/kitchen_driver_create_command.feature +64 -64
  28. data/features/kitchen_driver_discover_command.feature +25 -25
  29. data/features/kitchen_help_command.feature +16 -16
  30. data/features/kitchen_init_command.feature +274 -274
  31. data/features/kitchen_list_command.feature +104 -104
  32. data/features/kitchen_login_command.feature +62 -62
  33. data/features/kitchen_sink_command.feature +30 -30
  34. data/features/kitchen_test_command.feature +88 -88
  35. data/features/step_definitions/gem_steps.rb +36 -36
  36. data/features/step_definitions/git_steps.rb +5 -5
  37. data/features/step_definitions/output_steps.rb +5 -5
  38. data/features/support/env.rb +75 -75
  39. data/lib/kitchen.rb +150 -150
  40. data/lib/kitchen/base64_stream.rb +55 -55
  41. data/lib/kitchen/cli.rb +419 -419
  42. data/lib/kitchen/collection.rb +55 -55
  43. data/lib/kitchen/color.rb +65 -65
  44. data/lib/kitchen/command.rb +185 -185
  45. data/lib/kitchen/command/action.rb +45 -45
  46. data/lib/kitchen/command/console.rb +58 -58
  47. data/lib/kitchen/command/diagnose.rb +92 -92
  48. data/lib/kitchen/command/driver_discover.rb +105 -105
  49. data/lib/kitchen/command/exec.rb +41 -41
  50. data/lib/kitchen/command/list.rb +119 -119
  51. data/lib/kitchen/command/login.rb +43 -43
  52. data/lib/kitchen/command/sink.rb +54 -54
  53. data/lib/kitchen/command/test.rb +51 -51
  54. data/lib/kitchen/config.rb +322 -322
  55. data/lib/kitchen/configurable.rb +529 -529
  56. data/lib/kitchen/data_munger.rb +959 -959
  57. data/lib/kitchen/diagnostic.rb +141 -141
  58. data/lib/kitchen/driver.rb +56 -56
  59. data/lib/kitchen/driver/base.rb +134 -134
  60. data/lib/kitchen/driver/dummy.rb +108 -108
  61. data/lib/kitchen/driver/proxy.rb +72 -72
  62. data/lib/kitchen/driver/ssh_base.rb +357 -357
  63. data/lib/kitchen/errors.rb +229 -229
  64. data/lib/kitchen/generator/driver_create.rb +177 -177
  65. data/lib/kitchen/generator/init.rb +296 -296
  66. data/lib/kitchen/instance.rb +662 -662
  67. data/lib/kitchen/lazy_hash.rb +142 -142
  68. data/lib/kitchen/loader/yaml.rb +349 -349
  69. data/lib/kitchen/logger.rb +423 -423
  70. data/lib/kitchen/logging.rb +56 -56
  71. data/lib/kitchen/login_command.rb +52 -52
  72. data/lib/kitchen/metadata_chopper.rb +52 -52
  73. data/lib/kitchen/platform.rb +67 -67
  74. data/lib/kitchen/provisioner.rb +54 -54
  75. data/lib/kitchen/provisioner/base.rb +236 -236
  76. data/lib/kitchen/provisioner/chef/berkshelf.rb +114 -114
  77. data/lib/kitchen/provisioner/chef/common_sandbox.rb +322 -322
  78. data/lib/kitchen/provisioner/chef/librarian.rb +112 -112
  79. data/lib/kitchen/provisioner/chef_apply.rb +124 -124
  80. data/lib/kitchen/provisioner/chef_base.rb +341 -341
  81. data/lib/kitchen/provisioner/chef_solo.rb +88 -88
  82. data/lib/kitchen/provisioner/chef_zero.rb +245 -245
  83. data/lib/kitchen/provisioner/dummy.rb +79 -79
  84. data/lib/kitchen/provisioner/shell.rb +138 -138
  85. data/lib/kitchen/rake_tasks.rb +63 -63
  86. data/lib/kitchen/shell_out.rb +93 -93
  87. data/lib/kitchen/ssh.rb +276 -276
  88. data/lib/kitchen/state_file.rb +120 -120
  89. data/lib/kitchen/suite.rb +51 -51
  90. data/lib/kitchen/thor_tasks.rb +66 -66
  91. data/lib/kitchen/transport.rb +54 -54
  92. data/lib/kitchen/transport/base.rb +176 -176
  93. data/lib/kitchen/transport/dummy.rb +79 -79
  94. data/lib/kitchen/transport/ssh.rb +364 -364
  95. data/lib/kitchen/transport/winrm.rb +486 -486
  96. data/lib/kitchen/util.rb +147 -147
  97. data/lib/kitchen/verifier.rb +55 -55
  98. data/lib/kitchen/verifier/base.rb +235 -235
  99. data/lib/kitchen/verifier/busser.rb +277 -277
  100. data/lib/kitchen/verifier/dummy.rb +79 -79
  101. data/lib/kitchen/verifier/shell.rb +101 -101
  102. data/lib/kitchen/version.rb +21 -21
  103. data/lib/vendor/hash_recursive_merge.rb +82 -82
  104. data/spec/kitchen/base64_stream_spec.rb +77 -77
  105. data/spec/kitchen/cli_spec.rb +56 -56
  106. data/spec/kitchen/collection_spec.rb +80 -80
  107. data/spec/kitchen/color_spec.rb +54 -54
  108. data/spec/kitchen/config_spec.rb +408 -408
  109. data/spec/kitchen/configurable_spec.rb +1095 -1095
  110. data/spec/kitchen/data_munger_spec.rb +2694 -2694
  111. data/spec/kitchen/diagnostic_spec.rb +129 -129
  112. data/spec/kitchen/driver/base_spec.rb +121 -121
  113. data/spec/kitchen/driver/dummy_spec.rb +199 -199
  114. data/spec/kitchen/driver/proxy_spec.rb +138 -138
  115. data/spec/kitchen/driver/ssh_base_spec.rb +1115 -1115
  116. data/spec/kitchen/driver_spec.rb +112 -112
  117. data/spec/kitchen/errors_spec.rb +309 -309
  118. data/spec/kitchen/instance_spec.rb +1419 -1419
  119. data/spec/kitchen/lazy_hash_spec.rb +117 -117
  120. data/spec/kitchen/loader/yaml_spec.rb +774 -774
  121. data/spec/kitchen/logger_spec.rb +429 -429
  122. data/spec/kitchen/logging_spec.rb +59 -59
  123. data/spec/kitchen/login_command_spec.rb +68 -68
  124. data/spec/kitchen/metadata_chopper_spec.rb +82 -82
  125. data/spec/kitchen/platform_spec.rb +89 -89
  126. data/spec/kitchen/provisioner/base_spec.rb +386 -386
  127. data/spec/kitchen/provisioner/chef_apply_spec.rb +136 -136
  128. data/spec/kitchen/provisioner/chef_base_spec.rb +1161 -1161
  129. data/spec/kitchen/provisioner/chef_solo_spec.rb +557 -557
  130. data/spec/kitchen/provisioner/chef_zero_spec.rb +1001 -1001
  131. data/spec/kitchen/provisioner/dummy_spec.rb +99 -99
  132. data/spec/kitchen/provisioner/shell_spec.rb +566 -566
  133. data/spec/kitchen/provisioner_spec.rb +107 -107
  134. data/spec/kitchen/shell_out_spec.rb +150 -150
  135. data/spec/kitchen/ssh_spec.rb +693 -693
  136. data/spec/kitchen/state_file_spec.rb +129 -129
  137. data/spec/kitchen/suite_spec.rb +62 -62
  138. data/spec/kitchen/transport/base_spec.rb +89 -89
  139. data/spec/kitchen/transport/ssh_spec.rb +1255 -1255
  140. data/spec/kitchen/transport/winrm_spec.rb +1143 -1143
  141. data/spec/kitchen/transport_spec.rb +112 -112
  142. data/spec/kitchen/util_spec.rb +165 -165
  143. data/spec/kitchen/verifier/base_spec.rb +362 -362
  144. data/spec/kitchen/verifier/busser_spec.rb +610 -610
  145. data/spec/kitchen/verifier/dummy_spec.rb +99 -99
  146. data/spec/kitchen/verifier/shell_spec.rb +160 -160
  147. data/spec/kitchen/verifier_spec.rb +120 -120
  148. data/spec/kitchen_spec.rb +114 -114
  149. data/spec/spec_helper.rb +85 -85
  150. data/spec/support/powershell_max_size_spec.rb +40 -40
  151. data/support/busser_install_command.ps1 +14 -14
  152. data/support/busser_install_command.sh +14 -14
  153. data/support/chef-client-zero.rb +77 -77
  154. data/support/chef_base_init_command.ps1 +18 -18
  155. data/support/chef_base_init_command.sh +2 -2
  156. data/support/chef_base_install_command.ps1 +85 -85
  157. data/support/chef_base_install_command.sh +229 -229
  158. data/support/chef_zero_prepare_command_legacy.ps1 +9 -9
  159. data/support/chef_zero_prepare_command_legacy.sh +10 -10
  160. data/support/download_helpers.sh +109 -109
  161. data/support/dummy-validation.pem +27 -27
  162. data/templates/driver/CHANGELOG.md.erb +3 -3
  163. data/templates/driver/Gemfile.erb +3 -3
  164. data/templates/driver/README.md.erb +64 -64
  165. data/templates/driver/Rakefile.erb +21 -21
  166. data/templates/driver/driver.rb.erb +23 -23
  167. data/templates/driver/gemspec.erb +29 -29
  168. data/templates/driver/gitignore.erb +17 -17
  169. data/templates/driver/license_apachev2.erb +15 -15
  170. data/templates/driver/license_lgplv3.erb +16 -16
  171. data/templates/driver/license_mit.erb +22 -22
  172. data/templates/driver/license_reserved.erb +5 -5
  173. data/templates/driver/tailor.erb +4 -4
  174. data/templates/driver/travis.yml.erb +11 -11
  175. data/templates/driver/version.rb.erb +12 -12
  176. data/templates/init/chefignore.erb +1 -1
  177. data/templates/init/kitchen.yml.erb +18 -18
  178. data/test-kitchen.gemspec +62 -62
  179. data/test/integration/default/default_spec.rb +3 -3
  180. data/testing_windows.md +37 -37
  181. metadata +5 -4
@@ -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