net-ssh-simple 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/Gemfile +2 -0
- data/README.rdoc +44 -0
- data/Rakefile +25 -0
- data/lib/net/ssh/simple.rb +7 -0
- data/lib/net/ssh/simple/core.rb +502 -0
- data/lib/net/ssh/simple/version.rb +7 -0
- data/net-ssh-simple.gemspec +29 -0
- data/spec/net-ssh-simple.rb +275 -0
- metadata +173 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
@@ -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,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,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
|
+
|