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 +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
|
+
|