【Perl】Coro多线程telnet实现网络设备高速批量管理

作为网络工程师,日常工作大多数时间都是在用telnet同交换机、路由器等网络设备打交道。如果说perl中有什么好用的模块,当然得提Net::Telnet。Net::Telnet功能全面,对于日常管理写个针对几台、几十台交换机的小程序还是不错的选择。

Net::Telnet是单线程、阻塞的,如果要处理数百台交换机,只能一台一台交换机的访问,程序运行在堵塞等待上花掉了大量的时间。

使用Net::Telnet去探索一个二百多台交换机的网络拓扑,一台交换机耗时几秒,全部遍历完毕要花费30分钟,还要应对Net::Telnet模块中各种error(某台交换机CPU100%卡死、vty进程数太少连接占满等等),一个error die就得推倒重来。费时费力,效果不好。

这几年perl没落了,多线程、并发的贴子国内很少,有些即便是提到多线程coro、anyevent之类的,也基本都是http方向的。由于我对TCP协议研究较少,短时间内要完成从socket层面重写telnet难度太高。昨天工作之余发现了简书用户飞天神猫(在此表示感谢)一年前的一篇笔记性文章《Coro-Telnet和Coro-Telnet+Golang-ssh-proxy性能测试》中提到“利用perl io::socket::telnet,封装AE::socket+Coro::handle实现异步telnet登录交互”。作者虽然在其他简书中列出了github地址,但很遗憾他的coro telnet模块代码并没有公布。

幸运的是Coro::Socket模块让telnet异步成为了可能:

use Coro::Socket;
 
# listen on any other type of socket
my $socket = Coro::Socket->new_from_fh #利用该方法可以直接调用第三方模块
                (IO::Socket::UNIX->new(
                        Local  => "/tmp/socket",
                        Type   => SOCK_STREAM,
                    )
                );

Coro::Socket能够将IO::Socket模块非阻塞化。而飞天神猫提到的IO::Socket::Telnet恰好继承自IO::Socket::INET。理论上可以在Coro::Socket直接使用IO::Socket::Telnet,就可以实现多线程telnet,经过半天的修改调试结果喜人:

对于一个二百多台网络设备的树形拓扑,用Net::Telnet获取邻居信息并根据邻居信息探索拓扑,遍历所有节点耗时30分钟以上;而经过算法重写,并使用多线程coro的telnet仅仅使用20秒时间(当时看到各节点IP飞速的显示到屏幕上,我的内心是相当激动的)。

时间有限。先放一个小的异步脚本在这里,供大家参考,后续再完善博文。有几点要提下,IO::Socket::Telnet这个模块仅仅是在IO::Socket::INET模块基础上对telnet协议的字符进行了转义、处理,并不是像Net::Telnet的近似客户端的交互工具。

比如CPAN上的例子就是个坑:

use IO::Socket::Telnet;
my $socket = IO::Socket::Telnet->new(PeerAddr => 'random.server.org');

sleep(5); #我添加的
defined $socket->recv(my $x, 4096) or die $!; #我添加的

while (1) {
    $socket->send(scalar <>);

    sleep(5); #我添加的
    defined $socket->recv(my $x, 4096) or die $!;
    print $x;
}

如果按照这个例子(注释掉我添加的语句),你会发现显示并不正常,服务器回传信息会比你的输入滞后一个回车,原因是recv方法接收数据的时间太短。而CPAN上它的分支IO::Socket::Telnet::HalfDuplex,它的作者为了解决这个问题,使用了一个不算可靠的小技巧,这位作者认为recv接收数据的时间不会比使用icmp协议对telnet服务端发送ping的响应时间长,他将recv方法通过“与”逻辑(and语句)与调用ping方法的代码块连接起来,当ping动作完成返回真时recv停止接收数据并显示在标准输出中。这样做对于一般交换机命令来说是可以的,但是当使用类似show int/show run/display current等需要返回长文本信息的命令时就不灵验了。

根据这位作者的思路,我利用sleep(5)修改例子,延长接收数据时间5秒钟,发现程序终于像一个正常telnet客户端工具了,这就意味着recv是需要足够的时间接收数据的。如果单纯使用时间来决定telnet下一步操作的话,实现多线程就没有意义了。于是,我开始尝试摸索,替代sleep的方法。

根据Net::Telnet源码的思路,作者通过sysread方法循环来实现完整的接收数据。于是,我将recv方法置于循环体中,将接收到的信息通过字符串连接符“.”重新组合,至于如何退出循环体,同样借鉴的Net::Telnet的prompt属性,通过正则表达式判断telnet的提示符,主要包括Username:、Password:、telnet提示符>或<>、enble(system-view)提示符#或[XXXX](在日常调试过程中,请注意其他需要结束接的收字符,比如提示密码错误的“Bad passwords”等,以防死循环发生),当正则条件满足last退出循环:
 

sub read {  #对recv封装,实现serve回传数据的正常交互
    my $self = shift;
    my $start;
    while (1) {
        $self->recv(my $x, 4096);
    
        next if !$x;

        $start .= $x;

        last if $x =~ /(<.+>|\[.+\]|.+>|.+#)$/;
        $self->send(' ') if $x =~ /More/;
        last if $x =~ /(Password: ?|Username: ?)/;
    }   

    return $start;
}

你会看到有这样一条语句:

$self->send(' ') if $x =~ /More/;

熟悉网络设备的自然明白这是用来对付长文本翻页的,通过自动发送空格使返回信息显示完整。这里并没有做进一步的处理,实际使用中会发现-- More --信息周围会存在telnet数据的特殊字符——ASCII码值,如果介意可使用正则表达式进行进一步处理。

想要实现类似Net::Telnet中的waitfor语句,仅仅替换正则表达式的内容即可

sub waitfor { #简单模仿Net::Telnet中的waitfor方法
    my $self = shift;
    my $regx = shift;
    my $start;
    while (1) {
        $self->recv(my $x, 4096);
    
        next if !$x;

        $start .= $x;

        last if $x =~ /$regx/;
        $self->send(' ') if $x =~ /More/;
    }   

    return $start;
}

下面放出一个完成Coro telnet多线程脚本,供参考。其中需要注意的一点是从标准输入中输入命令是自动带换行符的,如果使用字符串传送特定命令是没有换行符的(会发生服务端长时间等待回车符以执行命令,无任何返回信息——程序假死),即使用send方法向telnet服务端发送网络命令需要用双引号(确保换行符转义)结尾带换行符\n :

use IO::Socket::Telnet;
use strict;
use AnyEvent;
use Coro;
use Coro::AnyEvent;
use Coro::Socket;

my $start = time;

my @host = ("192.168.1.112","192.168.1.113","192.168.1.114","192.168.1.115","192.168.1.116",
            "192.168.1.117","192.168.1.118",,"192.168.1.253","192.168.1.119","192.168.1.120","192.168.1.121",
            "192.168.1.122","192.168.1.123","192.168.1.124","192.168.1.125","192.168.1.126",
            "192.168.1.127","192.168.1.128","192.168.1.129","192.168.1.130","192.168.1.131");
my @coro;


for my $host (@host) { #此处到foreach循环结束都可以照搬照抄
    push @coro, async {
        my $socket = Coro::Socket->new_from_fh
                        (IO::Socket::Telnet->new(
                                 PeerAddr => $host,
                                 Timeout => 5
                            )
                        );
        &doit($host,$socket); #具体多线程操作均在此方法内
        return;
    }
}

foreach (@coro) {
    print "joining\n"; #验证多线程使用,可以删除
    $_->join;
    print "joined\n"; #验证多线程使用,可以删除
}

sub doit {
    my $result; #负责收集telnet服务端返回的信息
    my $host = shift;
    my $socket = shift;
    my $s;
    eval {$s = &read($socket);}; #IO::Socket::Telnet模块本身缺少异常处理,此处
                                 #eval的作用是检测服务器连接超时并报错。考虑到多
                                 #线程,不建议调用die中断程序
    return print "Can't connect $host\n" if $@;
    $result .= $s;
    if ($s =~ /Username:/) { #猜测交换机的telnet密码
        $socket->send("user\n");
        $s = &waitfor($socket,'Password: $');
        $result .= $s;
        $socket->send("password\n");
    } else {
        $socket->send("password\n");
    }
    $s = &waitfor($socket,'.+>$');
=pod
    $result .= $s;
    $socket->send("sh int status\n");
    $s = &read($socket);
    $result .= $s;
=cut
   
    $socket->send("sh cdp nei de\n");
    $s = &read($socket);
    $result .= $s;
 
    print $result."\n";
    
=pod
while (1) {
    my $buffer;
    $socket->send(scalar <>);
    print &read($socket);
}
=cut

    $socket->close;
}

print time-$start;

sub read {  #对recv封装,实现serve回传数据的正常交互
    my $self = shift;
    my $start;
    while (1) {
        $self->recv(my $x, 4096);
    
        next if !$x;

        $start .= $x;

        last if $x =~ /(<.+>|\[.+\]|.+>|.+#)$/;
        $self->send(' ') if $x =~ /More/;
        last if $x =~ /(Password: ?|Username: ?)/;
    }   

    return $start;
}

sub waitfor { #简单模仿Net::Telnet中的waitfor方法
    my $self = shift;
    my $regx = shift;
    my $start;
    while (1) {
        $self->recv(my $x, 4096);
    
        next if !$x;

        $start .= $x;

        last if $x =~ /$regx/;
        $self->send(' ') if $x =~ /More/;
    }   

    return $start;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值