-
Notifications
You must be signed in to change notification settings - Fork 581
/
ssh_base.rb
351 lines (311 loc) · 12.6 KB
/
ssh_base.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
#
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
#
# Copyright (C) 2012, Fletcher Nichol
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require "thor/util"
require_relative "../lazy_hash"
require_relative "../plugin_base"
require "benchmark" unless defined?(Benchmark)
module Kitchen
module Driver
# Legacy base class for a driver that uses SSH to communication with an
# instance. This class has been updated to use the Instance's Transport to
# issue commands and transfer files and no longer uses the `Kitchen:SSH`
# class directly.
#
# **NOTE:** Authors of new Drivers are encouraged to inherit from
# `Kitchen::Driver::Base` instead and existing Driver authors are
# encouraged to update their Driver class to inherit from
# `Kitchen::Driver::SSHBase`.
#
# A subclass must implement the following methods:
# * #create(state)
# * #destroy(state)
#
# @author Fletcher Nichol <fnichol@nichol.ca>
# @deprecated While all possible effort has been made to preserve the
# original behavior of this class, future improvements to the Driver,
# Transport, and Verifier subsystems may not be picked up in these
# Drivers. When legacy Driver::SSHBase support is removed, this class
# will no longer be available.
class SSHBase < Kitchen::Plugin::Base
include ShellOut
include Configurable
include Logging
default_config :sudo, true
default_config :port, 22
# needs to be one less than the configured sshd_config MaxSessions
default_config :max_ssh_sessions, 9
# Creates a new Driver object using the provided configuration data
# which will be merged with any default configuration.
#
# @param config [Hash] provided driver configuration
def initialize(config = {})
init_config(config)
end
# (see Base#create)
def create(state) # rubocop:disable Lint/UnusedMethodArgument
raise ClientError, "#{self.class}#create must be implemented"
end
# (see Base#converge)
def converge(state) # rubocop:disable Metrics/AbcSize
provisioner = instance.provisioner
provisioner.create_sandbox
sandbox_dirs = provisioner.sandbox_dirs
instance.transport.connection(backcompat_merged_state(state)) do |conn|
conn.execute(env_cmd(provisioner.install_command))
conn.execute(env_cmd(provisioner.init_command))
info("Transferring files to #{instance.to_str}")
conn.upload(sandbox_dirs, provisioner[:root_path])
debug("Transfer complete")
conn.execute(env_cmd(provisioner.prepare_command))
conn.execute(env_cmd(provisioner.run_command))
info("Downloading files from #{instance.to_str}")
provisioner[:downloads].to_h.each do |remotes, local|
debug("Downloading #{Array(remotes).join(", ")} to #{local}")
conn.download(remotes, local)
end
debug("Download complete")
end
rescue Kitchen::Transport::TransportFailed => ex
raise ActionFailed, ex.message
ensure
instance.provisioner.cleanup_sandbox
end
# (see Base#setup)
def setup(state)
verifier = instance.verifier
instance.transport.connection(backcompat_merged_state(state)) do |conn|
conn.execute(env_cmd(verifier.install_command))
end
rescue Kitchen::Transport::TransportFailed => ex
raise ActionFailed, ex.message
end
# (see Base#verify)
def verify(state) # rubocop:disable Metrics/AbcSize
verifier = instance.verifier
verifier.create_sandbox
sandbox_dirs = Util.list_directory(verifier.sandbox_path)
instance.transport.connection(backcompat_merged_state(state)) do |conn|
conn.execute(env_cmd(verifier.init_command))
info("Transferring files to #{instance.to_str}")
conn.upload(sandbox_dirs, verifier[:root_path])
debug("Transfer complete")
conn.execute(env_cmd(verifier.prepare_command))
conn.execute(env_cmd(verifier.run_command))
end
rescue Kitchen::Transport::TransportFailed => ex
raise ActionFailed, ex.message
ensure
instance.verifier.cleanup_sandbox
end
# (see Base#destroy)
def destroy(state) # rubocop:disable Lint/UnusedMethodArgument
raise ClientError, "#{self.class}#destroy must be implemented"
end
def legacy_state(state)
backcompat_merged_state(state)
end
# Package an instance.
#
# (see Base#package)
def package(state); end
# (see Base#login_command)
def login_command(state)
instance.transport.connection(backcompat_merged_state(state))
.login_command
end
# Executes an arbitrary command on an instance over an SSH connection.
#
# @param state [Hash] mutable instance and driver state
# @param command [String] the command to be executed
# @raise [ActionFailed] if the command could not be successfully completed
def remote_command(state, command)
instance.transport.connection(backcompat_merged_state(state)) do |conn|
conn.execute(env_cmd(command))
end
end
# **(Deprecated)** Executes a remote command over SSH.
#
# @param ssh_args [Array] ssh arguments
# @param command [String] remote command to invoke
# @deprecated This method should no longer be called directly and exists
# to support very old drivers. This will be removed in the future.
def ssh(ssh_args, command)
pseudo_state = { hostname: ssh_args[0], username: ssh_args[1] }
pseudo_state.merge!(ssh_args[2])
connection_state = backcompat_merged_state(pseudo_state)
instance.transport.connection(connection_state) do |conn|
conn.execute(env_cmd(command))
end
end
# Performs whatever tests that may be required to ensure that this driver
# will be able to function in the current environment. This may involve
# checking for the presence of certain directories, software installed,
# etc.
#
# @raise [UserError] if the driver will not be able to perform or if a
# documented dependency is missing from the system
def verify_dependencies; end
# Cache directory that a driver could implement to inform the provisioner
# that it can leverage it internally
#
# @return path [String] a path of the cache directory
def cache_directory; end
private
def backcompat_merged_state(state)
driver_ssh_keys = %w{
forward_agent hostname password port ssh_key username
}.map(&:to_sym)
config.select { |key, _| driver_ssh_keys.include?(key) }.rmerge(state)
end
# Builds arguments for constructing a `Kitchen::SSH` instance.
#
# @param state [Hash] state hash
# @return [Array] SSH constructor arguments
# @api private
def build_ssh_args(state)
combined = config.to_hash.merge(state)
opts = {}
opts[:user_known_hosts_file] = "/dev/null"
opts[:verify_host_key] = false
opts[:keys_only] = true if combined[:ssh_key]
opts[:password] = combined[:password] if combined[:password]
opts[:forward_agent] = combined[:forward_agent] if combined.key? :forward_agent
opts[:port] = combined[:port] if combined[:port]
opts[:keys] = Array(combined[:ssh_key]) if combined[:ssh_key]
opts[:logger] = logger
[combined[:hostname], combined[:username], opts]
end
# Adds http, https and ftp proxy environment variables to a command, if
# set in configuration data or on local workstation.
#
# @param cmd [String] command string
# @return [String] command string
# @api private
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize
def env_cmd(cmd)
return if cmd.nil?
env = "env"
http_proxy = config[:http_proxy] || ENV["http_proxy"] ||
ENV["HTTP_PROXY"]
https_proxy = config[:https_proxy] || ENV["https_proxy"] ||
ENV["HTTPS_PROXY"]
ftp_proxy = config[:ftp_proxy] || ENV["ftp_proxy"] ||
ENV["FTP_PROXY"]
no_proxy = if (!config[:http_proxy] && http_proxy) ||
(!config[:https_proxy] && https_proxy) ||
(!config[:ftp_proxy] && ftp_proxy)
ENV["no_proxy"] || ENV["NO_PROXY"]
end
env << " http_proxy=#{http_proxy}" if http_proxy
env << " https_proxy=#{https_proxy}" if https_proxy
env << " ftp_proxy=#{ftp_proxy}" if ftp_proxy
env << " no_proxy=#{no_proxy}" if no_proxy
env == "env" ? cmd : "#{env} #{cmd}"
end
# Executes a remote command over SSH.
#
# @param command [String] remove command to run
# @param connection [Kitchen::SSH] an SSH connection
# @raise [ActionFailed] if an exception occurs
# @api private
def run_remote(command, connection)
return if command.nil?
connection.exec(env_cmd(command))
rescue SSHFailed, Net::SSH::Exception => ex
raise ActionFailed, ex.message
end
# Transfers one or more local paths over SSH.
#
# @param locals [Array<String>] array of local paths
# @param remote [String] remote destination path
# @param connection [Kitchen::SSH] an SSH connection
# @raise [ActionFailed] if an exception occurs
# @api private
def transfer_path(locals, remote, connection)
return if locals.nil? || Array(locals).empty?
info("Transferring files to #{instance.to_str}")
debug("TIMING: scp asynch upload (Kitchen::Driver::SSHBase)")
elapsed = Benchmark.measure do
transfer_path_async(locals, remote, connection)
end
delta = Util.duration(elapsed.real)
debug("TIMING: scp async upload (Kitchen::Driver::SSHBase) took #{delta}")
debug("Transfer complete")
rescue SSHFailed, Net::SSH::Exception => ex
raise ActionFailed, ex.message
end
def transfer_path_async(locals, remote, connection)
waits = []
locals.map do |local|
waits.push connection.upload_path(local, remote)
waits.shift.wait while waits.length >= config[:max_ssh_sessions]
end
waits.each(&:wait)
end
# Blocks until a TCP socket is available where a remote SSH server
# should be listening.
#
# @param hostname [String] remote SSH server host
# @param username [String] SSH username (default: `nil`)
# @param options [Hash] configuration hash (default: `{}`)
# @api private
def wait_for_sshd(hostname, username = nil, options = {})
pseudo_state = { hostname: }
pseudo_state[:username] = username if username
pseudo_state.merge!(options)
instance.transport.connection(backcompat_merged_state(pseudo_state))
.wait_until_ready
end
# Intercepts any bare #puts calls in subclasses and issues an INFO log
# event instead.
#
# @param msg [String] message string
def puts(msg)
info(msg)
end
# Intercepts any bare #print calls in subclasses and issues an INFO log
# event instead.
#
# @param msg [String] message string
def print(msg)
info(msg)
end
# Delegates to Kitchen::ShellOut.run_command, overriding some default
# options:
#
# * `:use_sudo` defaults to the value of `config[:use_sudo]` in the
# Driver object
# * `:log_subject` defaults to a String representation of the Driver's
# class name
#
# @see ShellOut#run_command
def run_command(cmd, options = {})
base_options = {
use_sudo: config[:use_sudo],
log_subject: Thor::Util.snake_case(self.class.to_s),
}.merge(options)
super(cmd, base_options)
end
# Returns the Busser object associated with the driver.
#
# @return [Busser] a busser
def busser
instance.verifier
end
end
end
end