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.
- checksums.yaml +4 -4
- data/.cane +8 -8
- data/.gitattributes +3 -0
- data/.github/ISSUE_TEMPLATE.md +55 -55
- data/.gitignore +28 -28
- data/.kitchen.ci.yml +23 -23
- data/.kitchen.proxy.yml +27 -27
- data/.rubocop.yml +3 -3
- data/.travis.yml +70 -70
- data/.yardopts +3 -3
- data/Berksfile +3 -3
- data/CHANGELOG.md +1090 -1083
- data/CONTRIBUTING.md +14 -14
- data/Gemfile +19 -19
- data/Gemfile.proxy_tests +4 -4
- data/Guardfile +42 -42
- data/LICENSE +15 -15
- data/MAINTAINERS.md +23 -23
- data/README.md +135 -135
- data/Rakefile +61 -61
- data/appveyor.yml +44 -44
- data/features/kitchen_action_commands.feature +164 -164
- data/features/kitchen_command.feature +16 -16
- data/features/kitchen_console_command.feature +34 -34
- data/features/kitchen_defaults.feature +38 -38
- data/features/kitchen_diagnose_command.feature +96 -96
- data/features/kitchen_driver_create_command.feature +64 -64
- data/features/kitchen_driver_discover_command.feature +25 -25
- data/features/kitchen_help_command.feature +16 -16
- data/features/kitchen_init_command.feature +274 -274
- data/features/kitchen_list_command.feature +104 -104
- data/features/kitchen_login_command.feature +62 -62
- data/features/kitchen_sink_command.feature +30 -30
- data/features/kitchen_test_command.feature +88 -88
- data/features/step_definitions/gem_steps.rb +36 -36
- data/features/step_definitions/git_steps.rb +5 -5
- data/features/step_definitions/output_steps.rb +5 -5
- data/features/support/env.rb +75 -75
- data/lib/kitchen.rb +150 -150
- data/lib/kitchen/base64_stream.rb +55 -55
- data/lib/kitchen/cli.rb +419 -419
- data/lib/kitchen/collection.rb +55 -55
- data/lib/kitchen/color.rb +65 -65
- data/lib/kitchen/command.rb +185 -185
- data/lib/kitchen/command/action.rb +45 -45
- data/lib/kitchen/command/console.rb +58 -58
- data/lib/kitchen/command/diagnose.rb +92 -92
- data/lib/kitchen/command/driver_discover.rb +105 -105
- data/lib/kitchen/command/exec.rb +41 -41
- data/lib/kitchen/command/list.rb +119 -119
- data/lib/kitchen/command/login.rb +43 -43
- data/lib/kitchen/command/sink.rb +54 -54
- data/lib/kitchen/command/test.rb +51 -51
- data/lib/kitchen/config.rb +322 -322
- data/lib/kitchen/configurable.rb +529 -529
- data/lib/kitchen/data_munger.rb +959 -959
- data/lib/kitchen/diagnostic.rb +141 -141
- data/lib/kitchen/driver.rb +56 -56
- data/lib/kitchen/driver/base.rb +134 -134
- data/lib/kitchen/driver/dummy.rb +108 -108
- data/lib/kitchen/driver/proxy.rb +72 -72
- data/lib/kitchen/driver/ssh_base.rb +357 -357
- data/lib/kitchen/errors.rb +229 -229
- data/lib/kitchen/generator/driver_create.rb +177 -177
- data/lib/kitchen/generator/init.rb +296 -296
- data/lib/kitchen/instance.rb +662 -662
- data/lib/kitchen/lazy_hash.rb +142 -142
- data/lib/kitchen/loader/yaml.rb +349 -349
- data/lib/kitchen/logger.rb +423 -423
- data/lib/kitchen/logging.rb +56 -56
- data/lib/kitchen/login_command.rb +52 -52
- data/lib/kitchen/metadata_chopper.rb +52 -52
- data/lib/kitchen/platform.rb +67 -67
- data/lib/kitchen/provisioner.rb +54 -54
- data/lib/kitchen/provisioner/base.rb +236 -236
- data/lib/kitchen/provisioner/chef/berkshelf.rb +114 -114
- data/lib/kitchen/provisioner/chef/common_sandbox.rb +322 -322
- data/lib/kitchen/provisioner/chef/librarian.rb +112 -112
- data/lib/kitchen/provisioner/chef_apply.rb +124 -124
- data/lib/kitchen/provisioner/chef_base.rb +341 -341
- data/lib/kitchen/provisioner/chef_solo.rb +88 -88
- data/lib/kitchen/provisioner/chef_zero.rb +245 -245
- data/lib/kitchen/provisioner/dummy.rb +79 -79
- data/lib/kitchen/provisioner/shell.rb +138 -138
- data/lib/kitchen/rake_tasks.rb +63 -63
- data/lib/kitchen/shell_out.rb +93 -93
- data/lib/kitchen/ssh.rb +276 -276
- data/lib/kitchen/state_file.rb +120 -120
- data/lib/kitchen/suite.rb +51 -51
- data/lib/kitchen/thor_tasks.rb +66 -66
- data/lib/kitchen/transport.rb +54 -54
- data/lib/kitchen/transport/base.rb +176 -176
- data/lib/kitchen/transport/dummy.rb +79 -79
- data/lib/kitchen/transport/ssh.rb +364 -364
- data/lib/kitchen/transport/winrm.rb +486 -486
- data/lib/kitchen/util.rb +147 -147
- data/lib/kitchen/verifier.rb +55 -55
- data/lib/kitchen/verifier/base.rb +235 -235
- data/lib/kitchen/verifier/busser.rb +277 -277
- data/lib/kitchen/verifier/dummy.rb +79 -79
- data/lib/kitchen/verifier/shell.rb +101 -101
- data/lib/kitchen/version.rb +21 -21
- data/lib/vendor/hash_recursive_merge.rb +82 -82
- data/spec/kitchen/base64_stream_spec.rb +77 -77
- data/spec/kitchen/cli_spec.rb +56 -56
- data/spec/kitchen/collection_spec.rb +80 -80
- data/spec/kitchen/color_spec.rb +54 -54
- data/spec/kitchen/config_spec.rb +408 -408
- data/spec/kitchen/configurable_spec.rb +1095 -1095
- data/spec/kitchen/data_munger_spec.rb +2694 -2694
- data/spec/kitchen/diagnostic_spec.rb +129 -129
- data/spec/kitchen/driver/base_spec.rb +121 -121
- data/spec/kitchen/driver/dummy_spec.rb +199 -199
- data/spec/kitchen/driver/proxy_spec.rb +138 -138
- data/spec/kitchen/driver/ssh_base_spec.rb +1115 -1115
- data/spec/kitchen/driver_spec.rb +112 -112
- data/spec/kitchen/errors_spec.rb +309 -309
- data/spec/kitchen/instance_spec.rb +1419 -1419
- data/spec/kitchen/lazy_hash_spec.rb +117 -117
- data/spec/kitchen/loader/yaml_spec.rb +774 -774
- data/spec/kitchen/logger_spec.rb +429 -429
- data/spec/kitchen/logging_spec.rb +59 -59
- data/spec/kitchen/login_command_spec.rb +68 -68
- data/spec/kitchen/metadata_chopper_spec.rb +82 -82
- data/spec/kitchen/platform_spec.rb +89 -89
- data/spec/kitchen/provisioner/base_spec.rb +386 -386
- data/spec/kitchen/provisioner/chef_apply_spec.rb +136 -136
- data/spec/kitchen/provisioner/chef_base_spec.rb +1161 -1161
- data/spec/kitchen/provisioner/chef_solo_spec.rb +557 -557
- data/spec/kitchen/provisioner/chef_zero_spec.rb +1001 -1001
- data/spec/kitchen/provisioner/dummy_spec.rb +99 -99
- data/spec/kitchen/provisioner/shell_spec.rb +566 -566
- data/spec/kitchen/provisioner_spec.rb +107 -107
- data/spec/kitchen/shell_out_spec.rb +150 -150
- data/spec/kitchen/ssh_spec.rb +693 -693
- data/spec/kitchen/state_file_spec.rb +129 -129
- data/spec/kitchen/suite_spec.rb +62 -62
- data/spec/kitchen/transport/base_spec.rb +89 -89
- data/spec/kitchen/transport/ssh_spec.rb +1255 -1255
- data/spec/kitchen/transport/winrm_spec.rb +1143 -1143
- data/spec/kitchen/transport_spec.rb +112 -112
- data/spec/kitchen/util_spec.rb +165 -165
- data/spec/kitchen/verifier/base_spec.rb +362 -362
- data/spec/kitchen/verifier/busser_spec.rb +610 -610
- data/spec/kitchen/verifier/dummy_spec.rb +99 -99
- data/spec/kitchen/verifier/shell_spec.rb +160 -160
- data/spec/kitchen/verifier_spec.rb +120 -120
- data/spec/kitchen_spec.rb +114 -114
- data/spec/spec_helper.rb +85 -85
- data/spec/support/powershell_max_size_spec.rb +40 -40
- data/support/busser_install_command.ps1 +14 -14
- data/support/busser_install_command.sh +14 -14
- data/support/chef-client-zero.rb +77 -77
- data/support/chef_base_init_command.ps1 +18 -18
- data/support/chef_base_init_command.sh +2 -2
- data/support/chef_base_install_command.ps1 +85 -85
- data/support/chef_base_install_command.sh +229 -229
- data/support/chef_zero_prepare_command_legacy.ps1 +9 -9
- data/support/chef_zero_prepare_command_legacy.sh +10 -10
- data/support/download_helpers.sh +109 -109
- data/support/dummy-validation.pem +27 -27
- data/templates/driver/CHANGELOG.md.erb +3 -3
- data/templates/driver/Gemfile.erb +3 -3
- data/templates/driver/README.md.erb +64 -64
- data/templates/driver/Rakefile.erb +21 -21
- data/templates/driver/driver.rb.erb +23 -23
- data/templates/driver/gemspec.erb +29 -29
- data/templates/driver/gitignore.erb +17 -17
- data/templates/driver/license_apachev2.erb +15 -15
- data/templates/driver/license_lgplv3.erb +16 -16
- data/templates/driver/license_mit.erb +22 -22
- data/templates/driver/license_reserved.erb +5 -5
- data/templates/driver/tailor.erb +4 -4
- data/templates/driver/travis.yml.erb +11 -11
- data/templates/driver/version.rb.erb +12 -12
- data/templates/init/chefignore.erb +1 -1
- data/templates/init/kitchen.yml.erb +18 -18
- data/test-kitchen.gemspec +62 -62
- data/test/integration/default/default_spec.rb +3 -3
- data/testing_windows.md +37 -37
- 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
|