JunosのOn-box Pythonを使ってみる #2 Commit Script

# 概要

[前回](http://qiita.com/kazubu/items/c1205029862d057e61d6)、JunosのOn-box Pythonの簡単な使い方の一つであるOp Scriptを紹介しました。

今回は前回に続いて、Commit Scriptについて紹介していきたいと思います。

# Commit Scriptとは？

Commit Scriptとは、Junoscriptと呼ばれるJunosのスクリプティング機構のうち、「ユーザによるコミット」をトリガーとして実行されるスクリプトを指します。

Commit Scriptは、ユーザがコミットを実行したタイミングで呼び出され、コミットしたユーザや時間といった情報、これからコミットされるコンフィグ、現在の機器の状態などによって、そのコミットを制御する、またはコミットされるコンフィグを改変するといったことを行うことができます。

たとえば、以下のようなユースケースが考えられます。

* 設定の内容を独自のルールでチェックして、適合しないコンフィグはコミットさせないようにする
  * ホスト名が含まれていないコンフィグを拒否する(1)
  * Descriptionが書かれていないインターフェイスを拒否する
  * SSH秘密鍵が含まれていないユーザを追加させない
* コミット時に設定を変更する
    * [コミットしたユーザをコンフィグ内にメモする](http://qiita.com/kazubu/items/5de3e520946bca71c3ca)
    * 必須の設定をコミット時に追加する(2)
    * ACLが明示的に設定されていないIFには強制的にACLを適用する
* 独自の設定ノブを作り、スクリプトで実際のJunos設定に展開する(マクロ)
    * Descriptionが含まれていないインターフェイスにデフォルトのDescriptionを指定する(3)
* コミットをトリガーに何らかのコマンドを実行する
    * コミット前のシステム状態を保存しておくとか
* コミットをトリガーに何らかの外部APIを叩く
    * コンフィグの自動バックアップ
    * [ChatOps的な何か](http://qiita.com/kazubu/items/403b1b5c76607275a8a1)
    * サーバーで設定ポリシーのチェックとかもできるかも
* 等々…

と言ったような形で、アイデア次第で様々な活用法が考えられます。

今回は簡単なサンプルを交えながら、On-box PythonでのCommit Scriptを紹介していきます。

# 設定

Commit Scriptを使用するためには、以下のようにして使用したいスクリプトを設定内に記述する必要があります。

```
set system scripts language python
set system scripts commit file commit-script.py
```

ここで記述したスクリプトは、`/var/db/scripts/commit`内に用意しておきます。

なお、Commit時の設定の変更方法のうち、*Transient Configuration* という変更方法を有効にしたい場合は、追加で以下の設定を行います。

```
set system scripts commit allow-transients
```

詳しくは後述しますが、マクロによる設定の展開など、毎回スクリプトから動的に生成したい設定がある際に必要となります。

# 例1: 必須の設定が含まれていないコンフィグのコミットを拒否する

それではさっそく、Commit Scriptを試してみましょう。

たとえば、ホスト名が設定されていないConfigを拒否するスクリプトは以下のようになります。

```commit-script.py
from junos import Junos_Configuration
import jcs

def main():
  root = Junos_Configuration

  if root.find('./system/host-name') == None:
    jcs.emit_error("Hostname is not defined!!!")


if __name__ == '__main__':
  main()
```

独特な用語として、`Junos_Configuration`と`jcs`というものが出てきていますが、`Junos-Configuration`は、CommitされようとしているConfigurationのXML rootになっていて、これを確認することで設定の確認や変更が可能となります。
`jcs`は、Commit Script内でのイベント生成や状態取得などを行うためのメソッドが定義されている名前空間です。詳細は[公式ドキュメント](https://www.juniper.net/techpubs/en_US/junos16.1/topics/concept/junos-script-automation-junos-extension-functions-jcs-namespace.html)を参照してください。

スクリプトの内容としては単純で、コンフィグレーションの中から`/system/host-name`の位置にあるXMLを抽出し、その結果が`None`であったとき、`jcs.emit_error`を呼び出してエラーを出力する、というものです。

これによる出力結果は以下のような形になります。

```
[edit]
root@vmx1# show system host-name
host-name vmx1;

[edit]
root@vmx1# delete system host-name

[edit]
root@vmx1# commit check
error: Hostname is not defined!!!
error: 1 error reported by commit scripts
error: commit script failure

[edit]
root@vmx1# rollback 0
load complete

[edit]
root@vmx1# commit check
configuration check succeeds

```

このような形で、ホスト名が含まれていない場合にのみ、Commit checkが失敗するようになります。
ちなみに、Commit checkは通すけれども警告だけは表示したい、という時は、`jcs.emit_error`の代わりに`jcs.emit_warning`を使用します。

# 例2: 設定の内容を変更する

続いて、設定内容の変更を行う例を紹介していきたいと思います。

設定内容の変更には、通常のユーザーと同じように永続的な編集を行う方法のほか、永続的でない(Transientな)編集を行う方法の二種類があります。

ここではまず、永続的な編集を試してみましょう。たとえば、システムのタイムゾーンを`Asia/Tokyo`に変更するスクリプトの例は以下のようになります。

```commit-script.py
import jcs

def main():
  xml = '<system><time-zone>Asia/Tokyo</time-zone></system>'

  jcs.emit_change(xml, "change", "xml")

if __name__ == '__main__':
  main()
```

設定の変更は、`jcs.emit_change`メソッドに変更内容と変更方法、フォーマットを指定することによって行うことができます。なお、フォーマットは今のところxmlのみとなります。

このスクリプトを動かしてみると、以下のようになります。

```
[edit]
root@vmx1# show system time-zone

[edit]
root@vmx1# commit check
configuration check succeeds

[edit]
root@vmx1# show | compare
[edit system]
+  time-zone Asia/Tokyo;
```

このように、Commit Scriptが実行されたタイミングで、タイムゾーンが追加される形になります。

# 例3: 設定の動的な生成と非永続的な適用

次は、いわゆるマクロ的な物を実装し、生成された設定を透過的に機器に反映する例を紹介します。（単純かついい例があまり思いつかないのですが）例として、インターフェイスのDefault Descriptionを定義しておき、Descriptionが含まれていないインターフェイスが存在する場合にDefault Descriptionを設定するスクリプトの例は以下のようになります。

```commit-script.py
from junos import Junos_Configuration
import jcs

def main():
  root = Junos_Configuration

  apply_macro = root.find("./interfaces/apply-macro[name='default-description']")

  xml_tmpl = '<interfaces>{0}</interfaces>'
  interface_xml_tmpl = '<interface><name>{0}</name><description>{1}</description></interface>'

  xml = ""

  if apply_macro:
    description_template = apply_macro.find('./data/name').text

    for element in root.findall("./interfaces/interface"):
      if element.find('description') is None:
        ifd_name = element.find('name').text
        xml += interface_xml_tmpl.format(ifd_name, description_template.format(ifd_name))

    xml = xml_tmpl.format(xml)

    jcs.emit_change(xml, "transient-change", "xml")

if __name__ == '__main__':
  main()
```

このスクリプトでは、interfaces直下に`default-description`というMacroが定義されている場合、インターフェイス一覧を調べ、Descriptionが設定されていないインターフェイスには、default-descriptionマクロ内に指定されたテキストを指定するという動きをします。

XMLで変更を生成する点については先ほどと変わりはありませんが、`jcs.emit_change`の第二引数が`"change"`から`"transient-change"`に変わっています。これは、その設定を一時的なものとして扱い、設定ファイルの中には保存されないようにするオプションです。

なお前述したように、`transient-change`を使う場合、以下の設定を投入しておく必要があります。

```
set system scripts commit allow-transients
```

このスクリプトを適用すると、以下のような動きをします。

```
root@vmx1# show interfaces | display set
set interfaces apply-macro default-description "Description is not configured({0})"
set interfaces ge-0/0/0 disable
set interfaces ge-0/0/0 vlan-tagging
set interfaces ge-0/0/0 unit 1001 vlan-id 1001
set interfaces ge-0/0/0 unit 1001 family inet address 10.0.1.1/24
set interfaces ge-0/0/1 description test
set interfaces ge-0/0/1 unit 0 family inet
set interfaces fxp0 unit 0 family inet address 172.23.1.111/24

[edit]
root@vmx1# commit
commit complete

[edit]
root@vmx1# run show interfaces descriptions
Interface       Admin Link Description
ge-0/0/0        down  down Description is not configured(ge-0/0/0)
ge-0/0/1        up    down test
fxp0            up    up   Description is not configured(fxp0)

[edit]
root@vmx1# run show configuration interfaces | display set
set interfaces apply-macro default-description "Description is not configured({0})"
set interfaces ge-0/0/0 disable
set interfaces ge-0/0/0 vlan-tagging
set interfaces ge-0/0/0 unit 1001 vlan-id 1001
set interfaces ge-0/0/0 unit 1001 family inet address 10.0.1.1/24
set interfaces ge-0/0/1 description test
set interfaces ge-0/0/1 unit 0 family inet
set interfaces fxp0 unit 0 family inet address 172.23.1.111/24
```

先ほどとの違いがわかるでしょうか。#2 では、設定はcommit checkの段階で設定に変更が行われ、変更点は設定の中に現れていました。一方、`transient-change`を使うと、一見コンフィグに変更は行われていないように見えるのですが、実際の動作を確認すると、InterfaceにDescriptionが設定されていることがわかります。

`transient-change`を使うとこのように、設定をマクロ化して、本質的なパラメータ以外の実際の機器の設定を隠蔽するといったことが可能となります。たとえば、顧客が大量にいるため大量の設定が必要だが、顧客毎の違いは微々たる物である、といった場合などに効率的かつ見通しよく設定を行うことが可能になると考えられます。

ちなみに、実際に機器で動いている設定を確認したい場合は、以下のようにして確認することができます。

```
root@vmx1> show configuration interfaces | display set | display commit-scripts
set interfaces apply-macro default-description "Description is not configured({0})"
set interfaces ge-0/0/0 description "Description is not configured(ge-0/0/0)"
set interfaces ge-0/0/0 disable
set interfaces ge-0/0/0 vlan-tagging
set interfaces ge-0/0/0 unit 1001 vlan-id 1001
set interfaces ge-0/0/0 unit 1001 family inet address 10.0.1.1/24
set interfaces ge-0/0/1 description test
set interfaces ge-0/0/1 unit 0 family inet
set interfaces fxp0 description "Description is not configured(fxp0)"
set interfaces fxp0 unit 0 family inet address 172.23.1.111/24
```

# おわりに

今回はOn-box PythonによるJunoscriptの使いどころの一つとして、Commit Scriptを紹介しました。いろいろな使い方がありそうなことがなんとなくわかっていただければ幸いです。
今回紹介した内容は、これまででもXSLT/Slaxを使えば実装できるものばかりですが、Pythonで記述できるようになり、非常に取っつきやすくなったのではないかと思います。

次回はEvent Scriptでの利用方法を紹介してきたいと思います。