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