PyEZとJSNAPyを使ってみた。第三部: JSNAPy使ってみた編

PyEZとJSNAPyを使ってみた。の続編です。今回はJSNAPyを使ってみた内容を紹介してきます。

[PyEZとJSNAPyを使ってみた。第一部: 概要編](http://qiita.com/taijijiji/items/065eef08f7b673cb7bb6)
[PyEZとJSNAPyを使ってみた。第二部: PyEZ使ってみた編](http://qiita.com/taijijiji/items/626cef18757d638251cf) 
[PyEZとJSNAPyを使ってみた。第三部: JSNAPy使ってみた編](http://qiita.com/taijijiji/items/b6e3da3d3aeb351ff7ff)(イマココ)
[PyEZとJSNAPyを使ってみた。第四部: PyEZとJSNAPyでISP設定作業を自動化する編](http://qiita.com/taijijiji/items/983189f9bdebaa53e499)

# JSNAPy について
JSNAPyは、JUNOSルータの状態をスナップショットとして取得・管理し、事前に定義した条件に対してPassed(合格)/Failed(不合格)を判定するツールです。ルータとの通信はPyEZ同様 NETCONF over SSH (TCP 830)を利用します。

JSNAPyは、コマンドラインから実行するモードと、プログラムからPythonモジュールを介して実行するモードの２つが用意されています。

ここではPyEZで設定変更したルータに対して、正常性を確認するためのテストツールとしてJSNAPyを利用することを考えていきます。JSNAPyは様々なテストケースを書くことができるので、もしかするとユースケースによってはさらに有効的な使い方があるかもしれません。

## JSNAPyでできること
JSNAPyは大きく4つの機能を持っています。

 - jsnapy --snap < snapshot_filename > -f < config_file >
   - 実行された時点のルータ状態をスナップショットとして取得し保存する
 - jsnapy --check  < snapshot_filename1 > < snapshot_filename2 >  -f < config_file >
   - すでに保存されている２つのスナップショットを比較して、事前定義した条件に適合するか評価する
 - jsnapy --snapcheck < snapshot_filename > -f < config_file >
  -  実行された時点のルータ状態をスナップショットとして取得し、事前定義した条件に適合するか評価する
 - jsnapy --diff
  - すでに保存されている２つのスナップショットを比較して、文字列としての差分を表示させる

実際にテストを実装する場合は「jsnapy --snapcheck」と「jsnapy --check」が使う機会が多くなるかと思います。

定義ファイル(config_file)は、実際は以下のようにYML形式で記述します。testsで実際に比較する条件をテストファイルに定義します。testsは複数テストを指定することも可能です。

```:config_fileの例
hosts:
  - device: 192.168.34.16 
    username : user1
    passwd: password1
tests:
  - ./tests/test_hostname.yml
```

```:./tests/test_hostname.yml
test_hostname:
  - command: show version
  - item:
      xpath: /software-information
      tests:
        - is-equal: host-name, firefly1
          info: "Test : OK, host-name is <{{pre['host-name']}}>"
          err:  "Test : NG, host-name is <{{pre['host-name']}}>"
```

事前定義できる条件としては、「is-equal(同値であること)」以外にも 「is-gt(greather than, 値より大きいこと)」「is-lt(lesser than, 値より小さいこと)」「in-range(値が範囲内に含まれていること)」「no-diff(差分がないこと)」といったような多様なものがあり、プログラミング言語における条件文とほぼ同等のことができそうです。詳細について[Github wikiドキュメント](https://github.com/Juniper/jsnapy/wiki)をご参照ください。

取得したスナップショットは、デフォルトでは/etc/jsnapy/snapshotsディレクトリにXML-RPC形式で保存されています。「jsnapy --snapcheck」や「jsnapy --check」では、このスナップショット情報の「XML-PRCタグ名」を元に値を評価するため、「xpath」項目やテスト条件文でもこのタグ名を正しく指定する必要があります。(ここに若干のクセがあります。)

```
% less /etc/jsnapy/snapshots/192.168.34.16_snap_show_version.xml

<software-information>
<host-name>firefly1</host-name>
<product-model>firefly-perimeter</product-model>
<product-name>firefly-perimeter</product-name>
<package-information>
<name>junos</name>
<comment>JUNOS Software Release [12.1X47-D20.7]</comment>
</package-information>
</software-information>
```

上記のスナップショット情報はJUNOSコンフィグのXML-RPC形式に対応しているので、「show xxx | display xml」オプションコマンドでXMLタグ名にアタリを付けて、テストファイルを作成していくことになります。

```:ルータで確認
root@firefly1> show version | display xml
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/12.1X47/junos">
    <software-information>
        <host-name>firefly1</host-name>
        <product-model>firefly-perimeter</product-model>
        <product-name>firefly-perimeter</product-name>
        <package-information>
            <name>junos</name>
            <comment>JUNOS Software Release [12.1X47-D20.7]</comment>
        </package-information>
    </software-information>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>
```

## 参考資料
PyEZと比較しても、JSNAPyのドキュメントはまだ少ない印象です。サンプルコードやユースケースも少ないので、ドキュメントを見つつも、実際にdisplay xmlオプションやJSNAPyを実行してみて試すことが多くなるかと思います。 (今後こういうユースケースごとのサンプルテストファイルがまとまっている情報があるといいですね。)

- [Github wiki](https://github.com/juniper/jsnapy/wiki)
- [Github README](https://github.com/juniper/jsnapy)
- [Github samples](https://github.com/Juniper/jsnapy/tree/master/samples)

## 事前準備

ここではツールを動かすサーバにPython 2.7.10、JUNOSルータとしてfireflyを利用しています。

まずpipコマンドでJSNAPyをインストールします。
試して気づいたのですが最新リリースのversion 1.0.0にはCPU負荷が高まる危険な不具合があり、使わないほうがよさそうです。version1.1では修正予定だそうです (https://github.com/Juniper/jsnapy/issues/204)

ここではpipを介してgithubから直接最新版 version1.0.1をダウンロードするようにします。

```
pip install git+https://github.com/Juniper/jsnapy.git

pip list
jsnapy (1.0.1)
```

またJUNOSルータ側では、NETCONFを有効にするための設定を投入しておきます。

```:ルータ情報
root@firefly1> show version
Hostname: firefly1
Model: firefly-perimeter
JUNOS Software Release [12.1X47-D20.7]
```

```:NETCONF有効化のコンフィグ
system {
    services {
        netconf {
            ssh;
        }
      }
    }
}
```

## JSNAPy 利用例1: 「ルータのホスト名/製品モデル名を判定」をコマンドラインツールで実行
まずはコマンドラインツールで実行するプログラムを書いてみます。
ここでは単純なテストの例として、ルータのホスト名が「firefly1」、製品モデル名が「firefly-perimeter」であるかを判定しています。

プログラムはGithubで公開しています。
https://github.com/taijiji/sample_jsnapy


以下のコマンドでJSNAPyを実行することができます。


```:実行コマンド
jsnapy --snapcheck snap01 -f config_firefly1.yml
```

```yaml:config_firefly1.yml
hosts:
  - device: 192.168.34.16 
    username : user1
    passwd: password1
tests:
  - ./tests/test_hostname.yml
  - ./tests/test_model.yml
```

```yaml:./tests/test_hostname.yml
test_hostname:
  - command: show version
  - item:
      xpath: '/software-information'
      tests:
        - is-equal: host-name, firefly1
          info: "Test : OK, host-name is <{{pre['host-name']}}>"
          err:  "Test : NG, host-name is <{{pre['host-name']}}>"
```

```yaml:./tests/test_model.yml
test_hostname:
  - command: show version
  - item:
      xpath: '/software-information'
      tests:
        - is-equal: product-model, firefly-perimeter
          info: "Test : OK, Model is <{{pre['product-model']}}>"
          err:  "Test : NG, Model is <{{pre['product-model']}}>"
```

実行結果は以下のように表示されます。ちなみに -vオプションを追加することでデバックモードとして動作し、テストファイルで「info」項目で指定したメッセージを出力することができます。


![Screen Shot 2016-11-27 at 12.42.07 PM.png](https://qiita-image-store.s3.amazonaws.com/0/45596/33394436-209b-79df-1711-f6d94a78c7fb.png)



このように、JSNAPyでは複数のテストに対して合否の判定をした上で、結果を人が見やすいように可視化してくれる機能があります。

## JSNAPy 利用例2:「ルータのホスト名/製品モデル名を判定」をプログラムで実行
利用例1でJSNAPyコマンドラインで実行したテストを、今度はPythonプログラムで実行できるようにしてみます。

プログラムを実行する前にひとつ前準備が必要です。
デフォルトだとsnapcheck()関数を呼び出すと、利用例1で示したコマンドライン用テスト結果が表示されてしまうので、まずこの表示をオフにする必要があります。

コマンドライン用テスト結果の表示をオフにするには、/etc/jsnapy/logging.yml の下記部分を編集します。

```yaml:/etc/jsnapy/logging.yml
 61 root:
 62     level: DEBUG
 63     #handlers: [console, debug_file_handler] # ここをコメントアウト
 64     handlers: [debug_file_handler]           # ここを追加
```

上記のように編集ができたら、いよいよプログラムを動かしていきます。
今回は、JSNAPyによるテスト結果を変数として取得してそのまま表示させるプログラムを作っています。

プログラムはこちらです
https://github.com/taijiji/sample_jsnapy/blob/master/run_jsnapy_test_hostname_model.py

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

from jnpr.junos import Device
from jnpr.junos.utils.config import Config
from jnpr.jsnapy import SnapAdmin

from pprint import pprint, pformat

jsnapy_config =\
'''
tests:
- ./tests/test_hostname.yml
- ./tests/test_model.yml
'''

dev1 = Device(
        host = '192.168.34.16',
        user = 'user1',
        password = 'password1')
dev1.open()

jsnapy = SnapAdmin()
snapcheck_dict = jsnapy.snapcheck(
                  data = jsnapy_config,
                  dev = dev1,
                  file_name = "snap01")

print '##### JSNAPy Test : Start #####'
for snapcheck in snapcheck_dict:
    print "Devece : ",         snapcheck.device
    print "Final result : ",   snapcheck.result
    print "Total passed : ",   snapcheck.no_passed
    print "Total failed : ",   snapcheck.no_failed
    print 'snapcheck test_details : '
    print '-'*30
    pprint(dict(snapcheck.test_details)) 
    print '-'*30
print '##### JSNAPy Test : End #####'

dev1.close()
```

```:実行方法
python run_jsnapy_test_hostname_model.py
```

```:実行結果
% python run_jsnapy_test_hostname_model.py                                                                                                                                                   (git)-[master]

##### JSNAPy Test : Start #####
Devece :  192.168.34.16
Final result :  Passed
Total passed :  2
Total failed :  0
snapcheck test_details :
------------------------------
{'show version': [{'count': {'fail': 0, 'pass': 1},
                   'expected_node_value': 'firefly1',
                   'failed': [],
                   'node_name': 'host-name',
                   'passed': [{'actual_node_value': 'firefly1',
                               'id': {},
                               'post': {'host-name': 'firefly1'},
                               'pre': {'host-name': 'firefly1'}}],
                   'result': True,
                   'testoperation': 'is-equal',
                   'xpath': '/software-information'},
                  {'count': {'fail': 0, 'pass': 1},
                   'expected_node_value': 'firefly-perimeter',
                   'failed': [],
                   'node_name': 'product-model',
                   'passed': [{'actual_node_value': 'firefly-perimeter',
                               'id': {},
                               'post': {'product-model': 'firefly-perimeter'},
                               'pre': {'product-model': 'firefly-perimeter'}}],
                   'result': True,
                   'testoperation': 'is-equal',
                   'xpath': '/software-information'}]}
------------------------------
##### JSNAPy Test : End #####
```

上記のようにプログラムから、JSNAPyテスト結果を取得することができます。

## JSNAPyライブラリの惜しいところ
JSNAPyではコマンドラインでの利用が重要視されているのか、Pythonライブラリとしては惜しい部分もあります。JSNAPyは実装が始まってあまり時間が経ってないこともあり、まだ改善の余地はありそうです。せっかくオープンソースソフトウェアとして公開されているので、Pull Requestしてあげるのがよいかもしれません。

まず1つ目として、JSNAPyライブラリを使って少し凝ったことをやろうと思うとtest_details()関数で取得できる辞書型変数を参照して利用する必要があるのですが、ここで取得できる変数がルータ実行コマンド単位で格納されているので若干使いにくいです。例えば、ホスト名と機種名を取ろうとすると、いずれもshow versionコマンドを利用するため、'show version'をキーとしてまとめられてしまい、プログラムからテスト結果を参照することが難しくなってしまいます。

```
{'show version': [{'count': {'fail': 0, 'pass': 1},
                   'err': "Test : NG, hostname : {{pre['host-name']}}",
                   'expected_node_value': 'firefly1',
                   'failed': [],
                   'info': "Test : OK, hostname : {{pre['host-name']}}",
                   'node_name': 'host-name',
                   'passed': [{'actual_node_value': 'firefly1',
                               'id': {},
                               'post': {'host-name': 'firefly1'},
                               'pre': {'host-name': 'firefly1'}}],
                   'result': True,
                   'testoperation': 'is-equal',
                   'xpath': '/software-information'},
                  {'count': {'fail': 1, 'pass': 0},
                   'err': "Test : NG, Model is <{{pre['product-model']}}>",
                   'expected_node_value': 'firefly-perimeter',
                   'failed': [{'actual_node_value': None,
                               'id': {},
                               'post': {},
                               'pre': {},
                               'xpath_error': True}],
                   'info': "Test : OK, Model is <{{pre['product-model']}}>",
                   'node_name': 'product-model',
                   'passed': [],
                   'result': False,
                   'testoperation': 'is-equal',
                   'xpath': 'software-information'}]}
```

これでは「テストAに対応する実行コマンドはこれ」「テストBに対応する実行コマンドはこれ」とプログラム側で静的に定義する必要があり、さらに「テストAとテストBが同じ実行コマンドを使う」ケースではごちゃまぜに格納されてしまいます。「テスト単位でのテスト結果」を出力したい場合はひと工夫が必要です。
将来的に、テスト単位で変数に格納してくれるようになってくれるとありがたいです。

そして2つ目として、JSNAPyの設定ファイル(コンフィグファイルとテストファイル)ではテンプレートエンジンは使えません。YAMLファイル名を指定する形でしか受け付けられないため、下記のように似たようなテストファイルが多数扱う必要が出てきてしまいます。

```

% ls  ./tests/                                                                                     (git)-[master]
test_bgp_advertised_route_192-168-35-2.yml  test_hostname_firefly1.yml
test_bgp_advertised_route_192-168-35-3.yml  test_hostname_firefly2.yml
test_bgp_neighbor.yml                       test_interface_up_all.yml
test_bgp_neighbor_up_192-168-35-2.yml       test_interface_up_ge-0-0-0.yml
test_bgp_neighbor_up_192-168-35-3.yml       test_interface_up_ge-0-0-1.yml
test_bgp_received_route_192-168-35-2.yml    test_interface_up_ge-0-0-2.yml
test_bgp_received_route_192-168-35-3.yml    test_interface_up_ge-0-0-3.yml
test_bgp_summary.yml                        test_model.yml
test_hostname.yml
```

もし設定ファイルの一部を変数化しようと思うと以下のような手順を踏む必要があります。

1. Jinja2テンプレート形式で変数化した設定ファイルを予め準備
2. プログラム上で、Jinja2機能で変数に指定した値を埋め込みを実施
3. 2で作成された値が埋め込まれたファイルを、YAML形式でファイルに書き込み、保存
4. 改めて、YAMLテストファイルとして、snapcheck()関数で読み込み

これはこれで、例えばインタフェースやBGPネイバーのように大量にある場合は、保存されるYAMLテストファイルが大量に残ってしまうので管理含めて煩雑です。PyEZではJinaj2テンプレートをそのままルータコンフィグとして利用できるだけにちょっと残念です。

## JSNAPy 利用例3: 「ルータのホスト名/製品モデル名を判定」をプログラム + テンプレートファイルで実行

前述のように、JSNAPy はテンプレートエンジンをサポートしてません。それでもPyEZと一緒に活用することを考えると、JSNAPyを無理矢理テンプレートファイルを利用できるようにプログラムを作ってみました。
やっていることは単純に以下のことを実践しているだけです。

```
 1. Jinja2形式で変数化した設定ファイルを準備
 2. プログラム上で、Jinja2機能で変数に指定した値を埋め込みを実施
 3. 2で作成された値が埋め込まれたファイルを、YAML形式でファイルに書き込み、保存
 4. 改めて、YAMLファイルとして、snapcheck()関数で読み込み
```

プログラムを以下に示します。
https://github.com/taijiji/sample_jsnapy/blob/master/run_test_interface_up.py

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

import os

# arranged print
from pprint import pprint, pformat

# Jinja2 Template Engine
from jinja2 import Template, Environment

# JSNAPy
from jnpr.junos import Device
from jnpr.junos.utils.config import Config
from jnpr.jsnapy import SnapAdmin

template_dir_name  = './test_templates/'
template_base_name = 'test_interface_up.jinja2'
param_interface = {
    "interface_name"    : "ge-0/0/2",
}

print 'Load test_template : '
template_filename = template_dir_name + template_base_name
with open(template_filename, 'r') as conf:
    template_txt    = conf.read()
    test_yml        = Environment().from_string(template_txt).render(param_interface)
    test_base_name =\
        template_base_name.rstrip('.jinja2') +\
        '_' + param_interface["interface_name"] + '.yml'

test_base_name = test_base_name.replace('/','-')
print 'Test file : '    + test_base_name
print 'Test_yml: '      + test_yml

print 'Save test on ./tests : '
test_dir_name = './tests/'
test_filename = test_dir_name + test_base_name
with open(test_filename, 'w') as f:
    f.write(test_yml)
    print test_filename

jsnapy_config =\
'''
tests:
- %s
''' % (test_filename)

dev1 = Device(
        host = '192.168.34.16',
        user = 'user1',
        password = 'password1')
dev1.open()

jsnapy = SnapAdmin()

snapcheck_dict = jsnapy.snapcheck(
                  data = jsnapy_config,
                  dev = dev1,
                  file_name = "snap01")

print '##### JSNAPy Test : Start #####'
for snapcheck in snapcheck_dict:
    print "Devece : ",         snapcheck.device
    print "Final result : ",   snapcheck.result
    print "Total passed : ",   snapcheck.no_passed
    print "Total failed : ",   snapcheck.no_failed
    print 'snapcheck test_details : '
    print '-'*30
    pprint(dict(snapcheck.test_details)) 
    print '-'*30
print '##### JSNAPy Test : End #####'

dev1.close()
```

```:./test_templates/test_interface_up.jinja2
test_interface_{{ interface_name }}_up:
  - command: show interfaces terse {{ interface_name }}
  - item:
      xpath: physical-interface
      tests:
        - is-equal: admin-status, up
        - is-equal: oper-status, up
```

実行結果です。

```:実行結果
% python run_test_interface_up.py    

                                                     
Load test_template :
Test file : test_interface_up_ge-0-0-2.yml
Test_yml: test_interface_ge-0/0/2_up:
  - command: show interfaces terse ge-0/0/2
  - item:
      xpath: physical-interface
      tests:
        - is-equal: admin-status, up
        - is-equal: oper-status, up
Save test on ./tests :
./tests/test_interface_up_ge-0-0-2.yml
##### JSNAPy Test : Start #####
Devece :  192.168.34.16
Final result :  Passed
Total passed :  2
Total failed :  0
snapcheck test_details :
------------------------------
{'show interfaces terse ge-0/0/2': [{'count': {'fail': 0, 'pass': 1},
                                     'err': "Test FAILED: admin-status before was < {{pre['admin-status']}} > now it is < {{post['admin-status']}} > ",
                                     'expected_node_value': 'up',
                                     'failed': [],
                                     'info': "Test PASSED: admin-status before was < {{pre['admin-status']}} > now it is < {{post['admin-status']}} > ",
                                     'node_name': 'admin-status',
                                     'passed': [{'actual_node_value': 'up',
                                                 'id': {},
                                                 'post': {'admin-status': 'up'},
                                                 'pre': {'admin-status': 'up'}}],
                                     'result': True,
                                     'testoperation': 'is-equal',
                                     'xpath': 'physical-interface'},
                                    {'count': {'fail': 0, 'pass': 1},
                                     'err': "Test FAILED: oper-status before was < {{pre['oper-status']}} > now it is < {{post['oper-status']}} > ",
                                     'expected_node_value': 'up',
                                     'failed': [],
                                     'info': "Test PASSED: oper-status before was < {{pre['oper-status']}} > now it is < {{post['oper-status']}} > ",
                                     'node_name': 'oper-status',
                                     'passed': [{'actual_node_value': 'up',
                                                 'id': {},
                                                 'post': {'oper-status': 'up'},
                                                 'pre': {'oper-status': 'up'}}],
                                     'result': True,
                                     'testoperation': 'is-equal',
                                     'xpath': 'physical-interface'}]}
------------------------------
##### JSNAPy Test : End #####
```

実行結果をみると、以下のように、テンプレートファイル内の変数が置換され、一般的なYAMLテンプレートファイルとしてJSNAPyが実行されていることを確認することができました。

```
Test_yml: test_interface_ge-0/0/2_up:
  - command: show interfaces terse ge-0/0/2
  - item:
      xpath: physical-interface
      tests:
        - is-equal: admin-status, up
        - is-equal: oper-status, up
```

# まとめ
今回は利用例を中心にJSNAPyについて紹介しました。
本記事で紹介しなかった機能も幾つか試していたのでGithubに置いてあるサンプルコードを参考にしていただければと思います。
https://github.com/taijiji/sample_jsnapy

次回はいよいよ最終回、PyEZとJSNAPyを使って、実際のISPネットワーク作業の自動化プログラムを作ったことについて紹介していきます。
[PyEZとJSNAPyを使ってみた。第四部: PyEZとJSNAPyでISP設定作業を自動化する編](http://qiita.com/taijijiji/items/983189f9bdebaa53e499)