test-kitchen 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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