Full code follows at the bottom.


I've recently started hacking around on this again, and I'm stuck on a few 
things.  Firstly, this little work is a real memory hog, but I'm not skilled 
enough to be able to figure out why (anyone?).  Secondly, I have some minor 
issues that I'd like to solve, but I'm not sure how.  First, a little 
background of what this does.  This program basically logs you into any 
number of systems, using telnet and the same username and password for all 
of them.  Then you can run the same command on all of them by only typing it 
once.  So, I can say ./stelnet.rb host1 .. host20, log in just one time and 
then look at the root filesystems on all of them by running "ls /" one time 
and scrolling through the output.  So, on to my questions.  I'm having a 
hard time figuring out how to run some interactive commands which don't 
return any type of prompt (i.e. ex).  If I try to start an ex session (to 
edit a file) my program times out waiting for a prompt to return.  How can I 
get around this?  Also, how could I pass a CTRL-c through to ex instead of 
having my program die over it?  I guess the basic question is, how do I get 
this to act more like a real telnet session (i.e. more solid)?

Thanks

Code follows

#!/usr/gnu/bin/ruby

#  %W%        %G%
#
#  Program Name:        stelnet.rb
#  Date Created:        02/06/01
#  Creator:             Mike Wilson
#
#  SYNOPSIS
#       stelnet.rb host1..hostn
#
#  DESCRIPTION
#       stelnet will log you in to multiple systems, using telnet, and run
#       the specified commands.
#
require 'net/telnet'

module Net
    class Telnet
        # Allow us to get the hostname by printing the connection
        def to_s
            @options['Host']
        end
    end
end

module SuperTelnet
    class User
        # I'll add some stuff to this class at some point.
        attr_accessor :username, :passwd

        def initialize(user, pass)
            @username = user
            @passwd = pass
        end
    end

    class Connection
        # Keep track of all the open connections
        @@uservars = {}

        # This will be used when we only want to execute something once
        # i.e. help should only be displayed 1 time -- not 1 time per
        # connection.
        @@flag = 0

        def initialize(aHash, userObj)
            raise ArgumentError if not userObj.instance_of? 
SuperTelnet::User

            @conOptions = aHash
            @thisUser = userObj

            # Each user gets an array which will hold his connections
            # Hopefully, we'll be able to handle multiple users at some 
point.
            @@uservars[@thisUser] ||= []

            # Set some defaults so that you really only need to provide
            # the hostname to connect to within the conOptions hash.
            @conOptions['Prompt'] ||= Regexp.new('[#$%>] *\z', false, 'n')
            @conOptions['Timeout'] ||= 10
            @conOptions['Port'] ||= 23

            begin
	        @thisConnection = Net::Telnet.new(@conOptions)
            rescue SocketError
                yield "SocketError on #{@conOptions['Host']}, bad host?" if
                    block_given?
            rescue TimeoutError
                yield "TimeoutError on #{@conOptions['Host']}, host down?" 
if
                    block_given?
            rescue Errno::ECONNREFUSED
                yield "Connection refused on #{@conOptions['Host']}" if
                    block_given?
            else
                @@uservars[@thisUser] << self
            end

            @conOptions['Timeout'] = 30
        end

        # If the user enters something that looks like a meta command, but
        # isn't, method_missing will catch it and send it to the executeCmd
        # method to be issued on the command line.
        def method_missing(methodId)
            executeCmd(methodId.id2name) do |c|
                yield c if block_given?
            end
        end

        def to_s
            @thisConnection.to_s
        end

        def Connection.uservars
            @@uservars
        end

        # Always call this after looping through all of the hosts within
        # your implementation of this module.
        def Connection.flagReset
            @@flag = 0
        end

        # Feel free to redefine this as you add commands
        def Connection.help
            "\nAvaiable commands:\n" +
            "  !login(host [hostn])             -> autologin to hosts\n" +
            "  !logoff(host [hostn])            -> logoff hosts\n" +
            "  !su(host [hostn])                -> change user on hosts\n" +
            "  !su                              -> to su on all hosts\n" +
            "  !on(host [hostn]) somecommand    -> run only on these 
hosts\n" +
            "  !off(host [hostn]) somecommand   -> don't run on these\n" +
            "  !set key=value                   -> set user variable\n" +
            "                                      (reference via @key)\n" +
            "  !unset key                       -> unset user variable\n" +
            "  !quit(host [hostn])              -> logoff hosts\n" +
            "  !help                            -> this screen\n\n"
        end

        # Call this for each connection that's been created for thisUser.
        def loginOnConnection
            return if @thisConnection.nil?
            begin
                @thisConnection.login(@thisUser.username, @thisUser.passwd)
            rescue TimeoutError
                yield "TimeoutError on #{@thisConnection}, bad login?" if
                    block_given?
                logoffConnection
            end
        end

        # Call this when you're all done with the connection.  I use this
        # within the loginOnConnection method to remove the connection if
        # the login fails (i.e. bad login).
        def logoffConnection
            @@uservars[@thisUser].delete(self)
            @thisConnection.close
        end

        ####
        # Commands:  So far, you have 2 types of commands -- those that look
        # like !this and those that look like ?this.  !this being a regular
        # command, and ?this being more like a query. (i.e. ?threads #=> on)
        #
        # To add a new command, simply add the corresponding method.  Either
        # Bang_commandName(args, extras) or Qmark_commandName(args, extras)
        # depending on whether it's query or not.  Use the flag if
        # you only want it to execute once.  Other than that, you're done.
        ####

        # !help will print the help message.  Another reason to call
        # Connection.flagReset when you're done
        def Bang_help(args, extras)
            if @@flag == 0
                @@flag = 1
                Connection.help
            end
        end

        # This command equates to !su and !su(host1 host2) and !su host1 
host2
        # Here, we're allowing a method to perform su's so that you can
        # implement a way to hide the password.
        def Bang_su(args, extras)
            user, pass, *hosts = args
            if hosts.size.zero? or hosts.detect do |c|
                    c =~ @thisConnection.to_s
                end
                tmpHash = @conOptions

                # I'm assuming that you want the environment (i.e. the '-')
                tmpHash['String'] = "su - #{user}"
                tmpHash['Match'] = /[pP]assword:/

                @thisConnection.cmd(tmpHash) do |c| yield c if block_given? 
end

                tmpHash['String'] = pass
                tmpHash['Match'] = @conOptions['Prompt']
                @thisConnection.cmd(tmpHash) do |c| yield c if block_given? 
end
            end
        end

        # This command equates to !login(host1 host2) and !login host1 host2
        # Here, you'll be logged into the new hosts without having to 
provide
        # login info -- it'll just use your initial info.
        #
        # This also shows a case where you'd need to call 
Connection.flagReset
        def Bang_login(args, extras)
            if @@flag == 0
                @@flag = 1
                args.each do |arg|
                    Connection.new({'Host' => arg}, @thisUser) do |c|
                        puts c
                    end.loginOnConnection do |c|
                        puts c
                    end
                end
            end
            return
        end

        # This command is !logoff and !logoff(host1 host2) and !logoff host1
        # self-explanatory.
        def Bang_logoff(args, extras)
            raise EOFError if args.size.zero?
            if args.detect do |c|
                    c == @thisConnection.to_s
                end
                logoffConnection
            end
        end

        # Pretty much like the Unix env command at a basic level.  !env
        def Bang_env(args, extras)
            output = []
            if @@flag == 0
                @@flag = 1
                @@uservars.each do |k, v|
                    v = v.join(' ') if v.type == Array
                    output << "#{k}=#{v}\n"
                end
                output
            end
        end

        # This command equates to "!on(host1 host2) do some command"
        def Bang_on(args, extras)
            output = []
            args.each do |arg|
                arg == @thisConnection.to_s and
                    output << runCmdOnConnection(extras)
            end
            output
        end

        # Opposite of above, don't run on these hosts. "!off(host1 host2) 
..."
        def Bang_off(args, extras)
            output = []
            args.each do |arg|
                arg != @thisConnection.to_s and
                    output << runCmdOnConnection(extras)
            end
            output
        end

        # Set user variable.  Use the variable like @this.
        # So, !set foo=bar  can be used as @foo
        def Bang_set(args, extras)
            if @@flag == 0
                @@flag = 1
                k, v = args.join(' ').to_s.split('=')
                @@uservars[k] = v
            end
        end

        # Unset user variable.  !unset foo
        def Bang_unset(args, extras)
            if @@flag == 0
                @@flag = 1
                args.each do |arg|
                    @@uservars.delete(arg)
                end
            end
        end

        # Everything typed by the user (all commands, whether they're to
        # be processed internally or sent to the shell on each box) should
        # pass through this method.  So, really this should probably be 
called
        # proxyCmd or something.
        def executeCmd(cmd)
            # Substitute user variables out before anything else
            @@uservars.each do |k, v|
                cmd.sub!('@' + k.to_s, v.to_s)
            end

            output = case cmd
                when /^([!?=].*)/
                    # It might be an internal command.  Try to process it as
                    # such and if it fails, pass it to the shell
                    begin
                        executeMetaCmd($1)
                    rescue ArgumentError
                        runCmdOnConnection($1)
                    end
                else
                    # Didn't look like anything we're supposed to deal with.
                    # Send it to the shell.
                    runCmdOnConnection(cmd)
            end
            if output and not output.empty? and block_given?
                # Use yields rather than prints so that we can throw a GUI
                # frontend on this someday.
                yield "#{output}\n\n"
            end
            output
        end

        # This is kind of like a proxy for our internal commands.
        def executeMetaCmd(cmd)
            # extras is necessary for commands like:
            # ?on(host1 host2) ls -ltr /var
            # extras would equal "ls -ltr /var" here.
            extras = cmd.sub(/[!\?]\w+\s*\(.*\)\s*(.*)$/, '\1')

            # This is the command (i.e. login) and the hosts (generally)
            metaCmd, *args = cmd.sub(/([!\?]\w+)\s*\((.*)\).*$/, '\1 
\2').split

            case metaCmd
                when /!(\w+)/
                    # If the command was: !login foo bar
                    # call Bang_login(['foo', 'bar'], "")
                    meth = 'Bang_' + $1
                when /\?(\w+)/
                    # If the command was: ?on(oscar) ls -ltr /var
                    # call Qmark_on(['oscar'], "ls -ltr /var")
                    meth = 'Qmark_' + $1
            end
            send(meth, args, extras)
        end

        # This will send the command to the shell for this connection.
        def runCmdOnConnection(cmd)
            output = ["\n#{@thisConnection}:\n"]

            @thisConnection.cmd({
                'String'  => cmd,
                'Match'   => @conOptions['Prompt'],
                'Timeout' => false
            }) do |c|
                if c and c.chomp != cmd
                    output << c
                end
            end
            output
        end

        alias_method :Bang_quit, :Bang_logoff
        alias_method :Bang_exit, :Bang_logoff
    end
end

if __FILE__ == $0
    # We need to add a command "!threads(on|off)" so that we can have
    # sequential or threaded access.  We add all of the support methods, 
along
    # with redefining Connection.help to account for the new command, and
    # finally we add the actual command method Bang_threads.
    module SuperTelnet
        class Connection
            @@threading = 'on'

            def Connection.threading=(toggle)
                toggle = @@threading if toggle != 'on' and toggle != 'off'
                @@threading = toggle
                return
            end

            def Connection.threading
                @@threading
            end

            def Connection.help
                "\nAvaiable commands:\n" +
                "  !login(host [hostn])             -> autologin to hosts\n" 
+
                "  !logoff(host [hostn])            -> logoff hosts\n" +
                "  !su(host [hostn])                -> change user on 
hosts\n" +
                "  !su                              -> to su on all hosts\n" 
+
                "  !on(host [hostn]) somecommand    -> run only on these 
hosts\n" +
                "  !off(host [hostn]) somecommand   -> don't run on these\n" 
+
                "  !set key=value                   -> set user variable\n" 
+
                "                                      (reference via 
@key)\n" +
                "  !unset key                       -> unset user 
variable\n" +
                "  !threads <on|off>                -> set threaded or 
sequential\n" +
                "  ?threads                         -> return thread 
status\n" +
                "  !quit(host [hostn])              -> logoff hosts\n" +
                "  !help                            -> this screen\n\n"
            end

            def Bang_threads(args, extras)
                Connection.threading = args.to_s
            end

            def Qmark_threads(args, extras)
                Connection.threading
            end
        end
    end

    include SuperTelnet

    progname = File.basename $0

    def getUser
        print 'Username: '
        $stdin.gets.chomp
    end

    def getPwd
        print 'Password: '
        begin
            system 'stty -echo'
            $stdin.gets.chomp
        ensure
            puts
            system 'stty echo'
        end
    end

    def usage(progname)
        print "Usage: " + progname
        puts " host1..hostn"
        print Connection.help
        exit
    end

    usage(progname) if ARGV.size.zero? or ARGV[0] =~ /^-[?h]/
    user = User.new(getUser, getPwd)

    ARGV.each do |aHost|
        Connection.new({'Host'=>aHost}, user) do |c| puts c end
    end

    loginThreads = []
    Connection.uservars[user].each do |aCon|
        loginThreads << Thread.new(aCon) do |con|
            con.loginOnConnection do |c| puts c end
        end
    end
    loginThreads.each do |aThread| aThread.join end

    cmdThreads = []
    # Set threading on
    Connection.threading = 'on'
    begin
        while true
            exit if Connection.uservars[user].size.zero?

            print "#{progname}> "
            cmd = $stdin.readline.chomp

            # Within our module, the Bang_su method is called as
            # Bang_su(['username', 'password', 'host1', 'host2'], "")
            # Obviously, we don't want you to type it in on the commandline
            # like that (you'll expose your password), so we proxy instead.
            # You enter !su(host1 host2) and we call getUser and getPwd to
            # prompt you for you info.
            if cmd =~ /!su\s*\(?(.*)+\)?$/
                cmd = "!su(#{getUser} #{getPwd} #{$1})"
            end

            if Connection.threading == 'on'
                Connection.uservars[user].each do |aCon|
                    cmdThreads << Thread.new(aCon) do |con|
                        begin
                            con.executeCmd(cmd) do |c| puts c end
                        rescue Errno::ECONNRESET, Errno::EPIPE
                            puts "#{con}: Connection reset by peer - 
autologoff"
                            con.logoffConnection
                        end
                    end
                end
                cmdThreads.each do |aThread| aThread.join end
            else
                Connection.uservars[user].each do |con|
                    begin
                        con.executeCmd(cmd) do |c| puts c end
                    rescue Errno::ECONNRESET, Errno::EPIPE
                        puts "#{con}: Connection reset by peer - autologoff"
                        con.logoffConnection
                    end
                end
            end
            # Always do this when you've completed processing each command.
            Connection.flagReset
        end
    rescue Interrupt, EOFError
        Connection.uservars[user].each do |con|
            con.logoffConnection
        end
    rescue SocketError
        retry
    end
end




_________________________________________________________________
Chat with friends online, try MSN Messenger: http://messenger.msn.com