summaryrefslogtreecommitdiff
path: root/pkg/net-ssh-multi-1.1/lib/net
diff options
context:
space:
mode:
authordelano <delano.mandelbaum@gmail.com>2013-02-06 08:04:41 -0800
committerdelano <delano.mandelbaum@gmail.com>2013-02-06 08:04:41 -0800
commit345d20dd3f993b84bbdae265d4a156a7d87fbf28 (patch)
treeac7aa1eb480b727ed07f0d176130d0ad806abb27 /pkg/net-ssh-multi-1.1/lib/net
parent73d75b2c3a5465392fd6210dbb844c49dd043cdc (diff)
downloadnet-ssh-multi-345d20dd3f993b84bbdae265d4a156a7d87fbf28.tar.gz
Updated docs
Diffstat (limited to 'pkg/net-ssh-multi-1.1/lib/net')
-rw-r--r--pkg/net-ssh-multi-1.1/lib/net/ssh/multi.rb71
-rw-r--r--pkg/net-ssh-multi-1.1/lib/net/ssh/multi/channel.rb230
-rw-r--r--pkg/net-ssh-multi-1.1/lib/net/ssh/multi/channel_proxy.rb50
-rw-r--r--pkg/net-ssh-multi-1.1/lib/net/ssh/multi/dynamic_server.rb71
-rw-r--r--pkg/net-ssh-multi-1.1/lib/net/ssh/multi/pending_connection.rb112
-rw-r--r--pkg/net-ssh-multi-1.1/lib/net/ssh/multi/server.rb231
-rw-r--r--pkg/net-ssh-multi-1.1/lib/net/ssh/multi/server_list.rb80
-rw-r--r--pkg/net-ssh-multi-1.1/lib/net/ssh/multi/session.rb551
-rw-r--r--pkg/net-ssh-multi-1.1/lib/net/ssh/multi/session_actions.rb153
-rw-r--r--pkg/net-ssh-multi-1.1/lib/net/ssh/multi/subsession.rb48
-rw-r--r--pkg/net-ssh-multi-1.1/lib/net/ssh/multi/version.rb21
11 files changed, 1618 insertions, 0 deletions
diff --git a/pkg/net-ssh-multi-1.1/lib/net/ssh/multi.rb b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi.rb
new file mode 100644
index 0000000..1664d0e
--- /dev/null
+++ b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi.rb
@@ -0,0 +1,71 @@
+require 'net/ssh/multi/session'
+
+module Net; module SSH
+ # Net::SSH::Multi is a library for controlling multiple Net::SSH
+ # connections via a single interface. It exposes an API similar to that of
+ # Net::SSH::Connection::Session and Net::SSH::Connection::Channel, making it
+ # simpler to adapt programs designed for single connections to be used with
+ # multiple connections.
+ #
+ # This library is particularly useful for automating repetitive tasks that
+ # must be performed on multiple machines. It executes the commands in
+ # parallel, and allows commands to be executed on subsets of servers
+ # (defined by groups).
+ #
+ # require 'net/ssh/multi'
+ #
+ # 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
+ #
+ # See Net::SSH::Multi::Session for more documentation.
+ module Multi
+ # This is a convenience method for instantiating a new
+ # Net::SSH::Multi::Session. If a block is given, the session will be
+ # yielded to the block automatically closed (see Net::SSH::Multi::Session#close)
+ # when the block finishes. Otherwise, the new session will be returned.
+ #
+ # Net::SSH::Multi.start do |session|
+ # # ...
+ # end
+ #
+ # session = Net::SSH::Multi.start
+ # # ...
+ # session.close
+ #
+ # Any options are passed directly to Net::SSH::Multi::Session.new (q.v.).
+ def self.start(options={})
+ session = Session.new(options)
+
+ if block_given?
+ begin
+ yield session
+ session.loop
+ session.close
+ end
+ else
+ return session
+ end
+ end
+ end
+end; end \ No newline at end of file
diff --git a/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/channel.rb b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/channel.rb
new file mode 100644
index 0000000..34bd117
--- /dev/null
+++ b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/channel.rb
@@ -0,0 +1,230 @@
+module Net; module SSH; module Multi
+ # Net::SSH::Multi::Channel encapsulates a collection of Net::SSH::Connection::Channel
+ # instances from multiple different connections. It allows for operations to
+ # be performed on all contained channels, simultaneously, using an interface
+ # mostly identical to Net::SSH::Connection::Channel itself.
+ #
+ # You typically obtain a Net::SSH::Multi::Channel instance via
+ # Net::SSH::Multi::Session#open_channel or Net::SSH::Multi::Session#exec,
+ # though there is nothing stopping you from instantiating one yourself with
+ # a handful of Net::SSH::Connection::Channel objects (though they should be
+ # associated with connections managed by a Net::SSH::Multi::Session object
+ # for consistent behavior).
+ #
+ # channel = session.open_channel do |ch|
+ # # ...
+ # end
+ #
+ # channel.wait
+ class Channel
+ include Enumerable
+
+ # The Net::SSH::Multi::Session instance that controls this channel collection.
+ attr_reader :connection
+
+ # The collection of Net::SSH::Connection::Channel instances that this multi-channel aggregates.
+ attr_reader :channels
+
+ # A Hash of custom properties that may be set and queried on this object.
+ attr_reader :properties
+
+ # Instantiate a new Net::SSH::Multi::Channel instance, controlled by the
+ # given +connection+ (a Net::SSH::Multi::Session object) and wrapping the
+ # given +channels+ (Net::SSH::Connection::Channel instances).
+ #
+ # You will typically never call this directly; rather, you'll get your
+ # multi-channel references via Net::SSH::Multi::Session#open_channel and
+ # friends.
+ def initialize(connection, channels)
+ @connection = connection
+ @channels = channels
+ @properties = {}
+ end
+
+ # Iterate over each component channel object, yielding each in order to the
+ # associated block.
+ def each
+ @channels.each { |channel| yield channel }
+ end
+
+ # Retrieve the property (see #properties) with the given +key+.
+ #
+ # host = channel[:host]
+ def [](key)
+ @properties[key]
+ end
+
+ # Set the property (see #properties) with the given +key+ to the given
+ # +value+.
+ #
+ # channel[:visited] = true
+ def []=(key, value)
+ @properties[key] = value
+ end
+
+ # Perform an +exec+ command on all component channels. The block, if given,
+ # is passed to each component channel, so it will (potentially) be invoked
+ # once for every channel in the collection. The block will receive two
+ # parameters: the specific channel object being operated on, and a boolean
+ # indicating whether the exec succeeded or not.
+ #
+ # channel.exec "ls -l" do |ch, success|
+ # # ...
+ # end
+ #
+ # See the documentation in Net::SSH for Net::SSH::Connection::Channel#exec
+ # for more information on how to work with the callback.
+ def exec(command, &block)
+ channels.each { |channel| channel.exec(command, &block) }
+ self
+ end
+
+ # Perform a +request_pty+ command on all component channels. The block, if
+ # given, is passed to each component channel, so it will (potentially) be
+ # invoked once for every channel in the collection. The block will
+ # receive two parameters: the specific channel object being operated on,
+ # and a boolean indicating whether the pty request succeeded or not.
+ #
+ # channel.request_pty do |ch, success|
+ # # ...
+ # end
+ #
+ # See the documentation in Net::SSH for
+ # Net::SSH::Connection::Channel#request_pty for more information on how to
+ # work with the callback.
+ def request_pty(opts={}, &block)
+ channels.each { |channel| channel.request_pty(opts, &block) }
+ self
+ end
+
+ # Send the given +data+ to each component channel. It will be sent to the
+ # remote process, typically being received on the process' +stdin+ stream.
+ #
+ # channel.send_data "password\n"
+ def send_data(data)
+ channels.each { |channel| channel.send_data(data) }
+ self
+ end
+
+ # Returns true as long as any of the component channels are active.
+ #
+ # connection.loop { channel.active? }
+ def active?
+ channels.any? { |channel| channel.active? }
+ end
+
+ # Runs the connection's event loop until the channel is no longer active
+ # (see #active?).
+ #
+ # channel.exec "something"
+ # channel.wait
+ def wait
+ connection.loop { active? }
+ self
+ end
+
+ # Closes all component channels.
+ def close
+ channels.each { |channel| channel.close }
+ self
+ end
+
+ # Tells the remote process for each component channel not to expect any
+ # further data from this end of the channel.
+ def eof!
+ channels.each { |channel| channel.eof! }
+ self
+ end
+
+ # Registers a callback on all component channels, to be invoked when the
+ # remote process emits data (usually on its +stdout+ stream). The block
+ # will be invoked with two arguments: the specific channel object, and the
+ # data that was received.
+ #
+ # channel.on_data do |ch, data|
+ # puts "got data: #{data}"
+ # end
+ def on_data(&block)
+ channels.each { |channel| channel.on_data(&block) }
+ self
+ end
+
+ # Registers a callback on all component channels, to be invoked when the
+ # remote process emits "extended" data (typically on its +stderr+ stream).
+ # The block will be invoked with three arguments: the specific channel
+ # object, an integer describing the data type (usually a 1 for +stderr+)
+ # and the data that was received.
+ #
+ # channel.on_extended_data do |ch, type, data|
+ # puts "got extended data: #{data}"
+ # end
+ def on_extended_data(&block)
+ channels.each { |channel| channel.on_extended_data(&block) }
+ self
+ end
+
+ # Registers a callback on all component channels, to be invoked during the
+ # idle portion of the connection event loop. The callback will be invoked
+ # with one argument: the specific channel object being processed.
+ #
+ # channel.on_process do |ch|
+ # # ...
+ # end
+ def on_process(&block)
+ channels.each { |channel| channel.on_process(&block) }
+ self
+ end
+
+ # Registers a callback on all component channels, to be invoked when the
+ # remote server terminates the channel. The callback will be invoked
+ # with one argument: the specific channel object being closed.
+ #
+ # channel.on_close do |ch|
+ # # ...
+ # end
+ def on_close(&block)
+ channels.each { |channel| channel.on_close(&block) }
+ self
+ end
+
+ # Registers a callback on all component channels, to be invoked when the
+ # remote server has no further data to send. The callback will be invoked
+ # with one argument: the specific channel object being marked EOF.
+ #
+ # channel.on_eof do |ch|
+ # # ...
+ # end
+ def on_eof(&block)
+ channels.each { |channel| channel.on_eof(&block) }
+ self
+ end
+
+ # Registers a callback on all component channels, to be invoked when the
+ # remote server is unable to open the channel. The callback will be
+ # invoked with three arguments: the channel object that couldn't be
+ # opened, a description of the error (as a string), and an integer code
+ # representing the error.
+ #
+ # channel.on_open_failed do |ch, description, code|
+ # # ...
+ # end
+ def on_open_failed(&block)
+ channels.each { |channel| channel.on_open_failed(&block) }
+ self
+ end
+
+ # Registers a callback on all component channels, to be invoked when the
+ # remote server sends a channel request of the given +type+. The callback
+ # will be invoked with two arguments: the specific channel object receiving
+ # the request, and a Net::SSH::Buffer instance containing the request-specific
+ # data.
+ #
+ # channel.on_request("exit-status") do |ch, data|
+ # puts "exited with #{data.read_long}"
+ # end
+ def on_request(type, &block)
+ channels.each { |channel| channel.on_request(type, &block) }
+ self
+ end
+ end
+end; end; end \ No newline at end of file
diff --git a/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/channel_proxy.rb b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/channel_proxy.rb
new file mode 100644
index 0000000..df5c04c
--- /dev/null
+++ b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/channel_proxy.rb
@@ -0,0 +1,50 @@
+module Net; module SSH; module Multi
+
+ # The ChannelProxy is a delegate class that represents a channel that has
+ # not yet been opened. It is only used when Net::SSH::Multi is running with
+ # with a concurrent connections limit (see Net::SSH::Multi::Session#concurrent_connections).
+ #
+ # You'll never need to instantiate one of these directly, and will probably
+ # (if all goes well!) never even notice when one of these is in use. Essentially,
+ # it is spawned by a Net::SSH::Multi::PendingConnection when the pending
+ # connection is asked to open a channel. Any actions performed on the
+ # channel proxy will then be recorded, until a real channel is set as the
+ # delegate (see #delegate_to). At that point, all recorded actions will be
+ # replayed on the channel, and any subsequent actions will be immediately
+ # delegated to the channel.
+ class ChannelProxy
+ # This is the "on confirm" callback that gets called when the real channel
+ # is opened.
+ attr_reader :on_confirm
+
+ # Instantiates a new channel proxy with the given +on_confirm+ callback.
+ def initialize(&on_confirm)
+ @on_confirm = on_confirm
+ @recordings = []
+ @channel = nil
+ end
+
+ # Instructs the proxy to delegate all further actions to the given +channel+
+ # (which must be an instance of Net::SSH::Connection::Channel). All recorded
+ # actions are immediately replayed, in order, against the delegate channel.
+ def delegate_to(channel)
+ @channel = channel
+ @recordings.each do |sym, args, block|
+ @channel.__send__(sym, *args, &block)
+ end
+ end
+
+ # If a channel delegate has been specified (see #delegate_to), the method
+ # will be immediately sent to the delegate. Otherwise, the call is added
+ # to the list of recorded method calls, to be played back when a delegate
+ # is specified.
+ def method_missing(sym, *args, &block)
+ if @channel
+ @channel.__send__(sym, *args, &block)
+ else
+ @recordings << [sym, args, block]
+ end
+ end
+ end
+
+end; end; end \ No newline at end of file
diff --git a/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/dynamic_server.rb b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/dynamic_server.rb
new file mode 100644
index 0000000..cbdde2b
--- /dev/null
+++ b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/dynamic_server.rb
@@ -0,0 +1,71 @@
+require 'net/ssh/multi/server'
+
+module Net; module SSH; module Multi
+
+ # Represents a lazily evaluated collection of servers. This will usually be
+ # created via Net::SSH::Multi::Session#use(&block), and is useful for creating
+ # server definitions where the name or address of the servers are not known
+ # until run-time.
+ #
+ # session.use { lookup_ip_address_of_server }
+ #
+ # This prevents +lookup_ip_address_of_server+ from being invoked unless the
+ # server is actually needed, at which point it is invoked and the result
+ # cached.
+ #
+ # The callback should return either +nil+ (in which case no new servers are
+ # instantiated), a String (representing a connection specification), an
+ # array of Strings, or an array of Net::SSH::Multi::Server instances.
+ class DynamicServer
+ # The Net::SSH::Multi::Session instance that owns this dynamic server record.
+ attr_reader :master
+
+ # The Proc object to call to evaluate the server(s)
+ attr_reader :callback
+
+ # The hash of options that will be used to initialize the server records.
+ attr_reader :options
+
+ # Create a new DynamicServer record, owned by the given Net::SSH::Multi::Session
+ # +master+, with the given hash of +options+, and using the given Proc +callback+
+ # to lazily evaluate the actual server instances.
+ def initialize(master, options, callback)
+ @master, @options, @callback = master, options, callback
+ @servers = nil
+ end
+
+ # Returns the value for the given +key+ in the :properties hash of the
+ # +options+. If no :properties hash exists in +options+, this returns +nil+.
+ def [](key)
+ (options[:properties] ||= {})[key]
+ end
+
+ # Sets the given key/value pair in the +:properties+ key in the options
+ # hash. If the options hash has no :properties key, it will be created.
+ def []=(key, value)
+ (options[:properties] ||= {})[key] = value
+ end
+
+ # Iterates over every instantiated server record in this dynamic server.
+ # If the servers have not yet been instantiated, this does nothing (e.g.,
+ # it does _not_ automatically invoke #evaluate!).
+ def each
+ (@servers || []).each { |server| yield server }
+ end
+
+ # Evaluates the callback and instantiates the servers, memoizing the result.
+ # Subsequent calls to #evaluate! will simply return the cached list of
+ # servers.
+ def evaluate!
+ @servers ||= Array(callback[options]).map do |server|
+ case server
+ when String then Net::SSH::Multi::Server.new(master, server, options)
+ else server
+ end
+ end
+ end
+
+ alias to_ary evaluate!
+ end
+
+end; end; end \ No newline at end of file
diff --git a/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/pending_connection.rb b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/pending_connection.rb
new file mode 100644
index 0000000..6af0fac
--- /dev/null
+++ b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/pending_connection.rb
@@ -0,0 +1,112 @@
+require 'net/ssh/multi/channel_proxy'
+
+module Net; module SSH; module Multi
+
+ # A PendingConnection instance mimics a Net::SSH::Connection::Session instance,
+ # without actually being an open connection to a server. It is used by
+ # Net::SSH::Multi::Session when a concurrent connection limit is in effect,
+ # so that a server can hang on to a "connection" that isn't really a connection.
+ #
+ # Any requests against this connection (like #open_channel or #send_global_request)
+ # are not actually sent, but are added to a list of recordings. When the real
+ # session is opened and replaces this pending connection, all recorded actions
+ # will be replayed against that session.
+ #
+ # You'll never need to initialize one of these directly, and (if all goes well!)
+ # should never even notice that one of these is in use. Net::SSH::Multi::Session
+ # will instantiate these as needed, and only when there is a concurrent
+ # connection limit.
+ class PendingConnection
+ # Represents a #open_channel action.
+ class ChannelOpenRecording #:nodoc:
+ attr_reader :type, :extras, :channel
+
+ def initialize(type, extras, channel)
+ @type, @extras, @channel = type, extras, channel
+ end
+
+ def replay_on(session)
+ real_channel = session.open_channel(type, *extras, &channel.on_confirm)
+ channel.delegate_to(real_channel)
+ end
+ end
+
+ # Represents a #send_global_request action.
+ class SendGlobalRequestRecording #:nodoc:
+ attr_reader :type, :extra, :callback
+
+ def initialize(type, extra, callback)
+ @type, @extra, @callback = type, extra, callback
+ end
+
+ def replay_on(session)
+ session.send_global_request(type, *extra, &callback)
+ end
+ end
+
+ # The Net::SSH::Multi::Server object that "owns" this pending connection.
+ attr_reader :server
+
+ # Instantiates a new pending connection for the given Net::SSH::Multi::Server
+ # object.
+ def initialize(server)
+ @server = server
+ @recordings = []
+ end
+
+ # Instructs the pending session to replay all of its recordings against the
+ # given +session+, and to then replace itself with the given session.
+ def replace_with(session)
+ @recordings.each { |recording| recording.replay_on(session) }
+ @server.replace_session(session)
+ end
+
+ # Records that a channel open request has been made, and returns a new
+ # Net::SSH::Multi::ChannelProxy object to represent the (as yet unopened)
+ # channel.
+ def open_channel(type="session", *extras, &on_confirm)
+ channel = ChannelProxy.new(&on_confirm)
+ @recordings << ChannelOpenRecording.new(type, extras, channel)
+ return channel
+ end
+
+ # Records that a global request has been made. The request is not actually
+ # sent, and won't be until #replace_with is called.
+ def send_global_request(type, *extra, &callback)
+ @recordings << SendGlobalRequestRecording.new(type, extra, callback)
+ self
+ end
+
+ # Always returns +true+, so that the pending connection looks active until
+ # it can be truly opened and replaced with a real connection.
+ def busy?(include_invisible=false)
+ true
+ end
+
+ # Does nothing, except to make a pending connection quack like a real connection.
+ def close
+ self
+ end
+
+ # Returns an empty array, since a pending connection cannot have any real channels.
+ def channels
+ []
+ end
+
+ # Returns +true+, and does nothing else.
+ def preprocess
+ true
+ end
+
+ # Returns +true+, and does nothing else.
+ def postprocess(readers, writers)
+ true
+ end
+
+ # Returns an empty hash, since a pending connection has no real listeners.
+ def listeners
+ {}
+ end
+ end
+
+end; end; end \ No newline at end of file
diff --git a/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/server.rb b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/server.rb
new file mode 100644
index 0000000..bce228f
--- /dev/null
+++ b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/server.rb
@@ -0,0 +1,231 @@
+require 'net/ssh'
+
+module Net; module SSH; module Multi
+ # Encapsulates the connection information for a single remote server, as well
+ # as the Net::SSH session corresponding to that information. You'll rarely
+ # need to instantiate one of these directly: instead, you should use
+ # Net::SSH::Multi::Session#use.
+ class Server
+ include Comparable
+
+ # The Net::SSH::Multi::Session instance that manages this server instance.
+ attr_reader :master
+
+ # The host name (or IP address) of the server to connect to.
+ attr_reader :host
+
+ # The user name to use when logging into the server.
+ attr_reader :user
+
+ # The Hash of additional options to pass to Net::SSH when connecting
+ # (including things like :password, and so forth).
+ attr_reader :options
+
+ # The Net::SSH::Gateway instance to use to establish the connection. Will
+ # be +nil+ if the connection should be established without a gateway.
+ attr_reader :gateway
+
+ # Creates a new Server instance with the given connection information. The
+ # +master+ argument must be a reference to the Net::SSH::Multi::Session
+ # instance that will manage this server reference. The +options+ hash must
+ # conform to the options described for Net::SSH::start, with two additions:
+ #
+ # * :via => a Net::SSH::Gateway instance to use when establishing a
+ # connection to this server.
+ # * :user => the name of the user to use when logging into this server.
+ #
+ # The +host+ argument may include the username and port number, in which
+ # case those values take precedence over similar values given in the +options+:
+ #
+ # server = Net::SSH::Multi::Server.new(session, 'user@host:1234')
+ # puts server.user #-> user
+ # puts server.port #-> 1234
+ def initialize(master, host, options={})
+ @master = master
+ @options = options.dup
+
+ @user, @host, port = host.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3]
+
+ user_opt, port_opt = @options.delete(:user), @options.delete(:port)
+
+ @user = @user || user_opt || master.default_user
+ port ||= port_opt
+
+ @options[:port] = port.to_i if port
+
+ @gateway = @options.delete(:via)
+ @failed = false
+ end
+
+ # Returns the value of the server property with the given +key+. Server
+ # properties are described via the +:properties+ key in the options hash
+ # when defining the Server.
+ def [](key)
+ (options[:properties] || {})[key]
+ end
+
+ # Sets the given key/value pair in the +:properties+ key in the options
+ # hash. If the options hash has no :properties key, it will be created.
+ def []=(key, value)
+ (options[:properties] ||= {})[key] = value
+ end
+
+ # Returns the port number to use for this connection.
+ def port
+ options[:port] || 22
+ end
+
+ # Gives server definitions a sort order, and allows comparison.
+ def <=>(server)
+ [host, port, user] <=> [server.host, server.port, server.user]
+ end
+
+ alias :eql? :==
+
+ # Generates a +Fixnum+ hash value for this object. This function has the
+ # property that +a.eql?(b)+ implies +a.hash == b.hash+. The
+ # hash value is used by class +Hash+. Any hash value that exceeds the
+ # capacity of a +Fixnum+ will be truncated before being used.
+ def hash
+ @hash ||= [host, user, port].hash
+ end
+
+ # Returns a human-readable representation of this server instance.
+ def to_s
+ @to_s ||= begin
+ s = "#{user}@#{host}"
+ s << ":#{options[:port]}" if options[:port]
+ s
+ end
+ end
+
+ # Returns a human-readable representation of this server instance.
+ def inspect
+ @inspect ||= "#<%s:0x%x %s>" % [self.class.name, object_id, to_s]
+ end
+
+ # Returns +true+ if this server has ever failed a connection attempt.
+ def failed?
+ @failed
+ end
+
+ # Indicates (by default) that this server has just failed a connection
+ # attempt. If +flag+ is false, this can be used to reset the failed flag
+ # so that a retry may be attempted.
+ def fail!(flag=true)
+ @failed = flag
+ end
+
+ # Returns the Net::SSH session object for this server. If +require_session+
+ # is false and the session has not previously been created, this will
+ # return +nil+. If +require_session+ is true, the session will be instantiated
+ # if it has not already been instantiated, via the +gateway+ if one is
+ # given, or directly (via Net::SSH::start) otherwise.
+ #
+ # if server.session.nil?
+ # puts "connecting..."
+ # server.session(true)
+ # end
+ #
+ # Note that the sessions returned by this are "enhanced" slightly, to make
+ # them easier to deal with in a multi-session environment: they have a
+ # :server property automatically set on them, that refers to this object
+ # (the Server instance that spawned them).
+ #
+ # assert_equal server, server.session[:server]
+ def session(require_session=false)
+ return @session if @session || !require_session
+ @session ||= master.next_session(self)
+ end
+
+ # Returns +true+ if the session has been opened, and the session is currently
+ # busy (as defined by Net::SSH::Connection::Session#busy?).
+ def busy?(include_invisible=false)
+ session && session.busy?(include_invisible)
+ end
+
+ # Closes this server's session. If the session has not yet been opened,
+ # this does nothing.
+ def close
+ session.close if session
+ ensure
+ master.server_closed(self) if session
+ @session = nil
+ end
+
+ public # but not published, e.g., these are used internally only...
+
+ # Indicate that the session currently in use by this server instance
+ # should be replaced by the given +session+ argument. This is used when
+ # a pending session has been "realized". Note that this does not
+ # actually replace the session--see #update_session! for that.
+ def replace_session(session) #:nodoc:
+ @ready_session = session
+ end
+
+ # If a new session has been made ready (see #replace_session), this
+ # will replace the current session with the "ready" session. This
+ # method is called from the event loop to ensure that sessions are
+ # swapped in at the appropriate point (instead of in the middle of an
+ # event poll).
+ def update_session! #:nodoc:
+ if @ready_session
+ @session, @ready_session = @ready_session, nil
+ end
+ end
+
+ # Returns a new session object based on this server's connection
+ # criteria. Note that this will not associate the session with the
+ # server, and should not be called directly; it is called internally
+ # from Net::SSH::Multi::Session when a new session is required.
+ def new_session #:nodoc:
+ session = if gateway
+ gateway.ssh(host, user, options)
+ else
+ Net::SSH.start(host, user, options)
+ end
+
+ session[:server] = self
+ session
+ rescue ::Timeout::Error => error
+ raise Net::SSH::ConnectionTimeout.new("#{error.message} for #{host}")
+ rescue Net::SSH::AuthenticationFailed => error
+ raise Net::SSH::AuthenticationFailed.new("#{error.message}@#{host}")
+ end
+
+ # Closes all open channels on this server's session. If the session has
+ # not yet been opened, this does nothing.
+ def close_channels #:nodoc:
+ session.channels.each { |id, channel| channel.close } if session
+ end
+
+ # Runs the session's preprocess action, if the session has been opened.
+ def preprocess #:nodoc:
+ session.preprocess if session
+ end
+
+ # Returns all registered readers on the session, or an empty array if the
+ # session is not open.
+ def readers #:nodoc:
+ return [] unless session
+ session.listeners.keys.reject { |io| io.closed? }
+ end
+
+ # Returns all registered and pending writers on the session, or an empty
+ # array if the session is not open.
+ def writers #:nodoc:
+ readers.select do |io|
+ io.respond_to?(:pending_write?) && io.pending_write?
+ end
+ end
+
+ # Runs the post-process action on the session, if the session has been
+ # opened. Only the +readers+ and +writers+ that actually belong to this
+ # session will be postprocessed by this server.
+ def postprocess(readers, writers) #:nodoc:
+ return true unless session
+ listeners = session.listeners.keys
+ session.postprocess(listeners & readers, listeners & writers)
+ end
+ end
+end; end; end \ No newline at end of file
diff --git a/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/server_list.rb b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/server_list.rb
new file mode 100644
index 0000000..da1914f
--- /dev/null
+++ b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/server_list.rb
@@ -0,0 +1,80 @@
+require 'net/ssh/multi/server'
+require 'net/ssh/multi/dynamic_server'
+
+module Net; module SSH; module Multi
+
+ # Encapsulates a list of server objects, both dynamic (Net::SSH::Multi::DynamicServer)
+ # and static (Net::SSH::Multi::Server). It attempts to make it transparent whether
+ # a dynamic server set has been evaluated or not. Note that a ServerList is
+ # NOT an Array, though it is Enumerable.
+ class ServerList
+ include Enumerable
+
+ # Create a new ServerList that wraps the given server list. Duplicate entries
+ # will be discarded.
+ def initialize(list=[])
+ @list = list.uniq
+ end
+
+ # Adds the given server to the list, and returns the argument. If an
+ # identical server definition already exists in the collection, the
+ # argument is _not_ added, and the existing server record is returned
+ # instead.
+ def add(server)
+ index = @list.index(server)
+ if index
+ server = @list[index]
+ else
+ @list.push(server)
+ end
+ server
+ end
+
+ # Adds an array (or otherwise Enumerable list) of servers to this list, by
+ # calling #add for each argument. Returns +self+.
+ def concat(servers)
+ servers.each { |server| add(server) }
+ self
+ end
+
+ # Iterates over each distinct server record in the collection. This will
+ # correctly iterate over server records instantiated by a DynamicServer
+ # as well, but only if the dynamic server has been "evaluated" (see
+ # Net::SSH::Multi::DynamicServer#evaluate!).
+ def each
+ @list.each do |server|
+ case server
+ when Server then yield server
+ when DynamicServer then server.each { |item| yield item }
+ else raise ArgumentError, "server list contains non-server: #{server.class}"
+ end
+ end
+ self
+ end
+
+ # Works exactly as Enumerable#select, but returns the result as a new
+ # ServerList instance.
+ def select
+ subset = @list.select { |i| yield i }
+ ServerList.new(subset)
+ end
+
+ # Returns an array of all servers in the list, with dynamic server records
+ # expanded. The result is an array of distinct server records (duplicates
+ # are removed from the result).
+ def flatten
+ result = @list.inject([]) do |aggregator, server|
+ case server
+ when Server then aggregator.push(server)
+ when DynamicServer then aggregator.concat(server)
+ else raise ArgumentError, "server list contains non-server: #{server.class}"
+ end
+ end
+
+ result.uniq
+ end
+
+ alias to_ary flatten
+ end
+
+end; end; end \ No newline at end of file
diff --git a/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/session.rb b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/session.rb
new file mode 100644
index 0000000..333fdf3
--- /dev/null
+++ b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/session.rb
@@ -0,0 +1,551 @@
+require 'thread'
+require 'net/ssh/gateway'
+require 'net/ssh/multi/server'
+require 'net/ssh/multi/dynamic_server'
+require 'net/ssh/multi/server_list'
+require 'net/ssh/multi/channel'
+require 'net/ssh/multi/pending_connection'
+require 'net/ssh/multi/session_actions'
+require 'net/ssh/multi/subsession'
+
+module Net; module SSH; module Multi
+ # 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.
+ class Session
+ include SessionActions
+
+ # The Net::SSH::Multi::ServerList managed by this session.
+ attr_reader :server_list
+
+ # The default Net::SSH::Gateway instance to use to connect to the servers.
+ # If +nil+, no default gateway will be used.
+ attr_reader :default_gateway
+
+ # The hash of group definitions, mapping each group name to a corresponding
+ # Net::SSH::Multi::ServerList.
+ attr_reader :groups
+
+ # The number of allowed concurrent connections. No more than this number
+ # of sessions will be open at any given time.
+ attr_accessor :concurrent_connections
+
+ # 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.
+ attr_accessor :on_error
+
+ # 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.
+ attr_accessor :default_user
+
+ # The number of connections that are currently open.
+ attr_reader :open_connections #:nodoc:
+
+ # The list of "open" groups, which will receive subsequent server definitions.
+ # See #use and #group.
+ attr_reader :open_groups #:nodoc:
+
+ # 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
+ def initialize(options={})
+ @server_list = ServerList.new
+ @groups = Hash.new { |h,k| h[k] = ServerList.new }
+ @gateway = nil
+ @open_groups = []
+ @connect_threads = []
+ @on_error = :fail
+ @default_user = ENV['USER'] || ENV['USERNAME'] || "unknown"
+
+ @open_connections = 0
+ @pending_sessions = []
+ @session_mutex = Mutex.new
+
+ options.each { |opt, value| send("#{opt}=", value) }
+ 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
+ def group(*args)
+ mapping = args.last.is_a?(Hash) ? args.pop : {}
+
+ if mapping.any? && block_given?
+ raise ArgumentError, "must provide group mapping OR block, not both"
+ elsif block_given?
+ begin
+ saved_groups = open_groups.dup
+ open_groups.concat(args.map { |a| a.to_sym }).uniq!
+ yield self
+ ensure
+ open_groups.replace(saved_groups)
+ end
+ else
+ mapping.each do |key, value|
+ (open_groups + Array(key)).uniq.each do |grp|
+ groups[grp.to_sym].concat(Array(value))
+ end
+ end
+ end
+ 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.
+ def via(host, user, options={})
+ @default_gateway = Net::SSH::Gateway.new(host, user, options)
+ self
+ 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.
+ def use(*hosts, &block)
+ options = hosts.last.is_a?(Hash) ? hosts.pop : {}
+ options = { :via => default_gateway }.merge(options)
+
+ results = hosts.map do |host|
+ server_list.add(Server.new(self, host, options))
+ end
+
+ if block
+ results << server_list.add(DynamicServer.new(self, options, block))
+ end
+
+ group [] => results
+ results.length > 1 ? results : results.first
+ 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.
+ def servers
+ servers_for
+ 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 } })
+ def servers_for(*criteria)
+ if criteria.empty?
+ server_list.flatten
+ else
+ # normalize the criteria list, so that every entry is a key to a
+ # criteria hash (possibly empty).
+ criteria = criteria.inject({}) do |hash, entry|
+ case entry
+ when Hash then hash.merge(entry)
+ else hash.merge(entry => {})
+ end
+ end
+
+ list = criteria.inject([]) do |aggregator, (group, properties)|
+ raise ArgumentError, "the value for any group must be a Hash, but got a #{properties.class} for #{group.inspect}" unless properties.is_a?(Hash)
+ bad_keys = properties.keys - [:only, :except]
+ raise ArgumentError, "unknown constraint(s) #{bad_keys.inspect} for #{group.inspect}" unless bad_keys.empty?
+
+ servers = groups[group].select do |server|
+ (properties[:only] || {}).all? { |prop, value| server[prop] == value } &&
+ !(properties[:except] || {}).any? { |prop, value| server[prop] == value }
+ end
+
+ aggregator.concat(servers)
+ end
+
+ list.uniq
+ end
+ 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
+ def with(*groups)
+ subsession = Subsession.new(self, servers_for(*groups))
+ yield subsession if block_given?
+ subsession
+ 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')
+ def on(*servers)
+ subsession = Subsession.new(self, servers)
+ yield subsession if block_given?
+ subsession
+ end
+
+ # 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.
+ def close
+ server_list.each { |server| server.close_channels }
+ loop(0) { busy?(true) }
+ server_list.each { |server| server.close }
+ default_gateway.shutdown! if default_gateway
+ end
+
+ alias :loop_forever :loop
+
+ # 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).
+ def loop(wait=nil, &block)
+ running = block || Proc.new { |c| busy? }
+ loop_forever { break unless process(wait, &running) }
+ 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+).
+ def process(wait=nil, &block)
+ realize_pending_connections!
+ wait = @connect_threads.any? ? 0 : wait
+
+ return false unless preprocess(&block)
+
+ readers = server_list.map { |s| s.readers }.flatten
+ writers = server_list.map { |s| s.writers }.flatten
+
+ readers, writers, = IO.select(readers, writers, nil, wait)
+
+ if readers
+ return postprocess(readers, writers)
+ else
+ return true
+ end
+ end
+
+ # Runs the preprocess stage on all servers. Returns false if the block
+ # returns false, and true if there either is no block, or it returns true.
+ # This is called as part of the #process method.
+ def preprocess(&block) #:nodoc:
+ return false if block && !block[self]
+ server_list.each { |server| server.preprocess }
+ block.nil? || block[self]
+ end
+
+ # Runs the postprocess stage on all servers. Always returns true. This is
+ # called as part of the #process method.
+ def postprocess(readers, writers) #:nodoc:
+ server_list.each { |server| server.postprocess(readers, writers) }
+ true
+ end
+
+ # Takes the #concurrent_connections property into account, and tries to
+ # return a new session for the given server. If the concurrent connections
+ # limit has been reached, then a Net::SSH::Multi::PendingConnection instance
+ # will be returned instead, which will be realized into an actual session
+ # as soon as a slot opens up.
+ #
+ # If +force+ is true, the concurrent_connections check is skipped and a real
+ # connection is always returned.
+ def next_session(server, force=false) #:nodoc:
+ # don't retry a failed attempt
+ return nil if server.failed?
+
+ @session_mutex.synchronize do
+ if !force && concurrent_connections && concurrent_connections <= open_connections
+ connection = PendingConnection.new(server)
+ @pending_sessions << connection
+ return connection
+ end
+
+ @open_connections += 1
+ end
+
+ begin
+ server.new_session
+
+ # I don't understand why this should be necessary--StandardError is a
+ # subclass of Exception, after all--but without explicitly rescuing
+ # StandardError, things like Errno::* and SocketError don't get caught
+ # here!
+ rescue Exception, StandardError => e
+ server.fail!
+ @session_mutex.synchronize { @open_connections -= 1 }
+
+ case on_error
+ when :ignore then
+ # do nothing
+ when :warn then
+ warn("error connecting to #{server}: #{e.class} (#{e.message})")
+ when Proc then
+ go = catch(:go) { on_error.call(server); nil }
+ case go
+ when nil, :ignore then # nothing
+ when :retry then retry
+ when :raise then raise
+ else warn "unknown 'go' command: #{go.inspect}"
+ end
+ else
+ raise
+ end
+
+ return nil
+ end
+ end
+
+ # Tells the session that the given server has closed its connection. The
+ # session indicates that a new connection slot is available, which may be
+ # filled by the next pending connection on the next event loop iteration.
+ def server_closed(server) #:nodoc:
+ @session_mutex.synchronize do
+ unless @pending_sessions.delete(server.session)
+ @open_connections -= 1
+ end
+ end
+ end
+
+ # Invoked by the event loop. If there is a concurrent_connections limit in
+ # effect, this will close any non-busy sessions and try to open as many
+ # new sessions as it can. It does this in threads, so that existing processing
+ # can continue.
+ #
+ # If there is no concurrent_connections limit in effect, then this method
+ # does nothing.
+ def realize_pending_connections! #:nodoc:
+ return unless concurrent_connections
+
+ server_list.each do |server|
+ server.close if !server.busy?(true)
+ server.update_session!
+ end
+
+ @connect_threads.delete_if { |t| !t.alive? }
+
+ count = concurrent_connections ? (concurrent_connections - open_connections) : @pending_sessions.length
+ count.times do
+ session = @pending_sessions.pop or break
+ @connect_threads << Thread.new do
+ session.replace_with(next_session(session.server, true))
+ end
+ end
+ end
+ end
+end; end; end
diff --git a/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/session_actions.rb b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/session_actions.rb
new file mode 100644
index 0000000..2d87392
--- /dev/null
+++ b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/session_actions.rb
@@ -0,0 +1,153 @@
+module Net; module SSH; module Multi
+
+ # This module represents the actions that are available on session
+ # collections. Any class that includes this module needs only provide a
+ # +servers+ method that returns a list of Net::SSH::Multi::Server
+ # instances, and the rest just works. See Net::SSH::Multi::Session and
+ # Net::SSH::Multi::Subsession for consumers of this module.
+ module SessionActions
+ # Returns the session that is the "master". This defaults to +self+, but
+ # classes that include this module may wish to change this if they are
+ # subsessions that depend on a master session.
+ def master
+ self
+ end
+
+ # Connections are normally established lazily, as soon as they are needed.
+ # This method forces all servers in the current container to have their
+ # connections established immediately, blocking until the connections have
+ # been made.
+ def connect!
+ sessions
+ self
+ end
+
+ # Returns +true+ if any server in the current container has an open SSH
+ # session that is currently processing any channels. If +include_invisible+
+ # is +false+ (the default) then invisible channels (such as those created
+ # by port forwarding) will not be counted; otherwise, they will be.
+ def busy?(include_invisible=false)
+ servers.any? { |server| server.busy?(include_invisible) }
+ end
+
+ # Returns an array of all SSH sessions, blocking until all sessions have
+ # connected.
+ def sessions
+ threads = servers.map { |server| Thread.new { server.session(true) } if server.session.nil? }
+ threads.each { |thread| thread.join if thread }
+ servers.map { |server| server.session }.compact
+ end
+
+ # Sends a global request to the sessions for all contained servers
+ # (see #sessions). This can be used to (e.g.) ping the remote servers to
+ # prevent them from timing out.
+ #
+ # session.send_global_request("keep-alive@openssh.com")
+ #
+ # If a block is given, it will be invoked when the server responds, with
+ # two arguments: the Net::SSH connection that is responding, and a boolean
+ # indicating whether the request succeeded or not.
+ def send_global_request(type, *extra, &callback)
+ sessions.each { |ssh| ssh.send_global_request(type, *extra, &callback) }
+ self
+ end
+
+ # Asks all sessions for all contained servers (see #sessions) to open a
+ # new channel. When each server responds, the +on_confirm+ block will be
+ # invoked with a single argument, the channel object for that server. This
+ # means that the block will be invoked one time for each session.
+ #
+ # All new channels will be collected and returned, aggregated into a new
+ # Net::SSH::Multi::Channel instance.
+ #
+ # Note that the channels are "enhanced" slightly--they have two properties
+ # set on them automatically, to make dealing with them in a multi-session
+ # environment slightly easier:
+ #
+ # * :server => the Net::SSH::Multi::Server instance that spawned the channel
+ # * :host => the host name of the server
+ #
+ # Having access to these things lets you more easily report which host
+ # (e.g.) data was received from:
+ #
+ # session.open_channel do |channel|
+ # channel.exec "command" do |ch, success|
+ # ch.on_data do |ch, data|
+ # puts "got data #{data} from #{ch[:host]}"
+ # end
+ # end
+ # end
+ def open_channel(type="session", *extra, &on_confirm)
+ channels = sessions.map do |ssh|
+ ssh.open_channel(type, *extra) do |c|
+ c[:server] = c.connection[:server]
+ c[:host] = c.connection[:server].host
+ on_confirm[c] if on_confirm
+ end
+ end
+ Multi::Channel.new(master, channels)
+ end
+
+ # A convenience method for executing a command on multiple hosts and
+ # either displaying or capturing the output. It opens a channel on all
+ # active sessions (see #open_channel and #active_sessions), and then
+ # executes a command on each channel (Net::SSH::Connection::Channel#exec).
+ #
+ # If a block is given, it will be invoked whenever data is received across
+ # the channel, with three arguments: the channel object, a symbol identifying
+ # which output stream the data was received on (+:stdout+ or +:stderr+)
+ # and a string containing the data that was received:
+ #
+ # session.exec("command") do |ch, stream, data|
+ # puts "[#{ch[:host]} : #{stream}] #{data}"
+ # end
+ #
+ # If no block is given, all output will be written to +$stdout+ or
+ # +$stderr+, as appropriate.
+ #
+ # Note that #exec will also capture the exit status of the process in the
+ # +:exit_status+ property of each channel. Since #exec returns all of the
+ # channels in a Net::SSH::Multi::Channel object, you can check for the
+ # exit status like this:
+ #
+ # channel = session.exec("command") { ... }
+ # channel.wait
+ #
+ # if channel.any? { |c| c[:exit_status] != 0 }
+ # puts "executing failed on at least one host!"
+ # end
+ def exec(command, &block)
+ open_channel do |channel|
+ channel.exec(command) do |ch, success|
+ raise "could not execute command: #{command.inspect} (#{ch[:host]})" unless success
+
+ channel.on_data do |ch, data|
+ if block
+ block.call(ch, :stdout, data)
+ else
+ data.chomp.each_line do |line|
+ $stdout.puts("[#{ch[:host]}] #{line}")
+ end
+ end
+ end
+
+ channel.on_extended_data do |ch, type, data|
+ if block
+ block.call(ch, :stderr, data)
+ else
+ data.chomp.each_line do |line|
+ $stderr.puts("[#{ch[:host]}] #{line}")
+ end
+ end
+ end
+
+ channel.on_request("exit-status") do |ch, data|
+ ch[:exit_status] = data.read_long
+ end
+ end
+ end
+ end
+
+ end
+
+end; end; end \ No newline at end of file
diff --git a/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/subsession.rb b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/subsession.rb
new file mode 100644
index 0000000..29e7866
--- /dev/null
+++ b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/subsession.rb
@@ -0,0 +1,48 @@
+require 'net/ssh/multi/session_actions'
+
+module Net; module SSH; module Multi
+
+ # A trivial class for representing a subset of servers. It is used
+ # internally for restricting operations to a subset of all defined
+ # servers.
+ #
+ # subsession = session.with(:app)
+ # subsession.exec("hostname")
+ class Subsession
+ include SessionActions
+
+ # The master session that spawned this subsession.
+ attr_reader :master
+
+ # The list of servers that this subsession can operate on.
+ attr_reader :servers
+
+ # Create a new subsession of the given +master+ session, that operates
+ # on the given +server_list+.
+ def initialize(master, server_list)
+ @master = master
+ @servers = server_list.uniq
+ end
+
+ # Works as Array#slice, but returns a new subsession consisting of the
+ # given slice of servers in this subsession. The new subsession will have
+ # the same #master session as this subsession does.
+ #
+ # s1 = subsession.slice(0)
+ # s2 = subsession.slice(3, -1)
+ # s3 = subsession.slice(1..4)
+ def slice(*args)
+ Subsession.new(master, Array(servers.slice(*args)))
+ end
+
+ # Returns a new subsession that consists of only the first server in the
+ # server list of the current subsession. This is just convenience for
+ # #slice(0):
+ #
+ # s1 = subsession.first
+ def first
+ slice(0)
+ end
+ end
+
+end; end; end \ No newline at end of file
diff --git a/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/version.rb b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/version.rb
new file mode 100644
index 0000000..5b1bf82
--- /dev/null
+++ b/pkg/net-ssh-multi-1.1/lib/net/ssh/multi/version.rb
@@ -0,0 +1,21 @@
+require 'net/ssh/version'
+
+module Net; module SSH; module Multi
+ # A trivial class for representing the version of this library.
+ class Version < Net::SSH::Version
+ # The major component of the library's version
+ MAJOR = 1
+
+ # The minor component of the library's version
+ MINOR = 1
+
+ # The tiny component of the library's version
+ TINY = 0
+
+ # The library's version as a Version instance
+ CURRENT = new(MAJOR, MINOR, TINY)
+
+ # The library's version as a String instance
+ STRING = CURRENT.to_s
+ end
+end; end; end