em-ssh 0.3.0.pre1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +42 -24
- data/bin/em-ssh +4 -2
- data/bin/em-ssh-shell +37 -26
- data/lib/em-ssh/connection.rb +1 -1
- data/lib/em-ssh/shell.rb +86 -60
- data/lib/em-ssh/version.rb +1 -1
- metadata +4 -3
data/README.md
CHANGED
@@ -3,7 +3,7 @@ Em-ssh is a net-ssh adapter for EventMachine. For the most part you can take any
|
|
3
3
|
|
4
4
|
Em-ssh is not associated with the Jamis Buck's [net-ssh](http://net-ssh.github.com/) library. Please report any bugs with em-ssh to [https://github.com/simulacre/em-ssh/issues](https://github.com/simulacre/em-ssh/issues)
|
5
5
|
##Installation
|
6
|
-
gem install em-ssh
|
6
|
+
gem install em-ssh
|
7
7
|
|
8
8
|
##Synopsis
|
9
9
|
EM.run do
|
@@ -63,33 +63,51 @@ Em-ssh provides an expect-like shell abstraction layer on top of net-ssh in EM::
|
|
63
63
|
### Example
|
64
64
|
require 'em-ssh/shell'
|
65
65
|
EM.run {
|
66
|
-
EM::Ssh::Shell.new(host, '
|
67
|
-
shell.
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
66
|
+
EM::Ssh::Shell.new(host, ENV['USER'], "") do |shell|
|
67
|
+
shell.callback do
|
68
|
+
shell.expect('~]$ ')
|
69
|
+
shell.expect('~]$ ','uname -a')
|
70
|
+
shell.expect('~]$ ')
|
71
|
+
shell.expect('~]$ ', '/sbin/ifconfig -a')
|
72
|
+
EM.stop
|
73
|
+
end
|
74
|
+
shell.errback do
|
75
|
+
puts "error: #{err} (#{err.class})"
|
76
|
+
EM.stop
|
77
|
+
end
|
74
78
|
end
|
75
79
|
}
|
76
80
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
81
|
+
### Run Multiple Commands in Parallel
|
82
|
+
require 'em-ssh/shell'
|
83
|
+
EM.run do
|
84
|
+
EM::Ssh::Shell.new(host, ENV['USER'], '') do |shell|
|
85
|
+
shell.errback do |err|
|
86
|
+
puts "error: #{err} (#{err.class})"
|
87
|
+
EM.stop
|
88
|
+
end
|
89
|
+
|
90
|
+
shell.callback do
|
91
|
+
commands.clone.each do |command|
|
92
|
+
mys = shell.split # provides a second session over the same connection
|
93
|
+
mys.on(:closed) do
|
94
|
+
commands.delete(command)
|
95
|
+
EM.stop if commands.empty?
|
96
|
+
end
|
92
97
|
|
98
|
+
puts("waiting for: #{waitstr.inspect}")
|
99
|
+
# When given a block, Shell#expect does not 'block'
|
100
|
+
mys.expect(waitstr) do
|
101
|
+
puts "sending #{command.inspect} and waiting for #{waitstr.inspect}"
|
102
|
+
mys.expect(waitstr, command) do |result|
|
103
|
+
puts "#{mys} result: '#{result}'"
|
104
|
+
mys.close
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
93
111
|
|
94
112
|
## Other Examples
|
95
113
|
See bin/em-ssh for an example of a basic replacement for system ssh.
|
data/bin/em-ssh
CHANGED
@@ -43,8 +43,10 @@ end # host.nil?
|
|
43
43
|
abort("a host is required") if host.nil?
|
44
44
|
|
45
45
|
options[:user], host = *host.split('@') if host.include?('@')
|
46
|
-
options[:user], options[:password] = *options[:user].split(':') if options[:user].include?(':')
|
46
|
+
options[:user], options[:password] = *options[:user].split(':') if options[:user] && options[:user].include?(':')
|
47
47
|
host, options[:port] = *host.split(':') if host.include?(':')
|
48
|
+
options[:user] = ENV['USER'] unless options[:user]
|
49
|
+
options[:password] = HighLine.new.ask("#{options[:user]}'s password: "){|q| q.echo = "*" } unless options[:password]
|
48
50
|
connected = false
|
49
51
|
|
50
52
|
|
@@ -68,7 +70,7 @@ end
|
|
68
70
|
EM.run do
|
69
71
|
EM::Ssh.start(host, options[:user], options) do |ssh|
|
70
72
|
ssh.errback do |err|
|
71
|
-
puts "#{err} (#{err.class})"
|
73
|
+
puts "error: #{err} (#{err.class})"
|
72
74
|
EM.stop
|
73
75
|
end
|
74
76
|
|
data/bin/em-ssh-shell
CHANGED
@@ -15,14 +15,18 @@ def abort(msg)
|
|
15
15
|
Process.exit
|
16
16
|
end # abort(msg)
|
17
17
|
|
18
|
-
options
|
19
|
-
opts
|
18
|
+
options = {:auth_methods => ['publickey', 'password'], :port => 22}
|
19
|
+
opts = OptionParser.new
|
20
20
|
opts.banner += " [user:[password]@]host[:port] wait_string command [command command ...]"
|
21
|
-
|
21
|
+
|
22
22
|
opts.on('-u', '--user String', String) { |u| options[:user] = u }
|
23
23
|
opts.on('-p', '--password [String]', String) do |p|
|
24
24
|
options[:password] = p.nil? ? HighLine.new.ask("password: "){|q| q.echo = "*" } : p
|
25
25
|
end
|
26
|
+
opts.on('-t', '--timeout Integer', Integer) { |t| options[:timeout] = t }
|
27
|
+
opts.on('--[no-]publickey', "don't attempt public key auth") do |pk|
|
28
|
+
options[:auth_methods] = ['password'] unless pk
|
29
|
+
end
|
26
30
|
opts.on('-v', '--verbose') do
|
27
31
|
EM::Ssh.logger.level = EM::Ssh.logger.level - 1 unless EM::Ssh.logger.level == 0
|
28
32
|
options[:verbose] = EM::Ssh.logger.level
|
@@ -36,36 +40,43 @@ end # host.nil?
|
|
36
40
|
abort("a host is required") if host.nil?
|
37
41
|
|
38
42
|
options[:user], host = *host.split('@') if host.include?('@')
|
39
|
-
options[:user], options[:password] = *options[:user].split(':') if options[:user].include?(':')
|
43
|
+
options[:user], options[:password] = *options[:user].split(':') if options[:user] && options[:user].include?(':')
|
40
44
|
host, options[:port] = *host.split(':') if host.include?(':')
|
45
|
+
options[:user] = ENV['USER'] unless options[:user]
|
46
|
+
options[:password] = HighLine.new.ask("#{options[:user]}'s password: "){|q| q.echo = "*" } unless options[:password]
|
41
47
|
|
42
48
|
|
43
|
-
waitstr
|
49
|
+
waitstr = ARGV.shift
|
44
50
|
commands = ARGV
|
45
51
|
abort("wait_string is required") if waitstr.nil?
|
46
52
|
abort("command is required") if commands.empty?
|
53
|
+
waitstr = Regexp.escape(waitstr)
|
47
54
|
|
48
55
|
|
49
56
|
EM.run do
|
50
|
-
EM::Ssh::Shell.new(host, options[:user], options[:password], :net_ssh => options) do |shell|
|
51
|
-
|
52
|
-
|
53
|
-
mys.on(:closed) { info("#{mys} has closed") }
|
54
|
-
|
55
|
-
EM.next_tick do
|
56
|
-
Fiber.new {
|
57
|
-
debug("#{mys} waited for: '#{mys.wait_for(waitstr)}'")
|
58
|
-
debug("#{mys} send: #{command.inspect}")
|
59
|
-
puts "#{mys} result: '#{mys.send_and_wait(command, waitstr)}'"
|
60
|
-
mys.close
|
61
|
-
}.resume
|
62
|
-
end
|
63
|
-
end # |command|
|
64
|
-
|
65
|
-
shell.on(:childless) do
|
66
|
-
info("#{shell}'s children all closed")
|
67
|
-
shell.close
|
57
|
+
EM::Ssh::Shell.new(host, options[:user], options[:password], :timeout => options[:timeout], :net_ssh => options) do |shell|
|
58
|
+
shell.errback do |err|
|
59
|
+
puts "error: #{err} (#{err.class})"
|
68
60
|
EM.stop
|
69
|
-
end
|
70
|
-
|
71
|
-
|
61
|
+
end
|
62
|
+
|
63
|
+
shell.callback do
|
64
|
+
commands.clone.each do |command|
|
65
|
+
mys = shell.split
|
66
|
+
mys.on(:closed) do
|
67
|
+
commands.delete(command)
|
68
|
+
EM.stop if commands.empty?
|
69
|
+
end
|
70
|
+
|
71
|
+
puts("waiting for: #{waitstr.inspect}")
|
72
|
+
mys.expect(waitstr) do
|
73
|
+
puts "sending #{command.inspect} and waiting for #{waitstr.inspect}"
|
74
|
+
mys.expect(waitstr, command) do |result|
|
75
|
+
puts "#{mys} result: '#{result}'"
|
76
|
+
mys.close
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/em-ssh/connection.rb
CHANGED
@@ -261,7 +261,7 @@ module EventMachine
|
|
261
261
|
end # begin
|
262
262
|
end.resume
|
263
263
|
else
|
264
|
-
@queue.push(packet)
|
264
|
+
@queue.push(packet) unless packet.type >= CHANNEL_OPEN
|
265
265
|
if algorithms.allow?(packet)
|
266
266
|
fire(:packet, packet)
|
267
267
|
fire(:session_packet, packet) if packet.type >= GLOBAL_REQUEST
|
data/lib/em-ssh/shell.rb
CHANGED
@@ -6,8 +6,8 @@ module EventMachine
|
|
6
6
|
# @example Retrieve the output of ifconfig -a on a server
|
7
7
|
# EM.run{
|
8
8
|
# shell = EM::Ssh::Shell.new(host, user, password)
|
9
|
-
# shell.
|
10
|
-
# interfaces =
|
9
|
+
# shell.expect('~]$ ')
|
10
|
+
# interfaces = expect('~]$ ', '/sbin/ifconfig -a')
|
11
11
|
#
|
12
12
|
# Shells can be easily and quickly duplicated (#split) without the need to establish another connection.
|
13
13
|
# Shells provide :closed, :childless, and :split callbacks.
|
@@ -21,10 +21,11 @@ module EventMachine
|
|
21
21
|
#
|
22
22
|
# admin_shell = shell.split
|
23
23
|
# admin_shell.on(:closed) { warn("admin shell has closed") }
|
24
|
-
# admin_shell.
|
24
|
+
# admin_shell.expect(']$', 'sudo su -')
|
25
25
|
class Shell
|
26
26
|
include Log
|
27
27
|
include Callbacks
|
28
|
+
include EM::Deferrable
|
28
29
|
|
29
30
|
# Global timeout for wait operations; can be overriden by :timeout option to new
|
30
31
|
TIMEOUT = 15
|
@@ -75,11 +76,12 @@ module EventMachine
|
|
75
76
|
@reconnect = opts[:reconnect]
|
76
77
|
@buffer = ''
|
77
78
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
open
|
82
|
-
|
79
|
+
# TODO make all methods other than #callback and #errback inaccessible until connected? == true
|
80
|
+
yield self if block_given?
|
81
|
+
Fiber.new {
|
82
|
+
open rescue fail($!)
|
83
|
+
succeed(self) if connected? && !closed?
|
84
|
+
}.resume
|
83
85
|
end
|
84
86
|
|
85
87
|
# @return [Boolean] true if the connection should be automatically re-established; default: false
|
@@ -91,7 +93,7 @@ module EventMachine
|
|
91
93
|
# Disconnected shells cannot be split.
|
92
94
|
def disconnect
|
93
95
|
close
|
94
|
-
connection.close
|
96
|
+
connection && connection.close
|
95
97
|
end
|
96
98
|
|
97
99
|
# @return [Boolean] true if the connection is still alive
|
@@ -115,6 +117,40 @@ module EventMachine
|
|
115
117
|
@closed == true
|
116
118
|
end
|
117
119
|
|
120
|
+
# Wait for a number of seconds until a specified string or regexp is matched by the
|
121
|
+
# data returned from the ssh connection. Optionally send a given string first.
|
122
|
+
#
|
123
|
+
# If a block is not provided the current Fiber will yield until strregex matches or
|
124
|
+
# :timeout # is reached.
|
125
|
+
#
|
126
|
+
# If a block is provided expect will return.
|
127
|
+
#
|
128
|
+
# @param [String, Regexp] strregex to match against
|
129
|
+
# @param [String] send_str the data to send before waiting
|
130
|
+
# @param [Hash] opts
|
131
|
+
# @option opts [Fixnum] :timeout (@timeout) number of seconds to wait when there is no activity
|
132
|
+
# @return [Shell, String] all data received up to an including strregex if a block is not provided.
|
133
|
+
# the Shell if a block is provided
|
134
|
+
# @example expect a prompt
|
135
|
+
# expect(' ~]$ ')
|
136
|
+
# @example send a command and wait for a prompt
|
137
|
+
# expect(' ~]$ ', '/sbin/ifconfig')
|
138
|
+
# @example expect a prompt and within 5 seconds
|
139
|
+
# expect(' ~]$ ', :timeout => 5)
|
140
|
+
# @example send a command and wait up to 10 seconds for a prompt
|
141
|
+
# expect(' ~]$ ', '/etc/sysconfig/openvpn restart', :timeout => 10)
|
142
|
+
def expect(strregex, send_str = nil, opts = {})
|
143
|
+
send_str, opts = nil, send_str if send_str.is_a?(Hash)
|
144
|
+
if block_given?
|
145
|
+
Fiber.new {
|
146
|
+
yield send_str ? send_and_wait(send_str, strregex, opts) : wait_for(strregex, opts)
|
147
|
+
}.resume
|
148
|
+
self
|
149
|
+
else
|
150
|
+
send_str ? send_and_wait(send_str, strregex, opts) : wait_for(strregex, opts)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
118
154
|
# Send a string to the server and wait for a response containing a specified String or Regex.
|
119
155
|
# @param [String] send_str
|
120
156
|
# @return [String] all data in the buffer including the wait_str if it was found
|
@@ -155,12 +191,10 @@ module EventMachine
|
|
155
191
|
data_callback = on(:data) do
|
156
192
|
timer && timer.cancel
|
157
193
|
if matched
|
158
|
-
|
194
|
+
warn("data_callback invoked when already matched")
|
159
195
|
next
|
160
196
|
end
|
161
|
-
matched = @buffer.match(strregex)
|
162
|
-
if matched
|
163
|
-
debug("data matched")
|
197
|
+
if (matched = @buffer.match(strregex))
|
164
198
|
data_callback.cancel
|
165
199
|
@buffer=matched.post_match
|
166
200
|
f.resume(matched.pre_match + matched.to_s)
|
@@ -176,10 +210,13 @@ module EventMachine
|
|
176
210
|
|
177
211
|
timer = EM::Timer.new(opts[:timeout], &timeout)
|
178
212
|
debug("set timer: #{timer} for #{opts[:timeout]}")
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
213
|
+
Fiber.yield.tap do |res|
|
214
|
+
if res.is_a?(Exception)
|
215
|
+
res.set_backtrace(Array(res.backtrace) + trace)
|
216
|
+
raise res
|
217
|
+
end
|
218
|
+
yield(res) if block_given?
|
219
|
+
end
|
183
220
|
end
|
184
221
|
|
185
222
|
|
@@ -191,45 +228,35 @@ module EventMachine
|
|
191
228
|
f = Fiber.current
|
192
229
|
trace = caller
|
193
230
|
|
194
|
-
|
195
|
-
unless connected?
|
196
|
-
conerr = on(:error) do |e|
|
197
|
-
error("#{e} (#{e.class})")
|
198
|
-
e.set_backtrace(trace + Array(e.backtrace))
|
199
|
-
debug(e.backtrace)
|
200
|
-
conerr = e
|
201
|
-
f.resume(e)
|
202
|
-
end # |e|
|
231
|
+
begin
|
203
232
|
connect
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
return Fiber.yield
|
233
|
+
connection.open_channel do |channel|
|
234
|
+
debug "**** channel open: #{channel}"
|
235
|
+
channel.request_pty(options[:pty] || {}) do |pty,suc|
|
236
|
+
debug "***** pty open: #{pty}; suc: #{suc}"
|
237
|
+
pty.send_channel_request("shell") do |shell,success|
|
238
|
+
if !success
|
239
|
+
f.resume(ConnectionError.new("Failed to create shell").tap{|e| e.set_backtrace(caller) })
|
240
|
+
else
|
241
|
+
debug "***** shell open: #{shell}"
|
242
|
+
@closed = false
|
243
|
+
@shell = shell
|
244
|
+
@shell.on_data do |ch,data|
|
245
|
+
@buffer += data
|
246
|
+
debug("data: #{@buffer.dump}")
|
247
|
+
fire(:data)
|
248
|
+
end
|
249
|
+
Fiber.new { yield(self) if block_given? }.resume
|
250
|
+
f.resume(self)
|
251
|
+
end
|
252
|
+
end # |shell,success|
|
253
|
+
end # |pty,suc|
|
254
|
+
end # |channel|
|
255
|
+
rescue => e
|
256
|
+
raise ConnectionError.new("failed to create shell for #{host}: #{e} (#{e.class})")
|
257
|
+
end
|
258
|
+
|
259
|
+
return Fiber.yield.tap { |r| raise r if r.is_a?(Exception) }
|
233
260
|
end
|
234
261
|
|
235
262
|
# Create a new shell using the same ssh connection.
|
@@ -254,20 +281,19 @@ module EventMachine
|
|
254
281
|
# Does not open the shell; use #open or #split
|
255
282
|
# You generally won't need to call this on your own.
|
256
283
|
def connect
|
257
|
-
return if connected?
|
284
|
+
return @connection if connected?
|
258
285
|
trace = caller
|
259
286
|
f = Fiber.current
|
260
287
|
::EM::Ssh.start(host, user, connect_opts) do |connection|
|
261
288
|
connection.callback do |ssh|
|
262
289
|
f.resume(@connection = ssh)
|
263
|
-
end
|
290
|
+
end
|
264
291
|
connection.errback do |e|
|
265
292
|
e.set_backtrace(trace + Array(e.backtrace))
|
266
|
-
fire(:error, e)
|
267
293
|
f.resume(e)
|
268
|
-
end
|
294
|
+
end
|
269
295
|
end
|
270
|
-
return Fiber.yield
|
296
|
+
return Fiber.yield.tap { |r| raise r if r.is_a?(Exception) }
|
271
297
|
end
|
272
298
|
|
273
299
|
|
data/lib/em-ssh/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: em-ssh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.0
|
5
|
-
prerelease:
|
4
|
+
version: 0.3.0
|
5
|
+
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Caleb Crane
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-07-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: eventmachine
|
@@ -123,3 +123,4 @@ signing_key:
|
|
123
123
|
specification_version: 3
|
124
124
|
summary: An EventMachine compatible net-ssh
|
125
125
|
test_files: []
|
126
|
+
has_rdoc:
|