net-ssh-simple 1.5.0 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
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
+