ソフトウェアからルータにSSH(Exscript)で設定してみる


[NetOpsCoding AdventCalender](http://qiita.com/advent-calendar/2015/netopscoding) の12/2分の投稿です。

今回は、ネットワーク系の自動化ソフトウェアを作っている上で、鬼門となるルータ設定の自動化に挑戦してみます。

どういう点が鬼門かというと、2015年時点では、ルータを設定/制御するためにはAPIが提供されていないものがあり、また提供されていてもベンダ独自APIである場合が多く、マルチベンダー環境のネットワークにおいては、ルータ機種/OSごとに自動化プログラムを実装することになりがちです。

そんななかNETCONF/YANG/RESTCONF/Telemetryなどの業界標準的なAPIも仕様策定が進みつつあります。ただ現状としては、設定できる記述内容にメーカ差分があるものや、最新OSでしか動作しないもの、機能を限定してサポートしているものなど、まだまだマルチベンダー環境で利用する場合に課題が残っています。

そこで今回は提供されているAPIを一切利用せずに、最も汎用性の高い方法であるSSHを利用してソフトウェアからルータを設定を投入する流れを実装してみます。言い換えると、ネットワーク運用者が実際にSSHを使ってルータにログインしてCLIで設定を投入する一連の流れを、ソフトウェアで置き換えします。

プログラミング言語ごとに提供されているSSHやTelnetのパッケージを利用して自動化する方法は、NETCONFなどを使う場合に比べて汎用性が非常に高い（人がCLI設定できるものは何でもOK）なのですが、その一方で、本来は人が目視確認している表示コマンドの出力結果やエラーメッセージをソフトウェアが解析する必要があります。
特に、未知のエラーメッセージが出現したり、ルータOSのバージョンアップして表示結果が変わるたびにソフトウェアを都度修正する可能性があるのであまりおすすめはできませんが、現時点ではTelnet/SSHを使わないとルータ設定できない場合が多くあるため、手段の一つとして覚えておくと良いかと思います。

# 実行環境
- Python 2.7.10
- SSHパッケージ
  - Exscript 2.1.440

- 対象ルータOS
 - Juniper JUNOS

ルータへのSSHによる情報取得と設定は[Exscript]( https://github.com/knipknap/exscript) というパッケージを使います。
SSHパッケージとしては[expect](http://pexpect.readthedocs.org/en/latest/)が有名ですが、ExscriptはLinux/Unixの他にも、IOS, IOS-XR, JunOSなどがサポートされており、ルータからの出力結果を受け取る際に、ルータ専用の解析処理をする必要がないので非常に便利です。

今回はJUNOSルータを例にSSH設定プログラムを作りますが、SSH/Telnetが利用できるものであれば他メーカのルータでも同じような流れで設定することができます。

# 事前準備
ユーザアカウントとパスワードを利用して、対象サーバからルータにSSHログイン ができることを確認しておきます。SSH以外の特別な設定は不要です。

# showコマンドを実行するプログラム
まずルータにsshログインをして、インタフェースに設定されている内容を表示するコマンドを実行するプログラムを書いてみます。

```py:show_junos.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

from Exscript.protocols import SSH2
from Exscript.Account import Account

username = 'user1'
password = 'pass1'
ipv4 = '192.168.0.1'

# SSHセッションの確立
session = SSH2()
session.connect(ipv4)

# ルータにログイン
session.login(Account(name=username, password=password))

# ルータにコマンドを送信、出力結果を取得
print '='*40
print 'Step 1. run show command'
print '='*40
session.execute('show configuration interfaces xe-0/0/0 | no-more')
print session.response


# SSHセッションの切断
if session:
    session.send('exit\r')
    session.close()
else:
    raise AttributeError('Cannot find a livied session')
```

プログラムを実行するとJUNOSルータのインタフェースに設定されている内容が出力されます。

```
$ python show_junos.py

========================================
Step 1. run show command
========================================
show configuration interfaces xe-0/0/0 | no-more
disable;

user1@router>
```

ここではインターフェース xe-/0/0/0に「disable」のみが設定されていることがわかります。


# インタフェース設定を投入するプログラム
上記のプログラムの「session.execute()」関数の引数を工夫することでルータに設定を投入していきます。

ここでは事前に投入したいコンフィグをテキストファイルで保存しておき、プログラム実行時にコンフィグファイルを読み込むようにします。

```junos_config.txt
delete interfaces xe-0/0/0 disable
set interfaces xe-0/0/0 unit 0 family inet address 10.0.0.1/30
```

JUNOSでは、ルータにコンフィグを読み込むための方法が幾つか用意されています。
1. ルータのCLI上からsetコマンドを使って設定する方法
2. ルータ上にコンフィグファイルを転送・配置し、loadコマンドで読み込む方法

ここではCiscoルータなどでも実行できる汎用的な方法として「1. ルータのCLI上からsetコマンドを使って設定する方法」で設定します。

作成したプログラムは下記のようになります。



```py:set_junos.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import sys
from Exscript.protocols import SSH2
from Exscript.Account import Account

username = 'user1'
password = 'pass1'
ipv4 = '192.168.0.1'

session = SSH2()
session.connect(ipv4)
session.login(Account(name=username, password=password))


print '='*40
print 'Step 1. run show command'
print '='*40
session.execute('show configuration interfaces xe-0/0/0 | no-more')
print session.response


print '='*40
print 'Step 2. read config file'
print '='*40
config_filename = sys.argv[1]
with open(config_filename, 'r') as config_file :
    config_lines = config_file.readlines()
    print config_filename
    print config_lines


print '='*40
print 'Step 3. run configure command'
print '='*40
session.execute('configure')
for config_line in config_lines :
    session.execute(config_line)
    print session.response


print '='*40
print 'Step 4. commit check'
print '='*40
session.execute('show | compare')
print session.response
session.execute('commi check')
print session.response


print '='*40
print 'Step 5. commit'
print '='*40
print 'Do you commit? y/n'
choice = raw_input().lower()
if choice == 'y':
    session.execute('commit')
    print session.response
else:
    session.execute('rollback')
    print session.response
session.execute('exit')
print session.response


print '='*40
print 'Step 6. run show command(again)'
print '='*40
session.execute('show configuration interfaces xe-0/0/0 | no-more')
print session.response


if session:
    session.send('exit\r')
    session.close()
else:
```


上記では流れを追いやすいように、表示の工夫やエラー処理、ルータからの返り値の確認など、細かい点は気にせず書いています。
実際には、ルータが返す文字列に合わせたエラー処理やタイムアウト処理などを書く必要があります。エラー処理文が増えると見づらくなってしまうため、必要に応じて関数化することをおすすめします。

そしてプログラムを実行した結果がこちらです。

```
python set_junos.py junos_config.txt

========================================
Step 1. run show command
========================================
show configuration interfaces xe-0/0/0 | no-more
disable;

user1@router>
========================================
Step 2. read config file
========================================
junos_config.txt
['delete interfaces xe-0/0/0 disable\n', 'set interfaces xe-0/0/0 unit 0 family inet address 10.0.0.1/30\n']
========================================
Step 3. run configure command
========================================
delete interfaces xe-0/0/0 disable

[edit]
user1@router#

[edit]
user1@router#
set interfaces xe-0/0/0 unit 0 family inet address 10.0.0.1/30

[edit]
user1@router#

[edit]
user1@router#
========================================
Step 4. commit check
========================================
show | compare
[edit interfaces xe-0/0/0]
-   disable;
[edit interfaces xe-0/0/0]
+    unit 0 {
+        family inet {
+            address 10.0.0.1/30;
+        }
+    }

[edit]
user1@router#
commit check
configuration check succeeds

[edit]
user1@router#
========================================
Step 5. commit
========================================
Do you commit? y/n
y
commit
commit complete

[edit]
user1@router#
exit
Exiting configuration mode

user1@router>
========================================
Step 6. run show command(again)
========================================
show configuration interfaces xe-0/0/0 | no-more
unit 0 {
    family inet {
        address 10.0.0.1/30;
    }
}

user1@router>
```

このようにしてソフトウェアからSSHを使ってルータに設定するプログラムを書くことができます。


# これでゴールじゃない。
途中でも触れたように、実際にルータに設定を入れる場合には、「コンフィグが間違っていたときの処理」「ルータがエラーを返したときの処理」「SSHセッションが確立できなかった場合の処理」などをプログラムといて実装する必要があります。

さらには、今回は「show コマンドを確認する」「commitの実施の可否」を"人が判断する"という前提でプログラムを作っていますが、自動化の観点を考えると

```
プログラム自身が、
(1)showコマンドやエラー出力の結果を確認して、
(2)コンフィグを投入し
(3)問題がないことを判断した上で、
(4)commitを実施し、
(5)想定通りのネットワーク設定になったことをテストする
```

というプロセスが必要になります。

上記プロセスを実装しようと思うと、showコマンドによる出力の解析などが必要になるため、どうしても複雑なプログラムになってしまいます。

そういったプログラミングの煩雑さを減らすためにNETCONFやTelemetryといった、ソフトウェアにとって易しい仕組みが用意されつつあります。とはいえNETCONFやRESTCONFで楽になるのは、あくまでルータへの設定と情報取得の部分だけであり、それ以外のプロセスはやはり開発者自身で実装する必要があるので、それなりの自動化における課題は残っています。

# 最後に
課題を挙げるとキリが無いですが、試行錯誤をしながら少しずつ確実に動くものを作っていき、自分たちの工数を削減しながらさらにプログラムを進化させていく、という流れがネットワーク運用自動化への近道かなと感じています。
まずは自分でできるとことから、一歩ずつ一緒にがんばりましょう。そして一歩進めたら、ブログやNetOpsCodingイベントなどで盛大に自慢しましょう。あなたが困っていることはみんなが困っていることです。あなたの自慢をお待ちしています。