Rubyのexpect4rでCiscoルータにTelnet/SSHしてコマンド実行する

## はじめに
RubyとRubyGemsの[expect4r](https://github.com/jesnault/expect4r)を使って、CiscoルータにTelnet/SSHをした上で、
コマンドを実行する方法を紹介します。環境はUbuntu15.04とRuby2.2です。

## 目的
ルータのコマンドを実行する場合は、Linuxのexpectコマンドを使う場合が多いと思います。
しかしながら、showコマンドの実行結果を見てから、次のコマンド実行を判断するなど、
条件判断や反復などを組み合わせる場合、Linuxのexpectコマンドだけでは難しいです。

そこで、今回はRuby言語を利用して、ルータにアクセスして、コマンドを実行する方法を紹介します。
RubyGemsのexpect4rを利用して、比較的簡単にルータにアクセスすることができます。

## 概要
expect4rは、ルータにアクセスすることに特化したライブラリです。
名前の通りLinuxのexpectっぽい動作をします。

expect4rはルータへのログイン処理を柔軟に処理しているため、
ユーザ名無しなど、いろいろなログイン設定に対応できると思います。
CLIのenableモードやconfigurationモードなどもうまく取り扱ってくれます。

Cisco IOS以外にもvyatta、juniper、Cisco IOS XRなどにも対応している模様です。

Cisco IOSの場合、下記のように``show ip route``を実行することができます。
expct4rの[README.rdoc](https://github.com/jesnault/expect4r)を参考にしています。

```rb:sample.rb
ios = Expect4r::Ios.new_telnet :host=> "hostname", :user=> "username", :pwd => "password"
ios.login
ios.show_ip_route
ios.exec 'show ip route'
```


## 環境
実行環境はUbuntu15.04が入ったPCと、CiscoルータのCisco1812Jです。

![text3358-5.png](https://qiita-image-store.s3.amazonaws.com/0/12026/81449c3f-017d-a60b-9833-e7badfcdf83d.png)


## スクリプト
[``expect4r``](https://github.com/jesnault/expect4r)のサンプルを参考に、スクリプトを作成しました。
Telnetで``show interface description``を実行し、
SSHで``show running-config``を表示するスクリプトです。

今回はCisco1812JのCisco IOSを対象とするため、``Expect4r::Ios``クラスを使用します。

```rb:Gemfile
source 'https://rubygems.org'
gem 'expect4r'
```

```rb:sample1.rb
require 'expect4r'

HOST = '192.168.88.101'
USERNAME = 'cisco'
PASSWORD = 'cisco'
ENABLE_PASSWORD = 'cisco'

puts '*' * 60
puts 'Telnet access'
puts '*' * 60
ios = Expect4r::Ios.new_telnet(
  host: HOST,
  user: USERNAME,
  pwd: PASSWORD,
  enable_password: ENABLE_PASSWORD
)
puts ios.exec 'show int desc' # ここでルータのコマンド実行
puts

puts '*' * 60
puts 'SSH access'
puts '*' * 60
ios = Expect4r::Ios.new_ssh(
  host: HOST,
  user: USERNAME,
  pwd: PASSWORD,
  enable_password: ENABLE_PASSWORD
)
puts ios.exec 'show running-config' # ここでルータのコマンド実行
puts
```

## 結果
TelnetとSSHでコマンドを実行できました。

```zsh:expect4rのインストール
% bundle install
Using highline 1.7.8
Using expect4r 0.0.11
Using bundler 1.10.6
Bundle complete! 1 Gemfile dependency, 3 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.
```

```zsh:実行結果
% ruby sample1.rb
************************************************************
Telnet access
************************************************************
show int desc
Load for five secs: 0%/0%; one minute: 3%; five minutes: 2%
Time source is NTP, 00:33:29.560 JST Mon Nov 23 2015

Interface                      Status         Protocol Description
BR0                            admin down     down     
BR0:1                          admin down     down     
BR0:2                          admin down     down     
Fa0                            up             up       WAN
Fa1                            admin down     down     LAN
Fa2                            up             down     SW1
Fa3                            up             down     SW2
Fa4                            up             down     
Fa5                            up             down     
Fa6                            up             down     
Fa7                            up             down     
Fa8                            up             down     
Fa9                            up             down     
Vl1                            up             down     
R1#

************************************************************
SSH access
************************************************************
show running-config
Load for five secs: 0%/0%; one minute: 3%; five minutes: 2%
Time source is NTP, 00:33:30.616 JST Mon Nov 23 2015

Building configuration...

Current configuration : 1922 bytes
!
! No configuration change since last restart
version 15.1
service timestamps debug datetime msec localtime show-timezone year
service timestamps log datetime msec localtime show-timezone year
no service password-encryption
!
hostname R1
!
boot-start-marker
boot-end-marker
!
!
logging buffered 4096
logging console informational
enable password cisco
!
no aaa new-model
!
clock timezone JST 9 0
crypto pki token default removal timeout 0
!
!
dot11 syslog
ip source-route
!
!
!
!
!
ip cef
no ip domain lookup
ip domain name cisco.com
no ipv6 cef
!
multilink bundle-name authenticated
!
!
!
license udi pid CISCO1812-J/K9 sn ********
username cisco password 0 cisco
!
!
ip ssh version 2
! 
!
!
!
!
!
!
interface BRI0
 no ip address
 encapsulation hdlc
 shutdown
!
interface FastEthernet0
 description WAN
 ip address 192.168.88.101 255.255.255.0
 duplex auto
 speed auto
!
interface FastEthernet1
 description LAN
 no ip address
 shutdown
 duplex auto
 speed auto
!
interface FastEthernet2
 description SW1
 no ip address
!
interface FastEthernet3
 description SW2
 no ip address
!
interface FastEthernet4
 no ip address
!
interface FastEthernet5
 no ip address
!
interface FastEthernet6
 no ip address
!
interface FastEthernet7
 no ip address
!
interface FastEthernet8
 no ip address
!
interface FastEthernet9
 no ip address
!
interface Vlan1
 no ip address
!
ip forward-protocol nd
no ip http server
no ip http secure-server
!
!
ip route 0.0.0.0 0.0.0.0 FastEthernet0 192.168.88.1
!
logging origin-id hostname
!
!
!
!
snmp-server community public RO
snmp-server ifindex persist
snmp-server chassis-id R1
!
!
control-plane
!
!
!
line con 0
line aux 0
line vty 0 4
 exec-timeout 5 0
 logging synchronous
 login local
 exec prompt timestamp
 transport input telnet ssh
line vty 5 15
 exec-timeout 5 0
 logging synchronous
 login local
 exec prompt timestamp
 transport input telnet ssh
!
ntp server 133.243.238.244
end

R1#
```

## 付録
expect4rに関する参考情報です。

### コンフィグ保存の方法
コマンドによっては、確認用のプロンプトが返ってきて標準の``exec``メソッドではうまく動きません。
そこで下記を組み合わせることで、ルータのcopyコマンドをうまく動くように細工できます。

* ``putline``メソッドでコマンド送信
* ``putline``メソッドに``no_trim``オプションを付ける。空白行も送信してくれるようになる
* プロンプトに合わせた入力内容と改行（``\r``）

ただし、この方法は、予め確認プロンプトの数や
入力内容がわかっている場合に有効です。

ルータのcopyコマンドは保存先ファイル名を確認するプロンプトが1回あるので、
改行（``\r``）をコマンドの最後に1個追記しています。
``ios.putline("copy running-config startup-config\r", no_trim: true)``

```rb:sample2.rb
require 'expect4r'

HOST = '192.168.88.101'
USERNAME = 'cisco'
PASSWORD = 'cisco'
ENABLE_PASSWORD = 'cisco'

ios = Expect4r::Ios.new_telnet(
  host: HOST,
  user: USERNAME,
  pwd: PASSWORD,
  enable_password: ENABLE_PASSWORD
)
ios.login # putlineは自動的なlogin処理をしていないため明示的に指定する
ios.to_exec # copyコマンドはexec状態で実行する必要がある
puts ios.putline("copy running-config startup-config\r", no_trim: true)
```

```zsh:実行結果
% ruby sample2.rb
copy running-config startup-config
Destination filename [startup-config]? 
Building configuration...
[OK]
R1#
```

## 参考
* [jesnault/expect4r, Github](https://github.com/jesnault/expect4r)
* [ruby expect でルータ操作, # cat /var/log/stereocat | tail -n3](http://d.hatena.ne.jp/stereocat/20131229/1388311665)
* [Expectで定型業務を自動化しよう！, ネットワークエンジニアを目指して](http://www.itbook.info/study/p107.html)
* [ネットワーク運用のツール化と省力化, JANOG29 Meeting](http://www.janog.gr.jp/meeting/janog29/program/auto.html)