test-kitchen 1.3.1 → 1.4.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +2 -0
  3. data/.gitignore +4 -0
  4. data/CHANGELOG.md +45 -0
  5. data/Rakefile +15 -0
  6. data/features/kitchen_action_commands.feature +12 -9
  7. data/features/kitchen_defaults.feature +38 -0
  8. data/features/kitchen_init_command.feature +0 -1
  9. data/features/kitchen_list_command.feature +2 -2
  10. data/features/kitchen_login_command.feature +7 -1
  11. data/features/kitchen_test_command.feature +4 -4
  12. data/lib/kitchen.rb +40 -11
  13. data/lib/kitchen/cli.rb +38 -22
  14. data/lib/kitchen/command/list.rb +5 -2
  15. data/lib/kitchen/config.rb +45 -18
  16. data/lib/kitchen/configurable.rb +137 -1
  17. data/lib/kitchen/data_munger.rb +248 -17
  18. data/lib/kitchen/driver.rb +1 -1
  19. data/lib/kitchen/driver/base.rb +1 -83
  20. data/lib/kitchen/driver/dummy.rb +0 -5
  21. data/lib/kitchen/driver/ssh_base.rb +177 -22
  22. data/lib/kitchen/instance.rb +140 -20
  23. data/lib/kitchen/logger.rb +43 -8
  24. data/lib/kitchen/login_command.rb +14 -5
  25. data/lib/kitchen/platform.rb +19 -0
  26. data/lib/kitchen/provisioner.rb +5 -3
  27. data/lib/kitchen/provisioner/base.rb +46 -48
  28. data/lib/kitchen/provisioner/chef/common_sandbox.rb +322 -0
  29. data/lib/kitchen/provisioner/chef_base.rb +179 -286
  30. data/lib/kitchen/provisioner/chef_solo.rb +11 -5
  31. data/lib/kitchen/provisioner/chef_zero.rb +108 -94
  32. data/lib/kitchen/provisioner/dummy.rb +47 -0
  33. data/lib/kitchen/provisioner/shell.rb +45 -12
  34. data/lib/kitchen/rake_tasks.rb +1 -1
  35. data/lib/kitchen/ssh.rb +1 -1
  36. data/lib/kitchen/thor_tasks.rb +1 -1
  37. data/lib/kitchen/transport.rb +54 -0
  38. data/lib/kitchen/transport/base.rb +146 -0
  39. data/lib/kitchen/transport/dummy.rb +75 -0
  40. data/lib/kitchen/transport/ssh.rb +325 -0
  41. data/lib/kitchen/transport/winrm.rb +508 -0
  42. data/lib/kitchen/transport/winrm/command_executor.rb +188 -0
  43. data/lib/kitchen/transport/winrm/file_transporter.rb +454 -0
  44. data/lib/kitchen/transport/winrm/logging.rb +50 -0
  45. data/lib/kitchen/transport/winrm/template.rb +74 -0
  46. data/lib/kitchen/transport/winrm/tmp_zip.rb +187 -0
  47. data/lib/kitchen/verifier.rb +55 -0
  48. data/lib/kitchen/verifier/base.rb +191 -0
  49. data/lib/kitchen/verifier/busser.rb +266 -0
  50. data/lib/kitchen/verifier/dummy.rb +75 -0
  51. data/lib/kitchen/version.rb +1 -1
  52. data/spec/kitchen/cli_spec.rb +56 -0
  53. data/spec/kitchen/config_spec.rb +61 -20
  54. data/spec/kitchen/configurable_spec.rb +327 -1
  55. data/spec/kitchen/data_munger_spec.rb +777 -14
  56. data/spec/kitchen/driver/base_spec.rb +7 -38
  57. data/spec/kitchen/driver/dummy_spec.rb +0 -29
  58. data/spec/kitchen/driver/ssh_base_spec.rb +580 -236
  59. data/spec/kitchen/driver_spec.rb +1 -0
  60. data/spec/kitchen/instance_spec.rb +383 -83
  61. data/spec/kitchen/login_command_spec.rb +29 -10
  62. data/spec/kitchen/platform_spec.rb +58 -2
  63. data/spec/kitchen/provisioner/base_spec.rb +170 -18
  64. data/spec/kitchen/provisioner/chef_base_spec.rb +454 -104
  65. data/spec/kitchen/provisioner/chef_solo_spec.rb +307 -104
  66. data/spec/kitchen/provisioner/chef_zero_spec.rb +561 -230
  67. data/spec/kitchen/provisioner/dummy_spec.rb +91 -0
  68. data/spec/kitchen/provisioner/shell_spec.rb +158 -56
  69. data/spec/kitchen/provisioner_spec.rb +37 -0
  70. data/spec/kitchen/ssh_spec.rb +19 -19
  71. data/spec/kitchen/transport/base_spec.rb +89 -0
  72. data/spec/kitchen/transport/ssh_spec.rb +1147 -0
  73. data/spec/kitchen/transport/winrm/command_executor_spec.rb +400 -0
  74. data/spec/kitchen/transport/winrm/file_transporter_spec.rb +876 -0
  75. data/spec/kitchen/transport/winrm/logging_spec.rb +92 -0
  76. data/spec/kitchen/transport/winrm/template_spec.rb +51 -0
  77. data/spec/kitchen/transport/winrm/tmp_zip_spec.rb +132 -0
  78. data/spec/kitchen/transport/winrm_spec.rb +1069 -0
  79. data/spec/kitchen/transport_spec.rb +112 -0
  80. data/spec/kitchen/verifier/base_spec.rb +310 -0
  81. data/spec/kitchen/verifier/busser_spec.rb +540 -0
  82. data/spec/kitchen/verifier/dummy_spec.rb +91 -0
  83. data/spec/kitchen/verifier_spec.rb +120 -0
  84. data/spec/kitchen_spec.rb +7 -0
  85. data/spec/spec_helper.rb +8 -0
  86. data/spec/support/powershell_max_size_spec.rb +40 -0
  87. data/support/busser_install_command.ps1 +14 -0
  88. data/support/busser_install_command.sh +15 -0
  89. data/support/check_files.ps1.erb +48 -0
  90. data/support/chef_base_init_command.ps1 +18 -0
  91. data/support/chef_base_init_command.sh +2 -0
  92. data/support/chef_base_install_command.ps1 +76 -0
  93. data/support/chef_base_install_command.sh +137 -0
  94. data/support/chef_zero_prepare_command_legacy.ps1 +9 -0
  95. data/support/chef_zero_prepare_command_legacy.sh +10 -0
  96. data/support/decode_files.ps1.erb +61 -0
  97. data/test-kitchen.gemspec +2 -0
  98. metadata +97 -8
  99. data/lib/kitchen/busser.rb +0 -316
  100. data/spec/kitchen/busser_spec.rb +0 -490
  101. data/support/chef_helpers.sh +0 -16
data/lib/kitchen/ssh.rb CHANGED
@@ -149,7 +149,7 @@ module Kitchen
149
149
  args += %W[ -p #{port} ]
150
150
  args += %W[ #{username}@#{hostname} ]
151
151
 
152
- LoginCommand.new(["ssh", *args])
152
+ LoginCommand.new("ssh", args)
153
153
  end
154
154
 
155
155
  private
@@ -35,7 +35,7 @@ module Kitchen
35
35
  def initialize(*args)
36
36
  super
37
37
  @config = Kitchen::Config.new
38
- Kitchen.logger = Kitchen.default_file_logger
38
+ Kitchen.logger = Kitchen.default_file_logger(nil, false)
39
39
  yield self if block_given?
40
40
  define
41
41
  end
@@ -0,0 +1,54 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Salim Afiune (<salim@afiunemaya.com.mx>)
4
+ #
5
+ # Copyright (C) 2014, 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 "thor/util"
20
+
21
+ module Kitchen
22
+
23
+ # A transport is responsible for the communication with an instance,
24
+ # that is remote comands and other actions such as file transfer,
25
+ # login, etc.
26
+ #
27
+ # @author Salim Afiune <salim@afiunemaya.com.mx>
28
+ module Transport
29
+
30
+ # Default transport to use
31
+ DEFAULT_PLUGIN = "ssh".freeze
32
+
33
+ # Returns an instance of a transport given a plugin type string.
34
+ #
35
+ # @param plugin [String] a transport plugin type, to be constantized
36
+ # @param config [Hash] a configuration hash to initialize the transport
37
+ # @return [Transport::Base] a transport instance
38
+ # @raise [ClientError] if a transport instance could not be created
39
+ def self.for_plugin(plugin, config)
40
+ first_load = require("kitchen/transport/#{plugin}")
41
+
42
+ str_const = Thor::Util.camel_case(plugin)
43
+ klass = const_get(str_const)
44
+ object = klass.new(config)
45
+ object.verify_dependencies if first_load
46
+ object
47
+ rescue LoadError, NameError
48
+ raise ClientError,
49
+ "Could not load the '#{plugin}' transport from the load path." \
50
+ " Please ensure that your transport is installed as a gem or" \
51
+ " included in your Gemfile if using Bundler."
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,146 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Salim Afiune (<salim@afiunemaya.com.mx>)
4
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
5
+ #
6
+ # Copyright (C) 2014, Salim Afiune
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+
20
+ require "kitchen/errors"
21
+ require "kitchen/lazy_hash"
22
+ require "kitchen/login_command"
23
+
24
+ module Kitchen
25
+
26
+ module Transport
27
+
28
+ # Wrapped exception for any internally raised Transport errors.
29
+ #
30
+ # @author Salim Afiune <salim@afiunemaya.com.mx>
31
+ class TransportFailed < TransientFailure; end
32
+
33
+ # Base class for a transport.
34
+ #
35
+ # @author Salim Afiune <salim@afiunemaya.com.mx>
36
+ # @author Fletcher Nichol <fnichol@nichol.ca>
37
+ class Base
38
+
39
+ include Configurable
40
+ include Logging
41
+
42
+ # Create a new transport by providing a configuration hash.
43
+ #
44
+ # @param config [Hash] initial provided configuration
45
+ def initialize(config = {})
46
+ init_config(config)
47
+ end
48
+
49
+ # Creates a new Connection, configured by a merging of configuration
50
+ # and state data. Depending on the implementation, the Connection could
51
+ # be saved or cached to speed up multiple calls, given the same state
52
+ # hash as input.
53
+ #
54
+ # @param state [Hash] mutable instance state
55
+ # @return [Connection] a connection for this transport
56
+ # @raise [TransportFailed] if a connection could not be returned
57
+ def connection(state) # rubocop:disable Lint/UnusedMethodArgument
58
+ raise ClientError, "#{self.class}#connection must be implemented"
59
+ end
60
+
61
+ # A Connection instance can be generated and re-generated, given new
62
+ # connection details such as connection port, hostname, credentials, etc.
63
+ # This object is responsible for carrying out the actions on the remote
64
+ # host such as executing commands, transferring files, etc.
65
+ #
66
+ # @author Fletcher Nichol <fnichol@nichol.ca>
67
+ class Connection
68
+
69
+ include Logging
70
+
71
+ # Create a new Connection instance.
72
+ #
73
+ # @param options [Hash] connection options
74
+ # @yield [self] yields itself for block-style invocation
75
+ def initialize(options = {})
76
+ init_options(options)
77
+
78
+ if block_given?
79
+ yield self
80
+ end
81
+ end
82
+
83
+ # Closes the session connection, if it is still active.
84
+ def close
85
+ # this method may be left unimplemented if that is applicable
86
+ end
87
+
88
+ # Execute a command on the remote host.
89
+ #
90
+ # @param command [String] command string to execute
91
+ # @raise [TransportFailed] if the command does not exit successfully,
92
+ # which may vary by implementation
93
+ def execute(command) # rubocop:disable Lint/UnusedMethodArgument
94
+ raise ClientError, "#{self.class}#execute must be implemented"
95
+ end
96
+
97
+ # Builds a LoginCommand which can be used to open an interactive
98
+ # session on the remote host.
99
+ #
100
+ # @return [LoginCommand] an object containing the array of command line
101
+ # tokens and exec options to be used in a fork/exec
102
+ # @raise [ActionFailed] if the action could not be completed
103
+ def login_command
104
+ raise ActionFailed, "Remote login not supported in #{self.class}."
105
+ end
106
+
107
+ # Uploads local files or directories to remote host.
108
+ #
109
+ # @param locals [Array<String>] paths to local files or directories
110
+ # @param remote [String] path to remote destination
111
+ # @raise [TransportFailed] if the files could not all be uploaded
112
+ # successfully, which may vary by implementation
113
+ def upload(locals, remote) # rubocop:disable Lint/UnusedMethodArgument
114
+ raise ClientError, "#{self.class}#upload must be implemented"
115
+ end
116
+
117
+ # Block and return only when the remote host is prepared and ready to
118
+ # execute command and upload files. The semantics and details will
119
+ # vary by implementation, but a round trip through the hosted
120
+ # service is preferred to simply waiting on a socket to become
121
+ # available.
122
+ def wait_until_ready
123
+ # this method may be left unimplemented if that is applicable
124
+ end
125
+
126
+ private
127
+
128
+ # @return [Kitchen::Logger] a logger
129
+ # @api private
130
+ attr_reader :logger
131
+
132
+ # @return [Hash] connection options
133
+ # @api private
134
+ attr_reader :options
135
+
136
+ # Initialize incoming options for use by the object.
137
+ #
138
+ # @param options [Hash] configuration options
139
+ def init_options(options)
140
+ @options = options.dup
141
+ @logger = @options.delete(:logger) || Kitchen.logger
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,75 @@
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
+ default_config :sleep, 1
32
+ default_config :random_exit_code, 0
33
+
34
+ def connection(state, &block)
35
+ options = config.to_hash.merge(state)
36
+ Kitchen::Transport::Dummy::Connection.new(options, &block)
37
+ end
38
+
39
+ # TODO: comment
40
+ class Connection < Kitchen::Transport::Base::Connection
41
+
42
+ # (see Base#execute)
43
+ def execute(command)
44
+ report(:execute, command)
45
+ if options[:random_exit_code] != 0
46
+ info("Dummy exited (#{exit_code}) for command: [#{command}]")
47
+ end
48
+ end
49
+
50
+ def upload(locals, remote)
51
+ report(:upload, "#{locals.inspect} => #{remote}")
52
+ end
53
+
54
+ private
55
+
56
+ # Report what action is taking place, sleeping if so configured, and
57
+ # possibly fail randomly.
58
+ #
59
+ # @param action [Symbol] the action currently taking place
60
+ # @param state [Hash] the state hash
61
+ # @api private
62
+ def report(action, msg = "")
63
+ what = action.capitalize
64
+ info("[Dummy] #{what} #{msg} on Transport=Dummy")
65
+ sleep_if_set
66
+ debug("[Dummy] #{what} #{msg} completed (#{options[:sleep]}s).")
67
+ end
68
+
69
+ def sleep_if_set
70
+ sleep(options[:sleep].to_f) if options[:sleep].to_f > 0.0
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,325 @@
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
+
24
+ module Kitchen
25
+
26
+ module Transport
27
+
28
+ # Wrapped exception for any internally raised SSH-related errors.
29
+ #
30
+ # @author Fletcher Nichol <fnichol@nichol.ca>
31
+ class SshFailed < TransportFailed; end
32
+
33
+ # A Transport which uses the SSH protocol to execute commands and transfer
34
+ # files.
35
+ #
36
+ # @author Fletcher Nichol <fnichol@nichol.ca>
37
+ class Ssh < Kitchen::Transport::Base
38
+
39
+ default_config :port, 22
40
+ default_config :username, "root"
41
+ default_config :keepalive, true
42
+ default_config :keepalive_interval, 60
43
+ default_config :connection_timeout, 15
44
+ default_config :connection_retries, 5
45
+ default_config :connection_retry_sleep, 1
46
+ default_config :max_wait_until_ready, 600
47
+
48
+ # (see Base#connection)
49
+ def connection(state, &block)
50
+ options = connection_options(config.to_hash.merge(state))
51
+
52
+ if @connection && @connection_options == options
53
+ reuse_connection(&block)
54
+ else
55
+ create_new_connection(options, &block)
56
+ end
57
+ end
58
+
59
+ # A Connection instance can be generated and re-generated, given new
60
+ # connection details such as connection port, hostname, credentials, etc.
61
+ # This object is responsible for carrying out the actions on the remote
62
+ # host such as executing commands, transferring files, etc.
63
+ #
64
+ # @author Fletcher Nichol <fnichol@nichol.ca>
65
+ class Connection < Kitchen::Transport::Base::Connection
66
+
67
+ # (see Base::Connection#close)
68
+ def close
69
+ return if @session.nil?
70
+
71
+ logger.debug("[SSH] closing connection to #{self}")
72
+ session.close
73
+ ensure
74
+ @session = nil
75
+ end
76
+
77
+ # (see Base::Connection#execute)
78
+ def execute(command)
79
+ return if command.nil?
80
+ logger.debug("[SSH] #{self} (#{command})")
81
+ exit_code = execute_with_exit_code(command)
82
+
83
+ if exit_code != 0
84
+ raise Transport::SshFailed,
85
+ "SSH exited (#{exit_code}) for command: [#{command}]"
86
+ end
87
+ rescue Net::SSH::Exception => ex
88
+ raise SshFailed, "SSH command failed (#{ex.message})"
89
+ end
90
+
91
+ # (see Base::Connection#login_command)
92
+ def login_command
93
+ args = %W[ -o UserKnownHostsFile=/dev/null ]
94
+ args += %W[ -o StrictHostKeyChecking=no ]
95
+ args += %W[ -o IdentitiesOnly=yes ] if options[:keys]
96
+ args += %W[ -o LogLevel=#{logger.debug? ? "VERBOSE" : "ERROR"} ]
97
+ if options.key?(:forward_agent)
98
+ args += %W[ -o ForwardAgent=#{options[:forward_agent] ? "yes" : "no"} ]
99
+ end
100
+ Array(options[:keys]).each { |ssh_key| args += %W[ -i #{ssh_key} ] }
101
+ args += %W[ -p #{port} ]
102
+ args += %W[ #{username}@#{hostname} ]
103
+
104
+ LoginCommand.new("ssh", args)
105
+ end
106
+
107
+ # (see Base::Connection#upload)
108
+ def upload(locals, remote)
109
+ Array(locals).each do |local|
110
+ opts = File.directory?(local) ? { :recursive => true } : {}
111
+
112
+ session.scp.upload!(local, remote, opts) do |_ch, name, sent, total|
113
+ logger.debug("Uploaded #{name} (#{total} bytes)") if sent == total
114
+ end
115
+ end
116
+ rescue Net::SSH::Exception => ex
117
+ raise SshFailed, "SCP upload failed (#{ex.message})"
118
+ end
119
+
120
+ # (see Base::Connection#wait_until_ready)
121
+ def wait_until_ready
122
+ delay = 3
123
+ session(
124
+ :retries => max_wait_until_ready / delay,
125
+ :delay => delay,
126
+ :message => "Waiting for SSH service on #{hostname}:#{port}, " \
127
+ "retrying in #{delay} seconds"
128
+ )
129
+ execute(PING_COMMAND.dup)
130
+ end
131
+
132
+ private
133
+
134
+ PING_COMMAND = "echo '[SSH] Established'".freeze
135
+
136
+ RESCUE_EXCEPTIONS_ON_ESTABLISH = [
137
+ Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
138
+ Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
139
+ Net::SSH::Disconnect, Net::SSH::AuthenticationFailed, Timeout::Error
140
+ ].freeze
141
+
142
+ # @return [Integer] how many times to retry when failing to execute
143
+ # a command or transfer files
144
+ # @api private
145
+ attr_reader :connection_retries
146
+
147
+ # @return [Float] how many seconds to wait before attempting a retry
148
+ # when failing to execute a command or transfer files
149
+ # @api private
150
+ attr_reader :connection_retry_sleep
151
+
152
+ # @return [String] the hostname or IP address of the remote SSH host
153
+ # @api private
154
+ attr_reader :hostname
155
+
156
+ # @return [Integer] how many times to retry when invoking
157
+ # `#wait_until_ready` before failing
158
+ # @api private
159
+ attr_reader :max_wait_until_ready
160
+
161
+ # @return [String] the username to use when connecting to the remote
162
+ # SSH host
163
+ # @api private
164
+ attr_reader :username
165
+
166
+ # @return [Integer] the TCP port number to use when connecting to the
167
+ # remote SSH host
168
+ # @api private
169
+ attr_reader :port
170
+
171
+ # Establish an SSH session on the remote host.
172
+ #
173
+ # @param opts [Hash] retry options
174
+ # @option opts [Integer] :retries the number of times to retry before
175
+ # failing
176
+ # @option opts [Float] :delay the number of seconds to wait until
177
+ # attempting a retry
178
+ # @option opts [String] :message an optional message to be logged on
179
+ # debug (overriding the default) when a rescuable exception is raised
180
+ # @return [Net::SSH::Connection::Session] the SSH connection session
181
+ # @api private
182
+ def establish_connection(opts)
183
+ logger.debug("[SSH] opening connection to #{self}")
184
+ Net::SSH.start(hostname, username, options)
185
+ rescue *RESCUE_EXCEPTIONS_ON_ESTABLISH => e
186
+ if (opts[:retries] -= 1) > 0
187
+ message = if opts[:message]
188
+ logger.debug("[SSH] connection failed (#{e.inspect})")
189
+ opts[:message]
190
+ else
191
+ "[SSH] connection failed, retrying in #{opts[:delay]} seconds " \
192
+ "(#{e.inspect})"
193
+ end
194
+ logger.info(message)
195
+ sleep(opts[:delay])
196
+ retry
197
+ else
198
+ logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
199
+ raise SshFailed, "SSH session could not be established"
200
+ end
201
+ end
202
+
203
+ # Execute a remote command over SSH and return the command's exit code.
204
+ #
205
+ # @param command [String] command string to execute
206
+ # @return [Integer] the exit code of the command
207
+ # @api private
208
+ def execute_with_exit_code(command)
209
+ exit_code = nil
210
+ session.open_channel do |channel|
211
+
212
+ channel.request_pty
213
+
214
+ channel.exec(command) do |_ch, _success|
215
+
216
+ channel.on_data do |_ch, data|
217
+ logger << data
218
+ end
219
+
220
+ channel.on_extended_data do |_ch, _type, data|
221
+ logger << data
222
+ end
223
+
224
+ channel.on_request("exit-status") do |_ch, data|
225
+ exit_code = data.read_long
226
+ end
227
+ end
228
+ end
229
+ session.loop
230
+ exit_code
231
+ end
232
+
233
+ # (see Base::Connection#init_options)
234
+ def init_options(options)
235
+ super
236
+ @username = @options.delete(:username)
237
+ @hostname = @options.delete(:hostname)
238
+ @port = @options[:port] # don't delete from options
239
+ @connection_retries = @options.delete(:connection_retries)
240
+ @connection_retry_sleep = @options.delete(:connection_retry_sleep)
241
+ @max_wait_until_ready = @options.delete(:max_wait_until_ready)
242
+ end
243
+
244
+ # Returns a connection session, or establishes one when invoked the
245
+ # first time.
246
+ #
247
+ # @param retry_options [Hash] retry options for the initial connection
248
+ # @return [Net::SSH::Connection::Session] the SSH connection session
249
+ # @api private
250
+ def session(retry_options = {})
251
+ @session ||= establish_connection({
252
+ :retries => connection_retries.to_i,
253
+ :delay => connection_retry_sleep.to_i
254
+ }.merge(retry_options))
255
+ end
256
+
257
+ # String representation of object, reporting its connection details and
258
+ # configuration.
259
+ #
260
+ # @api private
261
+ def to_s
262
+ "#{username}@#{hostname}<#{options.inspect}>"
263
+ end
264
+ end
265
+
266
+ private
267
+
268
+ # Builds the hash of options needed by the Connection object on
269
+ # construction.
270
+ #
271
+ # @param data [Hash] merged configuration and mutable state data
272
+ # @return [Hash] hash of connection options
273
+ # @api private
274
+ def connection_options(data)
275
+ opts = {
276
+ :logger => logger,
277
+ :user_known_hosts_file => "/dev/null",
278
+ :paranoid => false,
279
+ :hostname => data[:hostname],
280
+ :port => data[:port],
281
+ :username => data[:username],
282
+ :keepalive => data[:keepalive],
283
+ :keepalive_interval => data[:keepalive_interval],
284
+ :timeout => data[:connection_timeout],
285
+ :connection_retries => data[:connection_retries],
286
+ :connection_retry_sleep => data[:connection_retry_sleep],
287
+ :max_wait_until_ready => data[:max_wait_until_ready]
288
+ }
289
+
290
+ opts[:keys_only] = true if data[:ssh_key]
291
+ opts[:keys] = Array(data[:ssh_key]) if data[:ssh_key]
292
+ opts[:password] = data[:password] if data.key?(:password)
293
+ opts[:forward_agent] = data[:forward_agent] if data.key?(:forward_agent)
294
+
295
+ opts
296
+ end
297
+
298
+ # Creates a new SSH Connection instance and save it for potential future
299
+ # reuse.
300
+ #
301
+ # @param options [Hash] conneciton options
302
+ # @return [Ssh::Connection] an SSH Connection instance
303
+ # @api private
304
+ def create_new_connection(options, &block)
305
+ if @connection
306
+ logger.debug("[SSH] shutting previous connection #{@connection}")
307
+ @connection.close
308
+ end
309
+
310
+ @connection_options = options
311
+ @connection = Kitchen::Transport::Ssh::Connection.new(options, &block)
312
+ end
313
+
314
+ # Return the last saved SSH connection instance.
315
+ #
316
+ # @return [Ssh::Connection] an SSH Connection instance
317
+ # @api private
318
+ def reuse_connection
319
+ logger.debug("[SSH] reusing existing connection #{@connection}")
320
+ yield @connection if block_given?
321
+ @connection
322
+ end
323
+ end
324
+ end
325
+ end