Gih's Blog

只言片语

Perl Net::Telnet的使用(多服务器远程管理)

2011-06-03 by gihnius, tagged as perl

Perl真是个好东西,虽然学习Perl不久,感觉就是容易上手.工作中要维护几百套系统.

而那些"老管理员"和这些"老机器"只开放远程Telnet服务. 开始时只是使用 Net::Telnet模块里介绍的几个例子,后来有些特殊需求,才扩展到下面这个复杂的脚本. Perler称之为: Ugly Perl Code! 脚本:

#!/usr/bin/perl -w

##
##  Author: gihnius@gmail.com
##

use strict;

use Net::Telnet;

## working under B-shell like remote shell

my $tn_prompt = '/[\$#>]/';     ## default shell prompt
my $tn_username = 'god';        ## public telnet account
my $tn_passwd = 'god';
my $tn_login_timeout = 15;      ## wait for login passwd prompt
my $tn_connect_timeout = 20;    ## wait for connecting
my $tn_cmd_timeout = 60;        ## wait for command
my $tn_max_buf_len = 5;         ## 5MB of output buffer size max
my $hook_script = "";           ## script file use to process output of cmdline
my $use_hook = 0;
my $cmd_file = "";        ## list cmds in this file
my $hosts_file = "";      ## list ip,username,passwd in this file
my $by_hosts_file = 0;    ## use command line default
my $by_cmd_file = 0;      ## ..
my $output_file = "";     ## stdout default
my $save_output = 0;
my $log_file = "$0" . '.log';     ## save log
my @tn_ips = ();
my @cmds = ();
my @prev_cmds = ();
my @post_cmds = ();
my @prs = ();
my @poc = ();
my $cmd_print = 1;              ## include cmd name in outputs ?
my $ip_print = 1;               ## print host's ip for current cmds ?

sub usage {
    print <<END_USAGE;
$0: usage
    -c commands (-c "hostname; uptime; errpt; ..." OR -c cmds_list_file)
    -prev commands (remote cmds before execute "-c commands", no cmds list file)
    -post commands (remote cmds after execute "-c commands", no cmds list file)
    -prs commands (local cmds before first login)
    -poc commands (local cmds after last logout)
    -H hook_script (call hook script {by system (script)} to process the output of each command.)
    -h hostsfile OR ip list (-h hosts.txt OR -h 10.0.0.1 192.168.0.1 ...)
       hostsfile format:(3 cols, username and password are opts)
       ip username password
    -m N (number N MB) max output buffer size
    -o output_file  (save output to file)
    -oc BOOL (1 OR 0 for output cmd name) default 1
    -oh BOOL (1 OR 0 for output ip) default 1
    -u user (for hostsfile that not hava a 'user' col)
    -p password
    -P PROMPT (-P 'bash >>') Your shell prompt
    -l logfile (log executed commands and the failed login host)
    -t [ login_timeout connect_timeout command_timeout or only one ] ( -t 45 45 90)
Example:
$0 -c "hostname; uptime; date" -h host.txt  ## host.txt has account
$0 -c cmd.txt -h 192.168.1.1 192.168.2.3 -u god -p god1234 -o output.log
$0 -c "errpt" -h host.txt -u god -p god     ## host.txt has no account
cmd.txt:
hostname
uname -uM
oslevel -r
errpt
END_USAGE
    exit 1;
}

## get command line args
sub get_cmd_line {
    my @ARGS = @ARGV;
    my $numargv = @ARGS;
    usage() unless ($numargv);
    my $counter = 0;

    for ($counter = 0; $counter < $numargv; $counter++) {

        if ($ARGS[$counter] =~ /^-c$/) {
            $counter++;
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                if (-f $ARGS[$counter]) {
                    $cmd_file = $ARGS[$counter]; $by_cmd_file = 1;
                } else {
                    my $cmd_str = $ARGS[$counter];
                    $by_cmd_file = 0;
                    @cmds = split (/;/,$cmd_str);
                }
            } else {
                print "Warning - use -c cmd_list.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-prev$/) {
            $counter++;
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                my $cmd_str = $ARGS[$counter];
                @prev_cmds = split (/;/,$cmd_str);
            } else {
                print "Warning - use -prev 'cmds..'.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-post$/) {
            $counter++;
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                my $cmd_str = $ARGS[$counter];
                @post_cmds = split (/;/,$cmd_str);
            } else {
                print "Warning - use -post 'cmds..'.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-prs$/) {
            $counter++;
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                my $cmd_str = $ARGS[$counter];
                @prs = split (/;/,$cmd_str);
            } else {
                print "Warning - use -prs 'cmds..'.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-poc$/) {
            $counter++;
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                my $cmd_str = $ARGS[$counter];
                @poc = split (/;/,$cmd_str);
            } else {
                print "Warning - use -poc 'cmds..'.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-H$/) {
            $counter++;
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                $hook_script = $ARGS[$counter];
                $use_hook = 1;
            } else {
                print "Warning - use -H hook_script.\n";
                $use_hook = 0;
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-h$/) {
            $counter++;
            while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) {
                if (-f $ARGS[$counter]) {
                    $hosts_file = $ARGS[$counter]; $by_hosts_file = 1;
                } elsif ($ARGS[$counter] =~ /(\d+\.){3}\d+/) {
                    push (@tn_ips, $ARGS[$counter]);
                    $by_hosts_file = 0;
                }
                $counter++;
            }
            $counter--;
        } elsif ($ARGS[$counter] =~ /^-o$/) {
            $counter++;
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                $output_file = $ARGS[$counter];
                $save_output = 1;
            } else {
                print "Warning - use -o output_filename.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-m$/) {
            $counter++;
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                $tn_max_buf_len = $ARGS[$counter];
            } else {
                print "Warning - use -m X.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-oc$/) {
            $counter++;
            if (defined $ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                $cmd_print = $ARGS[$counter];
            } else {
                print "Warning - use -oc 1 OR 0.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-oh$/) {
            $counter++;
            if (defined $ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                $ip_print = $ARGS[$counter];
            } else {
                print "Warning - use -oh 1 OR 0.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-u$/) {
            $counter++;
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                $tn_username = $ARGS[$counter];
            } else {
                print "Warning - use -u username.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-p$/) {
            $counter++;
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                $tn_passwd = $ARGS[$counter];
            } else {
                print "Warning - use -p password.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-P$/) {
            $counter++;
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                $tn_prompt = $ARGS[$counter];
            } else {
                print "Warning - use -P PROMPT.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-l$/) {
            $counter++;
            if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) {
                $log_file = $ARGS[$counter];
            } else {
                print "Warning - use -l logfile.\n";
                $counter--;
            }
        } elsif ($ARGS[$counter] =~ /^-t$/) {
            $counter++;
            my @to;
            my $i = 0;
            while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) {
                if ($i > 2) {
                    last;
                }
                if ($ARGS[$counter] =~ /\d+/) {
                    push (@to, $ARGS[$counter]);
                }
                $i++;
                $counter++;
            }
            $tn_login_timeout = $to[0];
            $tn_connect_timeout = $to[1];
            $tn_cmd_timeout = $to[2];
            $counter--;
        } else {
            print "Error: unknow option!\n";
            usage();
        }
    }                           ## end for
}


## get ip address from given file
sub get_ip_from_file {
    my @hosts;
    my $n = 0;
    open (HOSTS, "<$_[0]") || die "failed to open hosts file: $!";
    while (my $line = <HOSTS>) {
        chomp $line;
        if ($line =~ /^\s*$/ || $line =~ /^\s*#/) {
            next;
        }

        my @infos = split (/\s+/, $line);

        (defined $infos[0])?($hosts[$n]->{ip} = $infos[0]):($hosts[$n]->{ip} = undef);
        (defined $infos[1])?($hosts[$n]->{user} = $infos[1]):($hosts[$n]->{user} = undef);
        (defined $infos[2])?($hosts[$n]->{pass} = $infos[2]):($hosts[$n]->{pass} = undef);
        $n++;
    }
    close (HOSTS);
    return @hosts;
}

## get cmd list in file
## store command/commands each line, not a whole script
sub get_cmd_from_file {
    my @cmds;
    open (CMDS, "<$_[0]") || die "failed to open cmd list file: $!";
    while (my $cmd = <CMDS>) {
        chomp ($cmd);
        push (@cmds, $cmd);
    }
    close CMDS;
    return @cmds;
}

sub main {
    get_cmd_line();

    open (OUT_ERR, ">> ".$log_file) || die "Can't open logfile for append! $!";

    if ($save_output eq 1) {
        open (OUT_PUT, ">> ".$output_file) || die "Can't open output file for writing! $!";
    }

    my $logtime = localtime();
    print OUT_ERR "\nError log from : $logtime\n";

    my ($ip,$username,$passwd);
    my $i = 0;
    my $runh = 0;

    if ($by_hosts_file) {
        @tn_ips = get_ip_from_file($hosts_file);
    }
    if ($by_cmd_file) {
        @cmds = get_cmd_from_file($cmd_file);
    }

    foreach my $cmd (@prs) {
        system ("$cmd");
    }

    for ($i = 0; $i < ($#tn_ips + 1); $i++) {
        if (! $by_hosts_file) {
            $ip = $tn_ips[$i];
            $username = $tn_username;
            $passwd = $tn_passwd;
        }
        if ($by_hosts_file) {
            (defined $tn_ips[$i]->{ip})?($ip = $tn_ips[$i]->{ip}):($ip = $tn_ips[$i]);
            (defined $tn_ips[$i]->{user})?($username = $tn_ips[$i]->{user}):($username = $tn_username);
            (defined $tn_ips[$i]->{pass})?($passwd = $tn_ips[$i]->{pass}):($passwd = $tn_passwd);
        }

        my $t = new Net::Telnet(Timeout => 15, Prompt => $tn_prompt, Cmd_remove_mode => 1, Errmode => (sub {print OUT_ERR "\nBad $ip - unable to connect!\n"; print OUT_ERR "====================================\n"; return;}));
        $t -> max_buffer_length(1024*1024*$tn_max_buf_len);

        my $ret = 0;  ## get return of open
        $ret = $t->open(Host => $ip, Timeout => $tn_connect_timeout); ## open will return 1 if no error
        unless ($ret eq 1) {
            my $errmsg = $t->errmsg();
            print STDERR "$errmsg\n";
            print OUT_ERR "\nErr: $errmsg\n\n";
            next;
        }
        $ret = $t->login(Name => $username, Password => $passwd, Timeout => $tn_login_timeout);
        unless ($ret eq 1) {
            my $errmsg = $t->errmsg();
            print STDERR "$errmsg\n";
            print OUT_ERR "\nErr: $errmsg\n\n";
            next;
        }

        if ($save_output eq 1) {
            print OUT_PUT "\nRunning on $ip \n" if $ip_print;
        } else {
            print "\nRunning on $ip \n" if $ip_print;
        }
        $runh++;

        my @temp = $t->cmd(String => "PS1=\"@@@\"",  Prompt => '/@@@/');
        $t->buffer_empty;

        my $tmpfile = $ip.'_output.tmp'; ## for hook script
        if ($use_hook) {
            if (-f $tmpfile) {
                unlink $tmpfile;
            }
            open (TMP, ">> ".$tmpfile) || die "Can't open tmpfile for cmd output! $!";
            print TMP "\nRunning on $ip \n";
        }

        foreach my $cmd (@prev_cmds) {
            system ("$cmd");
        }

        foreach my $cmd (@cmds) {
            my @results = $t->cmd(String => "$cmd", Prompt => '/@@@/', Timeout => $tn_cmd_timeout);
            if ($use_hook) {
                print TMP "Executed $cmd >>\n" if $cmd_print;
                print TMP @results;
            } else {
                if ($save_output eq 1) {
                    print OUT_PUT "Executed $cmd >>\n" if $cmd_print;
                    print OUT_PUT @results;
                } else {
                    print "Executed $cmd >>\n" if $cmd_print;
                    print @results;
                }
            }
            $t->buffer_empty;
        }                       ## end foreach

        foreach my $cmd (@post_cmds) {
            system ("$cmd");
        }

        if ($use_hook) {
            close TMP;
            system ("$hook_script $tmpfile");
            unlink $tmpfile;
        }
        $t->close;
    }                           ## end for

    foreach my $cmd (@poc) {
        system ("$cmd");
    }

    print "\nFinished $runh host.\n" if $ip_print;
    print "Lost ". ($i - $runh) ." host.\n" if $ip_print;
    close OUT_ERR;              ## close log file
    if ($save_output eq 1) {
        close OUT_PUT;
    }
}

main();
其中有一大段代码是处理命令行的,你可能觉得奇怪,不是有 getopts 吗?确实,用getopts很快就搞定了, 我主要是为了练习一下,看看自己还能不能"折腾".可能大多数环境下不需要这么复杂,你可以直接使用最简单的情况:
use Net::Telnet ();
$t = new Net::Telnet (Timeout => 10,
                      Prompt => '/bash\$ $/');
$t->open("host");
$t->login($username, $passwd);
@lines = $t->cmd("who");
print @lines;
使用例子:
tnrun.pl -h telnet.hosts -c "uname -uM; oslevel -r; uptime" -u dwlinjian -p 'PassWord'
输出:
Running on 139.140.128.21 
Executed uname -uM >>
IBM,7026-6M1 IBM,0165FC30A
Executed  oslevel -r >>
5300-05
Executed  uptime >>
  02:43PM   up 308 days,  23:21,  1 user,  load average: 0.39, 0.52, 0.55

Running on 139.140.128.22 
Executed uname -uM >>
IBM,7026-6M1 IBM,0165FC28A
Executed  oslevel -r >>
5300-05
Executed  uptime >>
  02:43PM   up 303 days,  14:44,  1 user,  load average: 0.31, 0.41, 0.42
......
......
......
Running on 139.119.232.22 
Executed uname -uM >>
IBM,7026-6M1 IBM,0165F802A
Executed  oslevel -r >>
5300-05
Executed  uptime >>
  02:44PM   up 303 days,  13:46,  1 user,  load average: 0.95, 1.11, 1.10

Finished 20 host.
Lost 1 host.
日志:
Bad 1.1.100.110 - unable to connect!
====================================

Err: problem connecting to "139.118.100.110", port 23: connect timed-out
'Lost 1 host' 及日志表明有一台主机登录失败. 如果中间有主机登录失败,并不影响其余的. 因为 Net::Telnet模块提供了出错处理功能.在这里"Errmode"打印了出错信息,并返回继续处理.
my $t = new Net::Telnet(Timeout => 15, Prompt => $tn_prompt, Cmd_remove_mode => 1, Errmode => (sub {print OUT_ERR "\nBad $ip - unable to connect!\n"; print OUT_ERR "====================================\n"; return;}));
有时会遇到执行命令没有输出的情况, 尝试在server的 .profile加入
export TERM=vt100
set -o vi
就可以.