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 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, 'caleb', "") do |shell|
67
- shell.should be_a(EventMachine::Ssh::Shell)
68
- shell.wait_for(']$')
69
- shell.send_and_wait('uname -a', ']$')
70
- shell.wait_for(']$')
71
- shell.send_and_wait('/sbin/ifconfig -a', ']$')
72
- timer.cancel
73
- EM.stop
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
- #### Synchrony Example
78
- require 'em-ssh/shell'
79
- EM.run {
80
- Fiber.new {
81
- shell = EM::Ssh::Shell.new(host, 'caleb', '')
82
- shell.wait_for(']$')
83
- shell.send_and_wait('sudo su -', 'password for caleb: ')
84
- shell.send_and_wait('password', ']$')
85
- output = shell.send_and_wait('/etc/init.d/openvpn restart', ']$')
86
- # ...
87
- shell.send_and_wait('exit', ']$')
88
- shell.send_data('exit')
89
- }.resume
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 = OptionParser.new
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
- options[:port] = 22
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 = ARGV.shift
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
- commands.each do |command|
52
- mys = shell.split
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 # :childless
70
- end # |shell|
71
- end # EM.run
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
@@ -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.wait_for('@icaleb ~]$')
10
- # interfaces = send_and_wait('/sbin/ifconfig -a', '@icaleb ~]$')
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.send_and_wait('sudo su -', ']$')
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
- if block_given?
79
- Fiber.new { open(&blk).tap{|r| raise r if r.is_a?(Exception) } }.resume
80
- else
81
- open.tap{|r| raise r if r.is_a?(Exception) }
82
- end # block_given?
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
- debug("data_callback invoked when already matched")
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
- res = Fiber.yield
180
- raise(res, res.message, Array(res.backtrace) + trace) if res.is_a?(Exception)
181
- yield(res) if block_given?
182
- res
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
- conerr = nil
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
- end # connected?
205
-
206
- connection || raise(ConnectionError, "failed to create shell for #{host}: #{conerr} (#{conerr.class})")
207
-
208
- connection.open_channel do |channel|
209
- debug "**** channel open: #{channel}"
210
- channel.request_pty(options[:pty] || {}) do |pty,suc|
211
- debug "***** pty open: #{pty}; suc: #{suc}"
212
- pty.send_channel_request("shell") do |shell,success|
213
- unless success
214
- f.resume(ConnectionError.new("Failed to create shell").tap{|e| e.set_backtrace(caller) })
215
- end
216
- conerr && conerr.cancel
217
- debug "***** shell open: #{shell}"
218
- @closed = false
219
- @shell = shell
220
- @shell.on_data do |ch,data|
221
- debug("data: #{@buffer.dump}")
222
- @buffer += data
223
- fire(:data)
224
- end
225
-
226
- Fiber.new { yield(self) if block_given? }.resume
227
- f.resume(self)
228
- end # |shell,success|
229
- end # |pty,suc|
230
- end # |channel|
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 # ssh
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 # err
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
 
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  class Ssh
3
- VERSION='0.3.0.pre1'
3
+ VERSION='0.3.0'
4
4
  end # class::Ssh
5
5
  end # module::EventMachine
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.pre1
5
- prerelease: 6
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-06-04 00:00:00.000000000 Z
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: