NSO を使って、サーバに公開APIを実装してみる

# NSO を使ってサーバに公開APIを実装してみる

Cisco は NSO (Network Services Orchestrator) を無料で公開致しました。これを使用して、サーバを外部から以下の各APIで操作出来るようにしてみましょう。

- Netconf
- REST
- RESTCONF
- CLI
- SNMP
- JSON-RPC

また、本年のAdvent Calendar でも同僚が記事を作成していますので、こちらもぜひご確認ください。
[MAC に Free NSO を入れて Network 自動化の心の準備をしてみる](https://qiita.com/tkasakak/items/91d93da9cb061f285042)

## 遠隔操作してみたいもの

例として、Samba がインストールされた、CIFSサーバの共有ディレクトリを管理してみましょう。Samba の設定は smb.conf ファイルに記述し、

- 共有名
- ディレクトリパス

を追加・削除することで、その公開情報を設定することができます。共有ディレクトリの追加・削除を遠隔操作出来るようにしてみたいと思います。

## NSO を使った場合・使わない場合のアーキテクチャのイメージ

一般的には、各種インターフェースは各サーバアプリケーションが実装し、そのデータモデルもそれぞれ作られます。

![Arch_Others.png](https://qiita-image-store.s3.amazonaws.com/0/140504/80a157dc-4c2c-8462-8b9e-44684c028f6a.png)

Cisco IOS-XR では、sysdb というデータベースが間に配置され、各種インターフェースの管理サーバアプリケーションが動作しています。データモデルは、各インターフェース毎に別のものが存在します。

![Arch_cisco.png](https://qiita-image-store.s3.amazonaws.com/0/140504/9ddb2139-2e83-27f3-0809-129fe7a9d429.png)

NSO では、一つのデータモデルを定義し、全てのインターフェースで同じものを使用します。そのため、一度定義すれば、どのインターフェースを使用しても、同じデータフォーマットで使用することが出来るようになります。

![Arch_NSO.png](https://qiita-image-store.s3.amazonaws.com/0/140504/16738362-c4f6-d3f3-b2c0-250b1393ff1a.png)


## システムデータモデルの作成

NSO は RFC 6020 の Yang を使用して、そのデータモデルを定義します。
今回の例では、以下のように作成しました。

advent-cal2018.yang

```
module advent-cal2018 {
  namespace "http://tail-f.com/ns/example/adventcal2018";
  prefix advent-cal2018;

  import tailf-common {
    prefix tailf;
  }

  container samba {
    list share {
      key name;
      leaf name {
        type string {
          pattern "[a-zA-Z][a-zA-Z0-9]+";
        }
      }
      leaf path {
        type string;
      }
    }
  }
}
```

これによって、以下のような CLI が作成されます。ネットワークエンジニアにとっては、このほうが実装がどのような形になるか、分りやすいかもしれません。

```
# configure
(config)# samba share dept1
(config-share-dept1)# path /var/dept1
(config)# samba share dept2
(config-share-dept1)# path /var/dept2
```

## 実装方法

以下の２つが考えられます。

- CDB Subscriber による CDB の監視
    - Config変更を行い、CDB に対して変更があった場合を検知し、アプリケーションの設定ファイルを書き換える
- トランザクションの監視
    - NSOに内蔵されるコールバックの仕組み (Hook, Service) を利用し、Configの変更が行われた場合に、アプリケーションの設定ファイルを書き換える

前者の方法は、NSO の姉妹ソフト ConfD を使った組み込み機器でよく利用されています。後者の方法を使ってNSOで同機能を実装することは可能ですが、NEDを使用した Southbound 機器の設定が通常想定されており、本来のコンセプトとは少しズレがあると思います。

## CDB Subscriber

データモデルのスキーマや、その設定データそのものは、NSO では内蔵されているCDB (Configuration Database) に保存されます。特定のパスを監視することで、データの設定・変更・削除が行われると、そのパスを subscribe しているアプリケーションに、それが通知されます。変更が行われる前に、アプリケーション側でその内容をチェックし、変更そのものを Abort させることも可能です。

![CDB_subscriber.png](https://qiita-image-store.s3.amazonaws.com/0/140504/ea9e9357-4817-2e16-7c59-a4076f1dd423.png)

## NSO のパッケージ

NSO は NED も含め、独自拡張機能はパッケージとして作成します。その中にデータモデルやアプリケーションを含め、パッケージ単位でNSOのインスタンス間で移動やコピーをすることも可能です。Java や Python で記述出来、また組み合わせることも可能です。本サンプルでは、Java で作成しました。

以下で紹介するサンプルは、[こちら](https://github.com/akiwamot/samples/blob/master/advent-cal2018.tar.gz)からダウンロード出来ます。

ncs-make-package コマンドを使用して、空のパッケージを作成できますが、生成されるサンプルは何らかのコールバックを受けて動作するようになっています。Applicationにする場合は、以下のような変更が必要です。

- Java クラスが ApplicationComponent を実装
    - public void init()
    - public void finish()
    - public void run()
- package-meta-data.xml にcallback ではなく、application として登録

```
public class adventcal2018Dp implements ApplicationComponent  {
  public void init() {
  }
  public void finish() {
  }
  public void run() {
　　 while(true){

    }
  }
}
```
package-meta-data.xml

```
  <component>
    <name>AdventCalendar2018</name>
    <application>
      <java-class-name>com.example.adventcal2018.adventcal2018Dp</java-class-name>
    </application>
  </component>
```

このように設定したクラスは、NSO起動時にパッケージがロードされ init() が呼ばれた後、run() が別スレッドで呼ばれます。NSO終了時には、finish() が呼ばれます。

つまり、init() 中で CDB を subscribe し、run() 内で通知が来るのを待機するというコードが書けるようになります。

## プログラミング

run() 内で read() メソッドで待機中、CDBに変更が検知された場合は、変更が検知された subscription point を返却します。diffIterate を使用して、その変更内容を舐めて確認していきます。

```
while (!requestStop) {
    try {
        int[] point = cdbSubscription.read();
        CdbSession cdbSession =
            cdbData.startSession(CdbDBType.CDB_RUNNING);
        EnumSet<DiffIterateFlags> diffFlags =
            EnumSet.of(DiffIterateFlags.ITER_WANT_PREV);
        cdbSubscription.diffIterate(point[0], new DiffIterateImpl(),
                                    diffFlags, cdbSession);
        cdbSession.endSession();
    } finally {
        cdbSubscription.sync(CdbSubscriptionSyncType.DONE_PRIORITY);
    }
```

diffIterate メソッドへは、検知したConfig差分を確認する独自クラスを指定します。通常、以下のように実施された操作 (create/deleted/modified/set_value/etc.) について分岐させ、必要な処理を記述します。

CREATE はリストインスタンスの作成時に発生します。本件の例では、/samba/share を subscribe していますので、/samba/share{ key } が作成されたことがトリガとなります。/samba/share{ key } が削除された場合は DELETED, リストインスタンス内に変更が合った場合は MODIFIED が発生します。

```
private class DiffIterateImpl implements CdbDiffIterate {
    private Logger log = Logger.getLogger(DiffIterateImpl.class);
    public DiffIterateResultFlag  iterate(ConfObject[] kp,
                                          DiffIterateOperFlag op,
                                          ConfObject oldValue,
                                          ConfObject newValue,
                                          Object initstate) {

        switch(op){
            case MOP_CREATED: {

            }
            case MOP_DELETED: {

            }
            case MOP_MODIFIED: {

            }
       }
   }
}
```

例として作成した MOP_CREATED は以下です。kp (KeyPath) 変数には、変更されたパスが渡されます。kp[1] が share の場合は、kp[0] で得られる共有名を取得し、その配下に指定されている path の値も取得します。

その後、それを元に smb.conf の内容を書き換え、Samba をリロードすれば、NSO から Samba の状態を変更出来るようになります。

DELETED, MODIFIED についても同様に作成します。

```
// keypath passed
// /samba/share{key}
//   [2]   [1]  [0]
CdbSession cdbSession = (CdbSession)initstate;

switch(op){
    case MOP_CREATED: {
        ConfTag tag = (ConfTag)kp[1];
        if(tag.getTagHash() == adventCal2018._share){
            String shareName = ((ConfBuf)((ConfKey)kp[0]).elementAt(0)).toString();
            log.info("created: " + shareName);
            String pathName;
            try {
                cdbSession.cd("/samba/share{"+shareName+"}");
                pathName = cdbSession.getElem("path").toString();
                log.debug("pathname = " + pathName);

                // TODO
                // adding entry to smb.conf
                // reload smb.conf
                log.info(String.format("New share [%s](%s) has been created.", shareName, pathName));
            }catch(Exception e){
                log.error("error to read path");
            }
        }
        return DiffIterateResultFlag.ITER_CONTINUE;
    }
```

## 使用してみる

今回の例では、実際に Samba の設定ファイルを編集しておりませんので、変更時に出力されるログで動作を確認します。

### CLI

private1 という共有名を作成し、そのディレクトリパスを /private1 に設定します。
同様に、private2 という共有名を作成し、そのディレクトリパスを /private2 に設定します。

```
admin@ncs# config
Entering configuration mode terminal
admin@ncs(config)# samba share private1 path /private1
admin@ncs(config-share-private1)# samba share private2 path /private2
admin@ncs(config-share-private2)# commit
Commit complete.
admin@ncs(config-share-private2)# end
admin@ncs# show running-config samba
samba share private1
 path /private1
!
samba share private2
 path /private2
!
admin@ncs#
```

ログには、以下のように表示されました。それぞれの共有名が作成されています。

```
<INFO> 16-Dec-2018::00:14:39.504 adventcal2018Dp$DiffIterateImpl (advent-cal2018:DpSkeleton)-Run-20: - New share [private1](/private1) has been created.
<INFO> 16-Dec-2018::00:14:39.507 adventcal2018Dp$DiffIterateImpl (advent-cal2018:DpSkeleton)-Run-20: - New share [private2](/private2) has been created.
```

### REST

CLIから作成した private1 の設定を確認します。その後、ディレクトリパスを、/var/private1 へ変更します。

現在の設定を確認 (JSON)

```
$ curl -u admin:admin http://localhost:8080/api/running/samba/share/private1 -H "Accept: application/vnd.yang.data+json"
{
  "advent-cal2018:share": {
    "name": "private1",
    "path": "/private1"
  }
}
```

private1 の path を変更

```
$ curl -u admin:admin -X PATCH http://localhost:8080/api/running/samba/share -H "Content-Type: application/vnd.yang.data+json" -d '
{
  "advent-cal2018:share": {
    "name": "private1",
    "path": "/var/private1"
  }
}'
```

ログ上では、以下のように出力されました。

```
<INFO> 16-Dec-2018::00:16:00.716 adventcal2018Dp$DiffIterateImpl (advent-cal2018:DpSkeleton)-Run-20: - share [private1] has been updated. New path = (/var/private1)
```

### NETCONF

NETCONFから、private2 共有名を削除し、public1 共有名を作成します。その時のpathは /var/public1 とします。

```
$ netconf-console -i

* Enter a NETCONF operation, end with an empty line
<edit-config>
    <target><running/></target>
    <config xmlns="http://tail-f.com/ns/config/1.0">
      <samba xmlns="http://com/example/adventcal2018">
        <share xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" nc:operation="delete">
          <name>private2</name>
        </share>
        <share>
          <name>public1</name>
          <path>/var/public1</path>
        </share>
      </samba>
    </config>
</edit-config>

<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="2">
  <ok/>
</rpc-reply>
```

ログには以下のように出力されました。

```
<INFO> 16-Dec-2018::00:20:56.118 adventcal2018Dp$DiffIterateImpl (advent-cal2018:DpSkeleton)-Run-20: - share [private2] has been deleted.
<INFO> 16-Dec-2018::00:20:56.120 adventcal2018Dp$DiffIterateImpl (advent-cal2018:DpSkeleton)-Run-20: - New share [public1](/var/public1) has been created.
```

現状の設定を確認 (NETCONF)

```
* Enter a NETCONF operation, end with an empty line
<get-config>
  <source>
    <running/>
  </source>
  <filter type="subtree">
    <samba xmlns="http://com/example/adventcal2018"/>
  </filter>
</get-config>

<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="2">
  <data>
    <samba xmlns="http://com/example/adventcal2018">
      <share>
        <name>private1</name>
        <path>/var/private1</path>
      </share>
      <share>
        <name>public1</name>
        <path>/var/public1</path>
      </share>
    </samba>
  </data>
</rpc-reply>
```

### 最後にもう一度、CLI から現状の設定を確認

```
admin@ncs# show running-config samba
samba share private1
 path /var/private1
!
samba share public1
 path /var/public1
!
admin@ncs#
```


# さいごに

NSO が各種 Northbound Interface をサポートしており、一つのデータモデルを作成するだけで色々なクライアントから操作出来るようになります。ぜひ各種サーバを「アプライアンス」として見てください。