net-ssh-multi-1.2.1/0000755000175000017500000000000012522123035013217 5ustar lucaslucasnet-ssh-multi-1.2.1/LICENSE.txt0000644000175000017500000000204512522123035015043 0ustar lucaslucasCopyright © 2008 Jamis Buck Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. net-ssh-multi-1.2.1/lib/0000755000175000017500000000000012522123035013765 5ustar lucaslucasnet-ssh-multi-1.2.1/lib/net/0000755000175000017500000000000012522123035014553 5ustar lucaslucasnet-ssh-multi-1.2.1/lib/net/ssh/0000755000175000017500000000000012522123035015350 5ustar lucaslucasnet-ssh-multi-1.2.1/lib/net/ssh/multi/0000755000175000017500000000000012522123035016502 5ustar lucaslucasnet-ssh-multi-1.2.1/lib/net/ssh/multi/session_actions.rb0000644000175000017500000001356612522123035022245 0ustar lucaslucasmodule 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; endnet-ssh-multi-1.2.1/lib/net/ssh/multi/server.rb0000644000175000017500000002067012522123035020342 0ustar lucaslucasrequire '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?). # Also returns false if the server has failed to connect. def busy?(include_invisible=false) !failed? && 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 net-ssh-multi-1.2.1/lib/net/ssh/multi/channel.rb0000644000175000017500000001767012522123035020452 0ustar lucaslucasmodule 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; endnet-ssh-multi-1.2.1/lib/net/ssh/multi/channel_proxy.rb0000644000175000017500000000400712522123035021701 0ustar lucaslucasmodule 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; endnet-ssh-multi-1.2.1/lib/net/ssh/multi/dynamic_server.rb0000644000175000017500000000513612522123035022046 0ustar lucaslucasrequire '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; endnet-ssh-multi-1.2.1/lib/net/ssh/multi/pending_connection.rb0000644000175000017500000000724512522123035022702 0ustar lucaslucasrequire '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; endnet-ssh-multi-1.2.1/lib/net/ssh/multi/subsession.rb0000644000175000017500000000260012522123035021222 0ustar lucaslucasrequire '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; endnet-ssh-multi-1.2.1/lib/net/ssh/multi/version.rb0000644000175000017500000000104312522123035020512 0ustar lucaslucasrequire '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 = 2 # The tiny component of the library's version TINY = 1 # 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 net-ssh-multi-1.2.1/lib/net/ssh/multi/session.rb0000644000175000017500000005276712522123035020533 0ustar lucaslucasrequire '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 # Only increment the open_connections count if the connection # is not being forced. Incase of a force, it will already be # incremented. if !force @open_connections += 1 end 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 # Increment the open_connections count here to prevent # creation of connection thread again before that is # incremented by the thread. @session_mutex.synchronize { @open_connections += 1 } @connect_threads << Thread.new do session.replace_with(next_session(session.server, true)) end end end end end; end; end net-ssh-multi-1.2.1/lib/net/ssh/multi/server_list.rb0000644000175000017500000000502012522123035021365 0ustar lucaslucasrequire '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; endnet-ssh-multi-1.2.1/lib/net/ssh/multi.rb0000644000175000017500000000423712522123035017035 0ustar lucaslucasrequire '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; endnet-ssh-multi-1.2.1/metadata.gz.sig0000644000175000017500000000040012522123035016114 0ustar lucaslucas N+Be b'V ϥ՗U.!*VBzV`b W䦂>82*jR2j@ob( DFru) o}2gUsCnet-ssh-multi-1.2.1/data.tar.gz.sig0000644000175000017500000000040012522123035016032 0ustar lucaslucas֐AC&?nBõAg$sWTa~=n(P@v{Y?`ͬzp\P1gc]Ayh')梬IL' , "'&qs][IlcuMTȬ[+@m?*U*kw9=gc(r`r+)L88S2Iڹyd.Gj?~g#}Cڐunet-ssh-multi-1.2.1/metadata.yml0000644000175000017500000001153112522123035015523 0ustar lucaslucas--- !ruby/object:Gem::Specification name: net-ssh-multi version: !ruby/object:Gem::Version version: 1.2.1 prerelease: platform: ruby authors: - Jamis Buck - Delano Mandelbaum autorequire: bindir: bin cert_chain: - !binary |- LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUROakNDQWg2Z0F3SUJB Z0lCQURBTkJna3Foa2lHOXcwQkFRVUZBREJCTVE4d0RRWURWUVFEREFaa1pX eGgKYm04eEdUQVhCZ29Ka2lhSmsvSXNaQUVaRmdsemIyeDFkR2x2ZFhNeEV6 QVJCZ29Ka2lhSmsvSXNaQUVaRmdOagpiMjB3SGhjTk1UTXdNakEyTVRFMU56 UTFXaGNOTVRRd01qQTJNVEUxTnpRMVdqQkJNUTh3RFFZRFZRUUREQVprClpX eGhibTh4R1RBWEJnb0praWFKay9Jc1pBRVpGZ2x6YjJ4MWRHbHZkWE14RXpB UkJnb0praWFKay9Jc1pBRVoKRmdOamIyMHdnZ0VpTUEwR0NTcUdTSWIzRFFF QkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEZzFoTXRsMFhzTXVVSwpBS1RnWVd2 M2dqajd2dUVzRTJFalQrdnlCZzgvTHBxVlZ3WnppaWFlYkpUOUlaaVErc0NG cWJpYWtqMGI1M3BJCmhnMXlPYUJFbUg2L1cwTDdyd3pxYVJWOXNXMWVKczlK eEZZUUNuZDY3elVuemo4bm5SbE9qRytoaElHK1ZzaWoKbnBzR2J0MjhwZWZ1 TlpKak81cTJjbEFsZlNuaUlJSGZJc1U3L1N0RVl1NkZVR09qbndyeVowcjV5 SmxyOVJyRQpHcytxMERXOFFuWjlVcEFmdURGUVp1SXFlS1FGRkxFN25NbUNH YUErMEJOMW5MbDNmVkhOYkxIcTdBdms4K1orClp1dXZrZHNjYkhsTy9sKzN4 Q05RNW5Vbkh3cTBBREFiTUxPbG1pWVl6cVhvV0xqbWVJNm1lL2Nsa3RKQ2ZO MlIKb1pHM1VRdnZBZ01CQUFHak9UQTNNQWtHQTFVZEV3UUNNQUF3SFFZRFZS ME9CQllFRk1TSk9FdEh6RTRsMGF6dgpNMEpLMGtLTlRvSzFNQXNHQTFVZER3 UUVBd0lFc0RBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQXRPZEU3M3F4Ck9I MnlkaTlvVDJoUzVmOUcweTFaNzBUbHdoK1ZHRXh5Znh6VkU5WHdDK2lQcEp4 TnJhaUhZZ0YvOS9va3k3WloKUjlxMC90Sm5ldWhBZW5aZGlRa1g3b2k0TzN2 OXdSUzZZSG9XQnhNUEZLVlJMTlR6dlZKc2JtZnBDQWxwNS81ZwpwczR3UUZ5 NW1pYkVsR1ZsT29iZi9naHFaMjVIUzlKNmtkMC9DL3J5MEFVdFRvZ3NMN1R4 R3dUNGtiQ3g2M3ViCjN2eXdFRWhzSlV6ZmQ5N0dDQUJtdFFmUlRsZFgvajdG MXovNXdkOHAraGZkb3gxaWliZHM5WnRmYVpBM0t6S24Ka2NoV045QjZ6Zzly MVhNUThCTTJKejBYb1BhblBlMzU0K2xXd2pwa1JLYkZvdy9aYlFIY0NMQ3Ey NCtONmI2ZwpkZ0tmTkR6d2lEcHFDQT09Ci0tLS0tRU5EIENFUlRJRklDQVRF LS0tLS0K date: 2015-04-11 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: net-ssh requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 2.6.5 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 2.6.5 - !ruby/object:Gem::Dependency name: net-ssh-gateway requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 1.2.0 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 1.2.0 - !ruby/object:Gem::Dependency name: minitest requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: mocha requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' description: Control multiple Net::SSH connections via a single interface. email: net-ssh@solutious.com executables: [] extensions: [] extra_rdoc_files: - LICENSE.txt - README.rdoc files: - CHANGES.txt - LICENSE.txt - README.rdoc - Rakefile - gem-public_cert.pem - lib/net/ssh/multi.rb - lib/net/ssh/multi/channel.rb - lib/net/ssh/multi/channel_proxy.rb - lib/net/ssh/multi/dynamic_server.rb - lib/net/ssh/multi/pending_connection.rb - lib/net/ssh/multi/server.rb - lib/net/ssh/multi/server_list.rb - lib/net/ssh/multi/session.rb - lib/net/ssh/multi/session_actions.rb - lib/net/ssh/multi/subsession.rb - lib/net/ssh/multi/version.rb - net-ssh-multi.gemspec - test/channel_test.rb - test/common.rb - test/multi_test.rb - test/server_test.rb - test/session_actions_test.rb - test/session_test.rb - test/test_all.rb homepage: https://github.com/net-ssh/net-scp licenses: - MIT post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: net-ssh-multi rubygems_version: 1.8.23 signing_key: specification_version: 3 summary: Control multiple Net::SSH connections via a single interface. test_files: [] net-ssh-multi-1.2.1/net-ssh-multi.gemspec0000644000175000017500000000472512522123035017305 0ustar lucaslucas# Generated by jeweler # DO NOT EDIT THIS FILE DIRECTLY # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' # -*- encoding: utf-8 -*- Gem::Specification.new do |s| s.name = "net-ssh-multi" s.version = "1.2.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Jamis Buck", "Delano Mandelbaum"] s.cert_chain = ["gem-public_cert.pem"] s.date = "2015-04-11" s.description = "Control multiple Net::SSH connections via a single interface." s.email = "net-ssh@solutious.com" s.extra_rdoc_files = [ "LICENSE.txt", "README.rdoc" ] s.files = [ "CHANGES.txt", "LICENSE.txt", "README.rdoc", "Rakefile", "gem-public_cert.pem", "lib/net/ssh/multi.rb", "lib/net/ssh/multi/channel.rb", "lib/net/ssh/multi/channel_proxy.rb", "lib/net/ssh/multi/dynamic_server.rb", "lib/net/ssh/multi/pending_connection.rb", "lib/net/ssh/multi/server.rb", "lib/net/ssh/multi/server_list.rb", "lib/net/ssh/multi/session.rb", "lib/net/ssh/multi/session_actions.rb", "lib/net/ssh/multi/subsession.rb", "lib/net/ssh/multi/version.rb", "net-ssh-multi.gemspec", "test/channel_test.rb", "test/common.rb", "test/multi_test.rb", "test/server_test.rb", "test/session_actions_test.rb", "test/session_test.rb", "test/test_all.rb" ] s.homepage = "https://github.com/net-ssh/net-scp" s.licenses = ["MIT"] s.require_paths = ["lib"] s.rubyforge_project = "net-ssh-multi" s.rubygems_version = "1.8.23" s.signing_key = "/mnt/gem/gem-private_key.pem" s.summary = "Control multiple Net::SSH connections via a single interface." if s.respond_to? :specification_version then s.specification_version = 3 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_runtime_dependency(%q, [">= 2.6.5"]) s.add_runtime_dependency(%q, [">= 1.2.0"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) else s.add_dependency(%q, [">= 2.6.5"]) s.add_dependency(%q, [">= 1.2.0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) end else s.add_dependency(%q, [">= 2.6.5"]) s.add_dependency(%q, [">= 1.2.0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) end end net-ssh-multi-1.2.1/test/0000755000175000017500000000000012522123035014176 5ustar lucaslucasnet-ssh-multi-1.2.1/test/common.rb0000644000175000017500000000035012522123035016011 0ustar lucaslucasrequire 'minitest/autorun' require 'mocha/setup' if Minitest.const_defined?('Test') # We're on Minitest 5+. Nothing to do here. else # Minitest 4 doesn't have Minitest::Test yet. Minitest::Test = MiniTest::Unit::TestCase end net-ssh-multi-1.2.1/test/channel_test.rb0000644000175000017500000001343412522123035017177 0ustar lucaslucasrequire 'common' require 'net/ssh/multi/channel' class ChannelTest < Minitest::Test def test_each_should_iterate_over_each_component_channel channels = [c1 = mock('channel'), c2 = mock('channel'), c3 = mock('channel')] channel = Net::SSH::Multi::Channel.new(mock('session'), channels) result = [] channel.each { |c| result << c } assert_equal channels, result end def test_property_accessors channel = Net::SSH::Multi::Channel.new(mock('session'), []) channel[:foo] = "hello" assert_equal "hello", channel[:foo] channel['bar'] = "goodbye" assert_equal "goodbye", channel['bar'] assert_nil channel[:bar] assert_nil channel['foo'] end def test_exec_should_delegate_to_component_channels c1, c2, results = mock('channel'), mock('channel'), [] c1.expects(:exec).with('ls -l').yields(c1) c2.expects(:exec).with('ls -l').yields(c2) channel = Net::SSH::Multi::Channel.new(mock('session'), [c1, c2]) assert_equal channel, channel.exec('ls -l') { |c| results << c } assert_equal [c1, c2], results end def test_request_pty_should_delegate_to_component_channels c1, c2, results = mock('channel'), mock('channel'), [] c1.expects(:request_pty).with(:foo => 5).yields(c1) c2.expects(:request_pty).with(:foo => 5).yields(c2) channel = Net::SSH::Multi::Channel.new(mock('session'), [c1, c2]) assert_equal channel, channel.request_pty(:foo => 5) { |c| results << c } assert_equal [c1, c2], results end def test_send_data_should_delegate_to_component_channels c1, c2 = mock('channel'), mock('channel') c1.expects(:send_data).with("hello\n") c2.expects(:send_data).with("hello\n") channel = Net::SSH::Multi::Channel.new(mock('session'), [c1, c2]) assert_equal channel, channel.send_data("hello\n") end def test_active_should_be_true_if_all_component_channels_are_active c1, c2, c3 = stub('channel', :active? => true), stub('channel', :active? => true), stub('channel', :active? => true) channel = Net::SSH::Multi::Channel.new(stub('session'), [c1, c2, c3]) assert channel.active? end def test_active_should_be_true_if_any_component_channels_are_active c1, c2, c3 = stub('channel', :active? => true), stub('channel', :active? => false), stub('channel', :active? => false) channel = Net::SSH::Multi::Channel.new(stub('session'), [c1, c2, c3]) assert channel.active? end def test_active_should_be_false_if_no_component_channels_are_active c1, c2, c3 = stub('channel', :active? => false), stub('channel', :active? => false), stub('channel', :active? => false) channel = Net::SSH::Multi::Channel.new(stub('session'), [c1, c2, c3]) assert !channel.active? end def test_wait_should_block_until_active_is_false channel = Net::SSH::Multi::Channel.new(MockSession.new, []) channel.expects(:active?).times(4).returns(true,true,true,false) assert_equal channel, channel.wait end def test_close_should_delegate_to_component_channels c1, c2 = mock('channel'), mock('channel') c1.expects(:close) c2.expects(:close) channel = Net::SSH::Multi::Channel.new(mock('session'), [c1, c2]) assert_equal channel, channel.close end def test_eof_bang_should_delegate_to_component_channels c1, c2 = mock('channel'), mock('channel') c1.expects(:eof!) c2.expects(:eof!) channel = Net::SSH::Multi::Channel.new(mock('session'), [c1, c2]) assert_equal channel, channel.eof! end def test_on_data_should_delegate_to_component_channels c1, c2, results = mock('channel'), mock('channel'), [] c1.expects(:on_data).yields(c1) c2.expects(:on_data).yields(c2) channel = Net::SSH::Multi::Channel.new(mock('session'), [c1, c2]) assert_equal channel, channel.on_data { |c| results << c } assert_equal [c1, c2], results end def test_on_extended_data_should_delegate_to_component_channels c1, c2, results = mock('channel'), mock('channel'), [] c1.expects(:on_extended_data).yields(c1) c2.expects(:on_extended_data).yields(c2) channel = Net::SSH::Multi::Channel.new(mock('session'), [c1, c2]) assert_equal channel, channel.on_extended_data { |c| results << c } assert_equal [c1, c2], results end def test_on_process_should_delegate_to_component_channels c1, c2, results = mock('channel'), mock('channel'), [] c1.expects(:on_process).yields(c1) c2.expects(:on_process).yields(c2) channel = Net::SSH::Multi::Channel.new(mock('session'), [c1, c2]) assert_equal channel, channel.on_process { |c| results << c } assert_equal [c1, c2], results end def test_on_close_should_delegate_to_component_channels c1, c2, results = mock('channel'), mock('channel'), [] c1.expects(:on_close).yields(c1) c2.expects(:on_close).yields(c2) channel = Net::SSH::Multi::Channel.new(mock('session'), [c1, c2]) assert_equal channel, channel.on_close { |c| results << c } assert_equal [c1, c2], results end def test_on_eof_should_delegate_to_component_channels c1, c2, results = mock('channel'), mock('channel'), [] c1.expects(:on_eof).yields(c1) c2.expects(:on_eof).yields(c2) channel = Net::SSH::Multi::Channel.new(mock('session'), [c1, c2]) assert_equal channel, channel.on_eof { |c| results << c } assert_equal [c1, c2], results end def test_on_request_should_delegate_to_component_channels c1, c2, results = mock('channel'), mock('channel'), [] c1.expects(:on_request).with("exit-status").yields(c1) c2.expects(:on_request).with("exit-status").yields(c2) channel = Net::SSH::Multi::Channel.new(mock('session'), [c1, c2]) assert_equal channel, channel.on_request("exit-status") { |c| results << c } assert_equal [c1, c2], results end private class MockSession def loop while true do return if !yield(self) end end end end net-ssh-multi-1.2.1/test/session_actions_test.rb0000644000175000017500000001021212522123035020761 0ustar lucaslucasrequire 'common' require 'net/ssh/multi/server' require 'net/ssh/multi/session_actions' class SessionActionsTest < Minitest::Test class SessionActionsContainer include Net::SSH::Multi::SessionActions attr_reader :servers def initialize @servers = [] end def default_user "user" end def use(h, o={}) server = Net::SSH::Multi::Server.new(self, h, o) servers << server server end end def setup @session = SessionActionsContainer.new end def test_busy_should_be_true_if_any_server_is_busy srv1, srv2, srv3 = @session.use('h1'), @session.use('h2'), @session.use('h3') srv1.stubs(:busy?).returns(false) srv2.stubs(:busy?).returns(false) srv3.stubs(:busy?).returns(true) assert @session.busy? end def test_busy_should_be_false_if_all_servers_are_not_busy srv1, srv2, srv3 = @session.use('h1'), @session.use('h2'), @session.use('h3') srv1.stubs(:busy?).returns(false) srv2.stubs(:busy?).returns(false) srv3.stubs(:busy?).returns(false) assert !@session.busy? end def test_send_global_request_should_delegate_to_sessions s1 = mock('ssh') s2 = mock('ssh') s1.expects(:send_global_request).with("a", "b", "c").yields s2.expects(:send_global_request).with("a", "b", "c").yields @session.expects(:sessions).returns([s1, s2]) calls = 0 @session.send_global_request("a", "b", "c") { calls += 1 } assert_equal 2, calls end def test_open_channel_should_delegate_to_sessions_and_set_accessors_on_each_channel_and_return_multi_channel srv1 = @session.use('h1') srv2 = @session.use('h2') s1 = { :server => srv1 } s2 = { :server => srv2 } c1 = { :stub => :value } c2 = {} c1.stubs(:connection).returns(s1) c2.stubs(:connection).returns(s2) @session.expects(:sessions).returns([s1, s2]) s1.expects(:open_channel).with("session").yields(c1).returns(c1) s2.expects(:open_channel).with("session").yields(c2).returns(c2) results = [] channel = @session.open_channel do |c| results << c end assert_equal [c1, c2], results assert_equal "h1", c1[:host] assert_equal "h2", c2[:host] assert_equal srv1, c1[:server] assert_equal srv2, c2[:server] assert_instance_of Net::SSH::Multi::Channel, channel assert_equal [c1, c2], channel.channels end def test_exec_should_raise_exception_if_channel_cannot_exec_command c = { :host => "host" } @session.expects(:open_channel).yields(c).returns(c) c.expects(:exec).with('something').yields(c, false) assert_raises(RuntimeError) { @session.exec("something") } end def test_exec_with_block_should_pass_data_and_extended_data_to_block c = { :host => "host" } @session.expects(:open_channel).yields(c).returns(c) c.expects(:exec).with('something').yields(c, true) c.expects(:on_data).yields(c, "stdout") c.expects(:on_extended_data).yields(c, 1, "stderr") c.expects(:on_request) results = {} @session.exec("something") do |c, stream, data| results[stream] = data end assert_equal({:stdout => "stdout", :stderr => "stderr"}, results) end def test_exec_without_block_should_write_data_and_extended_data_lines_to_stdout_and_stderr c = { :host => "host" } @session.expects(:open_channel).yields(c).returns(c) c.expects(:exec).with('something').yields(c, true) c.expects(:on_data).yields(c, "stdout 1\nstdout 2\n") c.expects(:on_extended_data).yields(c, 1, "stderr 1\nstderr 2\n") c.expects(:on_request) $stdout.expects(:puts).with("[host] stdout 1\n") $stdout.expects(:puts).with("[host] stdout 2") $stderr.expects(:puts).with("[host] stderr 1\n") $stderr.expects(:puts).with("[host] stderr 2") @session.exec("something") end def test_exec_should_capture_exit_status_of_process c = { :host => "host" } @session.expects(:open_channel).yields(c).returns(c) c.expects(:exec).with('something').yields(c, true) c.expects(:on_data) c.expects(:on_extended_data) c.expects(:on_request).with("exit-status").yields(c, Net::SSH::Buffer.from(:long, 127)) @session.exec("something") assert_equal 127, c[:exit_status] end end net-ssh-multi-1.2.1/test/server_test.rb0000644000175000017500000001574612522123035017105 0ustar lucaslucasrequire 'common' require 'net/ssh/multi/server' class ServerTest < Minitest::Test def setup @master = stub('multi-session', :default_user => "bob") end def test_accessor_without_properties_should_access_empty_hash assert_nil server('host')[:foo] end def test_accessor_with_properties_should_access_properties assert_equal "hello", server('host', :properties => { :foo => "hello" })[:foo] end def test_port_should_return_22_by_default assert_equal 22, server('host').port end def test_port_should_return_given_port_when_present assert_equal 1234, server('host', :port => 1234).port end def test_port_should_return_parsed_port_when_present assert_equal 1234, server('host:1234', :port => 1235).port end def test_user_should_return_default_user_by_default assert_equal "bob", server('host').user end def test_user_should_return_given_user_when_present assert_equal "jim", server('host', :user => "jim").user end def test_user_should_return_parsed_user_when_present assert_equal "jim", server('jim@host', :user => "john").user end def test_equivalence_when_host_and_user_and_port_match s1 = server('user@host:1234') s2 = server('user@host:1234') assert s1.eql?(s2) assert_equal s1.hash, s2.hash assert s1 == s2 end def test_equivalence_when_host_mismatch s1 = server('user@host1:1234') s2 = server('user@host2:1234') assert !s1.eql?(s2) refute_equal s1.hash, s2.hash assert s1 != s2 end def test_equivalence_when_port_mismatch s1 = server('user@host:1234') s2 = server('user@host:1235') assert !s1.eql?(s2) refute_equal s1.hash, s2.hash assert s1 != s2 end def test_equivalence_when_user_mismatch s1 = server('user1@host:1234') s2 = server('user2@host:1234') assert !s1.eql?(s2) refute_equal s1.hash, s2.hash assert s1 != s2 end def test_to_s_should_include_user_and_host assert_equal "user@host", server('user@host').to_s end def test_to_s_should_include_user_and_host_and_port_when_port_is_given assert_equal "user@host:1234", server('user@host:1234').to_s end def test_gateway_should_be_nil_by_default assert_nil server('host').gateway end def test_gateway_should_be_set_with_the_via_value gateway = mock('gateway') assert_equal gateway, server('host', :via => gateway).gateway end def test_session_with_default_argument_should_not_instantiate_session assert_nil server('host').session end def test_session_with_true_argument_should_instantiate_and_cache_session srv = server('host') session = expect_connection_to(srv) assert_equal session, srv.session(true) assert_equal session, srv.session(true) assert_equal session, srv.session end def test_session_that_cannot_authenticate_adds_host_to_exception_message srv = server('host') Net::SSH.expects(:start).with('host', 'bob', {}).raises(Net::SSH::AuthenticationFailed.new('bob')) begin srv.new_session flunk rescue Net::SSH::AuthenticationFailed => e assert_equal "bob@host", e.message end end def test_close_channels_when_session_is_not_open_should_not_do_anything server('host').close_channels end def test_close_channels_when_session_is_open_should_iterate_over_open_channels_and_close_them srv = server('host') session = expect_connection_to(srv) c1 = mock('channel', :close => nil) c2 = mock('channel', :close => nil) c3 = mock('channel', :close => nil) session.expects(:channels).returns(1 => c1, 2 => c2, 3 => c3) assert_equal session, srv.session(true) srv.close_channels end def test_close_when_session_is_not_open_should_not_do_anything server('host').close end def test_close_when_session_is_open_should_close_session srv = server('host') session = expect_connection_to(srv) session.expects(:close) @master.expects(:server_closed).with(srv) assert_equal session, srv.session(true) srv.close end def test_busy_should_be_false_when_session_is_not_open assert !server('host').busy? end def test_busy_should_be_false_when_session_is_not_busy srv = server('host') session = expect_connection_to(srv) session.expects(:busy?).returns(false) srv.session(true) assert !srv.busy? end def test_busy_should_be_true_when_session_is_busy srv = server('host') session = expect_connection_to(srv) session.expects(:busy?).returns(true) srv.session(true) assert srv.busy? end def test_preprocess_should_be_nil_when_session_is_not_open assert_nil server('host').preprocess end def test_preprocess_should_return_result_of_session_preprocess srv = server('host') session = expect_connection_to(srv) session.expects(:preprocess).returns(:result) srv.session(true) assert_equal :result, srv.preprocess end def test_readers_should_return_empty_array_when_session_is_not_open assert_equal [], server('host').readers end def test_readers_should_return_all_listeners_when_session_is_open srv = server('host') session = expect_connection_to(srv) io1, io2, io3, io4 = Reader.new, Reader.new, Reader.new, Reader.new session.expects(:listeners).returns(io1 => 2, io2 => 4, io3 => 6, io4 => 8) srv.session(true) assert_equal [io1, io2, io3, io4], srv.readers.sort end def test_writers_should_return_empty_array_when_session_is_not_open assert_equal [], server('host').writers end def test_writers_should_return_all_listeners_that_are_pending_writes_when_session_is_open srv = server('host') session = expect_connection_to(srv) listeners = { Reader.new(true) => 1, MockIO.new => 2, MockIO.new => 3, Reader.new => 4, Reader.new(true) => 5 } session.expects(:listeners).returns(listeners) srv.session(true) assert_equal 2, srv.writers.length end def test_postprocess_should_return_true_when_session_is_not_open assert_equal true, server('host').postprocess([], []) end def test_postprocess_should_call_session_postprocess_with_ios_belonging_to_session srv = server('host') session = expect_connection_to(srv) session.expects(:listeners).returns(1 => 2, 3 => 4, 5 => 6, 7 => 8) session.expects(:postprocess).with([1,3], [7]).returns(:result) srv.session(true) assert_equal :result, srv.postprocess([1,11,3], [18,14,7,12]) end private class MockIO include Comparable @@identifier = 0 attr_reader :id def initialize @id = (@@identifier += 1) end def <=>(io) @id <=> io.id end def closed? false end end class Reader < MockIO def initialize(ready=false) super() @ready = ready end def pending_write? @ready end end def server(host, options={}) Net::SSH::Multi::Server.new(@master, host, options) end def expect_connection_to(server) session = {} @master.expects(:next_session).with(server).returns(session) return session end end net-ssh-multi-1.2.1/test/test_all.rb0000644000175000017500000000027512522123035016336 0ustar lucaslucas# $ ruby -Ilib -Itest -rrubygems test/test_all.rb # $ ruby -Ilib -Itest -rrubygems test/channel_test.rb Dir["#{File.dirname(__FILE__)}/**/*_test.rb"].each do |file| load(file) endnet-ssh-multi-1.2.1/test/session_test.rb0000644000175000017500000001607212522123035017253 0ustar lucaslucasrequire 'common' require 'net/ssh/multi/session' class SessionTest < Minitest::Test def setup @session = Net::SSH::Multi::Session.new end def test_group_should_fail_when_given_both_mapping_and_block assert_raises(ArgumentError) do @session.group(:app => mock('server')) { |s| } end end def test_group_with_block_should_use_groups_within_block_and_restore_on_exit @session.open_groups.concat([:first, :second]) assert_equal [:first, :second], @session.open_groups yielded = nil @session.group(:third, :fourth) do |s| yielded = s assert_equal [:first, :second, :third, :fourth], @session.open_groups end assert_equal [:first, :second], @session.open_groups assert_equal @session, yielded end def test_group_with_mapping_should_append_new_servers_to_specified_and_open_groups s1, s2, s3, s4 = @session.use('h1', 'h2', 'h3', 'h4') @session.group :second => s1 @session.open_groups.concat([:first, :second]) @session.group %w(third fourth) => [s2, s3], :fifth => s1, :sixth => [s4] assert_equal [s1, s2, s3, s4], @session.groups[:first].sort assert_equal [s1, s2, s3, s4], @session.groups[:second].sort assert_equal [s2, s3], @session.groups[:third].sort assert_equal [s2, s3], @session.groups[:fourth].sort assert_equal [s1], @session.groups[:fifth].sort assert_equal [s4], @session.groups[:sixth].sort end def test_via_should_instantiate_and_set_default_gateway Net::SSH::Gateway.expects(:new).with('host', 'user', :a => :b).returns(:gateway) assert_equal @session, @session.via('host', 'user', :a => :b) assert_equal :gateway, @session.default_gateway end def test_use_should_add_new_server_to_server_list @session.open_groups.concat([:first, :second]) server = @session.use('user@host', :a => :b) assert_equal [server], @session.servers assert_equal 'host', server.host assert_equal 'user', server.user assert_equal({:a => :b}, server.options) assert_nil server.gateway end def test_use_with_open_groups_should_add_new_server_to_server_list_and_groups @session.open_groups.concat([:first, :second]) server = @session.use('host') assert_equal [server], @session.groups[:first].sort assert_equal [server], @session.groups[:second].sort end def test_use_with_default_gateway_should_set_gateway_on_server Net::SSH::Gateway.expects(:new).with('host', 'user', {}).returns(:gateway) @session.via('host', 'user') server = @session.use('host2') assert_equal :gateway, server.gateway end def test_use_with_duplicate_server_will_not_add_server_twice s1, s2 = @session.use('host', 'host') assert_equal 1, @session.servers.length assert_equal s1.object_id, s2.object_id end def test_with_should_yield_new_subsession_with_servers_for_criteria yielded = nil @session.expects(:servers_for).with(:app, :web).returns([:servers]) result = @session.with(:app, :web) do |s| yielded = s end assert_equal result, yielded assert_equal [:servers], yielded.servers end def test_servers_for_with_unknown_constraint_should_raise_error assert_raises(ArgumentError) do @session.servers_for(:app => { :all => :foo }) end end def test_with_with_constraints_should_build_subsession_with_matching_servers conditions = { :app => { :only => { :primary => true }, :except => { :backup => true } } } @session.expects(:servers_for).with(conditions).returns([:servers]) assert_equal [:servers], @session.with(conditions).servers end def test_on_should_return_subsession_containing_only_the_given_servers s1, s2 = @session.use('h1', 'h2') subsession = @session.on(s1, s2) assert_equal [s1, s2], subsession.servers end def test_on_should_yield_subsession_if_block_is_given s1 = @session.use('h1') yielded = nil result = @session.on(s1) do |s| yielded = s assert_equal [s1], s.servers end assert_equal result, yielded end def test_servers_for_should_return_all_servers_if_no_arguments srv1, srv2, srv3 = @session.use('h1', 'h2', 'h3') assert_equal [srv1, srv2, srv3], @session.servers_for.sort end def test_servers_for_should_return_servers_only_for_given_group srv1, srv2, srv3 = @session.use('h1', 'h2', 'h3') @session.group :app => [srv1, srv2], :db => [srv3] assert_equal [srv1, srv2], @session.servers_for(:app).sort end def test_servers_for_should_not_return_duplicate_servers srv1, srv2, srv3 = @session.use('h1', 'h2', 'h3') @session.group :app => [srv1, srv2], :db => [srv2, srv3] assert_equal [srv1, srv2, srv3], @session.servers_for(:app, :db).sort end def test_servers_for_should_correctly_apply_only_and_except_constraints srv1, srv2, srv3 = @session.use('h1', :properties => {:a => 1}), @session.use('h2', :properties => {:a => 1, :b => 2}), @session.use('h3') @session.group :app => [srv1, srv2, srv3] assert_equal [srv1], @session.servers_for(:app => {:only => {:a => 1}, :except => {:b => 2}}) end def test_close_should_close_server_sessions srv1, srv2 = @session.use('h1', 'h2') srv1.expects(:close_channels) srv2.expects(:close_channels) srv1.expects(:close) srv2.expects(:close) @session.close end def test_close_should_shutdown_default_gateway gateway = mock('gateway') gateway.expects(:shutdown!) Net::SSH::Gateway.expects(:new).returns(gateway) @session.via('host', 'user') @session.close end def test_loop_should_loop_until_process_is_false @session.expects(:process).with(5).times(4).returns(true,true,true,false).yields yielded = false @session.loop(5) { yielded = true } assert yielded end def test_preprocess_should_immediately_return_false_if_block_returns_false srv = @session.use('h1') srv.expects(:preprocess).never assert_equal false, @session.preprocess { false } end def test_preprocess_should_call_preprocess_on_component_servers srv = @session.use('h1') srv.expects(:preprocess) assert_equal :hello, @session.preprocess { :hello } end def test_preprocess_should_succeed_even_without_block srv = @session.use('h1') srv.expects(:preprocess) assert_equal true, @session.preprocess end def test_postprocess_should_call_postprocess_on_component_servers srv = @session.use('h1') srv.expects(:postprocess).with([:a], [:b]) assert_equal true, @session.postprocess([:a], [:b]) end def test_process_should_return_false_if_preprocess_returns_false assert_equal false, @session.process { false } end def test_process_should_call_select_on_combined_readers_and_writers_from_all_servers @session.expects(:postprocess).with([:b, :c], [:a, :c]) srv1, srv2, srv3 = @session.use('h1', 'h2', 'h3') srv1.expects(:readers).returns([:a]) srv1.expects(:writers).returns([:a]) srv2.expects(:readers).returns([]) srv2.expects(:writers).returns([]) srv3.expects(:readers).returns([:b, :c]) srv3.expects(:writers).returns([:c]) IO.expects(:select).with([:a, :b, :c], [:a, :c], nil, 5).returns([[:b, :c], [:a, :c]]) @session.process(5) end end net-ssh-multi-1.2.1/test/multi_test.rb0000644000175000017500000000124212522123035016713 0ustar lucaslucasrequire 'common' require 'net/ssh/multi' class MultiTest < Minitest::Test def test_start_with_block_should_yield_session_and_then_close Net::SSH::Multi::Session.any_instance.expects(:loop) Net::SSH::Multi::Session.any_instance.expects(:close) yielded = false Net::SSH::Multi.start do |session| yielded = true assert_instance_of Net::SSH::Multi::Session, session end end def test_start_without_block_should_return_open_session Net::SSH::Multi::Session.any_instance.expects(:loop).never Net::SSH::Multi::Session.any_instance.expects(:close).never assert_instance_of Net::SSH::Multi::Session, Net::SSH::Multi.start end end net-ssh-multi-1.2.1/gem-public_cert.pem0000644000175000017500000000223012522123035016760 0ustar lucaslucas-----BEGIN CERTIFICATE----- MIIDNjCCAh6gAwIBAgIBADANBgkqhkiG9w0BAQUFADBBMQ8wDQYDVQQDDAZkZWxh bm8xGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZFgNj b20wHhcNMTMwMjA2MTE1NzQ1WhcNMTQwMjA2MTE1NzQ1WjBBMQ8wDQYDVQQDDAZk ZWxhbm8xGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZ FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDg1hMtl0XsMuUK AKTgYWv3gjj7vuEsE2EjT+vyBg8/LpqVVwZziiaebJT9IZiQ+sCFqbiakj0b53pI hg1yOaBEmH6/W0L7rwzqaRV9sW1eJs9JxFYQCnd67zUnzj8nnRlOjG+hhIG+Vsij npsGbt28pefuNZJjO5q2clAlfSniIIHfIsU7/StEYu6FUGOjnwryZ0r5yJlr9RrE Gs+q0DW8QnZ9UpAfuDFQZuIqeKQFFLE7nMmCGaA+0BN1nLl3fVHNbLHq7Avk8+Z+ ZuuvkdscbHlO/l+3xCNQ5nUnHwq0ADAbMLOlmiYYzqXoWLjmeI6me/clktJCfN2R oZG3UQvvAgMBAAGjOTA3MAkGA1UdEwQCMAAwHQYDVR0OBBYEFMSJOEtHzE4l0azv M0JK0kKNToK1MAsGA1UdDwQEAwIEsDANBgkqhkiG9w0BAQUFAAOCAQEAtOdE73qx OH2ydi9oT2hS5f9G0y1Z70Tlwh+VGExyfxzVE9XwC+iPpJxNraiHYgF/9/oky7ZZ R9q0/tJneuhAenZdiQkX7oi4O3v9wRS6YHoWBxMPFKVRLNTzvVJsbmfpCAlp5/5g ps4wQFy5mibElGVlOobf/ghqZ25HS9J6kd0/C/ry0AUtTogsL7TxGwT4kbCx63ub 3vywEEhsJUzfd97GCABmtQfRTldX/j7F1z/5wd8p+hfdox1iibds9ZtfaZA3KzKn kchWN9B6zg9r1XMQ8BM2Jz0XoPanPe354+lWwjpkRKbFow/ZbQHcCLCq24+N6b6g dgKfNDzwiDpqCA== -----END CERTIFICATE----- net-ssh-multi-1.2.1/Rakefile0000644000175000017500000000317012522123035014665 0ustar lucaslucasrequire "rubygems" require "rake" require "rake/clean" require "rdoc/task" task :default => ["build"] CLEAN.include [ 'pkg', 'rdoc' ] name = "net-ssh-multi" $:.unshift File.join(File.dirname(__FILE__), 'lib') require './lib/net/ssh/multi/version' version = Net::SSH::Multi::Version::STRING.dup begin require "jeweler" Jeweler::Tasks.new do |s| s.version = version s.name = name s.rubyforge_project = s.name s.summary = "Control multiple Net::SSH connections via a single interface." s.description = s.summary s.email = "net-ssh@solutious.com" s.homepage = "https://github.com/net-ssh/net-scp" s.authors = ["Jamis Buck", "Delano Mandelbaum"] s.add_dependency 'net-ssh', ">=2.6.5" s.add_dependency 'net-ssh-gateway', ">=1.2.0" s.add_development_dependency 'minitest' s.add_development_dependency 'mocha' s.license = "MIT" s.signing_key = File.join('/mnt/gem/', 'gem-private_key.pem') s.cert_chain = ['gem-public_cert.pem'] end Jeweler::GemcutterTasks.new rescue LoadError puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" end require 'rake/testtask' Rake::TestTask.new do |t| t.libs = ["lib", "test"] end extra_files = %w[LICENSE.txt THANKS.txt CHANGES.txt ] RDoc::Task.new do |rdoc| rdoc.rdoc_dir = "rdoc" rdoc.title = "#{name} #{version}" rdoc.generator = 'hanna' # gem install hanna-nouveau rdoc.main = 'README.rdoc' rdoc.rdoc_files.include("README*") rdoc.rdoc_files.include("bin/*.rb") rdoc.rdoc_files.include("lib/**/*.rb") extra_files.each { |file| rdoc.rdoc_files.include(file) if File.exists?(file) } end net-ssh-multi-1.2.1/CHANGES.txt0000644000175000017500000000141312522123035015027 0ustar lucaslucas === 1.2.1 / 11 Apr 2015 * Fix two problems with :concurrent_connections option (#4) [sersut] * Replaced test-unit with minitest [juliandunn] === 1.2.0 / 06 Feb 2013 * Added public cert. All gem releases are now signed. See INSTALL in readme. === 1.1 / 3 Apr 2011 * Rescue Timeout::Error so :on_error works as expected when server is unavailable. [Joel Watson] === 1.0.1 / 1 Feb 2009 * Remove redundant call to block_given? in Session#group [paddor] * Add Channel#on_open_failed callback hook [Jamis Buck] === 1.0.0 / 1 May 2008 * (no changes since the last preview release) === 1.0 Preview Release 2 (0.99.1) / 19 Apr 2008 * Don't try to select on closed IO streams [Jamis Buck] === 1.0 Preview Release 1 (0.99.0) / 10 Apr 2008 * First release of Net::SSH::Multi net-ssh-multi-1.2.1/README.rdoc0000644000175000017500000000765112522123035015036 0ustar lucaslucas= Net::SSH::Multi * Docs: http://net-ssh.github.com/net-ssh-multi * Issues: https://github.com/net-ssh/net-ssh-multi/issues * Codes: https://github.com/net-ssh/net-ssh-multi * Email: net-ssh@solutious.com As of v1.1.1, all gem releases are signed. See INSTALL. == DESCRIPTION: 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). == FEATURES: * Easily manage multiple connections * Open channels, spawn processes, etc. on multiple connections in parallel * Transparently limit concurrent connections when dealing with large numbers of servers (Net::SSH::Multi::Session#concurrent_connections) * Specify a default gateway machine through which connections should be tunneled, or even specify a different gateway machine for each server == SYNOPSIS: In a nutshell: 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. == REQUIREMENTS: * net-ssh (version 2) * net-ssh-gateway If you want to run the tests or use any of the Rake tasks, you'll need: * Echoe (for the Rakefile) * Mocha (for the tests) == INSTALL: * gem install net-ssh-multi However, in order to be sure the code you're installing hasn't been tampered with, it's recommended that you verify the signiture[http://docs.rubygems.org/read/chapter/21]. To do this, you need to add my public key as a trusted certificate (you only need to do this once): # Add the public key as a trusted certificate # (You only need to do this once) $ curl -O https://raw.github.com/net-ssh/net-ssh/master/gem-public_cert.pem $ gem cert --add gem-public_cert.pem Then, when install the gem, do so with high security: $ gem install net-ssh-multi -P HighSecurity If you don't add the public key, you'll see an error like "Couldn't verify data signature". If you're still having trouble let me know and I'll give you a hand. == LICENSE: (The MIT License) Copyright (c) 2008 Jamis Buck Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.