Class Net::SSH::Multi::Session
In: lib/net/ssh/multi/session.rb
Parent: Object

Represents a collection of connections to various servers. It provides an interface for organizing the connections (group), as well as a way to scope commands to a subset of all connections (with). You can also provide a default gateway connection that servers should use when connecting (via). It exposes an interface similar to Net::SSH::Connection::Session for opening SSH channels and executing commands, allowing for these operations to be done in parallel across multiple connections.

  Net::SSH::Multi.start do |session|
    # access servers via a gateway
    session.via 'gateway', 'gateway-user'

    # define the servers we want to use
    session.use 'user1@host1'
    session.use 'user2@host2'

    # define servers in groups for more granular access
    session.group :app do
      session.use 'user@app1'
      session.use 'user@app2'
    end

    # execute commands on all servers
    session.exec "uptime"

    # execute commands on a subset of servers
    session.with(:app).exec "hostname"

    # run the aggregated event loop
    session.loop
  end

Note that connections are established lazily, as soon as they are needed. You can force the connections to be opened immediately, though, using the connect! method.

Concurrent Connection Limiting

Sometimes you may be dealing with a large number of servers, and if you try to have connections open to all of them simultaneously you‘ll run into open file handle limitations and such. If this happens to you, you can set the concurrent_connections property of the session. Net::SSH::Multi will then ensure that no more than this number of connections are ever open simultaneously.

  Net::SSH::Multi.start(:concurrent_connections => 5) do |session|
    # ...
  end

Opening channels and executing commands will still work exactly as before, but Net::SSH::Multi will transparently close finished connections and open pending ones.

Controlling Connection Errors

By default, Net::SSH::Multi will raise an exception if a connection error occurs when connecting to a server. This will typically bubble up and abort the entire connection process. Sometimes, however, you might wish to ignore connection errors, for instance when starting a daemon on a large number of boxes and you know that some of the boxes are going to be unavailable.

To do this, simply set the on_error property of the session to :ignore (or to :warn, if you want a warning message when a connection attempt fails):

  Net::SSH::Multi.start(:on_error => :ignore) do |session|
    # ...
  end

The default is :fail, which causes the exception to bubble up. Additionally, you can specify a Proc object as the value for on_error, which will be invoked with the server in question if the connection attempt fails. You can force the connection attempt to retry by throwing the :go symbol, with :retry as the payload, or force the exception to be reraised by throwing :go with :raise as the payload:

  handler = Proc.new do |server|
    server[:connection_attempts] ||= 0
    if server[:connection_attempts] < 3
      server[:connection_attempts] += 1
      throw :go, :retry
    else
      throw :go, :raise
    end
  end

  Net::SSH::Multi.start(:on_error => handler) do |session|
    # ...
  end

Any other thrown value (or no thrown value at all) will result in the failure being ignored.

Lazily Evaluated Server Definitions

Sometimes you might be dealing with an environment where you don‘t know the names or addresses of the servers until runtime. You can certainly dynamically build server names and pass them to use, but if the operation to determine the server names is expensive, you might want to defer it until the server is actually needed (especially if the logic of your program is such that you might not even need to connect to that server every time the program runs).

You can do this by passing a block to use:

  session.use do |opt|
    lookup_ip_address_of_remote_host
  end

See use for more information about this usage.

Methods

close   group   loop   new   on   process   servers   servers_for   use   via   with  

Included Modules

SessionActions

External Aliases

loop -> loop_forever

Attributes

concurrent_connections  [RW]  The number of allowed concurrent connections. No more than this number of sessions will be open at any given time.
default_gateway  [R]  The default Net::SSH::Gateway instance to use to connect to the servers. If nil, no default gateway will be used.
default_user  [RW]  The default user name to use when connecting to a server. If a user name is not given for a particular server, this value will be used. It defaults to ENV[‘USER’] || ENV[‘USERNAME’], or "unknown" if neither of those are set.
groups  [R]  The hash of group definitions, mapping each group name to a corresponding Net::SSH::Multi::ServerList.
on_error  [RW]  How connection errors should be handled. This defaults to :fail, but may be set to :ignore if connection errors should be ignored, or :warn if connection errors should cause a warning.
server_list  [R]  The Net::SSH::Multi::ServerList managed by this session.

Public Class methods

Creates a new Net::SSH::Multi::Session instance. Initially, it contains no server definitions, no group definitions, and no default gateway.

You can set the concurrent_connections property in the options. Setting it to nil (the default) will cause Net::SSH::Multi to ignore any concurrent connection limit and allow all defined sessions to be open simultaneously. Setting it to an integer will cause Net::SSH::Multi to allow no more than that number of concurrently open sessions, opening subsequent sessions only when other sessions finish and close.

  Net::SSH::Multi.start(:concurrent_connections => 10) do |session|
    session.use ...
  end

[Source]

     # File lib/net/ssh/multi/session.rb, line 171
171:     def initialize(options={})
172:       @server_list = ServerList.new
173:       @groups = Hash.new { |h,k| h[k] = ServerList.new }
174:       @gateway = nil
175:       @open_groups = []
176:       @connect_threads = []
177:       @on_error = :fail
178:       @default_user = ENV['USER'] || ENV['USERNAME'] || "unknown"
179: 
180:       @open_connections = 0
181:       @pending_sessions = []
182:       @session_mutex = Mutex.new
183: 
184:       options.each { |opt, value| send("#{opt}=", value) }
185:     end

Public Instance methods

Closes the multi-session by shutting down all open server sessions, and the default gateway (if one was specified using via). Note that other gateway connections (e.g., those passed to use directly) will not be closed by this method, and must be managed externally.

[Source]

     # File lib/net/ssh/multi/session.rb, line 402
402:     def close
403:       server_list.each { |server| server.close_channels }
404:       loop(0) { busy?(true) }
405:       server_list.each { |server| server.close }
406:       default_gateway.shutdown! if default_gateway
407:     end

At its simplest, this associates a named group with a server definition. It can be used in either of two ways:

First, you can use it to associate a group (or array of groups) with a server definition (or array of server definitions). The server definitions must already exist in the server_list array (typically by calling use):

  server1 = session.use('host1', 'user1')
  server2 = session.use('host2', 'user2')
  session.group :app => server1, :web => server2
  session.group :staging => [server1, server2]
  session.group %w(xen linux) => server2
  session.group %w(rackspace backup) => [server1, server2]

Secondly, instead of a mapping of groups to servers, you can just provide a list of group names, and then a block. Inside the block, any calls to use will automatically associate the new server definition with those groups. You can nest group calls, too, which will aggregate the group definitions.

  session.group :rackspace, :backup do
    session.use 'host1', 'user1'
    session.group :xen do
      session.use 'host2', 'user2'
    end
  end

[Source]

     # File lib/net/ssh/multi/session.rb, line 213
213:     def group(*args)
214:       mapping = args.last.is_a?(Hash) ? args.pop : {}
215: 
216:       if mapping.any? && block_given?
217:         raise ArgumentError, "must provide group mapping OR block, not both"
218:       elsif block_given?
219:         begin
220:           saved_groups = open_groups.dup
221:           open_groups.concat(args.map { |a| a.to_sym }).uniq!
222:           yield self
223:         ensure
224:           open_groups.replace(saved_groups)
225:         end
226:       else
227:         mapping.each do |key, value|
228:           (open_groups + Array(key)).uniq.each do |grp|
229:             groups[grp.to_sym].concat(Array(value))
230:           end
231:         end
232:       end
233:     end

Run the aggregated event loop for all open server sessions, until the given block returns false. If no block is given, the loop will run for as long as busy? returns true (in other words, for as long as there are any (non-invisible) channels open).

[Source]

     # File lib/net/ssh/multi/session.rb, line 415
415:     def loop(wait=nil, &block)
416:       running = block || Proc.new { |c| busy? }
417:       loop_forever { break unless process(wait, &running) }
418:     end

Works as with, but for specific servers rather than groups. It will return a new subsession (Net::SSH::Multi::Subsession) consisting of the given servers. (Note that it requires that the servers in question have been created via calls to use on this session object, or things will not work quite right.) If a block is given, the new subsession will also be yielded to the block.

  srv1 = session.use('host1', 'user')
  srv2 = session.use('host2', 'user')
  # ...
  session.on(srv1, srv2).exec('hostname')

[Source]

     # File lib/net/ssh/multi/session.rb, line 392
392:     def on(*servers)
393:       subsession = Subsession.new(self, servers)
394:       yield subsession if block_given?
395:       subsession
396:     end

Run a single iteration of the aggregated event loop for all open server sessions. The wait parameter indicates how long to wait for an event to appear on any of the different sessions; nil (the default) means "wait forever". If the block is given, then it will be used to determine whether process returns true (the block did not return false), or false (the block returned false).

[Source]

     # File lib/net/ssh/multi/session.rb, line 426
426:     def process(wait=nil, &block)
427:       realize_pending_connections!
428:       wait = @connect_threads.any? ? 0 : wait
429: 
430:       return false unless preprocess(&block)
431: 
432:       readers = server_list.map { |s| s.readers }.flatten
433:       writers = server_list.map { |s| s.writers }.flatten
434: 
435:       readers, writers, = IO.select(readers, writers, nil, wait)
436: 
437:       if readers
438:         return postprocess(readers, writers)
439:       else
440:         return true
441:       end
442:     end

Essentially an alias for servers_for without any arguments. This is used primarily to satistfy the expectations of the Net::SSH::Multi::SessionActions module.

[Source]

     # File lib/net/ssh/multi/session.rb, line 293
293:     def servers
294:       servers_for
295:     end

Returns the set of servers that match the given criteria. It can be used in any (or all) of three ways.

First, you can omit any arguments. In this case, the full list of servers will be returned.

  all = session.servers_for

Second, you can simply specify a list of group names. All servers in all named groups will be returned. If a server belongs to multiple matching groups, then it will appear only once in the list (the resulting list will contain only unique servers).

  servers = session.servers_for(:app, :db)

Last, you can specify a hash with group names as keys, and property constraints as the values. These property constraints are either "only" constraints (which restrict the set of servers to "only" those that match the given properties) or "except" constraints (which restrict the set of servers to those whose properties do not match). Properties are described when the server is defined (via the :properties key):

  session.group :db do
    session.use 'dbmain', 'user', :properties => { :primary => true }
    session.use 'dbslave', 'user2'
    session.use 'dbslve2', 'user2'
  end

  # return ONLY on the servers in the :db group which have the :primary
  # property set to true.
  primary = session.servers_for(:db => { :only => { :primary => true } })

You can, naturally, combine these methods:

  # all servers in :app and :web, and all servers in :db with the
  # :primary property set to true
  servers = session.servers_for(:app, :web, :db => { :only => { :primary => true } })

[Source]

     # File lib/net/ssh/multi/session.rb, line 334
334:     def servers_for(*criteria)
335:       if criteria.empty?
336:         server_list.flatten
337:       else
338:         # normalize the criteria list, so that every entry is a key to a
339:         # criteria hash (possibly empty).
340:         criteria = criteria.inject({}) do |hash, entry|
341:           case entry
342:           when Hash then hash.merge(entry)
343:           else hash.merge(entry => {})
344:           end
345:         end
346: 
347:         list = criteria.inject([]) do |aggregator, (group, properties)|
348:           raise ArgumentError, "the value for any group must be a Hash, but got a #{properties.class} for #{group.inspect}" unless properties.is_a?(Hash)
349:           bad_keys = properties.keys - [:only, :except]
350:           raise ArgumentError, "unknown constraint(s) #{bad_keys.inspect} for #{group.inspect}" unless bad_keys.empty?
351: 
352:           servers = groups[group].select do |server|
353:             (properties[:only] || {}).all? { |prop, value| server[prop] == value } &&
354:             !(properties[:except] || {}).any? { |prop, value| server[prop] == value }
355:           end
356: 
357:           aggregator.concat(servers)
358:         end
359: 
360:         list.uniq
361:       end
362:     end

Defines a new server definition, to be managed by this session. The server is at the given host, and will be connected to as the given user. The other options are passed as-is to the Net::SSH session constructor.

If a default gateway has been specified previously (with via) it will be passed to the new server definition. You can override this by passing a different Net::SSH::Gateway instance (or nil) with the :via key in the options.

  session.use 'host'
  session.use 'user@host2', :via => nil
  session.use 'host3', :user => "user3", :via => Net::SSH::Gateway.new('gateway.host', 'user')

If only a single host is given, the new server instance is returned. You can give multiple hosts at a time, though, in which case an array of server instances will be returned.

  server1, server2 = session.use "host1", "host2"

If given a block, this will save the block as a Net::SSH::Multi::DynamicServer definition, to be evaluated lazily the first time the server is needed. The block will recive any options hash given to use, and should return nil (if no servers are to be added), a String or an array of Strings (to be interpreted as a connection specification), or a Server or an array of Servers.

[Source]

     # File lib/net/ssh/multi/session.rb, line 274
274:     def use(*hosts, &block)
275:       options = hosts.last.is_a?(Hash) ? hosts.pop : {}
276:       options = { :via => default_gateway }.merge(options)
277: 
278:       results = hosts.map do |host|
279:         server_list.add(Server.new(self, host, options))
280:       end
281: 
282:       if block
283:         results << server_list.add(DynamicServer.new(self, options, block))
284:       end
285: 
286:       group [] => results
287:       results.length > 1 ? results : results.first
288:     end

Sets up a default gateway to use when establishing connections to servers. Note that any servers defined prior to this invocation will not use the default gateway; it only affects servers defined subsequently.

  session.via 'gateway.host', 'user'

You may override the default gateway on a per-server basis by passing the :via key to the use method; see use for details.

[Source]

     # File lib/net/ssh/multi/session.rb, line 243
243:     def via(host, user, options={})
244:       @default_gateway = Net::SSH::Gateway.new(host, user, options)
245:       self
246:     end

Returns a new Net::SSH::Multi::Subsession instance consisting of the servers that meet the given criteria. If a block is given, the subsession will be yielded to it. See servers_for for a discussion of how these criteria are interpreted.

  session.with(:app).exec('hostname')

  session.with(:app, :db => { :primary => true }) do |s|
    s.exec 'date'
    s.exec 'uptime'
  end

[Source]

     # File lib/net/ssh/multi/session.rb, line 375
375:     def with(*groups)
376:       subsession = Subsession.new(self, servers_for(*groups))
377:       yield subsession if block_given?
378:       subsession
379:     end

[Validate]