em-ssh 0.3.0.pre1 → 0.3.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/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:
|