Class: EventMachine::Ssh::Shell

Inherits:
Object
  • Object
show all
Includes:
EM::Deferrable, Callbacks, Log
Defined in:
lib/em-ssh/shell.rb

Overview

EM::Ssh::Shell encapsulates interaction with a user shell on an SSH server. Shells can be easily and quickly duplicated (#split) without the need to establish another connection. Shells provide :closed, :childless, and :split callbacks.

Examples:

Retrieve the output of ifconfig -a on a server

EM.run{
  shell = EM::Ssh::Shell.new(host, user, password)
  shell.expect('~]$ ')
  interfaces = expect('~]$ ', '/sbin/ifconfig -a')

Start another shell using the same connection

shell.on(:childless) do
  info("#{shell}'s children all closed")
  shell.disconnect
  EM.stop
end

admin_shell = shell.split
admin_shell.on(:closed) { warn("admin shell has closed") }
admin_shell.expect(']$', 'sudo su -')

Constant Summary collapse

TIMEOUT =

Global timeout for wait operations; can be overriden by :timeout option to new

15

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Callbacks

#callbacks, #callbacks=, #fire, #on, #on_next

Methods included from Log

#log

Constructor Details

#initialize(address, user, pass, opts = {}) {|_self| ... } ⇒ Shell

Connect to an ssh server then start a user shell.

Parameters:

  • address (String)
  • user (String)
  • pass (String, nil)

    by default publickey and password auth will be attempted

  • opts (Hash) (defaults to: {})

Options Hash (opts):

  • :net_ssh (Hash)

    options to pass to Net::SSH; see Net::SSH.start

  • :timeout (Fixnum) — default: TIMEOUT

    default timeout for all #wait_for and #send_wait calls

  • :reconnect (Boolean)

    when disconnected reconnect

Yields:

  • (_self)

Yield Parameters:



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/em-ssh/shell.rb', line 74

def initialize(address, user, pass, opts = {}, &blk)
  @timeout         = opts[:timeout].is_a?(Fixnum) ? opts[:timeout] : TIMEOUT
  @host            = address
  @user            = user
  @pass            = pass
  @options         = opts
  @logger          = opts[:logger] if opts[:logger]
  @connect_opts    = {
    password: pass,
    port: opts[:port] || 22,
    auth_methods: ['publickey', 'password'],
    logger: log
  }.merge(opts[:net_ssh] || {})
  @session         = opts[:session]
  @parent          = opts[:parent]
  @children        = []
  @reconnect       = opts[:reconnect]

  # TODO make all methods other than #callback and #errback inaccessible until connected? == true
  yield self if block_given?
  Fiber.new {
    begin
      open
      succeed(self) if connected? && !closed?
    rescue => e
      fail(e)
    end
  }.resume
end

Instance Attribute Details

#childrenArray (readonly)

Returns all shells that have been split off from this one.

Returns:

  • (Array)

    all shells that have been split off from this one.



51
52
53
# File 'lib/em-ssh/shell.rb', line 51

def children
  @children
end

#connect_optsHash (readonly)

Returns the options to pass to connect automatically. They will be extracted from the opptions on initialization.

Returns:

  • (Hash)

    the options to pass to connect automatically. They will be extracted from the opptions on initialization



42
43
44
# File 'lib/em-ssh/shell.rb', line 42

def connect_opts
  @connect_opts
end

#connectionEM::Ssh::Connection (readonly)

Returns:

  • (EM::Ssh::Connection)


38
39
40
# File 'lib/em-ssh/shell.rb', line 38

def connection
  @connection
end

#hostString (readonly)

Returns the host to login to.

Returns:

  • (String)

    the host to login to



45
46
47
# File 'lib/em-ssh/shell.rb', line 45

def host
  @host
end

#optionsHash (readonly)

Returns the options passed to initialize.

Returns:

  • (Hash)

    the options passed to initialize



40
41
42
# File 'lib/em-ssh/shell.rb', line 40

def options
  @options
end

#parentShell (readonly)

Returns the parent of this shell.

Returns:

  • (Shell)

    the parent of this shell



53
54
55
# File 'lib/em-ssh/shell.rb', line 53

def parent
  @parent
end

#passString (readonly)

Returns the password to authenticate with - can be nil.

Returns:

  • (String)

    the password to authenticate with - can be nil



49
50
51
# File 'lib/em-ssh/shell.rb', line 49

def pass
  @pass
end

#sessionEM::Ssh::Session (readonly)

Returns:

  • (EM::Ssh::Session)


36
37
38
# File 'lib/em-ssh/shell.rb', line 36

def session
  @session
end

#shellNet::SSH::Connection::Channel (readonly)

Returns The shell to which we can send_data.

Returns:



34
35
36
# File 'lib/em-ssh/shell.rb', line 34

def shell
  @shell
end

#userString (readonly)

Returns The user to authenticate as.

Returns:

  • (String)

    The user to authenticate as



47
48
49
# File 'lib/em-ssh/shell.rb', line 47

def user
  @user
end

Instance Method Details

#clear_buffer!Object

Remove any data in the buffer.



351
352
353
# File 'lib/em-ssh/shell.rb', line 351

def clear_buffer!
  shell.clear_buffer!
end

#closeObject

Close this shell and all children. Even when a shell is closed it is still connected to the server. Fires :closed event.

See Also:



139
140
141
142
143
144
# File 'lib/em-ssh/shell.rb', line 139

def close
  shell.close.tap{ debug("closing") } if shell && shell.active?
  @closed = true
  children.each { |c| c.close }
  fire(:closed)
end

#closed?Boolean

Returns Has this shell been closed.

Returns:

  • (Boolean)

    Has this shell been closed.



147
148
149
# File 'lib/em-ssh/shell.rb', line 147

def closed?
  @closed == true
end

#connectObject

Connect to the server. Does not open the shell; use #open or #split You generally won’t need to call this on your own.



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/em-ssh/shell.rb', line 264

def connect
  return @session if connected?
  trace = caller
  f = Fiber.current
  ::EM::Ssh.start(host, user, connect_opts) do |connection|
    @connection = connection
    connection.callback do |ssh|
      f.resume(@session = ssh) if f.alive?
    end
    connection.errback do |e|
      e.set_backtrace(trace + Array(e.backtrace))
      f.resume(e) if f.alive?
    end
  end
  return Fiber.yield.tap { |r| raise r if r.is_a?(Exception) }
end

#connected?Boolean

Returns true if the session is still alive.

Returns:

  • (Boolean)

    true if the session is still alive



130
131
132
# File 'lib/em-ssh/shell.rb', line 130

def connected?
  session && !session.closed?
end

#debug(msg = nil, &blk) ⇒ Object



355
356
357
# File 'lib/em-ssh/shell.rb', line 355

def debug(msg = nil, &blk)
  super("#{host} #{msg}", &blk)
end

#disconnect(timeout = nil) ⇒ Object

Close the connection to the server and all child shells. Disconnected shells cannot be split.



111
112
113
114
115
116
117
118
119
# File 'lib/em-ssh/shell.rb', line 111

def disconnect(timeout = nil)
  if timeout
    EM::Timer.new(timeout) { disconnect! }
  end
  close
  @session && @session.close
  @shell = nil
  disconnect!
end

#disconnect!Object



121
122
123
124
125
126
127
# File 'lib/em-ssh/shell.rb', line 121

def disconnect!
  @session = nil
  if @connection
    @connection.close_connection
    @connection = nil
  end
end

#error(msg = nil, &blk) ⇒ Object



371
372
373
# File 'lib/em-ssh/shell.rb', line 371

def error(msg = nil, &blk)
  super("#{host} #{msg}", &blk)
end

#expect(strregex, send_str = nil, opts = {}, &blk) ⇒ Shell, String

Wait for a number of seconds until a specified string or regexp is matched by the data returned from the ssh connection. Optionally send a given string first.

If a block is not provided the current Fiber will yield until strregex matches or :timeout # is reached.

If a block is provided expect will return immediately.

Examples:

expect a prompt

expect(' ~]$ ')

send a command and wait for a prompt

expect(' ~]$ ', '/sbin/ifconfig')

expect a prompt and within 5 seconds

expect(' ~]$ ', :timeout => 5)

send a command and wait up to 10 seconds for a prompt

expect(' ~]$ ', '/etc/sysconfig/openvpn restart', :timeout => 10)

Parameters:

  • strregex (String, Regexp)

    to match against

  • send_str (String) (defaults to: nil)

    the data to send before waiting

  • opts (Hash) (defaults to: {})

Options Hash (opts):

  • :timeout (Fixnum) — default: @timeout

    number of seconds to wait when there is no activity

Returns:

  • (Shell, String)

    all data received up to an including strregex if a block is not provided. the Shell if a block is provided



310
311
312
313
314
315
316
# File 'lib/em-ssh/shell.rb', line 310

def expect(strregex, send_str = nil, opts = {}, &blk)
  assert_channel!
  shell.expect(strregex,
               send_str,
               {:timeout => @timeout, :log => self }.merge(opts),
              &blk)
end

#fatal(msg = nil, &blk) ⇒ Object



363
364
365
# File 'lib/em-ssh/shell.rb', line 363

def fatal(msg = nil, &blk)
  super("#{host} #{msg}", &blk)
end

#info(msg = nil, &blk) ⇒ Object



359
360
361
# File 'lib/em-ssh/shell.rb', line 359

def info(msg = nil, &blk)
  super("#{host} #{msg}", &blk)
end

#line_terminatorString

Returns a string (rn) to append to every command.

Returns:

  • (String)

    a string (rn) to append to every command



56
57
58
# File 'lib/em-ssh/shell.rb', line 56

def line_terminator
  shell ? shell.line_terminator : "\n"
end

#line_terminator=(lt) ⇒ Object

@param lt a string (rn) to append to every command



61
62
63
64
# File 'lib/em-ssh/shell.rb', line 61

def line_terminator=(lt)
  @line_terminator = lt
  shell.line_terminator = lt if shell
end

#open(&blk) ⇒ self, Exception

Open a shell on the server. You generally don’t need to call this.

Returns:

  • (self, Exception)


160
161
162
163
164
165
166
167
168
169
170
# File 'lib/em-ssh/shell.rb', line 160

def open(&blk)
  f       = Fiber.current
  trace   = caller
  on_open do |s|
    Fiber.new { yield(self) if block_given? }.resume
    f.resume(self)
  end
  @on_open_err = proc { |e| f.resume(e) }
  open!
  return Fiber.yield.tap { |r| raise r if r.is_a?(Exception) }
end

#open?Boolean

Returns Is the shell open?.

Returns:

  • (Boolean)

    Is the shell open?



153
154
155
# File 'lib/em-ssh/shell.rb', line 153

def open?
  !closed? && @shell
end

#reconnect?Boolean

Returns true if the connection should be automatically re-established; default: false.

Returns:

  • (Boolean)

    true if the connection should be automatically re-established; default: false



105
106
107
# File 'lib/em-ssh/shell.rb', line 105

def reconnect?
  @reconnect == true
end

#send_and_wait(send_str, wait_str = nil, opts = {}) ⇒ String

Send a string to the server and wait for a response containing a specified String or Regex.

Parameters:

  • send_str (String)

Returns:

  • (String)

    all data in the buffer including the wait_str if it was found



321
322
323
324
325
326
# File 'lib/em-ssh/shell.rb', line 321

def send_and_wait(send_str, wait_str = nil, opts = {})
  assert_channel!
  shell.send_and_wait(send_str,
                      wait_str,
                      {:timeout => @timeout, :log => self }.merge(opts))
end

#send_data(d, send_newline = true) ⇒ Object

Send data to the ssh server shell. You generally don’t need to call this.

Parameters:

  • d (String)

    the data to send encoded as a string

See Also:



332
333
334
335
# File 'lib/em-ssh/shell.rb', line 332

def send_data(d, send_newline=true)
  assert_channel!
  shell.send_data(d, send_newline)
end

#split {|Shell| ... } ⇒ Shell

Create a new shell using the same ssh connection. A connection will be established if this shell is not connected.

If a block is provided the call to split must be inside of a Fiber. The child will be closed after yielding. The block will not be yielded until the remote PTY has been opened.

Yields:

Returns:



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/em-ssh/shell.rb', line 242

def split
  connect unless connected?
  child = self.class.new(host, user, pass, {:session => session, :parent => self}.merge(options))
  child.line_terminator = line_terminator
  children.push(child)
  child.on(:closed) do
    children.delete(child)
    fire(:childless).tap{ info("fired :childless") } if children.empty?
  end
  fire(:split, child)
  if block_given?
    # requires that the caller be in a Fiber
    child.open
    yield(child).tap { child.close }
  else
    child
  end
end

#wait_for(strregex, opts = { }) ⇒ String

Wait for the shell to send data containing the given string.

Parameters:

  • strregex (String, Regexp)

    a string or regex to match the console output against.

  • opts (Hash) (defaults to: { })

Options Hash (opts):

  • :timeout (Fixnum) — default: Session::TIMEOUT

    the maximum number of seconds to wait

Returns:

  • (String)

    the contents of the buffer or a TimeoutError

Raises:

  • Disconnected

  • ClosedChannel

  • TimeoutError



345
346
347
348
# File 'lib/em-ssh/shell.rb', line 345

def wait_for(strregex, opts = { })
  assert_channel!
  shell.wait_for(strregex, {:timeout => @timeout, :log => self }.merge(opts))
end

#warn(msg = nil, &blk) ⇒ Object



367
368
369
# File 'lib/em-ssh/shell.rb', line 367

def warn(msg = nil, &blk)
  super("#{host} #{msg}", &blk)
end