net-ssh-simple 0.9.0

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.
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .yardoc
6
+ coverage.data
7
+ coverage
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
@@ -0,0 +1,44 @@
1
+ = Net::SSH::Simple
2
+
3
+ Net::SSH::Simple is a simple wrapper around Net::SSH and Net::SCP.
4
+
5
+ == Installation
6
+
7
+ gem install net-ssh-simple
8
+
9
+ == Examples
10
+
11
+ === Block Syntax (synchronous)
12
+ Net::SSH::Simple.sync do
13
+ r = ssh 'example1.com', 'echo "Hello World."'
14
+ puts r.stdout #=> "Hello World."
15
+
16
+ scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
17
+ scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
18
+ end
19
+
20
+ === Block Syntax (asynchronous)
21
+ a = Net::SSH::Simple.async do
22
+ ssh 'example1.com', 'echo "Hello World."'
23
+ scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
24
+ scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
25
+ end
26
+ b = Net::SSH::Simple.async do
27
+ ssh 'example4.com', 'echo "Hello World."'
28
+ scp_ul 'example5.com', '/tmp/local_foo', '/tmp/remote_bar'
29
+ scp_dl 'example6.com', '/tmp/remote_foo', '/tmp/local_bar'
30
+ end
31
+ a.value # Wait for thread A to finish and capture result
32
+ b.value # Wait for thread B to finish and capture result
33
+
34
+ === Using an instance
35
+ s = Net::SSH::Simple.new
36
+ s.ssh 'example1.com', 'echo "Hello World."'
37
+ s.scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
38
+ s.scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
39
+ s.close
40
+
41
+ == Documentation
42
+
43
+ See {Net::SSH::Simple}[http://rubydoc.info/gems/net-ssh-simple/Net/SSH/Simple] for more examples and full API.
44
+
@@ -0,0 +1,25 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'bundler/setup'
3
+ Bundler::GemHelper.install_tasks
4
+ require 'rspec/core/rake_task'
5
+
6
+ task :default => :test
7
+
8
+ RSpec::Core::RakeTask.new("test:spec") do |t|
9
+ t.pattern = 'spec/*.rb'
10
+ t.rcov = false
11
+ #t.rspec_opts = '-b -c -f progress'
12
+ t.rspec_opts = '-b -c -f documentation'
13
+ end
14
+
15
+ namespace :test do
16
+ task :coverage do
17
+ require 'cover_me'
18
+ CoverMe.complete!
19
+ end
20
+ end
21
+
22
+ desc 'Run full test suite'
23
+ task :test => [ 'test:spec', 'test:coverage' ]
24
+
25
+
@@ -0,0 +1,7 @@
1
+ require 'net/ssh/simple/version'
2
+ require 'blockenspiel'
3
+ require 'hashie'
4
+ require 'net/ssh'
5
+ require 'net/scp'
6
+ require 'net/ssh/simple/core'
7
+
@@ -0,0 +1,502 @@
1
+ module Net
2
+ module SSH
3
+ # Net::SSH::Simple is a simple wrapper around Net::SSH and Net::SCP.
4
+ #
5
+ # @example
6
+ # # Block Syntax (synchronous)
7
+ # Net::SSH::Simple.sync do
8
+ # ssh 'example1.com', 'echo "Hello World."'
9
+ # scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
10
+ # scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
11
+ # end
12
+ #
13
+ # @example
14
+ # # Block Syntax (asynchronous)
15
+ # a = Net::SSH::Simple.async do
16
+ # ssh 'example1.com', 'echo "Hello World."'
17
+ # scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
18
+ # scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
19
+ # end
20
+ # b = Net::SSH::Simple.async do
21
+ # ssh 'example4.com', 'echo "Hello World."'
22
+ # scp_ul 'example5.com', '/tmp/local_foo', '/tmp/remote_bar'
23
+ # scp_dl 'example6.com', '/tmp/remote_foo', '/tmp/local_bar'
24
+ # end
25
+ # a.value # Wait for thread A to finish and capture result
26
+ # b.value # Wait for thread B to finish and capture result
27
+ #
28
+ # @example
29
+ # # Using an instance
30
+ # s = Net::SSH::Simple.new
31
+ # s.ssh 'example1.com', 'echo "Hello World."'
32
+ # s.scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
33
+ # s.scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
34
+ # s.close
35
+ #
36
+ # @example
37
+ # # Using no instance
38
+ # # Note: This will create a new connection for each operation!
39
+ # # Use instance- or block-syntax for better performance.
40
+ # Net::SSH::Simple.ssh 'example1.com', 'echo "Hello World."'
41
+ # Net::SSH::Simple.scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
42
+ # Net::SSH::Simple.scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
43
+ #
44
+ # @example
45
+ # # Error Handling with Block Syntax (synchronous)
46
+ # begin
47
+ # Net::SSH::Simple.sync do
48
+ # r = ssh 'example1.com', 'echo "Hello World."'
49
+ # if r.success and r.stdout == 'Hello World.'
50
+ # puts "Success! I Helloed World."
51
+ # end
52
+ #
53
+ # scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
54
+ # if r.success and r.sent == r.total
55
+ # puts "Success! Uploaded #{r.sent} of #{r.total} bytes."
56
+ # end
57
+ #
58
+ # scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
59
+ # if r.success and r.sent == r.total
60
+ # puts "Success! Downloaded #{r.sent} of #{r.total} bytes."
61
+ # end
62
+ # end
63
+ # rescue Net::SSH::Simple::Error => e
64
+ # puts "Something bad happened!"
65
+ # puts e # Human readable error
66
+ # puts e.wrapped # Original Exception from Net::SSH
67
+ # puts e.context # Config that triggered the error
68
+ # end
69
+ #
70
+ # @example
71
+ # # Error Handling with Block Syntax (asynchronous)
72
+ # #
73
+ # # Exceptions are thrown inside your thread.
74
+ # # You are free to handle them or pass them outwards.
75
+ # #
76
+ #
77
+ # a = Net::SSH::Simple.async do
78
+ # begin
79
+ # ssh 'example1.com', 'echo "Hello World."'
80
+ # scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
81
+ # scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
82
+ # rescue Net::SSH::Result => e
83
+ # # return our exception to the parent thread
84
+ # e
85
+ # end
86
+ # end
87
+ # r = a.value # Wait for thread to finish and capture result
88
+ #
89
+ # unless r.is_a? Net::SSH::Result
90
+ # puts "Something bad happened!"
91
+ # puts r
92
+ # end
93
+ #
94
+ # @example
95
+ # # Error Handling with an instance
96
+ # s = Net::SSH::Simple.new
97
+ # begin
98
+ # r = s.ssh 'example1.com', 'echo "Hello World."'
99
+ # if r.success and r.stdout == 'Hello World.'
100
+ # puts "Success! I Helloed World."
101
+ # end
102
+ #
103
+ # r = s.scp_ul 'example2.com', '/tmp/local_foo', '/tmp/remote_bar'
104
+ # if r.success and r.sent == r.total
105
+ # puts "Success! Uploaded #{r.sent} of #{r.total} bytes."
106
+ # end
107
+ #
108
+ # r = s.scp_dl 'example3.com', '/tmp/remote_foo', '/tmp/local_bar'
109
+ # if r.success and r.sent == r.total
110
+ # puts "Success! Downloaded #{r.sent} of #{r.total} bytes."
111
+ # end
112
+ # rescue Net::SSH::Simple::Error => e
113
+ # puts "Something bad happened!"
114
+ # puts e # Human readable error
115
+ # puts e.wrapped # Original Exception from Net::SSH
116
+ # puts e.context # Config that triggered the error
117
+ # ensure
118
+ # s.close # don't forget the clean up!
119
+ # end
120
+ #
121
+ # @example
122
+ # # Parametrizing Net::SSH
123
+ # Net::SSH::Simple.sync do
124
+ # ssh('example1.com', 'echo "Hello World."',
125
+ # {:user => 'tom', :password => 'jerry', :port => 1234})
126
+ # end
127
+ #
128
+ # @example
129
+ # # Using the SCP progress callback
130
+ # Net::SSH::Simple.sync do
131
+ # scp_ul 'example1.com', '/tmp/local_foo', '/tmp/remote_bar' do |sent, total|
132
+ # puts "Bytes uploaded: #{sent} of #{total}"
133
+ # end
134
+ # end
135
+ #
136
+ # @author moe@busyloop.net
137
+ #
138
+ class Simple
139
+ include Blockenspiel::DSL
140
+
141
+ #
142
+ # Result of a Net::SSH::Simple::Operation.
143
+ #
144
+ # @return [Net::SSH::Simple::Result] Result of the current operation
145
+ attr_reader :result
146
+
147
+ #
148
+ # Perform ssh command on a remote host and capture the result.
149
+ # This will create a new connection for each invocation.
150
+ #
151
+ # @example
152
+ # Net::SSH::Simple.ssh('localhost', 'echo Hello').class #=> Net::SSH::Simple::Result
153
+ #
154
+ # @example
155
+ # Net::SSH::Simple.ssh('localhost', 'echo Hello').stdout #=> "Hello"
156
+ #
157
+ # @param (see Net::SSH::Simple#ssh)
158
+ # @raise [Net::SSH::Simple::Error]
159
+ # @return [Net::SSH::Simple::Result] Result
160
+ def self.ssh(*args)
161
+ s = self.new
162
+ r = s.ssh(*args)
163
+ s.close
164
+ r
165
+ end
166
+
167
+ #
168
+ # SCP upload to a remote host.
169
+ # This will create a new connection for each invocation.
170
+ #
171
+ # @example
172
+ # # SCP Upload
173
+ # Net::SSH::Simple.scp_ul('localhost', '/tmp/local_foo', '/tmp/remote_bar')
174
+ #
175
+ # @example
176
+ # # Pass a block to monitor progress
177
+ # Net::SSH::Simple.scp_ul('localhost', '/tmp/local_foo', '/tmp/remote_bar') do |sent, total|
178
+ # puts "Bytes uploaded: #{sent} of #{total}"
179
+ # end
180
+ #
181
+ # @param (see Net::SSH::Simple#scp_ul)
182
+ # @raise [Net::SSH::Simple::Error]
183
+ # @return [Net::SSH::Simple::Result] Result
184
+ def self.scp_ul(*args, &block)
185
+ s = self.new
186
+ r = s.scp_ul(*args, &block)
187
+ s.close
188
+ r
189
+ end
190
+
191
+ #
192
+ # SCP download from a remote host.
193
+ # This will create a new connection for each invocation.
194
+ #
195
+ # @example
196
+ # # SCP Download
197
+ # Net::SSH::Simple.scp_dl('localhost', '/tmp/remote_foo', '/tmp/local_bar')
198
+ #
199
+ # @example
200
+ # # Pass a block to monitor progress
201
+ # Net::SSH::Simple.scp_dl('localhost', '/tmp/remote_foo', '/tmp/local_bar') do |sent, total|
202
+ # puts "Bytes downloaded: #{sent} of #{total}"
203
+ # end
204
+ #
205
+ # @param (see Net::SSH::Simple#scp_dl)
206
+ # @raise [Net::SSH::Simple::Error]
207
+ # @return [Net::SSH::Simple::Result] Result
208
+ def self.scp_dl(*args, &block)
209
+ s = self.new
210
+ r = s.scp_dl(*args, &block)
211
+ s.close
212
+ r
213
+ end
214
+
215
+ #
216
+ # SCP upload to a remote host.
217
+ # The underlying Net::SSH::Simple instance will re-use
218
+ # existing connections for optimal performance.
219
+ #
220
+ # @param [String] host Destination hostname or ip-address
221
+ # @param [String] cmd Shell command to execute
222
+ # @param opts (see Net::SSH::Simple#ssh)
223
+ # @param [Block] block Progress callback (optional)
224
+ # @return [Net::SSH::Simple::Result] Result
225
+ #
226
+ def scp_ul(host, src, dst, opts={}, &block)
227
+ scp(:upload, host, src, dst, opts, &block)
228
+ end
229
+
230
+ #
231
+ # SCP download from a remote host.
232
+ # The underlying Net::SSH::Simple instance will re-use
233
+ # existing connections for optimal performance.
234
+ #
235
+ # @param [String] host Destination hostname or ip-address
236
+ # @param [String] cmd Shell command to execute
237
+ # @param [Hash] opts Parameters for the underlying Net::SSH
238
+ # @param [Block] block Progress callback (optional)
239
+ # @return [Net::SSH::Simple::Result] Result
240
+ # @see Net::SSH::Simple#scp_ul
241
+ #
242
+ def scp_dl(host, src, dst, opts={}, &block)
243
+ scp(:download, host, src, dst, opts, &block)
244
+ end
245
+
246
+ #
247
+ # Perform SSH operation on a remote host and capture the result.
248
+ # The underlying Net::SSH::Simple instance will re-use
249
+ # existing connections for optimal performance.
250
+ #
251
+ # @return [Net::SSH::Simple::Result] Result
252
+ # @param [String] host Destination hostname or ip-address
253
+ # @param [String] cmd Shell command to execute
254
+ # @param [Hash] opts Parameters for the underlying Net::SSH
255
+ # @option opts [Array] :auth_methods
256
+ # an array of authentication methods to try
257
+ #
258
+ # @option opts [String] :compression
259
+ # the compression algorithm to use,
260
+ # or true to use whatever is supported.
261
+ #
262
+ # @option opts [Number] :compression_level
263
+ # the compression level to use when sending data
264
+ #
265
+ # @option opts [String/boolean] :opts (true)
266
+ # set to true to load the default OpenSSH opts files
267
+ # (~/.ssh/opts, /etc/ssh_opts), or to false to not load them,
268
+ # or to a file-name (or array of file-names) to load those
269
+ # specific configuration files.
270
+ #
271
+ # @option opts [Array] :encryption
272
+ # the encryption cipher (or ciphers) to use
273
+ #
274
+ # @option opts [boolean] :forward_agent
275
+ # set to true if you want the SSH agent connection to be forwarded
276
+ #
277
+ # @option opts [String/Array] :global_known_hosts_file
278
+ # (['/etc/ssh/known_hosts','/etc/ssh/known_hosts2'])
279
+ # the location of the global known hosts file.
280
+ # Set to an array if you want to specify multiple
281
+ # global known hosts files.
282
+ #
283
+ # @option opts [String/Array] :hmac
284
+ # the hmac algorithm (or algorithms) to use
285
+ #
286
+ # @option opts [String] :host_key
287
+ # the host key algorithm (or algorithms) to use
288
+ #
289
+ # @option opts [String] :host_key_alias
290
+ # the host name to use when looking up or adding a host to a known_hosts dictionary file
291
+ #
292
+ # @option opts [String] :host_name
293
+ # the real host name or IP to log into. This is used instead of the host parameter,
294
+ # and is primarily only useful when specified in an SSH configuration file.
295
+ # It lets you specify an alias, similarly to adding an entry in /etc/hosts but
296
+ # without needing to modify /etc/hosts.
297
+ #
298
+ # @option opts [String/Array] :kex
299
+ # the key exchange algorithm (or algorithms) to use
300
+ #
301
+ # @option opts [Array] :keys
302
+ # an array of file names of private keys to use for publickey and hostbased authentication
303
+ #
304
+ # @option opts [Array] :key_data
305
+ # an array of strings, with each element of the array being a raw private key in PEM format.
306
+ #
307
+ # @option opts [boolean] :keys_only
308
+ # set to true to use only private keys from keys and key_data parameters, even if
309
+ # ssh-agent offers more identities. This option is intended for situations where
310
+ # ssh-agent offers many different identites.
311
+ #
312
+ # @option opts [Logger] :logger
313
+ # the logger instance to use when logging
314
+ #
315
+ # @option opts [boolean/:very] :paranoid
316
+ # either true, false, or :very, specifying how strict host-key verification should be
317
+ #
318
+ # @option opts [String] :passphrase (nil)
319
+ # the passphrase to use when loading a private key (default is nil, for no passphrase)
320
+ #
321
+ # @option opts [String] :password
322
+ # the password to use to login
323
+ #
324
+ # @option opts [Integer] :port
325
+ # the port to use when connecting to the remote host
326
+ #
327
+ # @option opts [Hash] :properties
328
+ # a hash of key/value pairs to add to the new connection’s properties
329
+ # (see Net::SSH::Connection::Session#properties)
330
+ #
331
+ # @option opts [String] :proxy
332
+ # a proxy instance (see Proxy) to use when connecting
333
+ #
334
+ # @option opts [Integer] :rekey_blocks_limit
335
+ # the max number of blocks to process before rekeying
336
+ #
337
+ # @option opts [Integer] :rekey_limit
338
+ # the max number of bytes to process before rekeying
339
+ #
340
+ # @option opts [Integer] :rekey_packet_limit
341
+ # the max number of packets to process before rekeying
342
+ #
343
+ # @option opts [Integer] :timeout
344
+ # how long to wait for the initial connection to be made
345
+ #
346
+ # @option opts [String] :user
347
+ # the username to log in as
348
+ #
349
+ # @option opts [String/Array] :user_known_hosts_file
350
+ # (['~/.ssh/known_hosts, ~/.ssh/known_hosts2'])
351
+ # the location of the user known hosts file. Set to an array to specify multiple
352
+ # user known hosts files.
353
+ #
354
+ # @option opts [Symbol] :verbose
355
+ # how verbose to be (Logger verbosity constants, Logger::DEBUG is very verbose,
356
+ # Logger::FATAL is all but silent). Logger::FATAL is the default. The symbols
357
+ # :debug, :info, :warn, :error, and :fatal are also supported and are translated
358
+ # to the corresponding Logger constant.
359
+ #
360
+ # @see http://net-ssh.github.com/ssh/v2/api/classes/Net/SSH/Config.html
361
+ # Net::SSH documentation for the 'opts'-hash
362
+ def ssh(host, cmd, opts={})
363
+ with_session(host, opts) do |session|
364
+ @result = Result.new(
365
+ { :host => host, :cmd => cmd, :start_at => Time.new,
366
+ :stdout => '' , :stderr => '' , :success => nil
367
+ } )
368
+
369
+ channel = session.open_channel do |chan|
370
+ chan.exec cmd do |ch, success|
371
+ @result[:success] = success
372
+ ch.on_data do |c, data|
373
+ @result[:stdout] += data.to_s
374
+ end
375
+ ch.on_extended_data do |c, type, data|
376
+ @result[:stderr] += data.to_s
377
+ end
378
+ ch.on_request('exit-status') do |c, data|
379
+ @result[:exit_code] = data.read_long
380
+ end
381
+ ch.on_request('exit-signal') do |c, data|
382
+ @result[:exit_signal] = data.read_string
383
+ @result[:success] = false
384
+ raise "Killed by SIG#{@result[:exit_signal]}"
385
+ end
386
+ end
387
+ end
388
+ channel.wait
389
+ @result[:finish_at] = Time.new
390
+ @result
391
+ end
392
+ end
393
+
394
+ dsl_methods false
395
+
396
+ def initialize()
397
+ @sessions = {}
398
+ @result = Result.new
399
+ end
400
+
401
+ #
402
+ # Spawn a Thread to perform a sequence of ssh/scp operations.
403
+ #
404
+ # @param [Block] block
405
+ # @return [Thread] Thread executing the SSH-Block.
406
+ #
407
+ def self.async(&block)
408
+ Thread.new do
409
+ self.sync(&block)
410
+ end
411
+ end
412
+
413
+ #
414
+ # Perform a sequence of ssh/scp operations.
415
+ #
416
+ # @return [Net::SSH::Simple::Result] Result
417
+ #
418
+ def self.sync(&block)
419
+ b = self.new
420
+ r = Blockenspiel.invoke(block, b)
421
+ b.close
422
+ r
423
+ end
424
+
425
+ #
426
+ # Close and cleanup.
427
+ #
428
+ # @return [Net::SSH::Simple::Result] Result
429
+ #
430
+ def close
431
+ @sessions.values.each do |session|
432
+ session.close
433
+ end
434
+ @result
435
+ end
436
+
437
+ private
438
+ def with_session(host, opts, &block)
439
+ begin
440
+ session = @sessions[host.hash] = @sessions[host.hash] ||\
441
+ Net::SSH.start(*[host, opts[:user], opts])
442
+ block.call(session)
443
+ rescue => e
444
+ opts[:password].gsub!(/./,'*') if opts.include? :password
445
+ @result[:exception] = e
446
+ @result[:context] = [host,opts]
447
+ raise Net::SSH::Simple::Error, [e, [host,opts]]
448
+ end
449
+ end
450
+
451
+ def scp(mode, host, src, dst, opts={}, &block)
452
+ @result = Result.new(
453
+ { :host => host, :cmd => :scp_dl, :start_at => Time.new,
454
+ :src => src , :dst => dst , :success => false
455
+ } )
456
+ with_session(host, opts) do |session|
457
+ lt = 0
458
+ channel = session.scp.send(mode, src, dst) do |ch, name, sent, total|
459
+ @result[:name] ||= name
460
+ @result[:total] ||= total
461
+ @result[:sent] = sent
462
+ block.call(sent, total) unless block.nil?
463
+ end
464
+ channel.wait
465
+ @result[:finish_at] = Time.new
466
+ @result[:success] = @result[:sent] == @result[:total]
467
+ @result
468
+ end
469
+ end
470
+
471
+ #
472
+ # Encapsulates any Errors that may occur
473
+ # during a Net::SSH::Simple operation.
474
+ #
475
+ class Error < RuntimeError
476
+ # Reference to the underlying Net::SSH Exception
477
+ attr_reader :wrapped
478
+ # The opts-hash of the operation that triggered the Error
479
+ attr_reader :context
480
+
481
+ def initialize(msg, e=$!)
482
+ super(msg)
483
+ @wrapped = e
484
+ @context = msg[1]
485
+ end
486
+
487
+ def to_s
488
+ "#{super[0]} @ #{super[1]}"
489
+ end
490
+ end
491
+
492
+ #
493
+ # Result of a Net::SSH::Simple operation.
494
+ #
495
+ # This Mash contains various information that may
496
+ # be relevant to your interests.
497
+ #
498
+ class Result < Hashie::Mash; end
499
+ end
500
+ end
501
+ end
502
+
@@ -0,0 +1,7 @@
1
+ module Net
2
+ module SSH
3
+ class Simple
4
+ VERSION = "0.9.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "net/ssh/simple/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "net-ssh-simple"
7
+ s.version = Net::SSH::Simple::VERSION
8
+ s.authors = ["Moe"]
9
+ s.email = ["moe@busyloop.net"]
10
+ s.homepage = "https://github.com/busyloop/net-ssh-simple"
11
+ s.description = %q{Net::SSH::Simple is a simple wrapper around Net::SSH and Net::SCP.}
12
+ s.summary = %q{SSH without the headache}
13
+
14
+ s.rubyforge_project = "net-ssh-simple"
15
+
16
+ s.add_dependency "net-ssh", "~> 2.1.4"
17
+ s.add_dependency "net-scp", "~> 1.0.4"
18
+ s.add_dependency "blockenspiel", "~> 0.4.3"
19
+ s.add_dependency "hashie", "~> 1.1.0"
20
+
21
+ s.add_development_dependency "rake"
22
+ s.add_development_dependency "rspec"
23
+ s.add_development_dependency "cover_me"
24
+
25
+ s.files = `git ls-files`.split("\n")
26
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
28
+ s.require_paths = ["lib"]
29
+ end
@@ -0,0 +1,275 @@
1
+ require 'cover_me'
2
+ require 'net/ssh/simple'
3
+ require 'digest/md5'
4
+ require 'securerandom'
5
+
6
+ #
7
+ # In order to run this test-suite you must
8
+ # have ssh-access to localhost.
9
+ #
10
+ # 1. Add your own ssh-key to authorized_keys:
11
+ # cat >>~/.ssh/authorized_keys ~/.ssh/id_rsa.pub
12
+ #
13
+ # 2. Add something like this to ~/.ssh/config:
14
+ #
15
+ # Host localhost
16
+ # User my_name
17
+ # Port 22
18
+ #
19
+ # The test-suite will (over)write the following files on localhost:
20
+ # /tmp/ssh_test_in{0,1,2,3,4}
21
+ # /tmp/ssh_test_out{0,1,2,3,4}
22
+ #
23
+
24
+ describe Net::SSH::Simple do
25
+ describe "singleton" do
26
+ before :each do
27
+ (0..4).each do |i|
28
+ begin
29
+ File.unlink(File.join('/tmp', "ssh_test_in#{i}"))
30
+ rescue; end
31
+ begin
32
+ File.unlink(File.join('/tmp', "ssh_test_out#{i}"))
33
+ rescue; end
34
+ File.open(File.join('/tmp', "ssh_test_in#{i}"), 'w') do |fd|
35
+ fd.write(SecureRandom.random_bytes(1024+SecureRandom.random_number(8192)))
36
+ end
37
+ end
38
+ end
39
+
40
+ it "fails gently" do
41
+ lambda {
42
+ Net::SSH::Simple.ssh('localhost', 'true', {:port => 0})
43
+ }.should raise_error(Net::SSH::Simple::Error)
44
+
45
+ begin
46
+ Net::SSH::Simple.ssh('localhost', 'true', {:port => 0})
47
+ rescue => e
48
+ e.to_s.should == 'Connection refused - connect(2) @ ["localhost", {:port=>0}]'
49
+ end
50
+ end
51
+
52
+ it "returns a result" do
53
+ Net::SSH::Simple.ssh('localhost', 'true').success.should == true
54
+ end
55
+
56
+ it "recognizes exit-codes" do
57
+ Net::SSH::Simple.ssh('localhost', 'true').exit_code.should == 0
58
+ Net::SSH::Simple.ssh('localhost', 'false').exit_code.should == 1
59
+ end
60
+
61
+ it "reads stdout" do
62
+ Net::SSH::Simple.ssh('localhost', 'echo hello').stdout.should == "hello\n"
63
+ long = Net::SSH::Simple.ssh('localhost', 'seq 1 100000').stdout
64
+ Digest::MD5.hexdigest(long).should == 'dea9193b768319cbb4ff1a137ac03113'
65
+ end
66
+
67
+ it "reads stderr" do
68
+ Net::SSH::Simple.ssh('localhost', 'echo hello 1>&2').stderr.should == "hello\n"
69
+ long = Net::SSH::Simple.ssh('localhost', 'seq 1 100000 1>&2').stderr
70
+ Digest::MD5.hexdigest(long).should == 'dea9193b768319cbb4ff1a137ac03113'
71
+ end
72
+
73
+ it "uploads via scp" do
74
+ mockback = mock(:progress_callback)
75
+ mockback.should_receive(:ping).at_least(:once)
76
+ r = Net::SSH::Simple.scp_ul('localhost', '/tmp/ssh_test_in0', '/tmp/ssh_test_out0') do |sent,total|
77
+ mockback.ping
78
+ end
79
+ r.success.should == true
80
+ Digest::MD5.file('/tmp/ssh_test_in0').should == Digest::MD5.file('/tmp/ssh_test_out0')
81
+ end
82
+
83
+ it "downloads via scp" do
84
+ mockback = mock(:progress_callback)
85
+ mockback.should_receive(:ping).at_least(:once)
86
+ r = Net::SSH::Simple.scp_dl('localhost', '/tmp/ssh_test_in0', '/tmp/ssh_test_out0') do |sent,total|
87
+ mockback.ping
88
+ end
89
+ r.success.should == true
90
+ Digest::MD5.file('/tmp/ssh_test_in0').should == Digest::MD5.file('/tmp/ssh_test_out0')
91
+ end
92
+ end
93
+
94
+ describe "instance" do
95
+ before :each do
96
+ @s = Net::SSH::Simple.new
97
+
98
+ (0..4).each do |i|
99
+ begin
100
+ File.unlink(File.join('/tmp', "ssh_test_in#{i}"))
101
+ rescue; end
102
+ begin
103
+ File.unlink(File.join('/tmp', "ssh_test_out#{i}"))
104
+ rescue; end
105
+ File.open(File.join('/tmp', "ssh_test_in#{i}"), 'w') do |fd|
106
+ fd.write(SecureRandom.random_bytes(1024+SecureRandom.random_number(8192)))
107
+ end
108
+ end
109
+ end
110
+
111
+ after :each do
112
+ @s.close
113
+ end
114
+
115
+ it "returns a result" do
116
+ @s.ssh('localhost', 'true').success.should == true
117
+ end
118
+
119
+ it "recognizes exit-codes" do
120
+ @s.ssh('localhost', 'true').exit_code.should == 0
121
+ @s.ssh('localhost', 'false').exit_code.should == 1
122
+ end
123
+
124
+ it "reads stdout" do
125
+ @s.ssh('localhost', 'echo hello').stdout.should == "hello\n"
126
+ long = @s.ssh('localhost', 'seq 1 100000').stdout
127
+ Digest::MD5.hexdigest(long).should == 'dea9193b768319cbb4ff1a137ac03113'
128
+ end
129
+
130
+ it "reads stderr" do
131
+ @s.ssh('localhost', 'echo hello 1>&2').stderr.should == "hello\n"
132
+ long = @s.ssh('localhost', 'seq 1 100000 1>&2').stderr
133
+ Digest::MD5.hexdigest(long).should == 'dea9193b768319cbb4ff1a137ac03113'
134
+ end
135
+
136
+ it "uploads via scp" do
137
+ mockback = mock(:progress_callback)
138
+ mockback.should_receive(:ping).at_least(:once)
139
+ r = @s.scp_ul('localhost', '/tmp/ssh_test_in0', '/tmp/ssh_test_out0') do |sent,total|
140
+ mockback.ping
141
+ end
142
+ r.success.should == true
143
+ Digest::MD5.file('/tmp/ssh_test_in0').should == Digest::MD5.file('/tmp/ssh_test_out0')
144
+ end
145
+
146
+ it "downloads via scp" do
147
+ mockback = mock(:progress_callback)
148
+ mockback.should_receive(:ping).at_least(:once)
149
+ r = @s.scp_dl('localhost', '/tmp/ssh_test_in0', '/tmp/ssh_test_out0') do |sent,total|
150
+ mockback.ping
151
+ end
152
+ r.success.should == true
153
+ Digest::MD5.file('/tmp/ssh_test_in0').should == Digest::MD5.file('/tmp/ssh_test_out0')
154
+ end
155
+ end
156
+
157
+ describe "synchronous block syntax" do
158
+ it "returns a result" do
159
+ Net::SSH::Simple.sync do
160
+ ssh('localhost', 'true').success.should == true
161
+ end
162
+ end
163
+
164
+ it "recognizes exit-codes" do
165
+ Net::SSH::Simple.sync do
166
+ ssh('localhost', 'true').exit_code.should == 0
167
+ ssh('localhost', 'false').exit_code.should == 1
168
+ end
169
+ end
170
+
171
+ it "reads stdout" do
172
+ Net::SSH::Simple.sync do
173
+ ssh('localhost', 'echo hello').stdout.should == "hello\n"
174
+ long = ssh('localhost', 'seq 1 100000').stdout
175
+ Digest::MD5.hexdigest(long).should == 'dea9193b768319cbb4ff1a137ac03113'
176
+ end
177
+ end
178
+
179
+ it "reads stderr" do
180
+ Net::SSH::Simple.sync do
181
+ ssh('localhost', 'echo hello 1>&2').stderr.should == "hello\n"
182
+ long = ssh('localhost', 'seq 1 100000 1>&2').stderr
183
+ Digest::MD5.hexdigest(long).should == 'dea9193b768319cbb4ff1a137ac03113'
184
+ end
185
+ end
186
+
187
+ it "uploads via scp" do
188
+ Net::SSH::Simple.sync do
189
+ mockback = mock(:progress_callback)
190
+ mockback.should_receive(:ping).at_least(:once)
191
+ r = scp_ul('localhost', '/tmp/ssh_test_in0', '/tmp/ssh_test_out0') do |sent,total|
192
+ mockback.ping
193
+ end
194
+ r.success.should == true
195
+ Digest::MD5.file('/tmp/ssh_test_in0').should == Digest::MD5.file('/tmp/ssh_test_out0')
196
+ end
197
+ end
198
+
199
+ it "downloads via scp" do
200
+ Net::SSH::Simple.sync do
201
+ mockback = mock(:progress_callback)
202
+ mockback.should_receive(:ping).at_least(:once)
203
+ r = scp_dl('localhost', '/tmp/ssh_test_in0', '/tmp/ssh_test_out0') do |sent,total|
204
+ mockback.ping
205
+ end
206
+ r.success.should == true
207
+ Digest::MD5.file('/tmp/ssh_test_in0').should == Digest::MD5.file('/tmp/ssh_test_out0')
208
+ end
209
+ end
210
+ end
211
+
212
+ describe "asynchronous block syntax" do
213
+ before :each do
214
+ (0..4).each do |i|
215
+ begin
216
+ File.unlink(File.join('/tmp', "ssh_test_in#{i}"))
217
+ rescue; end
218
+ begin
219
+ File.unlink(File.join('/tmp', "ssh_test_out#{i}"))
220
+ rescue; end
221
+ File.open(File.join('/tmp', "ssh_test_in#{i}"), 'w') do |fd|
222
+ fd.write(SecureRandom.random_bytes(1024+SecureRandom.random_number(8192)))
223
+ end
224
+ end
225
+ end
226
+
227
+ it "copes with a little concurrency" do
228
+ t = []
229
+ (0..4).each do |i|
230
+ t[i] = Net::SSH::Simple.async do
231
+ mockback = mock(:progress_callback)
232
+ mockback.should_receive(:ping).at_least(:once)
233
+ r = nil
234
+ if 0 == i % 2
235
+ r = scp_dl('localhost', "/tmp/ssh_test_in#{i}", "/tmp/ssh_test_out#{i}") do |sent,total|
236
+ mockback.ping
237
+ end
238
+ else
239
+ r = scp_ul('localhost', "/tmp/ssh_test_in#{i}", "/tmp/ssh_test_out#{i}") do |sent,total|
240
+ mockback.ping
241
+ end
242
+ end
243
+ r.success.should == true
244
+ ssh('localhost', "echo hello #{i}")
245
+ end
246
+ end
247
+
248
+ (0..4).each do |i|
249
+ r = t[i].value
250
+ r.stdout.should == "hello #{i}\n"
251
+ Digest::MD5.file("/tmp/ssh_test_in#{i}").should == Digest::MD5.file("/tmp/ssh_test_out#{i}")
252
+ end
253
+ end
254
+
255
+ it "handles signals" do
256
+ victim = Net::SSH::Simple.async do
257
+ begin
258
+ ssh('localhost', 'sleep 1020304157')
259
+ rescue => e
260
+ e
261
+ end
262
+ end
263
+
264
+ killer = Net::SSH::Simple.async do
265
+ ssh('localhost', "pkill -f 'sleep 1020304157'")
266
+ end
267
+
268
+ k = killer.value
269
+ k.success.should == true
270
+
271
+ v = victim.value
272
+ v.to_s.should == 'Killed by SIGTERM @ ["localhost", {}]'
273
+ end
274
+ end
275
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: net-ssh-simple
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 9
8
+ - 0
9
+ version: 0.9.0
10
+ platform: ruby
11
+ authors:
12
+ - Moe
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-10-22 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: net-ssh
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 2
29
+ - 1
30
+ - 4
31
+ version: 2.1.4
32
+ type: :runtime
33
+ prerelease: false
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: net-scp
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 1
44
+ - 0
45
+ - 4
46
+ version: 1.0.4
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: blockenspiel
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 0
59
+ - 4
60
+ - 3
61
+ version: 0.4.3
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: *id003
65
+ - !ruby/object:Gem::Dependency
66
+ name: hashie
67
+ requirement: &id004 !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ~>
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 1
74
+ - 1
75
+ - 0
76
+ version: 1.1.0
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: *id004
80
+ - !ruby/object:Gem::Dependency
81
+ name: rake
82
+ requirement: &id005 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: *id005
93
+ - !ruby/object:Gem::Dependency
94
+ name: rspec
95
+ requirement: &id006 !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ segments:
101
+ - 0
102
+ version: "0"
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: *id006
106
+ - !ruby/object:Gem::Dependency
107
+ name: cover_me
108
+ requirement: &id007 !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ segments:
114
+ - 0
115
+ version: "0"
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: *id007
119
+ description: Net::SSH::Simple is a simple wrapper around Net::SSH and Net::SCP.
120
+ email:
121
+ - moe@busyloop.net
122
+ executables: []
123
+
124
+ extensions: []
125
+
126
+ extra_rdoc_files: []
127
+
128
+ files:
129
+ - .gitignore
130
+ - Gemfile
131
+ - README.rdoc
132
+ - Rakefile
133
+ - lib/net/ssh/simple.rb
134
+ - lib/net/ssh/simple/core.rb
135
+ - lib/net/ssh/simple/version.rb
136
+ - net-ssh-simple.gemspec
137
+ - spec/net-ssh-simple.rb
138
+ has_rdoc: true
139
+ homepage: https://github.com/busyloop/net-ssh-simple
140
+ licenses: []
141
+
142
+ post_install_message:
143
+ rdoc_options: []
144
+
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ none: false
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ hash: 1030525555759223160
153
+ segments:
154
+ - 0
155
+ version: "0"
156
+ required_rubygems_version: !ruby/object:Gem::Requirement
157
+ none: false
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ hash: 1030525555759223160
162
+ segments:
163
+ - 0
164
+ version: "0"
165
+ requirements: []
166
+
167
+ rubyforge_project: net-ssh-simple
168
+ rubygems_version: 1.3.7
169
+ signing_key:
170
+ specification_version: 3
171
+ summary: SSH without the headache
172
+ test_files: []
173
+