diff options
author | delano <delano.mandelbaum@gmail.com> | 2013-02-06 08:04:41 -0800 |
---|---|---|
committer | delano <delano.mandelbaum@gmail.com> | 2013-02-06 08:04:41 -0800 |
commit | 345d20dd3f993b84bbdae265d4a156a7d87fbf28 (patch) | |
tree | ac7aa1eb480b727ed07f0d176130d0ad806abb27 /pkg/net-ssh-multi-1.1/lib/net | |
parent | 73d75b2c3a5465392fd6210dbb844c49dd043cdc (diff) | |
download | net-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.rb | 71 | ||||
-rw-r--r-- | pkg/net-ssh-multi-1.1/lib/net/ssh/multi/channel.rb | 230 | ||||
-rw-r--r-- | pkg/net-ssh-multi-1.1/lib/net/ssh/multi/channel_proxy.rb | 50 | ||||
-rw-r--r-- | pkg/net-ssh-multi-1.1/lib/net/ssh/multi/dynamic_server.rb | 71 | ||||
-rw-r--r-- | pkg/net-ssh-multi-1.1/lib/net/ssh/multi/pending_connection.rb | 112 | ||||
-rw-r--r-- | pkg/net-ssh-multi-1.1/lib/net/ssh/multi/server.rb | 231 | ||||
-rw-r--r-- | pkg/net-ssh-multi-1.1/lib/net/ssh/multi/server_list.rb | 80 | ||||
-rw-r--r-- | pkg/net-ssh-multi-1.1/lib/net/ssh/multi/session.rb | 551 | ||||
-rw-r--r-- | pkg/net-ssh-multi-1.1/lib/net/ssh/multi/session_actions.rb | 153 | ||||
-rw-r--r-- | pkg/net-ssh-multi-1.1/lib/net/ssh/multi/subsession.rb | 48 | ||||
-rw-r--r-- | pkg/net-ssh-multi-1.1/lib/net/ssh/multi/version.rb | 21 |
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 |