net-ssh-simple 1.5.0 → 1.5.1
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.
- data/README.rdoc +7 -0
- data/lib/net/ssh/simple.rb +682 -1
- data/lib/net/ssh/simple/version.rb +1 -1
- metadata +17 -18
- data/lib/net/ssh/simple/core.rb +0 -701
data/README.rdoc
CHANGED
@@ -2,6 +2,13 @@
|
|
2
2
|
|
3
3
|
Net::SSH::Simple is a simple wrapper around Net::SSH and Net::SCP.
|
4
4
|
|
5
|
+
It greatly reduces the amount of boilerplate code that you need
|
6
|
+
to write for handling SSH-connections and thereby prevents many common
|
7
|
+
mistakes related to error-handling, threading, timeouts and keep-alive.
|
8
|
+
|
9
|
+
It also makes it very easy to talk to many hosts in parallel
|
10
|
+
synchronously or asynchronously.
|
11
|
+
|
5
12
|
== Features
|
6
13
|
|
7
14
|
* Friendly, flexible API for SSH and SCP (synchronous and asynchronous)
|
data/lib/net/ssh/simple.rb
CHANGED
@@ -19,10 +19,691 @@
|
|
19
19
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
20
|
# THE SOFTWARE.
|
21
21
|
#
|
22
|
+
|
22
23
|
require 'net/ssh/simple/version'
|
23
24
|
require 'blockenspiel'
|
24
25
|
require 'hashie'
|
25
26
|
require 'timeout'
|
26
27
|
require 'net/ssh'
|
27
28
|
require 'net/scp'
|
28
|
-
|
29
|
+
|
30
|
+
module Net
|
31
|
+
module SSH
|
32
|
+
# Net::SSH::Simple is a simple wrapper around Net::SSH and Net::SCP.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# # Block Syntax (synchronous)
|
36
|
+
# Net::SSH::Simple.sync do
|
37
|
+
# r = ssh 'example1.com', 'echo "Hello World."'
|
38
|
+
# puts r.stdout #=> "Hello World."
|
39
|
+
# puts r.exit_code #=> 0
|
40
|
+
#
|
41
|
+
# scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
|
42
|
+
# scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# # Block Syntax (asynchronous)
|
47
|
+
# t1 = Net::SSH::Simple.async do
|
48
|
+
# scp_ul 'example1.com', '/tmp/local_foo', '/tmp/remote_bar'
|
49
|
+
# ssh 'example3.com', 'echo "Hello World A."'
|
50
|
+
# end
|
51
|
+
# t2 = Net::SSH::Simple.async do
|
52
|
+
# scp_dl 'example6.com', '/tmp/remote_foo', '/tmp/local_bar'
|
53
|
+
# ssh 'example7.com', 'echo "Hello World B."'
|
54
|
+
# end
|
55
|
+
# r1 = t1.value # wait for t1 to finish and grab return value
|
56
|
+
# r2 = t2.value # wait for t2 to finish and grab return value
|
57
|
+
#
|
58
|
+
# puts r1.stdout #=> "Hello World A."
|
59
|
+
# puts r2.stdout #=> "Hello World B."
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# # Using an instance
|
63
|
+
# s = Net::SSH::Simple.new
|
64
|
+
# s.ssh 'example1.com', 'echo "Hello World."'
|
65
|
+
# s.scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
|
66
|
+
# s.scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
|
67
|
+
# s.close
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# # Using no instance
|
71
|
+
# # Note: This will create a new connection for each operation!
|
72
|
+
# # Use instance- or block-syntax for better performance.
|
73
|
+
# Net::SSH::Simple.ssh 'example1.com', 'echo "Hello World."'
|
74
|
+
# Net::SSH::Simple.scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
|
75
|
+
# Net::SSH::Simple.scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# # Error Handling with Block Syntax (synchronous)
|
79
|
+
# begin
|
80
|
+
# Net::SSH::Simple.sync do
|
81
|
+
# r = ssh 'example1.com', 'echo "Hello World."'
|
82
|
+
# if r.success and r.stdout == 'Hello World.'
|
83
|
+
# puts "Success! I Helloed World."
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# r = scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
|
87
|
+
# if r.success and r.sent == r.total
|
88
|
+
# puts "Success! Uploaded #{r.sent} of #{r.total} bytes."
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# r = scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
|
92
|
+
# if r.success and r.sent == r.total
|
93
|
+
# puts "Success! Downloaded #{r.sent} of #{r.total} bytes."
|
94
|
+
# end
|
95
|
+
# end
|
96
|
+
# rescue Net::SSH::Simple::Error => e
|
97
|
+
# puts "Something bad happened!"
|
98
|
+
# puts e # Human readable error
|
99
|
+
# puts e.wrapped # Original Exception
|
100
|
+
# puts e.result # Net::SSH::Simple::Result
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# @example
|
104
|
+
# # Error Handling with Block Syntax (asynchronous)
|
105
|
+
# #
|
106
|
+
# # Exceptions are raised inside your thread.
|
107
|
+
# # You are free to handle them or pass them outwards.
|
108
|
+
#
|
109
|
+
# a = Net::SSH::Simple.async do
|
110
|
+
# begin
|
111
|
+
# ssh 'example1.com', 'echo "Hello World."'
|
112
|
+
# scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
|
113
|
+
# scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
|
114
|
+
# rescue Net::SSH::Simple::Error => e
|
115
|
+
# # return our exception to the parent thread
|
116
|
+
# e
|
117
|
+
# end
|
118
|
+
# end
|
119
|
+
# r = a.value # Wait for thread to finish and capture result
|
120
|
+
#
|
121
|
+
# unless r.is_a? Net::SSH::Simple::Result
|
122
|
+
# puts "Something bad happened!"
|
123
|
+
# puts r
|
124
|
+
# end
|
125
|
+
#
|
126
|
+
# @example
|
127
|
+
# # Error Handling with an instance
|
128
|
+
# s = Net::SSH::Simple.new
|
129
|
+
# begin
|
130
|
+
# r = s.ssh 'example1.com', 'echo "Hello World."'
|
131
|
+
# if r.success and r.stdout == 'Hello World.'
|
132
|
+
# puts "Success! I Helloed World."
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# r = s.scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
|
136
|
+
# if r.success and r.sent == r.total
|
137
|
+
# puts "Success! Uploaded #{r.sent} of #{r.total} bytes."
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# r = s.scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
|
141
|
+
# if r.success and r.sent == r.total
|
142
|
+
# puts "Success! Downloaded #{r.sent} of #{r.total} bytes."
|
143
|
+
# end
|
144
|
+
# rescue Net::SSH::Simple::Error => e
|
145
|
+
# puts "Something bad happened!"
|
146
|
+
# puts e # Human readable error
|
147
|
+
# puts e.wrapped # Original Exception
|
148
|
+
# puts e.result # Net::SSH::Simple::Result (partial result)
|
149
|
+
# ensure
|
150
|
+
# s.close # don't forget the clean up!
|
151
|
+
# end
|
152
|
+
#
|
153
|
+
# @example
|
154
|
+
# # Parameters
|
155
|
+
# Net::SSH::Simple.sync do
|
156
|
+
# ssh('example1.com', 'echo "Hello World."',
|
157
|
+
# {:user => 'tom', :password => 'jerry', :port => 1234})
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# # Parameter inheritance
|
161
|
+
# Net::SSH::Simple.sync({:user => 'tom', :port => 1234}) do
|
162
|
+
# # Both commands will inherit :user and :port
|
163
|
+
# ssh('example1.com', 'echo "Hello World."', {:password => 'jerry'})
|
164
|
+
# scp_ul('example2.com', '/tmp/a', '/tmp/a', {:password => 's3cr3t'})
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# @example
|
168
|
+
# # Using the SCP progress callback
|
169
|
+
# Net::SSH::Simple.sync do
|
170
|
+
# scp_ul 'example1.com', '/tmp/local_foo', '/tmp/remote_bar' do |sent, total|
|
171
|
+
# puts "Bytes uploaded: #{sent} of #{total}"
|
172
|
+
# end
|
173
|
+
# end
|
174
|
+
#
|
175
|
+
# @example
|
176
|
+
# #
|
177
|
+
# # Here be dragons: Using the event-API for a stdin->stdout pipeline
|
178
|
+
# #
|
179
|
+
# r = Net::SSH::Simple.sync do
|
180
|
+
# # open a shell
|
181
|
+
# ssh('localhost', '/bin/sh') do |e,c,d|
|
182
|
+
# # e = :start, :stdout, :stderr, :exit_code, :exit_signal or :finish
|
183
|
+
# # c = our Net::SSH::Connection::Channel instance
|
184
|
+
# # d = data for this event
|
185
|
+
# case e
|
186
|
+
# # :start is triggered exactly once per connection
|
187
|
+
# when :start
|
188
|
+
# # we can send data using Channel#send_data
|
189
|
+
# c.send_data("echo 'hello stdout'\n")
|
190
|
+
# c.send_data("echo 'hello stderr' 1>&2\n")
|
191
|
+
# # don't forget to eof when done feeding!
|
192
|
+
# c.eof!
|
193
|
+
#
|
194
|
+
# # :stdout is triggered when there's stdout data from remote.
|
195
|
+
# # by default the data is also appended to result[:stdout].
|
196
|
+
# # you may return :no_append as seen below to avoid that.
|
197
|
+
# when :stdout
|
198
|
+
# # read the input line-wise (it *will* arrive fragmented!)
|
199
|
+
# (@buf ||= '') << d
|
200
|
+
# while line = @buf.slice!(/(.*)\r?\n/)
|
201
|
+
# puts line #=> "hello stdout"
|
202
|
+
# end
|
203
|
+
# :no_append
|
204
|
+
#
|
205
|
+
# # :stderr is triggered when there's stderr data from remote.
|
206
|
+
# # by default the data is also appended to result[:stderr].
|
207
|
+
# # you may return :no_append as seen below to avoid that.
|
208
|
+
# when :stderr
|
209
|
+
# # read the input line-wise (it *will* arrive fragmented!)
|
210
|
+
# (@buf ||= '') << d
|
211
|
+
# while line = @buf.slice!(/(.*)\r?\n/)
|
212
|
+
# puts line #=> "hello stderr"
|
213
|
+
# end
|
214
|
+
# :no_append
|
215
|
+
#
|
216
|
+
# # :exit_code is triggered when the remote process exits normally.
|
217
|
+
# # it does *not* trigger when the remote process exits by signal!
|
218
|
+
# when :exit_code
|
219
|
+
# puts d #=> 0
|
220
|
+
#
|
221
|
+
# # :exit_signal is triggered when the remote is killed by signal.
|
222
|
+
# # this would normally raise a Net::SSH::Simple::Error but
|
223
|
+
# # we suppress that here by returning :no_raise
|
224
|
+
# when :exit_signal
|
225
|
+
# puts d # won't fire in this example, could be "TERM"
|
226
|
+
# :no_raise
|
227
|
+
#
|
228
|
+
# # :finish triggers after :exit_code when the command exits normally.
|
229
|
+
# # it does *not* trigger when the remote process exits by signal!
|
230
|
+
# when :finish
|
231
|
+
# puts "we are finished!"
|
232
|
+
# end
|
233
|
+
# end
|
234
|
+
# end
|
235
|
+
#
|
236
|
+
# # Our Result has been populated normally, except for
|
237
|
+
# # :stdout and :stdin (because we used :no_append).
|
238
|
+
# puts r #=> Net::SSH::Simple::Result
|
239
|
+
# puts r.exit_code #=> 0
|
240
|
+
# puts r.stdout #=> ''
|
241
|
+
# puts r.stderr #=> ''
|
242
|
+
#
|
243
|
+
#
|
244
|
+
# @author moe@busyloop.net
|
245
|
+
#
|
246
|
+
class Simple
|
247
|
+
include Blockenspiel::DSL
|
248
|
+
|
249
|
+
#
|
250
|
+
# Result of the current Net::SSH::Simple::Operation.
|
251
|
+
#
|
252
|
+
# @return [Net::SSH::Simple::Result] Result of the current operation
|
253
|
+
attr_reader :result
|
254
|
+
|
255
|
+
#
|
256
|
+
# Perform ssh command on a remote host and capture the result.
|
257
|
+
# This will create a new connection for each invocation.
|
258
|
+
#
|
259
|
+
# @example
|
260
|
+
# Net::SSH::Simple.ssh('localhost', 'echo Hello').class #=> Net::SSH::Simple::Result
|
261
|
+
#
|
262
|
+
# @example
|
263
|
+
# Net::SSH::Simple.ssh('localhost', 'echo Hello').stdout #=> "Hello"
|
264
|
+
#
|
265
|
+
# @param (see Net::SSH::Simple#ssh)
|
266
|
+
# @raise [Net::SSH::Simple::Error]
|
267
|
+
# @return [Net::SSH::Simple::Result] Result
|
268
|
+
def self.ssh(*args, &block)
|
269
|
+
s = self.new
|
270
|
+
r = s.ssh(*args, &block)
|
271
|
+
s.close
|
272
|
+
r
|
273
|
+
end
|
274
|
+
|
275
|
+
#
|
276
|
+
# SCP upload to a remote host.
|
277
|
+
# This will create a new connection for each invocation.
|
278
|
+
#
|
279
|
+
# @example
|
280
|
+
# # SCP Upload
|
281
|
+
# Net::SSH::Simple.scp_ul('localhost', '/tmp/local_foo', '/tmp/remote_bar')
|
282
|
+
#
|
283
|
+
# @example
|
284
|
+
# # Pass a block to monitor progress
|
285
|
+
# Net::SSH::Simple.scp_ul('localhost', '/tmp/local_foo', '/tmp/remote_bar') do |sent, total|
|
286
|
+
# puts "Bytes uploaded: #{sent} of #{total}"
|
287
|
+
# end
|
288
|
+
#
|
289
|
+
# @param (see Net::SSH::Simple#scp_ul)
|
290
|
+
# @raise [Net::SSH::Simple::Error]
|
291
|
+
# @return [Net::SSH::Simple::Result] Result
|
292
|
+
def self.scp_ul(*args, &block)
|
293
|
+
s = self.new
|
294
|
+
r = s.scp_ul(*args, &block)
|
295
|
+
s.close
|
296
|
+
r
|
297
|
+
end
|
298
|
+
|
299
|
+
#
|
300
|
+
# SCP download from a remote host.
|
301
|
+
# This will create a new connection for each invocation.
|
302
|
+
#
|
303
|
+
# @example
|
304
|
+
# # SCP Download
|
305
|
+
# Net::SSH::Simple.scp_dl('localhost', '/tmp/remote_foo', '/tmp/local_bar')
|
306
|
+
#
|
307
|
+
# @example
|
308
|
+
# # Pass a block to monitor progress
|
309
|
+
# Net::SSH::Simple.scp_dl('localhost', '/tmp/remote_foo', '/tmp/local_bar') do |sent, total|
|
310
|
+
# puts "Bytes downloaded: #{sent} of #{total}"
|
311
|
+
# end
|
312
|
+
#
|
313
|
+
# @param (see Net::SSH::Simple#scp_dl)
|
314
|
+
# @raise [Net::SSH::Simple::Error]
|
315
|
+
# @return [Net::SSH::Simple::Result] Result
|
316
|
+
#
|
317
|
+
def self.scp_dl(*args, &block)
|
318
|
+
s = self.new
|
319
|
+
r = s.scp_dl(*args, &block)
|
320
|
+
s.close
|
321
|
+
r
|
322
|
+
end
|
323
|
+
|
324
|
+
#
|
325
|
+
# SCP upload to a remote host.
|
326
|
+
# The underlying Net::SSH::Simple instance will re-use
|
327
|
+
# existing connections for optimal performance.
|
328
|
+
#
|
329
|
+
# @param [String] host Destination hostname or ip-address
|
330
|
+
# @param [String] cmd Shell command to execute
|
331
|
+
# @param opts (see Net::SSH::Simple#ssh)
|
332
|
+
# @param [Block] &block Progress callback (optional)
|
333
|
+
# @return [Net::SSH::Simple::Result] Result
|
334
|
+
#
|
335
|
+
def scp_ul(host, src, dst, opts={}, &block)
|
336
|
+
opts = @opts.merge(opts)
|
337
|
+
scp(:upload, host, src, dst, opts, &block)
|
338
|
+
end
|
339
|
+
|
340
|
+
#
|
341
|
+
# SCP download from a remote host.
|
342
|
+
# The underlying Net::SSH::Simple instance will re-use
|
343
|
+
# existing connections for optimal performance.
|
344
|
+
#
|
345
|
+
# @param [String] host Destination hostname or ip-address
|
346
|
+
# @param [String] cmd Shell command to execute
|
347
|
+
# @param opts (see Net::SSH::Simple#ssh)
|
348
|
+
# @param [Block] &block Progress callback (optional)
|
349
|
+
# @return [Net::SSH::Simple::Result] Result
|
350
|
+
# @see Net::SSH::Simple#scp_ul
|
351
|
+
#
|
352
|
+
def scp_dl(host, src, dst, opts={}, &block)
|
353
|
+
opts = @opts.merge(opts)
|
354
|
+
scp(:download, host, src, dst, opts, &block)
|
355
|
+
end
|
356
|
+
|
357
|
+
#
|
358
|
+
# Perform SSH operation on a remote host and capture the result.
|
359
|
+
# The underlying Net::SSH::Simple instance will re-use
|
360
|
+
# existing connections for optimal performance.
|
361
|
+
#
|
362
|
+
# @return [Net::SSH::Simple::Result] Result
|
363
|
+
# @param [String] host Destination hostname or ip-address
|
364
|
+
# @param [String] cmd Shell command to execute
|
365
|
+
# @param [Block] &block Use the event-API (see example above)
|
366
|
+
# @param [Hash] opts SSH options
|
367
|
+
# @option opts [Array] :auth_methods
|
368
|
+
# an array of authentication methods to try
|
369
|
+
#
|
370
|
+
# @option opts [String] :compression
|
371
|
+
# the compression algorithm to use,
|
372
|
+
# or true to use whatever is supported.
|
373
|
+
#
|
374
|
+
# @option opts [Number] :compression_level
|
375
|
+
# the compression level to use when sending data
|
376
|
+
#
|
377
|
+
# @option opts [String/boolean] :opts (true)
|
378
|
+
# set to true to load the default OpenSSH opts files
|
379
|
+
# (~/.ssh/opts, /etc/ssh_opts), or to false to not load them,
|
380
|
+
# or to a file-name (or array of file-names) to load those
|
381
|
+
# specific configuration files.
|
382
|
+
#
|
383
|
+
# @option opts [Array] :encryption
|
384
|
+
# the encryption cipher (or ciphers) to use
|
385
|
+
#
|
386
|
+
# @option opts [boolean] :forward_agent
|
387
|
+
# set to true if you want the SSH agent connection to be forwarded
|
388
|
+
#
|
389
|
+
# @option opts [String/Array] :global_known_hosts_file
|
390
|
+
# (['/etc/ssh/known_hosts','/etc/ssh/known_hosts2'])
|
391
|
+
# the location of the global known hosts file.
|
392
|
+
# Set to an array if you want to specify multiple
|
393
|
+
# global known hosts files.
|
394
|
+
#
|
395
|
+
# @option opts [String/Array] :hmac
|
396
|
+
# the hmac algorithm (or algorithms) to use
|
397
|
+
#
|
398
|
+
# @option opts [String] :host_key
|
399
|
+
# the host key algorithm (or algorithms) to use
|
400
|
+
#
|
401
|
+
# @option opts [String] :host_key_alias
|
402
|
+
# the host name to use when looking up or adding a host to a known_hosts dictionary file
|
403
|
+
#
|
404
|
+
# @option opts [String] :host_name
|
405
|
+
# the real host name or IP to log into. This is used instead of the host parameter,
|
406
|
+
# and is primarily only useful when specified in an SSH configuration file.
|
407
|
+
# It lets you specify an alias, similarly to adding an entry in /etc/hosts but
|
408
|
+
# without needing to modify /etc/hosts.
|
409
|
+
#
|
410
|
+
# @option opts [String/Array] :kex
|
411
|
+
# the key exchange algorithm (or algorithms) to use
|
412
|
+
#
|
413
|
+
# @option opts [Array] :keys
|
414
|
+
# an array of file names of private keys to use for publickey and hostbased authentication
|
415
|
+
#
|
416
|
+
# @option opts [Array] :key_data
|
417
|
+
# an array of strings, with each element of the array being a raw private key in PEM format.
|
418
|
+
#
|
419
|
+
# @option opts [boolean] :keys_only
|
420
|
+
# set to true to use only private keys from keys and key_data parameters, even if
|
421
|
+
# ssh-agent offers more identities. This option is intended for situations where
|
422
|
+
# ssh-agent offers many different identites.
|
423
|
+
#
|
424
|
+
# @option opts [Logger] :logger
|
425
|
+
# the logger instance to use when logging
|
426
|
+
#
|
427
|
+
# @option opts [boolean/:very] :paranoid
|
428
|
+
# either true, false, or :very, specifying how strict host-key verification should be
|
429
|
+
#
|
430
|
+
# @option opts [String] :passphrase (nil)
|
431
|
+
# the passphrase to use when loading a private key (default is nil, for no passphrase)
|
432
|
+
#
|
433
|
+
# @option opts [String] :password
|
434
|
+
# the password to use to login
|
435
|
+
#
|
436
|
+
# @option opts [Integer] :port
|
437
|
+
# the port to use when connecting to the remote host
|
438
|
+
#
|
439
|
+
# @option opts [Hash] :properties
|
440
|
+
# a hash of key/value pairs to add to the new connection's properties
|
441
|
+
# (see Net::SSH::Connection::Session#properties)
|
442
|
+
#
|
443
|
+
# @option opts [String] :proxy
|
444
|
+
# a proxy instance (see Proxy) to use when connecting
|
445
|
+
#
|
446
|
+
# @option opts [Integer] :rekey_blocks_limit
|
447
|
+
# the max number of blocks to process before rekeying
|
448
|
+
#
|
449
|
+
# @option opts [Integer] :rekey_limit
|
450
|
+
# the max number of bytes to process before rekeying
|
451
|
+
#
|
452
|
+
# @option opts [Integer] :rekey_packet_limit
|
453
|
+
# the max number of packets to process before rekeying
|
454
|
+
#
|
455
|
+
# @option opts [Integer] :timeout (60)
|
456
|
+
# maximum idle time before a connection will time out (0 = disable).
|
457
|
+
#
|
458
|
+
# @option opts [Integer] :operation_timeout (3600)
|
459
|
+
# maximum time before aborting an operation (0 = disable).
|
460
|
+
# you may use this to guard against run-away processes.
|
461
|
+
#
|
462
|
+
# @option opts [Integer] :keepalive_interval (60)
|
463
|
+
# send keep-alive probes at this interval to prevent connections
|
464
|
+
# from timing out unexpectedly.
|
465
|
+
#
|
466
|
+
# @option opts [Integer] :close_timeout (5)
|
467
|
+
# grace-period on close before the connection will be terminated forcefully
|
468
|
+
# (0 = terminate immediately).
|
469
|
+
#
|
470
|
+
# @option opts [String] :user
|
471
|
+
# the username to log in as
|
472
|
+
#
|
473
|
+
# @option opts [String/Array] :user_known_hosts_file
|
474
|
+
# (['~/.ssh/known_hosts, ~/.ssh/known_hosts2'])
|
475
|
+
# the location of the user known hosts file. Set to an array to specify multiple
|
476
|
+
# user known hosts files.
|
477
|
+
#
|
478
|
+
# @option opts [Symbol] :verbose
|
479
|
+
# how verbose to be (Logger verbosity constants, Logger::DEBUG is very verbose,
|
480
|
+
# Logger::FATAL is all but silent). Logger::FATAL is the default. The symbols
|
481
|
+
# :debug, :info, :warn, :error, and :fatal are also supported and are translated
|
482
|
+
# to the corresponding Logger constant.
|
483
|
+
#
|
484
|
+
# @see http://net-ssh.github.com/ssh/v2/api/classes/Net/SSH.html#M000002
|
485
|
+
# Net::SSH documentation for the 'opts'-hash
|
486
|
+
def ssh(host, cmd, opts={}, &block)
|
487
|
+
opts = @opts.merge(opts)
|
488
|
+
with_session(host, opts) do |session|
|
489
|
+
@result = Result.new(
|
490
|
+
{ :op => :ssh, :host => host, :cmd => cmd, :start_at => Time.new,
|
491
|
+
:last_event_at => Time.new, :opts => opts, :stdout => '', :stderr => '',
|
492
|
+
:success => nil
|
493
|
+
} )
|
494
|
+
|
495
|
+
channel = session.open_channel do |chan|
|
496
|
+
chan.exec cmd do |ch, success|
|
497
|
+
@result[:success] = success
|
498
|
+
ch.on_data do |c, data|
|
499
|
+
@result[:last_event_at] = Time.new
|
500
|
+
r = block.call(:stdout, ch, data) if block
|
501
|
+
@result[:stdout] += data.to_s unless r == :no_append
|
502
|
+
end
|
503
|
+
ch.on_extended_data do |c, type, data|
|
504
|
+
@result[:last_event_at] = Time.new
|
505
|
+
r = block.call(:stderr, ch, data) if block
|
506
|
+
@result[:stderr] += data.to_s unless r == :no_append
|
507
|
+
end
|
508
|
+
ch.on_request('exit-status') do |c, data|
|
509
|
+
@result[:last_event_at] = Time.new
|
510
|
+
exit_code = data.read_long
|
511
|
+
block.call(:exit_code, ch, exit_code) if block
|
512
|
+
@result[:exit_code] = exit_code
|
513
|
+
end
|
514
|
+
ch.on_request('exit-signal') do |c, data|
|
515
|
+
@result[:last_event_at] = Time.new
|
516
|
+
exit_signal = data.read_string
|
517
|
+
r = block.call(:exit_signal, ch, exit_signal) if block
|
518
|
+
@result[:exit_signal] = exit_signal
|
519
|
+
@result[:success] = false
|
520
|
+
unless r == :no_raise
|
521
|
+
raise "Killed by SIG#{@result[:exit_signal]}"
|
522
|
+
end
|
523
|
+
end
|
524
|
+
block.call(:start, ch, nil) if block
|
525
|
+
end
|
526
|
+
end
|
527
|
+
wait_for_channel session, channel, @result, opts
|
528
|
+
@result[:finish_at] = Time.new
|
529
|
+
block.call(:finish, channel, nil) if block
|
530
|
+
@result
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
dsl_methods false
|
535
|
+
|
536
|
+
def initialize(opts={})
|
537
|
+
@opts = opts
|
538
|
+
Thread.current[:ssh_simple_sessions] = {}
|
539
|
+
@result = Result.new
|
540
|
+
end
|
541
|
+
|
542
|
+
#
|
543
|
+
# Spawn a Thread to perform a sequence of ssh/scp operations.
|
544
|
+
#
|
545
|
+
# @param [Block] block
|
546
|
+
# @param opts (see Net::SSH::Simple#ssh)
|
547
|
+
# @return [Thread] Thread executing the SSH-Block.
|
548
|
+
#
|
549
|
+
def self.async(opts={}, &block)
|
550
|
+
Thread.new do
|
551
|
+
self.sync(opts, &block)
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
#
|
556
|
+
# Spawn a Thread to perform a sequence of ssh/scp operations.
|
557
|
+
#
|
558
|
+
# @param [Block] block
|
559
|
+
# @param opts (see Net::SSH::Simple#ssh)
|
560
|
+
# @return [Thread] Thread executing the SSH-Block.
|
561
|
+
#
|
562
|
+
def async(opts={}, &block)
|
563
|
+
self.class.async(opts, &block)
|
564
|
+
end
|
565
|
+
|
566
|
+
#
|
567
|
+
# Perform a sequence of ssh/scp operations.
|
568
|
+
#
|
569
|
+
# @param opts (see Net::SSH::Simple#ssh)
|
570
|
+
# @return [Net::SSH::Simple::Result] Result
|
571
|
+
#
|
572
|
+
def self.sync(opts={}, &block)
|
573
|
+
s = self.new(opts)
|
574
|
+
r = Blockenspiel.invoke(block, s)
|
575
|
+
s.close
|
576
|
+
r
|
577
|
+
end
|
578
|
+
|
579
|
+
#
|
580
|
+
# Close and cleanup.
|
581
|
+
#
|
582
|
+
# @return [Net::SSH::Simple::Result] Result
|
583
|
+
#
|
584
|
+
def close
|
585
|
+
Thread.current[:ssh_simple_sessions].values.each do |session|
|
586
|
+
begin
|
587
|
+
Timeout.timeout(@opts[:close_timeout] || 5) { session.close }
|
588
|
+
rescue => e
|
589
|
+
begin
|
590
|
+
session.shutdown!
|
591
|
+
rescue
|
592
|
+
end
|
593
|
+
end
|
594
|
+
end
|
595
|
+
@result
|
596
|
+
end
|
597
|
+
|
598
|
+
|
599
|
+
private
|
600
|
+
EXTRA_OPTS = [:operation_timeout, :close_timeout, :keepalive_interval]
|
601
|
+
|
602
|
+
def with_session(host, opts={}, &block)
|
603
|
+
opts[:timeout] ||= 60
|
604
|
+
opts[:timeout] = 2**32 if opts[:timeout] == 0
|
605
|
+
opts[:operation_timeout] ||= 3600
|
606
|
+
opts[:operation_timeout] = 2**32 if opts[:operation_timeout] == 0
|
607
|
+
opts[:close_timeout] ||= 5
|
608
|
+
opts[:keepalive_interval] ||= 60
|
609
|
+
begin
|
610
|
+
net_ssh_opts = opts.reject{|k,v| EXTRA_OPTS.include? k }
|
611
|
+
Timeout.timeout(opts[:operation_timeout]) do
|
612
|
+
session = Thread.current[:ssh_simple_sessions][host.hash] \
|
613
|
+
= Thread.current[:ssh_simple_sessions][host.hash] \
|
614
|
+
|| Net::SSH.start(*[host, opts[:user], net_ssh_opts])
|
615
|
+
block.call(session)
|
616
|
+
end
|
617
|
+
rescue => e
|
618
|
+
opts[:password].gsub!(/./,'*') if opts.include? :password
|
619
|
+
@result[:exception] = e
|
620
|
+
@result[:success] = false
|
621
|
+
@result[:timed_out] = true if e.is_a? Timeout::Error
|
622
|
+
@result[:finish_at] = Time.new
|
623
|
+
raise Net::SSH::Simple::Error, [e, @result]
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
def wait_for_channel(session, channel, result, opts)
|
628
|
+
session.loop(1) do
|
629
|
+
if opts[:timeout] < Time.now - result[:last_event_at]
|
630
|
+
raise Timeout::Error, 'idle timeout'
|
631
|
+
end
|
632
|
+
|
633
|
+
# Send keep-alive probes at the configured interval.
|
634
|
+
if opts[:keepalive_interval] < Time.now.to_i - (@result[:last_keepalive_at]||0).to_i
|
635
|
+
session.send_global_request('keep-alive@openssh.com')
|
636
|
+
@result[:last_keepalive_at] = Time.now
|
637
|
+
end
|
638
|
+
channel.active?
|
639
|
+
end
|
640
|
+
end
|
641
|
+
|
642
|
+
def scp(mode, host, src, dst, opts={}, &block)
|
643
|
+
@result = Result.new(
|
644
|
+
{ :op => :scp, :host => host, :opts => opts, :cmd => :scp_dl,
|
645
|
+
:last_event_at => Time.new, :start_at => Time.new,
|
646
|
+
:src => src, :dst => dst, :success => false
|
647
|
+
} )
|
648
|
+
with_session(host, opts) do |session|
|
649
|
+
lt = 0
|
650
|
+
channel = session.scp.send(mode, src, dst) do |ch, name, sent, total|
|
651
|
+
@result[:name] ||= name
|
652
|
+
@result[:total] ||= total
|
653
|
+
@result[:sent] = sent
|
654
|
+
@result[:last_event_at] = Time.new
|
655
|
+
block.call(sent, total) unless block.nil?
|
656
|
+
end
|
657
|
+
wait_for_channel session, channel, @result, opts
|
658
|
+
@result[:finish_at] = Time.new
|
659
|
+
@result[:success] = @result[:sent] == @result[:total]
|
660
|
+
@result
|
661
|
+
end
|
662
|
+
end
|
663
|
+
|
664
|
+
#
|
665
|
+
# Error that occured during a Net::SSH::Simple operation.
|
666
|
+
#
|
667
|
+
class Error < RuntimeError
|
668
|
+
# Reference to the underlying Exception
|
669
|
+
attr_reader :wrapped
|
670
|
+
|
671
|
+
# {Net::SSH::Simple::Result} of the interrupted operation.
|
672
|
+
attr_reader :result
|
673
|
+
|
674
|
+
def initialize(msg, e=$!)
|
675
|
+
super(msg)
|
676
|
+
@wrapped = e
|
677
|
+
@result = msg[1]
|
678
|
+
end
|
679
|
+
|
680
|
+
def to_s
|
681
|
+
"#{super[0]} @ #{super[1]}"
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
#
|
686
|
+
# Result of a Net::SSH::Simple operation.
|
687
|
+
#
|
688
|
+
# @attr [String] host Hostname/IP address
|
689
|
+
# @attr [Symbol] op :ssh or :scp
|
690
|
+
# @attr [String] cmd Shell command (SSH only)
|
691
|
+
# @attr [Time] start_at Timestamp of operation start
|
692
|
+
# @attr [Time] finish_at Timestamp of operation finish
|
693
|
+
# @attr [Time] last_keepalive_at Timestamp of last keepalive (if any)
|
694
|
+
# @attr [Time] last_event_at Timestamp of last activity
|
695
|
+
# @attr [Boolean] timed_out True if the operation timed out
|
696
|
+
# @attr [String] stdout Output captured on stdout (SSH only)
|
697
|
+
# @attr [String] stderr Output captured on stderr (SSH only)
|
698
|
+
# @attr [boolean] success Indicates whether the transport-connection was successful
|
699
|
+
# @attr [String] exit_code UNIX exit code (SSH only)
|
700
|
+
# @attr [Integer] total Size of requested file (in bytes, SCP only)
|
701
|
+
# @attr [Integer] sent Number of bytes transferred (SCP only)
|
702
|
+
# @attr [String] exit_signal
|
703
|
+
# Only present if the remote command terminated due to a signal (SSH only)
|
704
|
+
#
|
705
|
+
class Result < Hashie::Mash; end
|
706
|
+
end
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|