test-kitchen 1.7.0 → 1.7.1.dev

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