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 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)
@@ -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
- require 'net/ssh/simple/core'
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
+