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

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

- [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)

# PyEZ について
PyEZは、JUNOSルータを設定するためのPythonライブラリです。ちなみに[RubyEZ](https://github.com/Juniper/ruby-junos-ez-stdlib)というRubyライブラリも用意されているようです。

![Screen Shot 2016-11-02 at 11.00.31 AM.png](https://qiita-image-store.s3.amazonaws.com/0/45596/2251a213-e4b3-0244-f8d6-4d70fff71c85.png)

ルータとの通信は[NETCONF over SSH](https://tools.ietf.org/html/rfc6242)を利用し、デフォルトではRFCで推奨されているTCP 830 portで動作するように作られています。

PyEZは、JUNOS version 11.4からPyEZが利用できるようです。他社メーカの場合だと比較新しいOS versionでしか自動化ツールが利用できないケースが多いのですが、JUNOS + PyEZの組み合わせではすでに多くの運用現場で導入・運用されているJUNOSルータで利用することができるので、導入の敷居の低さからも利用しやすいツールだと言えます。
(実際のネットワーク運用現場では性能劣化や不具合を回避するため、バグが枯れたOS versionしか採用しないポリシーの組織が多いのではないでしょうか。「新しいOSはバグが枯れないと使えない」「結果的に自動化の検討・導入も数年単位で先延ばし」となってしまうケースもあるのかと想像しています。)

## PyEZができること
- ルータのコンフィグ投入
 - load (merge/overload/replace オプション指定可能)
 - lock / unlock
 - commit check / diff
 - commit
- ルータの情報取得
    - hostname, Model, OS version, Serial Numberなどの機器情報取得
    - テーブル情報(ルーティングテーブルやARPテーブル) の取得
    - ping, tracerouteの実行,取得
- ソフトウェア更新

全機能を試したわけではありませんが、手動で設定できることはほぼほぼPyEZでできそうな印象です。もしPyEZを利用されている中で"できないこと"を見つけられた方は、ぜひ知見を共有してほしいです。


## 参考資料
以下の記事/ドキュメントを見ながら進めると理解が早く進みます。参考になった順番に並べています。

日本語紹介ブログ
- [Juniper JUNOS PyEz (python library)を試すメモ 1 ~PyEz概要~](http://qiita.com/hiromiarts/items/aade90a161acd63bc8ed)
- [Juniper JUNOS PyEz (python library)を試すメモ 2 ~PyEzによる情報取得~](http://qiita.com/hiromiarts/items/f44ec5eab6a7508f050a#_reference-52d108e43bb587187d80)
- [Juniper JUNOS PyEz (python library)を試すメモ ３ ~PyEzによる設定変更~](http://qiita.com/hiromiarts/items/ccaaeed4709d9961cdcc#_reference-75abb91c856511fe5b64)

PyEZ Github
- [Juniper/py-junos-eznc](https://github.com/Juniper/py-junos-eznc)

Juniperオフィシャルドキュメント
- [API document](http://junos-pyez.readthedocs.io/en/2.0.1/
)
- [Junos PyEZ](http://www.juniper.net/techpubs/en_US/release-independent/junos-pyez/information-products/pathway-pages/index.html#JsOpHJUblKGKfvc1.97)
- [J-Net>TechWiki>Automation](http://www.juniper.net/techpubs/en_US/junos-pyez2.0/information-products/pathway-pages/junos-pyez-developer-guide.html#QHGwywIqZfAzZjFI.97)

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

まずpipコマンドでPyEZをインストールします。ここでは最新のversion 2.0.1を利用します。

```
pip install junos-eznc

pip list
junos-eznc (2.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;
        }
      }
    }
}
```

ルータには上記のように設定するだけで NETCONF over SSH用にTCP 830をリッスンし始めます。

```
root@firefly1> show system connections | grep 830
tcp4       0      0  *.830     *.*      LISTEN
```

## PyEZ 利用例1: ルータのホスト名を変更
PyEZが動かすための最小構成プログラムを以下に示します。ここではホスト名を firefly1 から firefly1_changed_by_PyEZ に設定変更することを実施しています。


```python:set_firefly1_change_hostname.pyのPyEZ設定部分のみを抜粋
#! /usr/bin/env python
# -*- coding: utf-8 -*-

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

# 制御対象の情報を定義
dev1 = Device(
            host="192.168.34.16",
            user="user1",
            password="password1"
        )   
dev1.open()

# セッションを確立
dev1.open()
dev1.bind(cu=Config)

# ホスト名を表示
print "Hostname : ",
print dev1.facts["hostname"]

# 他セッションから設定変更されないようにロック
dev1.cu.lock()

# コンフィグファイル(./configs/change_hostname.conf")を読み込み
# この時点では candidateコンフィグが更新されるだけで、commit は未実施
conf_filename = "./configs/change_hostname.conf"
dev1.cu.load(path=conf_filename, format="text", merge=True)

# commit checkを実施。設定内容に問題がないか判定。
print "Commit Check : ",
if dev1.cu.commit_check() :
    print "OK"
else:
    print "Error"

# ユーザによるcommitの実施可否の選択を委ねる (もちろん自動commitも可能)
print "Do you commit? y/n"
choice = raw_input().lower()
if choice == "y":
    # commitを実施。
    dev1.cu.commit()
    print "Commit candidate config : OK"
else:
    # commitを実施せず、rollback。candidateコンフィグが削除される。
    dev1.cu.rollback()
    print "Rollback : OK"

# セッションロック解除
dev1.cu.unlock()

# セッション終了
dev1.close()
```

```:./configs/change_hostname.conf
system {
    host-name firefly1_changed_by_PyEZ;
}
```

上記のようなプログラムで、ホスト名変更コンフィグを投入することが可能になります。

実際にデモ用コードとして、表示部分を加えて実装したものはgithubで公開しています。
https://github.com/taijiji/sample_pyez/blob/master/set_firefly1_change_hostname.py

実行結果は以下のようになります。
![Screen Shot 2016-11-26 at 7.09.58 PM.png](https://qiita-image-store.s3.amazonaws.com/0/45596/98961c93-fa93-d473-6c71-bfe8d792ae1b.png)


1. ルータコンフィグが記載されたファイル(./configs/change_hostname.conf)を、ルータに読み込み(この時点ではcommit未実施)
2. diffで設定差分を確認
3. commit check
4. commitを実行

という手動設定と同じ実行手順で、Hostnameを変更することができました。もちろん実際のルータのホスト名が変更されたことも確認できました。

```:ルータでホスト名確認
root@firefly1_changed_by_PyEZ>
root@firefly1_changed_by_PyEZ> show version
Hostname: firefly1_changed_by_PyEZ
Model: firefly-perimeter
JUNOS Software Release [12.1X47-D20.7]
```

## PyEZ 利用例2: ルータのインタフェースを追加 (コンフィグファイルの一部を変数化)
さきほどはコンフィグファイルは事前に静的に生成したコンフィグを利用しましたが、PyEZではテンプレートエンジンを使うことで以下のようにコンフィグファイルの一部を変数化することが可能です。

```
interfaces {
   {{ if_name }} {
        unit 0 {            
            description {{ if_description }};
            family inet {
                address {{ if_address_ipv4 }}/{{ if_subnet_ipv4 }};
            }
        }
    }
} 
```

静的なコンフィグを利用する場合だと、インタフェースやBGP neighborなどパラメータが複数ある設定定に関して同じようなコンフィグファイルを都度準備/管理する必要があり、結果的に大量のコンフィグファイルを管理する必要がでてきてしまいます。ここでコンフィグファイルの一部を変数化/テンプレート化することによって、管理すべきコンフィグファイルの対象を減らすことができます。

PyEZでは、コンフィグファイルを変数化するために、テンプレートエンジンの一つであるJinaja2を利用しています。Jinja2については、過去にブログで紹介しているのでこちらを参照してください。
[Network Engineer のためのTemplate Engine活用術](http://qiita.com/taijijiji/items/3d9d113c056a4488c32e)

ここでは PyEZ 利用例1 との差分のみを紹介します。
実際書いたデモ用サンプルコードは、githubをご確認ください。
https://github.com/taijiji/sample_pyez/blob/master/set_firefly1_add_interface.py

```python:set_firefly1_add_interface.pyの一部を抜粋
dev1 = Device(
            host="192.168.34.16",
            user="user1",
            password="password1"
        )   
dev1.open()

# interface ge-0/0/2の設定内容をRPC形式で表示
if_info = dev1.rpc.get_interface_information(
                    interface_name='ge-0/0/2',
                    terse=True)
print etree.tostring(if_info)

# テンプレートファイル名を指定
template_filename = "./configs/add_interface.jinja2"
# テンプレートファイル内の変数を指定
add_if_param = {
    'if_name' : 'ge-0/0/2',
    'if_description' : 'add_by_PyEZ',
    'if_address_ipv4' : '192.168.35.1',
    'if_subnet_ipv4' : '30'
}

# テンプレートファイルを利用して、コンフィグを読み込み
dev1.cu.load(
    template_path=template_filename, 
    template_vars=add_if_param, 
    format="text",
    merge=True
)

# コンフィグをcommit
dev1.cu.commit()
```

```:./configs/add_interface.jinja2
interfaces {
   {{ if_name }} {
        unit 0 {            
            description {{ if_description }};
            family inet {
                address {{ if_address_ipv4 }}/{{ if_subnet_ipv4 }};
            }
        }
    }
} 
```

デモプログラムの実行結果です。

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

##### Operation : Start #######
Connecting to device :  OK
Hostname :  firefly1
Interfaces ge-0/0/2 :
##############################
<interface-information style="terse">
<physical-interface>
<name>
ge-0/0/2
</name>
<admin-status>
up
</admin-status>
<oper-status>
up
</oper-status>
</physical-interface>
</interface-information>

##############################
Load config :
OK
Target template : ./configs/add_interface.jinja2
##############################
interfaces {
   ge-0/0/2 {
        unit 0 {
            description add_by_PyEZ;
            family inet {
                address 192.168.35.1/30;
            }
        }
    }
}
##############################
Diff :
##############################

[edit interfaces]
+   ge-0/0/2 {
+       unit 0 {
+           description add_by_PyEZ;
+           family inet {
+               address 192.168.35.1/30;
+           }
+       }
+   }

##############################
Commit Check :  OK
Do you commit? y/n
y
Commit candidate config : OK
Interfaces ge-0/0/2 :
##############################
<interface-information style="terse">
<physical-interface>
<name>
ge-0/0/2
</name>
<admin-status>
up
</admin-status>
<oper-status>
up
</oper-status>
<logical-interface>
<name>
ge-0/0/2.0
</name>
<admin-status>
up
</admin-status>
<oper-status>
up
</oper-status>
<description>
add_by_PyEZ
</description>
<filter-information>
</filter-information>
<address-family>
<address-family-name>
inet
</address-family-name>
<interface-address>
<ifa-local emit="emit">
192.168.35.1/30
</ifa-local>
</interface-address>
</address-family>
</logical-interface>
</physical-interface>
</interface-information>

##############################
##### Operation : End #####
```

PyEZでの情報取得がXML-RPC形式になっていて若干わかりにくいですが、設定前は無かったge-0/0/2にIPアドレスとディスクリプションを設定されていることがわかります。

実際のルータで確認してみると以下のように設定されていることがわかります。

```:ルータで確認
root@firefly1> show configuration interfaces ge-0/0/2
unit 0 {
    description add_by_PyEZ;
    family inet {
        address 192.168.35.1/30;
    }
}
```

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

次回はJSNAPyについて紹介していきます。
[PyEZとJSNAPyを使ってみた。第三部: JSNAPy使ってみた編](http://qiita.com/taijijiji/items/b6e3da3d3aeb351ff7ff)