NSO と Ansible を組み合わせてみる

# はじめに
この記事はシスコの同志による Advent Calendar の一部として投稿しています。

|年度|URL|
|:-:|:-:|
|2017年|https://qiita.com/advent-calendar/2017/cisco|
|2018年|https://qiita.com/advent-calendar/2018/cisco|
|2019年|https://qiita.com/advent-calendar/2019/cisco|
|2020年|https://qiita.com/advent-calendar/2020/cisco|
|2020年 2枚目|https://qiita.com/advent-calendar/2020/cisco2|
 
「ネットワークの自動化」ということが言われ始めてしばらく経ちますが、そのためのソリューションが数多く登場しました。「ネットワークの自動化」を行う際には具体的なゴールを決定する必要があり、それに合わせて必要なソリューションを組み合わせる事で、よりスムーズな自動化が可能となることもあると思います。

Ciscoが提供するソリューションの一つが、Network Services Orchestrator (NSO) です。機器の設定を自動化することや、必要な機器を選択するようなオーケストレータとしての役目を行うためのフレームワークとなっています。ワークフロー管理ツールとして、あるいはネットワーク機器の設定を行うツールとして使用されることが多いです。

Overviewについては、以前のこちらの記事も参考にして頂けたらと思います。
[Cisco Network Services Orchestrator (NSO) ができること](https://qiita.com/akiraiwamoto/items/0816f523d24a21917d51)

Ansible は、Redhat 社が保有するオープンソースの自動化ソリューションです。冪等性という一つのポリシーの元、非常に多くの登録されたモジュールを使用して、リモート機器の設定を行うことが出来ます。

この記事では、これら2つを組み合わせて使ってみる方法について紹介します。

# NSO と Ansible の違い

NSO と Ansible　を比較すること自体、そもそも別の製品なのになぜ、と思われる方は居るかも知れません。しかし、実はNSO の本領発揮の部分の一つである、"Orchestrator" 部分を抜いて比較されることも多いのです。別の目的をもった製品達ですが、リモート機器を設定するという観点で少し比較してみます。

## NSO にできて Ansible に出来ないこと

ネットワーク機器の設定を行う、という観点ではどちらも同じようなことをすることが出来ます。例えばcisco ios ルータの interface Ethernet 1/1 というインターフェースの description を "hello" から "cisco" に変更する、といったことをしたい場合、NSO では cisco-ios NED を使用、Ansible では ios_config モジュールを使用すれば良いと思います。

では、設定した値をもとに戻したい場合はどうしましょう。NSO ではロールバック機能があるため、"description hello" といったコマンドを自動生成することが出来るのですが、Ansible では元の値を何らかの方法で保持しておいて、再度変更のための Playbook を用意する必要があると思います。これは、NSO がデバイスのステート情報を保持していることと、コマンドのスキーマ情報を持っているため可能となっています。

## Ansible にできて NSO に出来ないこと

Ansible のサーバ設定を行う機能は卓越しており、NSO の NED でも特に対象としているものはないため、NSO を使っている環境においても、Ansible を使うことに大きな意味があります。例えば Redhat Linux 上に Apache パッケージをインストールするとした場合、非常にシンプルな Playbook を使用することで実施出来ます。

# NSO を Ansible から使用する

実は、Ansibleにも nso モジュールが存在します。

- nso_config
- nso_action
- nso_query
- nso_show
- nso_verify

これらを使うことで、Ansible から NSO を操作する事が可能です。内部では REST が使用されており、NSOが提供する Northbound インターフェースを拡張するイメージでも使用可能です。

この記事では、逆に NSO から Ansible を使ってみようと思います。

# NSO から Ansible を使用する

NSO は Orchestrator なのですが、通常一緒に使用する NED が非常に大きな意味を持っており、これも NSO の大きな魅力の一つです。リモート機器を管理するという役割を負っており、Southbound 接続への接続点となっています。

CLI NED を使用する場合は内蔵の telnet/ssh クライアントを使用します。NETCONF NED を使用する場合は、NSO 内部に持っている NETCONF モジュールを外部向けに使用しています。（なので、CLI NED の SSH クライアントと NETCONF NED の SSH クライアントは別物です）

他に Generic NED があり、これは Southbound 接続が telnet/ssh/netconf ではないものに使用されます。telnet/ssh/netconf NED の場合は、ある程度 NSOが管理を行いますが、Generic NEDの場合はすべてを NED 内で管理する必要がありますが、あらゆる Southbound 接続に対応させることが出来ます。例えば、Cisco ACIのAPICへ接続するためには、APICの REST プロトコルを理解する必要がありますが、Generic NED として実装されています。

# Generic NED の実装

## データモデル

サーバの設定には「設定」するものや、不変なものなどがあります。また、サーバ稼働時にデータが変更されるものもあります。

-　設定するもの - config
-　不変なデータ - operational data
-　変動するデータ - live-status (operational data)

これらをモデル化する必要があります。

今回は以下を作成しました。

<details><summary>Config データ用 linux.yang</summary><div>

```yang
$ cat linux/src/yang/linux.yang
module linux {

  namespace "http://com/example/linux";
  prefix linux;

  import tailf-ncs {
    prefix ncs;
  }
  import tailf-common {
    prefix tailf;
  }
  import linux-gen {
    prefix family;
  }

  container dns {
    leaf-list name-servers {
      type string;
    }
    leaf-list search {
      type string;
    }

  }

  leaf hostname {
    type string;
  }
  list packages {
    key name;
    leaf name {
      type string;
    }
    leaf service-state {
      type enumeration {
        enum started;
        enum stopped;
      }
    }
    httpd-param {
      when "../name = 'httpd'";
      list vserver {
        key name;
        leaf name {
          type string;
        }
      }
    }
    container samba-param {
      when "../name = 'samba'";

    }
    tailf:action restart {
      tailf:actionpoint restart;
    }
  }
  container firewall {
    leaf-list open-port {
      type string;
    }
  }
```
</div></details>

<details><summary>Operational データ用 linux-oper.yang</summary><div>

```yang
$ cat linux/src/yang/linux-oper.yang
module linux-oper {
  namespace "http://com/example/linux";
  prefix linux;

  import tailf-ncs {
    prefix ncs;
  }
  import tailf-common {
    prefix tailf;
  }
  import linux-gen {
    prefix family;
  }
  augment /ncs:devices/ncs:device {
    when "derived-from(./ncs:device-type/ncs:generic/ncs:ned-id,'family:linux-gen')";
    container linux-facts {
      config false;
      tailf:cdb-oper {
        tailf:persistent true;
      }
      leaf architecture {
        type string;
      }
      leaf bios_date {
        type string;
      }
      leaf bios_version {
        type string;
      }
      leaf distribution {
        type string;
      }
      leaf distribution_release {
        type string;
      }
      leaf distribution_version {
        type string;
      }
      leaf tz {
        type string;
      }
      list devices {
        key name;
        leaf name {
          type string;
        }
      }
   }
  }
}

```
</div></details>

list packages で、パッケージ情報をリスト化して設定するようにしています。また、各種不変データは、operational データに入っています。

## データの取得

サーバの状態の取得には、Ansible Factsを利用します。具体的には、setup, packages_facts や services_facts モジュールを使用することで、必要な情報が得られます。

主に必要な作業は以下です。
- NEDの show method 内で ansible のコマンドを実行し、json 形式でデータを取得
- 取得したデータを、作成したデバイスモデルへマップ

NED の作成時でも、CDBへのパス操作については、基本的にサービスを作成持と同じです。

### データ取得のための ansible コマンド

通常 ansible を使用する際には、ansible.cfg や inventory ファイルが必要です。しかし、NEDから使用する場合には、その対象が変化することがあり、コマンド実行前にファイルを作成する必要があります。キャッシュファイルのようなものとなり、できれば作成したくありません。
そこで、Adhoc コマンドで取得することとし、以下のようなコードとなりました。
module には、setup, package_facts や service_facts が入ります。

```java
ProcessBuilder pb = new ProcessBuilder(
        "ansible",
        "-i",
        host+",",
        "all",
        "-e",
        "ansible_user="+user+ " ansible_password="+password,
        "-b",
        "-m",
        module
        );
Map<String, String> env = pb.environment();
env.put("ANSIBLE_REMOTE_TMP", "/tmp");
env.put("ANSIBLE_STDOUT_CALLBACK", "json");
env.put("ANSIBLE_LOAD_CALLBACK_PLUGINS", "True");

Process p = pb.start();
```

Linux 上で実際に実行してみたい場合は、以下のように試すことが出来ます。
ANSIBLE_STDOUT_CALLBACK 環境変数によって、出力を json としています。Adhoc コマンドでそれを変更するには、ANSIBLE_LOAD_CALLBACK_PLUGINS 環境変数の設定が必要です。

```
ANSIBLE_REMOTE_TMP=/tmp ANSIBLE_STDOUT_CALLBACK=json ANSIBLE_LOAD_CALLBACK_PLUGINS=True ansible -i 192.168.1.1, all  -e "ansible_user=ansible ansible_password=ansible123" -m setup -b
```

Json のパースには、nanojson ライブラリを使用しました。p.getInputStream() から作成した出力をそのまま使用することが出来るため、非常に扱いが楽です。

```java
JsonObject ob = JsonParser.object().from(jsonOutputSb.toString());
obForHost = ob.getArray("plays")
    .getObject(0)
    .getArray("tasks")
    .getObject(0)
    .getObject("hosts").getObject(host).getObject("ansible_facts");
```

### データ変更のための ansible playbook の生成

例えばパッケージを追加する、といった diff が NSO で作成された場合、それに合わせてplaybook を作成します。
ncs-make-packages コマンドを使用して雛形を作成すると、MOP に合わせた method がそれぞれ存在するようなコードが作成されます。今回はそのまま使用しましたので、NedEditOp.CREATED については結果以下のようなコードとなりました。

AnsiblePlaybook/Task クラスは、今回のために作成した yaml 作成のためのユーティリティクラスです。

```java
    public void create(NedWorker worker, NedEditOp op, AnsiblePlaybook ap, StringBuilder dryRun)
        throws Exception  {

        ConfObject[] kp = getKP(op);
        ConfKey key = (ConfKey)kp[0];
        ConfTag tag = (ConfTag)kp[1];

        if(tag.getTag().equals("packages")){
            // new package is added
            Task yumTask = new Task("yum");
            ap.addTask(yumTask);
            yumTask.addParam("name", key.elementAt(0).toString());
            yumTask.addParam("state", "latest");
        }
```

同様に、NedEditOp.DELETED/NedEditOp.VALUE_SET についても実装します。

# 完成品

作成した Generic NED を使用してみます。NSOへの設定は他のNEDの使用時と変わりません。

```
admin@ncs# show running-config devices device webserver
devices device webserver
 address   192.168.1.1
 port      22
 authgroup webserver
 device-type generic ned-id linux-gen-1.0
 state admin-state unlocked
admin@ncs#
```

## sync-from してみる

Ansible Adhoc コマンドによって得た内容がCDBに入っています。

```
admin@ncs# devices device webserver sync-from
result true
admin@ncs#
admin@ncs# show running-config devices device webserver config services
devices device webserver
 config
  services NetworkManager-dispatcher.service
   status enabled
  !
  services NetworkManager-wait-online.service
   status enabled
  !
  services NetworkManager.service
   status enabled
  !
...
admin@ncs# show running-config devices device webserver config packages
devices device webserver
 config
  packages NetworkManager
  !
  packages NetworkManager-libnm
  !
  packages NetworkManager-team
  !
  packages NetworkManager-tui
...
```

一部不変な値は、Operational データとしました。

```
admin@ncs# show devices device webserver linux-facts
linux-facts architecture x86_64
linux-facts bios_date 12/12/2018
linux-facts bios_version 6.00
linux-facts distribution CentOS
linux-facts distribution_release Core
linux-facts distribution_version 7.7
admin@ncs#
```

## Package をインストールしたり、削除したりしてみる。

NSO が作成する diff に合わせ、適切な ansible 用の playbook を作ります。

```
admin@ncs# conf
Entering configuration mode terminal
admin@ncs(config)# devices device webserver config
admin@ncs(config-config)# packages httpd
admin@ncs(config-packages-httpd)# packages firewalld
admin@ncs(config-packages-firewalld)# firewall open-port [ 80 443 ]
admin@ncs(config-config)# services httpd
admin@ncs(config-services-httpd)# status enabled
admin@ncs(config-services-httpd)# exit
admin@ncs(config-config)# no packages acl
admin@ncs(config-config)# commit dry-run outformat native
native {
    device {
        name webserver
        data - name: Configuration from NSO
               hosts: 192.168.1.1
               tasks:
                 - service:
                     name: httpd
                     state: started
                     enabled: enabled
                 - yum:
                     name: acl
                     state: absent
                 - yum:
                     name: httpd
                     state: latest
                 - firewalld:
                     port: 443
                     permanent: yes
                     state: enabled
                 - firewalld:
                     port: 80
                     permanent: yes
                     state: enabled
    }
}
admin@ncs(config-config)#
```

firewalld は既にインストールされているため、diff が作成されませんでした。そのため、上記結果には含まれていません。NSO の良い部分ですが、Ansible の冪等性の利点は無視していますね（笑

backend では、以下相当のものが動作します。残念ながら、ansible-playbook コマンドを実行するためには、yaml ファイルを作成しなければいけないようで、完全な "ワンライナー" は諦めました。

```
$ ANSIBLE_REMOTE_TMP=/tmp ANSIBLE_STDOUT_CALLBACK=json ANSIBLE_LOAD_CALLBACK_PLUGINS=True ansible-playbook -i 192.168.1.1, -e "ansible_user=root ansible_password=cisco123" test.yaml
```

## ロールバック！

NSO の良いところです。rollback のdiff を作成することが出来ますので、それに合わせた playbook　を自動生成することが出来ます。これは　Ansible単体だと難しいのでは。

```
admin@ncs(config)# rollback configuration
admin@ncs(config)# commit dry-run
cli {
    local-node {
        data  devices {
                  device webserver {
                      config {
             +            packages acl {
             +            }
             -            packages httpd {
             -            }
             -            services httpd {
             -                status enabled;
             -            }
                          firewall {
             -                open-port [ 443 80 ];
                          }
                      }
                  }
              }
    }
}
admin@ncs(config)#
admin@ncs(config)# commit dry-run outformat native
native {
    device {
        name webserver
        data - name: Configuration from NSO
               hosts: 192.168.1.1
               tasks:
                 - service:
                     name: httpd
                     enabled: disabled
                 - yum:
                     name: acl
                     state: latest
                 - yum:
                     name: httpd
                     state: absent
                 - firewalld:
                     port: 443
                     permanent: yes
                     state: disabled
                 - firewalld:
                     port: 80
                     permanent: yes
                     state: disabled
    }
}
admin@ncs(config)#
```
初回変更では、acl パッケージを削除したため、再度インストールしています。

# 最後に

今回の例は、NSO と Ansible の良いとこ取りをしたような使い方かと思います。現時点で Ciscoは Ansible Generic NED を公開していませんので、ユーザ自身で作ってみるのも面白いと思います。

これからは、NSO vs Ansible ではなく、NSO + Ansible でいきましょう！

# 免責事項

本サイトおよび対応するコメントにおいて表明される意見は、投稿者本人の個人的意見であり、シスコの意見ではありません。本サイトの内容は、情報の提供のみを目的として掲載されており、シスコや他の関係者による推奨や表明を目的としたものではありません。各利用者は、本Webサイトへの掲載により、投稿、リンクその他の方法でアップロードした全ての情報の内容に対して全責任を負い、本Web サイトの利用に関するあらゆる責任からシスコを免責することに同意したものとします。