test-kitchen 1.6.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +8 -7
  3. data/.github/ISSUE_TEMPLATE.md +56 -0
  4. data/.gitignore +28 -27
  5. data/.kitchen.ci.yml +23 -0
  6. data/.kitchen.proxy.yml +27 -0
  7. data/.rubocop.yml +3 -3
  8. data/.travis.yml +70 -53
  9. data/.yardopts +3 -3
  10. data/Berksfile +3 -0
  11. data/CHANGELOG.md +1083 -1051
  12. data/CONTRIBUTING.md +14 -14
  13. data/Gemfile +19 -14
  14. data/Gemfile.proxy_tests +4 -5
  15. data/Guardfile +42 -42
  16. data/LICENSE +15 -15
  17. data/MAINTAINERS.md +23 -24
  18. data/README.md +135 -135
  19. data/Rakefile +61 -76
  20. data/appveyor.yml +44 -34
  21. data/features/kitchen_action_commands.feature +164 -164
  22. data/features/kitchen_command.feature +16 -16
  23. data/features/kitchen_console_command.feature +34 -34
  24. data/features/kitchen_defaults.feature +38 -38
  25. data/features/kitchen_diagnose_command.feature +96 -96
  26. data/features/kitchen_driver_create_command.feature +64 -64
  27. data/features/kitchen_driver_discover_command.feature +25 -25
  28. data/features/kitchen_help_command.feature +16 -16
  29. data/features/kitchen_init_command.feature +274 -274
  30. data/features/kitchen_list_command.feature +104 -104
  31. data/features/kitchen_login_command.feature +62 -62
  32. data/features/kitchen_sink_command.feature +30 -30
  33. data/features/kitchen_test_command.feature +88 -88
  34. data/features/step_definitions/gem_steps.rb +36 -36
  35. data/features/step_definitions/git_steps.rb +5 -5
  36. data/features/step_definitions/output_steps.rb +5 -5
  37. data/features/support/env.rb +75 -75
  38. data/lib/kitchen.rb +150 -150
  39. data/lib/kitchen/base64_stream.rb +55 -55
  40. data/lib/kitchen/cli.rb +419 -419
  41. data/lib/kitchen/collection.rb +55 -55
  42. data/lib/kitchen/color.rb +65 -65
  43. data/lib/kitchen/command.rb +185 -185
  44. data/lib/kitchen/command/action.rb +45 -45
  45. data/lib/kitchen/command/console.rb +58 -58
  46. data/lib/kitchen/command/diagnose.rb +92 -92
  47. data/lib/kitchen/command/driver_discover.rb +105 -105
  48. data/lib/kitchen/command/exec.rb +41 -41
  49. data/lib/kitchen/command/list.rb +119 -119
  50. data/lib/kitchen/command/login.rb +43 -43
  51. data/lib/kitchen/command/sink.rb +54 -54
  52. data/lib/kitchen/command/test.rb +51 -51
  53. data/lib/kitchen/config.rb +322 -322
  54. data/lib/kitchen/configurable.rb +529 -529
  55. data/lib/kitchen/data_munger.rb +959 -960
  56. data/lib/kitchen/diagnostic.rb +141 -141
  57. data/lib/kitchen/driver.rb +56 -56
  58. data/lib/kitchen/driver/base.rb +134 -134
  59. data/lib/kitchen/driver/dummy.rb +108 -108
  60. data/lib/kitchen/driver/proxy.rb +72 -72
  61. data/lib/kitchen/driver/ssh_base.rb +357 -357
  62. data/lib/kitchen/errors.rb +229 -229
  63. data/lib/kitchen/generator/driver_create.rb +177 -177
  64. data/lib/kitchen/generator/init.rb +296 -296
  65. data/lib/kitchen/instance.rb +662 -662
  66. data/lib/kitchen/lazy_hash.rb +142 -142
  67. data/lib/kitchen/loader/yaml.rb +349 -349
  68. data/lib/kitchen/logger.rb +423 -423
  69. data/lib/kitchen/logging.rb +56 -56
  70. data/lib/kitchen/login_command.rb +52 -52
  71. data/lib/kitchen/metadata_chopper.rb +52 -52
  72. data/lib/kitchen/platform.rb +67 -67
  73. data/lib/kitchen/provisioner.rb +54 -54
  74. data/lib/kitchen/provisioner/base.rb +236 -236
  75. data/lib/kitchen/provisioner/chef/berkshelf.rb +114 -114
  76. data/lib/kitchen/provisioner/chef/common_sandbox.rb +322 -322
  77. data/lib/kitchen/provisioner/chef/librarian.rb +112 -112
  78. data/lib/kitchen/provisioner/chef_apply.rb +124 -125
  79. data/lib/kitchen/provisioner/chef_base.rb +341 -294
  80. data/lib/kitchen/provisioner/chef_solo.rb +88 -89
  81. data/lib/kitchen/provisioner/chef_zero.rb +245 -245
  82. data/lib/kitchen/provisioner/dummy.rb +79 -79
  83. data/lib/kitchen/provisioner/shell.rb +138 -138
  84. data/lib/kitchen/rake_tasks.rb +63 -63
  85. data/lib/kitchen/shell_out.rb +93 -93
  86. data/lib/kitchen/ssh.rb +276 -276
  87. data/lib/kitchen/state_file.rb +120 -120
  88. data/lib/kitchen/suite.rb +51 -51
  89. data/lib/kitchen/thor_tasks.rb +66 -66
  90. data/lib/kitchen/transport.rb +54 -54
  91. data/lib/kitchen/transport/base.rb +176 -176
  92. data/lib/kitchen/transport/dummy.rb +79 -79
  93. data/lib/kitchen/transport/ssh.rb +364 -364
  94. data/lib/kitchen/transport/winrm.rb +486 -486
  95. data/lib/kitchen/util.rb +147 -147
  96. data/lib/kitchen/verifier.rb +55 -55
  97. data/lib/kitchen/verifier/base.rb +235 -235
  98. data/lib/kitchen/verifier/busser.rb +277 -277
  99. data/lib/kitchen/verifier/dummy.rb +79 -79
  100. data/lib/kitchen/verifier/shell.rb +101 -101
  101. data/lib/kitchen/version.rb +21 -21
  102. data/lib/vendor/hash_recursive_merge.rb +82 -82
  103. data/spec/kitchen/base64_stream_spec.rb +77 -77
  104. data/spec/kitchen/cli_spec.rb +56 -56
  105. data/spec/kitchen/collection_spec.rb +80 -80
  106. data/spec/kitchen/color_spec.rb +54 -54
  107. data/spec/kitchen/config_spec.rb +408 -408
  108. data/spec/kitchen/configurable_spec.rb +1095 -1062
  109. data/spec/kitchen/data_munger_spec.rb +2694 -2383
  110. data/spec/kitchen/diagnostic_spec.rb +129 -129
  111. data/spec/kitchen/driver/base_spec.rb +121 -121
  112. data/spec/kitchen/driver/dummy_spec.rb +199 -199
  113. data/spec/kitchen/driver/proxy_spec.rb +138 -138
  114. data/spec/kitchen/driver/ssh_base_spec.rb +1115 -1115
  115. data/spec/kitchen/driver_spec.rb +112 -112
  116. data/spec/kitchen/errors_spec.rb +309 -309
  117. data/spec/kitchen/instance_spec.rb +1419 -1419
  118. data/spec/kitchen/lazy_hash_spec.rb +117 -117
  119. data/spec/kitchen/loader/yaml_spec.rb +774 -774
  120. data/spec/kitchen/logger_spec.rb +429 -429
  121. data/spec/kitchen/logging_spec.rb +59 -59
  122. data/spec/kitchen/login_command_spec.rb +68 -68
  123. data/spec/kitchen/metadata_chopper_spec.rb +82 -82
  124. data/spec/kitchen/platform_spec.rb +89 -89
  125. data/spec/kitchen/provisioner/base_spec.rb +386 -386
  126. data/spec/kitchen/provisioner/chef_apply_spec.rb +136 -136
  127. data/spec/kitchen/provisioner/chef_base_spec.rb +1161 -1067
  128. data/spec/kitchen/provisioner/chef_solo_spec.rb +557 -557
  129. data/spec/kitchen/provisioner/chef_zero_spec.rb +1001 -1001
  130. data/spec/kitchen/provisioner/dummy_spec.rb +99 -99
  131. data/spec/kitchen/provisioner/shell_spec.rb +566 -566
  132. data/spec/kitchen/provisioner_spec.rb +107 -107
  133. data/spec/kitchen/shell_out_spec.rb +150 -150
  134. data/spec/kitchen/ssh_spec.rb +693 -693
  135. data/spec/kitchen/state_file_spec.rb +129 -129
  136. data/spec/kitchen/suite_spec.rb +62 -62
  137. data/spec/kitchen/transport/base_spec.rb +89 -89
  138. data/spec/kitchen/transport/ssh_spec.rb +1255 -1255
  139. data/spec/kitchen/transport/winrm_spec.rb +1143 -1143
  140. data/spec/kitchen/transport_spec.rb +112 -112
  141. data/spec/kitchen/util_spec.rb +165 -165
  142. data/spec/kitchen/verifier/base_spec.rb +362 -362
  143. data/spec/kitchen/verifier/busser_spec.rb +610 -610
  144. data/spec/kitchen/verifier/dummy_spec.rb +99 -99
  145. data/spec/kitchen/verifier/shell_spec.rb +160 -158
  146. data/spec/kitchen/verifier_spec.rb +120 -120
  147. data/spec/kitchen_spec.rb +114 -114
  148. data/spec/spec_helper.rb +85 -85
  149. data/spec/support/powershell_max_size_spec.rb +40 -40
  150. data/support/busser_install_command.ps1 +14 -14
  151. data/support/busser_install_command.sh +14 -14
  152. data/support/chef-client-zero.rb +77 -77
  153. data/support/chef_base_init_command.ps1 +18 -18
  154. data/support/chef_base_init_command.sh +2 -2
  155. data/support/chef_base_install_command.ps1 +85 -85
  156. data/support/chef_base_install_command.sh +229 -229
  157. data/support/chef_zero_prepare_command_legacy.ps1 +9 -9
  158. data/support/chef_zero_prepare_command_legacy.sh +10 -10
  159. data/support/download_helpers.sh +109 -109
  160. data/support/dummy-validation.pem +27 -27
  161. data/templates/driver/CHANGELOG.md.erb +3 -3
  162. data/templates/driver/Gemfile.erb +3 -3
  163. data/templates/driver/README.md.erb +64 -64
  164. data/templates/driver/Rakefile.erb +21 -21
  165. data/templates/driver/driver.rb.erb +23 -23
  166. data/templates/driver/gemspec.erb +29 -29
  167. data/templates/driver/gitignore.erb +17 -17
  168. data/templates/driver/license_apachev2.erb +15 -15
  169. data/templates/driver/license_lgplv3.erb +16 -16
  170. data/templates/driver/license_mit.erb +22 -22
  171. data/templates/driver/license_reserved.erb +5 -5
  172. data/templates/driver/tailor.erb +4 -4
  173. data/templates/driver/travis.yml.erb +11 -11
  174. data/templates/driver/version.rb.erb +12 -12
  175. data/templates/init/chefignore.erb +1 -1
  176. data/templates/init/kitchen.yml.erb +18 -18
  177. data/test-kitchen.gemspec +62 -62
  178. data/test/integration/default/default_spec.rb +3 -0
  179. data/testing_windows.md +37 -37
  180. metadata +23 -11
@@ -1,79 +1,79 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Author:: Salim Afiune (<salim@afiunemaya.com.mx>)
4
- #
5
- # Copyright (C) 2013, Salim Afiune
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 "kitchen"
20
-
21
- module Kitchen
22
-
23
- module Transport
24
-
25
- # Dummy transport for Kitchen. This transport does nothing but report what would
26
- # happen if this transport did anything of consequence. As a result it may
27
- # be a useful transport to use when debugging or developing new features or
28
- # plugins.
29
- class Dummy < Kitchen::Transport::Base
30
-
31
- kitchen_transport_api_version 1
32
-
33
- plugin_version Kitchen::VERSION
34
-
35
- default_config :sleep, 1
36
- default_config :random_exit_code, 0
37
-
38
- def connection(state, &block)
39
- options = config.to_hash.merge(state)
40
- Kitchen::Transport::Dummy::Connection.new(options, &block)
41
- end
42
-
43
- # TODO: comment
44
- class Connection < Kitchen::Transport::Base::Connection
45
-
46
- # (see Base#execute)
47
- def execute(command)
48
- report(:execute, command)
49
- if options[:random_exit_code] != 0
50
- info("Dummy exited (#{exit_code}) for command: [#{command}]")
51
- end
52
- end
53
-
54
- def upload(locals, remote)
55
- report(:upload, "#{locals.inspect} => #{remote}")
56
- end
57
-
58
- private
59
-
60
- # Report what action is taking place, sleeping if so configured, and
61
- # possibly fail randomly.
62
- #
63
- # @param action [Symbol] the action currently taking place
64
- # @param state [Hash] the state hash
65
- # @api private
66
- def report(action, msg = "")
67
- what = action.capitalize
68
- info("[Dummy] #{what} #{msg} on Transport=Dummy")
69
- sleep_if_set
70
- debug("[Dummy] #{what} #{msg} completed (#{options[:sleep]}s).")
71
- end
72
-
73
- def sleep_if_set
74
- sleep(options[:sleep].to_f) if options[:sleep].to_f > 0.0
75
- end
76
- end
77
- end
78
- end
79
- end
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Salim Afiune (<salim@afiunemaya.com.mx>)
4
+ #
5
+ # Copyright (C) 2013, Salim Afiune
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 "kitchen"
20
+
21
+ module Kitchen
22
+
23
+ module Transport
24
+
25
+ # Dummy transport for Kitchen. This transport does nothing but report what would
26
+ # happen if this transport did anything of consequence. As a result it may
27
+ # be a useful transport to use when debugging or developing new features or
28
+ # plugins.
29
+ class Dummy < Kitchen::Transport::Base
30
+
31
+ kitchen_transport_api_version 1
32
+
33
+ plugin_version Kitchen::VERSION
34
+
35
+ default_config :sleep, 1
36
+ default_config :random_exit_code, 0
37
+
38
+ def connection(state, &block)
39
+ options = config.to_hash.merge(state)
40
+ Kitchen::Transport::Dummy::Connection.new(options, &block)
41
+ end
42
+
43
+ # TODO: comment
44
+ class Connection < Kitchen::Transport::Base::Connection
45
+
46
+ # (see Base#execute)
47
+ def execute(command)
48
+ report(:execute, command)
49
+ if options[:random_exit_code] != 0
50
+ info("Dummy exited (#{exit_code}) for command: [#{command}]")
51
+ end
52
+ end
53
+
54
+ def upload(locals, remote)
55
+ report(:upload, "#{locals.inspect} => #{remote}")
56
+ end
57
+
58
+ private
59
+
60
+ # Report what action is taking place, sleeping if so configured, and
61
+ # possibly fail randomly.
62
+ #
63
+ # @param action [Symbol] the action currently taking place
64
+ # @param state [Hash] the state hash
65
+ # @api private
66
+ def report(action, msg = "")
67
+ what = action.capitalize
68
+ info("[Dummy] #{what} #{msg} on Transport=Dummy")
69
+ sleep_if_set
70
+ debug("[Dummy] #{what} #{msg} completed (#{options[:sleep]}s).")
71
+ end
72
+
73
+ def sleep_if_set
74
+ sleep(options[:sleep].to_f) if options[:sleep].to_f > 0.0
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,364 +1,364 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
- #
5
- # Copyright (C) 2014, Fletcher Nichol
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
-
19
- require "kitchen"
20
-
21
- require "net/ssh"
22
- require "net/scp"
23
- require "timeout"
24
-
25
- module Kitchen
26
-
27
- module Transport
28
-
29
- # Wrapped exception for any internally raised SSH-related errors.
30
- #
31
- # @author Fletcher Nichol <fnichol@nichol.ca>
32
- class SshFailed < TransportFailed; end
33
-
34
- # A Transport which uses the SSH protocol to execute commands and transfer
35
- # files.
36
- #
37
- # @author Fletcher Nichol <fnichol@nichol.ca>
38
- class Ssh < Kitchen::Transport::Base
39
-
40
- kitchen_transport_api_version 1
41
-
42
- plugin_version Kitchen::VERSION
43
-
44
- default_config :port, 22
45
- default_config :username, "root"
46
- default_config :keepalive, true
47
- default_config :keepalive_interval, 60
48
- default_config :connection_timeout, 15
49
- default_config :connection_retries, 5
50
- default_config :connection_retry_sleep, 1
51
- default_config :max_wait_until_ready, 600
52
-
53
- default_config :ssh_key, nil
54
- expand_path_for :ssh_key
55
-
56
- default_config :compression, true
57
- required_config :compression
58
-
59
- default_config :compression_level do |transport|
60
- transport[:compression] == false ? 0 : 6
61
- end
62
-
63
- def finalize_config!(instance)
64
- super
65
-
66
- # zlib was never a valid value and breaks in net-ssh >= 2.10
67
- # TODO: remove these backwards compatiable casts in 2.0
68
- case config[:compression]
69
- when "zlib"
70
- config[:compression] = "zlib@openssh.com"
71
- when "none"
72
- config[:compression] = false
73
- end
74
-
75
- self
76
- end
77
-
78
- # (see Base#connection)
79
- def connection(state, &block)
80
- options = connection_options(config.to_hash.merge(state))
81
-
82
- if @connection && @connection_options == options
83
- reuse_connection(&block)
84
- else
85
- create_new_connection(options, &block)
86
- end
87
- end
88
-
89
- # (see Base#cleanup!)
90
- def cleanup!
91
- if @connection
92
- logger.debug("[SSH] shutting previous connection #{@connection}")
93
- @connection.close
94
- @connection = @connection_options = nil
95
- end
96
- end
97
-
98
- # A Connection instance can be generated and re-generated, given new
99
- # connection details such as connection port, hostname, credentials, etc.
100
- # This object is responsible for carrying out the actions on the remote
101
- # host such as executing commands, transferring files, etc.
102
- #
103
- # @author Fletcher Nichol <fnichol@nichol.ca>
104
- class Connection < Kitchen::Transport::Base::Connection
105
-
106
- # (see Base::Connection#close)
107
- def close
108
- return if @session.nil?
109
-
110
- logger.debug("[SSH] closing connection to #{self}")
111
- session.close
112
- ensure
113
- @session = nil
114
- end
115
-
116
- # (see Base::Connection#execute)
117
- def execute(command)
118
- return if command.nil?
119
- logger.debug("[SSH] #{self} (#{command})")
120
- exit_code = execute_with_exit_code(command)
121
-
122
- if exit_code != 0
123
- raise Transport::SshFailed,
124
- "SSH exited (#{exit_code}) for command: [#{command}]"
125
- end
126
- rescue Net::SSH::Exception => ex
127
- raise SshFailed, "SSH command failed (#{ex.message})"
128
- end
129
-
130
- # (see Base::Connection#login_command)
131
- def login_command
132
- args = %W[ -o UserKnownHostsFile=/dev/null ]
133
- args += %W[ -o StrictHostKeyChecking=no ]
134
- args += %W[ -o IdentitiesOnly=yes ] if options[:keys]
135
- args += %W[ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} ]
136
- if options.key?(:forward_agent)
137
- args += %W[ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} ]
138
- end
139
- Array(options[:keys]).each { |ssh_key| args += %W[ -i #{ssh_key} ] }
140
- args += %W[ -p #{port} ]
141
- args += %W[ #{username}@#{hostname} ]
142
-
143
- LoginCommand.new("ssh", args)
144
- end
145
-
146
- # (see Base::Connection#upload)
147
- def upload(locals, remote)
148
- Array(locals).each do |local|
149
- opts = File.directory?(local) ? { :recursive => true } : {}
150
-
151
- session.scp.upload!(local, remote, opts) do |_ch, name, sent, total|
152
- logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total
153
- end
154
- end
155
- rescue Net::SSH::Exception => ex
156
- raise SshFailed, "SCP upload failed (#{ex.message})"
157
- end
158
-
159
- # (see Base::Connection#wait_until_ready)
160
- def wait_until_ready
161
- delay = 3
162
- session(
163
- :retries => max_wait_until_ready / delay,
164
- :delay => delay,
165
- :message => "Waiting for SSH service on #{hostname}:#{port}, " \
166
- "retrying in #{delay} seconds"
167
- )
168
- execute(PING_COMMAND.dup)
169
- end
170
-
171
- private
172
-
173
- PING_COMMAND = "echo '[SSH] Established'".freeze
174
-
175
- RESCUE_EXCEPTIONS_ON_ESTABLISH = [
176
- Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
177
- Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
178
- Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout,
179
- Timeout::Error
180
- ].freeze
181
-
182
- # @return [Integer] how many times to retry when failing to execute
183
- # a command or transfer files
184
- # @api private
185
- attr_reader :connection_retries
186
-
187
- # @return [Float] how many seconds to wait before attempting a retry
188
- # when failing to execute a command or transfer files
189
- # @api private
190
- attr_reader :connection_retry_sleep
191
-
192
- # @return [String] the hostname or IP address of the remote SSH host
193
- # @api private
194
- attr_reader :hostname
195
-
196
- # @return [Integer] how many times to retry when invoking
197
- # `#wait_until_ready` before failing
198
- # @api private
199
- attr_reader :max_wait_until_ready
200
-
201
- # @return [String] the username to use when connecting to the remote
202
- # SSH host
203
- # @api private
204
- attr_reader :username
205
-
206
- # @return [Integer] the TCP port number to use when connecting to the
207
- # remote SSH host
208
- # @api private
209
- attr_reader :port
210
-
211
- # Establish an SSH session on the remote host.
212
- #
213
- # @param opts [Hash] retry options
214
- # @option opts [Integer] :retries the number of times to retry before
215
- # failing
216
- # @option opts [Float] :delay the number of seconds to wait until
217
- # attempting a retry
218
- # @option opts [String] :message an optional message to be logged on
219
- # debug (overriding the default) when a rescuable exception is raised
220
- # @return [Net::SSH::Connection::Session] the SSH connection session
221
- # @api private
222
- def establish_connection(opts)
223
- logger.debug("[SSH] opening connection to #{self}")
224
- Net::SSH.start(hostname, username, options)
225
- rescue *RESCUE_EXCEPTIONS_ON_ESTABLISH => e
226
- if (opts[:retries] -= 1) > 0
227
- message = if opts[:message]
228
- logger.debug("[SSH] connection failed (#{e.inspect})")
229
- opts[:message]
230
- else
231
- "[SSH] connection failed, retrying in #{opts[:delay]} seconds " \
232
- "(#{e.inspect})"
233
- end
234
- logger.info(message)
235
- sleep(opts[:delay])
236
- retry
237
- else
238
- logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
239
- raise SshFailed, "SSH session could not be established"
240
- end
241
- end
242
-
243
- # Execute a remote command over SSH and return the command's exit code.
244
- #
245
- # @param command [String] command string to execute
246
- # @return [Integer] the exit code of the command
247
- # @api private
248
- def execute_with_exit_code(command)
249
- exit_code = nil
250
- session.open_channel do |channel|
251
-
252
- channel.request_pty
253
-
254
- channel.exec(command) do |_ch, _success|
255
-
256
- channel.on_data do |_ch, data|
257
- logger << data
258
- end
259
-
260
- channel.on_extended_data do |_ch, _type, data|
261
- logger << data
262
- end
263
-
264
- channel.on_request("exit-status") do |_ch, data|
265
- exit_code = data.read_long
266
- end
267
- end
268
- end
269
- session.loop
270
- exit_code
271
- end
272
-
273
- # (see Base::Connection#init_options)
274
- def init_options(options)
275
- super
276
- @username = @options.delete(:username)
277
- @hostname = @options.delete(:hostname)
278
- @port = @options[:port] # don't delete from options
279
- @connection_retries = @options.delete(:connection_retries)
280
- @connection_retry_sleep = @options.delete(:connection_retry_sleep)
281
- @max_wait_until_ready = @options.delete(:max_wait_until_ready)
282
- end
283
-
284
- # Returns a connection session, or establishes one when invoked the
285
- # first time.
286
- #
287
- # @param retry_options [Hash] retry options for the initial connection
288
- # @return [Net::SSH::Connection::Session] the SSH connection session
289
- # @api private
290
- def session(retry_options = {})
291
- @session ||= establish_connection({
292
- :retries => connection_retries.to_i,
293
- :delay => connection_retry_sleep.to_i
294
- }.merge(retry_options))
295
- end
296
-
297
- # String representation of object, reporting its connection details and
298
- # configuration.
299
- #
300
- # @api private
301
- def to_s
302
- "#{username}@#{hostname}<#{options.inspect}>"
303
- end
304
- end
305
-
306
- private
307
-
308
- # Builds the hash of options needed by the Connection object on
309
- # construction.
310
- #
311
- # @param data [Hash] merged configuration and mutable state data
312
- # @return [Hash] hash of connection options
313
- # @api private
314
- def connection_options(data) # rubocop:disable Metrics/MethodLength
315
- opts = {
316
- :logger => logger,
317
- :user_known_hosts_file => "/dev/null",
318
- :paranoid => false,
319
- :hostname => data[:hostname],
320
- :port => data[:port],
321
- :username => data[:username],
322
- :compression => data[:compression],
323
- :compression_level => data[:compression_level],
324
- :keepalive => data[:keepalive],
325
- :keepalive_interval => data[:keepalive_interval],
326
- :timeout => data[:connection_timeout],
327
- :connection_retries => data[:connection_retries],
328
- :connection_retry_sleep => data[:connection_retry_sleep],
329
- :max_wait_until_ready => data[:max_wait_until_ready]
330
- }
331
-
332
- opts[:keys_only] = true if data[:ssh_key]
333
- opts[:keys] = Array(data[:ssh_key]) if data[:ssh_key]
334
- opts[:auth_methods] = ["publickey"] if data[:ssh_key]
335
- opts[:password] = data[:password] if data.key?(:password)
336
- opts[:forward_agent] = data[:forward_agent] if data.key?(:forward_agent)
337
-
338
- opts
339
- end
340
-
341
- # Creates a new SSH Connection instance and save it for potential future
342
- # reuse.
343
- #
344
- # @param options [Hash] conneciton options
345
- # @return [Ssh::Connection] an SSH Connection instance
346
- # @api private
347
- def create_new_connection(options, &block)
348
- cleanup!
349
- @connection_options = options
350
- @connection = Kitchen::Transport::Ssh::Connection.new(options, &block)
351
- end
352
-
353
- # Return the last saved SSH connection instance.
354
- #
355
- # @return [Ssh::Connection] an SSH Connection instance
356
- # @api private
357
- def reuse_connection
358
- logger.debug("[SSH] reusing existing connection #{@connection}")
359
- yield @connection if block_given?
360
- @connection
361
- end
362
- end
363
- end
364
- end
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2014, Fletcher Nichol
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require "kitchen"
20
+
21
+ require "net/ssh"
22
+ require "net/scp"
23
+ require "timeout"
24
+
25
+ module Kitchen
26
+
27
+ module Transport
28
+
29
+ # Wrapped exception for any internally raised SSH-related errors.
30
+ #
31
+ # @author Fletcher Nichol <fnichol@nichol.ca>
32
+ class SshFailed < TransportFailed; end
33
+
34
+ # A Transport which uses the SSH protocol to execute commands and transfer
35
+ # files.
36
+ #
37
+ # @author Fletcher Nichol <fnichol@nichol.ca>
38
+ class Ssh < Kitchen::Transport::Base
39
+
40
+ kitchen_transport_api_version 1
41
+
42
+ plugin_version Kitchen::VERSION
43
+
44
+ default_config :port, 22
45
+ default_config :username, "root"
46
+ default_config :keepalive, true
47
+ default_config :keepalive_interval, 60
48
+ default_config :connection_timeout, 15
49
+ default_config :connection_retries, 5
50
+ default_config :connection_retry_sleep, 1
51
+ default_config :max_wait_until_ready, 600
52
+
53
+ default_config :ssh_key, nil
54
+ expand_path_for :ssh_key
55
+
56
+ default_config :compression, true
57
+ required_config :compression
58
+
59
+ default_config :compression_level do |transport|
60
+ transport[:compression] == false ? 0 : 6
61
+ end
62
+
63
+ def finalize_config!(instance)
64
+ super
65
+
66
+ # zlib was never a valid value and breaks in net-ssh >= 2.10
67
+ # TODO: remove these backwards compatiable casts in 2.0
68
+ case config[:compression]
69
+ when "zlib"
70
+ config[:compression] = "zlib@openssh.com"
71
+ when "none"
72
+ config[:compression] = false
73
+ end
74
+
75
+ self
76
+ end
77
+
78
+ # (see Base#connection)
79
+ def connection(state, &block)
80
+ options = connection_options(config.to_hash.merge(state))
81
+
82
+ if @connection && @connection_options == options
83
+ reuse_connection(&block)
84
+ else
85
+ create_new_connection(options, &block)
86
+ end
87
+ end
88
+
89
+ # (see Base#cleanup!)
90
+ def cleanup!
91
+ if @connection
92
+ logger.debug("[SSH] shutting previous connection #{@connection}")
93
+ @connection.close
94
+ @connection = @connection_options = nil
95
+ end
96
+ end
97
+
98
+ # A Connection instance can be generated and re-generated, given new
99
+ # connection details such as connection port, hostname, credentials, etc.
100
+ # This object is responsible for carrying out the actions on the remote
101
+ # host such as executing commands, transferring files, etc.
102
+ #
103
+ # @author Fletcher Nichol <fnichol@nichol.ca>
104
+ class Connection < Kitchen::Transport::Base::Connection
105
+
106
+ # (see Base::Connection#close)
107
+ def close
108
+ return if @session.nil?
109
+
110
+ logger.debug("[SSH] closing connection to #{self}")
111
+ session.close
112
+ ensure
113
+ @session = nil
114
+ end
115
+
116
+ # (see Base::Connection#execute)
117
+ def execute(command)
118
+ return if command.nil?
119
+ logger.debug("[SSH] #{self} (#{command})")
120
+ exit_code = execute_with_exit_code(command)
121
+
122
+ if exit_code != 0
123
+ raise Transport::SshFailed,
124
+ "SSH exited (#{exit_code}) for command: [#{command}]"
125
+ end
126
+ rescue Net::SSH::Exception => ex
127
+ raise SshFailed, "SSH command failed (#{ex.message})"
128
+ end
129
+
130
+ # (see Base::Connection#login_command)
131
+ def login_command
132
+ args = %W[ -o UserKnownHostsFile=/dev/null ]
133
+ args += %W[ -o StrictHostKeyChecking=no ]
134
+ args += %W[ -o IdentitiesOnly=yes ] if options[:keys]
135
+ args += %W[ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} ]
136
+ if options.key?(:forward_agent)
137
+ args += %W[ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} ]
138
+ end
139
+ Array(options[:keys]).each { |ssh_key| args += %W[ -i #{ssh_key} ] }
140
+ args += %W[ -p #{port} ]
141
+ args += %W[ #{username}@#{hostname} ]
142
+
143
+ LoginCommand.new("ssh", args)
144
+ end
145
+
146
+ # (see Base::Connection#upload)
147
+ def upload(locals, remote)
148
+ Array(locals).each do |local|
149
+ opts = File.directory?(local) ? { :recursive => true } : {}
150
+
151
+ session.scp.upload!(local, remote, opts) do |_ch, name, sent, total|
152
+ logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total
153
+ end
154
+ end
155
+ rescue Net::SSH::Exception => ex
156
+ raise SshFailed, "SCP upload failed (#{ex.message})"
157
+ end
158
+
159
+ # (see Base::Connection#wait_until_ready)
160
+ def wait_until_ready
161
+ delay = 3
162
+ session(
163
+ :retries => max_wait_until_ready / delay,
164
+ :delay => delay,
165
+ :message => "Waiting for SSH service on #{hostname}:#{port}, " \
166
+ "retrying in #{delay} seconds"
167
+ )
168
+ execute(PING_COMMAND.dup)
169
+ end
170
+
171
+ private
172
+
173
+ PING_COMMAND = "echo '[SSH] Established'".freeze
174
+
175
+ RESCUE_EXCEPTIONS_ON_ESTABLISH = [
176
+ Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
177
+ Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
178
+ Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout,
179
+ Timeout::Error
180
+ ].freeze
181
+
182
+ # @return [Integer] how many times to retry when failing to execute
183
+ # a command or transfer files
184
+ # @api private
185
+ attr_reader :connection_retries
186
+
187
+ # @return [Float] how many seconds to wait before attempting a retry
188
+ # when failing to execute a command or transfer files
189
+ # @api private
190
+ attr_reader :connection_retry_sleep
191
+
192
+ # @return [String] the hostname or IP address of the remote SSH host
193
+ # @api private
194
+ attr_reader :hostname
195
+
196
+ # @return [Integer] how many times to retry when invoking
197
+ # `#wait_until_ready` before failing
198
+ # @api private
199
+ attr_reader :max_wait_until_ready
200
+
201
+ # @return [String] the username to use when connecting to the remote
202
+ # SSH host
203
+ # @api private
204
+ attr_reader :username
205
+
206
+ # @return [Integer] the TCP port number to use when connecting to the
207
+ # remote SSH host
208
+ # @api private
209
+ attr_reader :port
210
+
211
+ # Establish an SSH session on the remote host.
212
+ #
213
+ # @param opts [Hash] retry options
214
+ # @option opts [Integer] :retries the number of times to retry before
215
+ # failing
216
+ # @option opts [Float] :delay the number of seconds to wait until
217
+ # attempting a retry
218
+ # @option opts [String] :message an optional message to be logged on
219
+ # debug (overriding the default) when a rescuable exception is raised
220
+ # @return [Net::SSH::Connection::Session] the SSH connection session
221
+ # @api private
222
+ def establish_connection(opts)
223
+ logger.debug("[SSH] opening connection to #{self}")
224
+ Net::SSH.start(hostname, username, options)
225
+ rescue *RESCUE_EXCEPTIONS_ON_ESTABLISH => e
226
+ if (opts[:retries] -= 1) > 0
227
+ message = if opts[:message]
228
+ logger.debug("[SSH] connection failed (#{e.inspect})")
229
+ opts[:message]
230
+ else
231
+ "[SSH] connection failed, retrying in #{opts[:delay]} seconds " \
232
+ "(#{e.inspect})"
233
+ end
234
+ logger.info(message)
235
+ sleep(opts[:delay])
236
+ retry
237
+ else
238
+ logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
239
+ raise SshFailed, "SSH session could not be established"
240
+ end
241
+ end
242
+
243
+ # Execute a remote command over SSH and return the command's exit code.
244
+ #
245
+ # @param command [String] command string to execute
246
+ # @return [Integer] the exit code of the command
247
+ # @api private
248
+ def execute_with_exit_code(command)
249
+ exit_code = nil
250
+ session.open_channel do |channel|
251
+
252
+ channel.request_pty
253
+
254
+ channel.exec(command) do |_ch, _success|
255
+
256
+ channel.on_data do |_ch, data|
257
+ logger << data
258
+ end
259
+
260
+ channel.on_extended_data do |_ch, _type, data|
261
+ logger << data
262
+ end
263
+
264
+ channel.on_request("exit-status") do |_ch, data|
265
+ exit_code = data.read_long
266
+ end
267
+ end
268
+ end
269
+ session.loop
270
+ exit_code
271
+ end
272
+
273
+ # (see Base::Connection#init_options)
274
+ def init_options(options)
275
+ super
276
+ @username = @options.delete(:username)
277
+ @hostname = @options.delete(:hostname)
278
+ @port = @options[:port] # don't delete from options
279
+ @connection_retries = @options.delete(:connection_retries)
280
+ @connection_retry_sleep = @options.delete(:connection_retry_sleep)
281
+ @max_wait_until_ready = @options.delete(:max_wait_until_ready)
282
+ end
283
+
284
+ # Returns a connection session, or establishes one when invoked the
285
+ # first time.
286
+ #
287
+ # @param retry_options [Hash] retry options for the initial connection
288
+ # @return [Net::SSH::Connection::Session] the SSH connection session
289
+ # @api private
290
+ def session(retry_options = {})
291
+ @session ||= establish_connection({
292
+ :retries => connection_retries.to_i,
293
+ :delay => connection_retry_sleep.to_i
294
+ }.merge(retry_options))
295
+ end
296
+
297
+ # String representation of object, reporting its connection details and
298
+ # configuration.
299
+ #
300
+ # @api private
301
+ def to_s
302
+ "#{username}@#{hostname}<#{options.inspect}>"
303
+ end
304
+ end
305
+
306
+ private
307
+
308
+ # Builds the hash of options needed by the Connection object on
309
+ # construction.
310
+ #
311
+ # @param data [Hash] merged configuration and mutable state data
312
+ # @return [Hash] hash of connection options
313
+ # @api private
314
+ def connection_options(data) # rubocop:disable Metrics/MethodLength
315
+ opts = {
316
+ :logger => logger,
317
+ :user_known_hosts_file => "/dev/null",
318
+ :paranoid => false,
319
+ :hostname => data[:hostname],
320
+ :port => data[:port],
321
+ :username => data[:username],
322
+ :compression => data[:compression],
323
+ :compression_level => data[:compression_level],
324
+ :keepalive => data[:keepalive],
325
+ :keepalive_interval => data[:keepalive_interval],
326
+ :timeout => data[:connection_timeout],
327
+ :connection_retries => data[:connection_retries],
328
+ :connection_retry_sleep => data[:connection_retry_sleep],
329
+ :max_wait_until_ready => data[:max_wait_until_ready]
330
+ }
331
+
332
+ opts[:keys_only] = true if data[:ssh_key]
333
+ opts[:keys] = Array(data[:ssh_key]) if data[:ssh_key]
334
+ opts[:auth_methods] = ["publickey"] if data[:ssh_key]
335
+ opts[:password] = data[:password] if data.key?(:password)
336
+ opts[:forward_agent] = data[:forward_agent] if data.key?(:forward_agent)
337
+
338
+ opts
339
+ end
340
+
341
+ # Creates a new SSH Connection instance and save it for potential future
342
+ # reuse.
343
+ #
344
+ # @param options [Hash] conneciton options
345
+ # @return [Ssh::Connection] an SSH Connection instance
346
+ # @api private
347
+ def create_new_connection(options, &block)
348
+ cleanup!
349
+ @connection_options = options
350
+ @connection = Kitchen::Transport::Ssh::Connection.new(options, &block)
351
+ end
352
+
353
+ # Return the last saved SSH connection instance.
354
+ #
355
+ # @return [Ssh::Connection] an SSH Connection instance
356
+ # @api private
357
+ def reuse_connection
358
+ logger.debug("[SSH] reusing existing connection #{@connection}")
359
+ yield @connection if block_given?
360
+ @connection
361
+ end
362
+ end
363
+ end
364
+ end