perl/perl-Expect

目的

やること :

  • perl-expectを使ったCisco機器操作の自動化
    • 自動ログイン
    • コマンド実行の自動化(及びログ取得)
    • コマンド出力の加工

参照 :


ExpectとかPerl-Expectについてのまとまった資料ってあんまりないんだよな。みんなそれなりに使ってそうなんだが。

準備

テスト用ダミー端末 : 手元にCiscoルータがあるからそれ使えばいいんだけど、機械がないとテストができないというのも面倒なのでダミーを作ってみた。Cisco ぽいプロンプトとメッセージを返すだけの適当なプログラム……なのだが、これ作るのが結構面倒だった。コマンドのマッチングは適当。ワードごとのマッチングやりゃいいんだけど、面倒だし。今回は login (login localではなく)かつ、Enable Secret 設定を想定している。

#!/usr/bin/perl

use strict;
use warnings;


sub get_password {
  my $input = "";

  print "Password: ";
  system "stty -echo";
  $input = <STDIN>;
  print "\n";
  system "stty echo";
  chomp $input;
  $input;
}


# my $username_in = "";
# my $username_str = "stereocat";
my $password_in = "";
my $password_str = "hogehoge";
my $retry = 0;
my $max_retry = 3;

print "User Access Verification\n\n";
until (#($username_in eq $username_str) and
       ($password_in eq $password_str)) {
  if ($retry >= $max_retry) {
    print "% Bad passwords\n";
    exit 0;
  }
  #   print "Username: ";
  #   $username_in = <STDIN>;
  #   chomp $username_in;
  $password_in = get_password;
  $retry++;
}

my $hostname = "router";
my $password_en_str = "enhoge";
my $password_en_in = "";
my $input = "";

my %mode2prompt = (
                   "enable" => ">",
                   "config" => "#",
                   "config-term" => "(config)#",
                   "config-if" => "(config-if)#"
                  );

my $mode = "enable";
my @modehist = ();
push(@modehist, $mode);

while (1) {
  my $prompt = $hostname.$mode2prompt{$mode};
  print $prompt;
  $input = <STDIN>;
  chomp $input;

  if ($mode eq "enable" and $input =~ /^\s*en.*/) {
    $password_in = "";
    $retry = 0;
    until ($password_in eq $password_en_str) {
      if ($retry >= $max_retry) {
        print "% Bad passwords\n\n";
        last;
      }
      $password_in = get_password;
      $retry++;
    }
    if ($password_in eq $password_en_str) {
      push(@modehist, $mode);
      $mode = "config";
    }
  } elsif ($mode eq "config" and $input =~ /^\s*sh.*/) {
    print <<EOM
Building configuration...

Current configuration : 100 bytes
!
interface Port-channel1
 switchport trunk allowed vlan 100-4094
 switchport mode trunk
 switchport nonegotiate
end
EOM
  } elsif ($mode eq "config" and $input =~ /^\s*conf.*/) {
    push(@modehist, $mode);
    $mode = "config-term";
  } elsif ($input =~ /exit/) {
    $mode = pop(@modehist);
    exit 0 if($mode eq "enable");
  } elsif ($input =~ /^\s*.+/) {
    print "% Invalid input detected at '^' marker.\n";
  }
}

使えるコマンドは

  • en[able]
  • conf[igure terminal]
  • sh[ow running-config interface port-channel 1]
  • exit

ということにしてある。一応、exitによるモード変更も可能(config-ifまではやろうかと思ったけど無駄だから止めた)。

Perl-Expectサンプル

ようやっと本題。まずはサンプルを載せてしまうか。

#!/usr/bin/perl -w
# -*- cperl -*-

use strict;
use Expect;

my $username = "stereocat";
my $password = "hogehoge";
my $password_en = "enhoge";

my $exp = Expect->spawn("perl dummyprompt.pl")
  or die "Error : Cannot spawn $!\n";
my $timeout = 10;

my $spawn_ok;
my $prompt1 = qr'>$';
my $prompt2 = qr'#$';
my $logfile_name = "$0.log";

print("run Expect.\n");


# login
$exp->expect($timeout,
             [
              'Login invalid' ,
              sub {
                die "Error: Invalid Login Password.";
              }
             ],
             [
              'Username: $' ,
              sub {
                $spawn_ok = 1;
                my $fh = shift;
                $fh->send("$username\n");
                exp_continue;
              }
             ],
             [
              'Password: $' ,
              sub {
                my $fh = shift;
                $fh->send("$password\n");
                exp_continue;
              }
             ],
             [
              eof =>
              sub {
                if ($spawn_ok) {
                  die "Error: Premature EOF in login.";
                } else {
                  die "Error: Could not spawn telnet.";
                }
              }
             ],
             [
              'timeout' =>
              sub {
                die "Error: Timeout... No Login.";
              }
             ],
             '-re', $prompt1,
             '-re', $prompt2
            );
$exp->send("\n");

# enable
$exp->expect($timeout,
             [
              'Bad passwords' ,
              sub {
                die "Error: Invalid Enable password";
              }
             ],
             [
              $prompt1 =>
              sub {
                my $fh = shift;
                $fh->send("enable\n");
                exp_continue;
              }
             ],
             [
              'Password: $' ,
              sub {
                my $fh = shift;
                $fh->send("$password_en\n");
                exp_continue;
              }
             ],
             '-re', $prompt2
            );
$exp->send("\n");

# logging output
$exp->log_file($logfile_name, "w");
my @cmds = ("sow run int po 1\n",
            "show run int po 1\n");
foreach my $cmd_str (@cmds) {
  my $cmd_run = 0;
  $exp->expect($timeout, 
               [
                '% Invalid input detected at',
                sub {
                  my $fh = shift;
                  print STDERR "\n----\n";
                  print STDERR $fh->before();
                  print STDERR "\n----\n";
#                 die "Error : Command Cannot Execute.";
                }
               ],
               [
                $prompt2,
                sub {
                  my $fh = shift;
                  if ($cmd_run == 0) {
                    $fh->send($cmd_str);
                    $cmd_run = 1;
                    exp_continue;
                  } else {
                    $fh->send("\n");
                  }
                }
               ]
              );
}
print("\n\n============================================================\n");
print("** before \n", $exp->before());
print("\n\n============================================================\n");
print("** after  \n", $exp->after());

print("\n\n============================================================\n");
print("** check\n");
foreach my $line (split(/\n/, $exp->before())) {
  print("> ", $line, "\n");
}

$exp->expect($timeout, [$prompt2]);
$exp->log_file(undef);
$exp->send("exit\n");
$exp->soft_close();

print("End Script.\n");

コメントブロックごとに解説

  • login
    • ログイン処理。
    • Username/Passwordの入力および各種エラー処理を行っている。exp-continueに行き着くと再び同じExpect文の中でマッチング処理を行う。
    • 最後のprompt1/2のところにいった段階(ログインが成功してプロンプトが返ってきたところ)でここは終わり。Spawnしたプロセスからの返答(expect)-コマンド送信(send)でペアにして処理を書きたかったので、空行送っている。
    • このブロックについては login でも login local(要ユーザ名) でも対応しているはず。
  • enable
    • Enable mode 移行処理。
    • プロンプトが "hostname>" の場合に enable を送るだけなのだが、一応パスワードをミスしたときのエラー処理をつけてみた(というのも、パスワード指定をミスったらここで無限ループが発生したから)。
    • プロセスからの返答に対するマッチング処理は上から順にみるようなので、マッチ条件の順番に注意。
  • logging output
    • ここから本処理。
    • Expect::log-file()を使うと、プロセスからの返答を全部ファイルに保存することができる。引数にundefを渡すとログ取得を止める。単にファイル名を指定しただけだと、append でログを取るので、上書きしたい場合はオプション"w"が必要。
    • 2種類のコマンド "sow"(エラー処理テスト), "show"(出力の取得、および加工のテスト) を実行する。
      • エラー "Invalid input..." が起きると、相手が返してきたメッセージを Expect::before() を使って全部吐き出す。ここで die すると次のテストができないのでコメントアウトしてある。
      • 正常に処理ができた(Invalid inputが返らなかった)場合は、その下のところで、Expect::before()を使って得られた情報を1行ずつ処理している。

実行結果

$ perl expect-demo.pl
run Expect.
User Access Verification

Password: hogehoge

router>
router>enable
Password: enhoge

router#
router#sow run int po 1
% Invalid input detected at '^' marker.
router#
----
sow run int po 1

----
show run int po 1
Building configuration...

Current configuration : 100 bytes
!
interface Port-channel1
 switchport trunk allowed vlan 100-4094
 switchport mode trunk
 switchport nonegotiate
end
router#

============================================================
** before
show run int po 1
Building configuration...

Current configuration : 100 bytes
!
interface Port-channel1
 switchport trunk allowed vlan 100-4094
 switchport mode trunk
 switchport nonegotiate
end
router

============================================================
** after


============================================================
** check
> show run int po 1
> Building configuration...
>
> Current configuration : 100 bytes
> !
> interface Port-channel1
>  switchport trunk allowed vlan 100-4094
>  switchport mode trunk
>  switchport nonegotiate
> end
> router

router#exit
End Script.

何故かパスワードが丸見えになっているが(stty -echoしてるのにな…手動でやると問題ないのだけど)。

まとめ

  • perl-expect
    • 自動ログイン、処理の自動化はできた。
    • 実行結果のログ取得ができた。 : Expect::log_file
    • コマンド実行結果の取得および加工ができた。 : Expect::before() : 直前のExpectでのプロセス応答を取得

たまーにしかコードを書かないので毎回似たようなことで引っかかってしまうな。いま見ると書き方もどうも怪しげ(統一されてないし)だけど、一応やりたいことができるということがわかったのでその辺は次のネタで何とかしよう。