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