Nornir3によるネットワーク自動化 ②設定変更編

# 1. はじめに
前回の記事で、自動化フレームワーク「Nonir」のセットアップ方法、NW機器への接続、showコマンド実行と出力結果のパース等を行いました。

[Nornir3によるネットワーク自動化 ①セットアップ、showコマンド実行編](https://qiita.com/tech_kitara/items/925f1426148a21ffcf7a)

今回はその続きで、同じくCisco IOS/NX-OS機器に対して設定変更、設定保存、事後のConfig反映確認を行ってみました。


# 2. セットアップ
### 2-1. Nornir + Plugins
Netmikoで設定変更と保存を行うために`nornir_netmiko`プラグイン、Jinja2テンプレートからConfigを自動生成するために`nornir_jinja2`プラグインを使います。
前回インストールしていなかった後者を追加インストールします。

```
pip install nornir_jinja2
```

# 3. Inventory / Configファイル
前回と同じものを使います。

# 4. Tasks (Pythonコード) と実行結果
## 4-1. 例1: Configファイルからの設定変更
簡単な例として、以下のNTPサーバ設定ファイルを流し込んでみます。

```text:ntp_config.txt
ntp server 1.1.1.1 prefer
ntp server 2.2.2.2
```

```python:nornir20.py
from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result
from nornir_netmiko.tasks import netmiko_send_config

# 初期化
nr = InitNornir(config_file="config.yaml")

# タスクを実行し、result変数に格納
result = nr.run(
    # 実行結果に任意で名前を付けられる。指定しない場合はタスク名が使用される
    name="configure ntp server",
    # nornir_netmikoプラグイン内の関数を指定
    task=netmiko_send_config,
    # 設定ファイル(str型)
    config_file="ntp_config.txt"
    )

# 実行結果を表示
print_result(result)
```

### 実行結果
2台とも問題なく設定が行われました。複数回実行しても同じ結果だったため、Ansibleのような冪等性を担保する実装はされていないようです。

```
$ python nornir20.py
configure ntp server************************************************************
* iosvl2-0 ** changed : True ***************************************************
vvvv configure ntp server ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.
iosvl2-0(config)#ntp server 1.1.1.1 prefer
iosvl2-0(config)#ntp server 2.2.2.2
iosvl2-0(config)#end
iosvl2-0#
^^^^ END configure ntp server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* nxos-0 ** changed : True *****************************************************
vvvv configure ntp server ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.

nxos-0(config)# ntp server 1.1.1.1 prefer

nxos-0(config)# ntp server 2.2.2.2

nxos-0(config)# end

nxos-0# 
^^^^ END configure ntp server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

## 4-2. 例2: PythonコードによるConfig生成 + 設定変更
続いて、PythonコードでNTPサーバ設定を動的に生成し、設定変更を行ってみます。
タスクを定義する関数`configure_ntp_server`の冒頭で、hostsファイルからリスト形式のNTPサーバアドレスを読み込み、1つ目の要素は`prefer`オプションあり、2つ目以降の要素はオプションなしでConfigに変換しています。
NTPサーバアドレスはhostsファイルではなくdefaultsファイルの`data`キー内に定義していましたが、自動的にhostsファイルの`data`キーの値として認識されるようです。

```python:nornir21.py
from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result
from nornir_netmiko.tasks import netmiko_send_config


def configure_ntp_server(task: Task) -> Result:
    # NTPサーバ設定Configの生成
    ntp_command = []
    num = 0
    for ip in nr.inventory.hosts[task.host.name]["ntp_server"]:
        ntp_command.append(f"ntp server {ip}{' prefer' if not num else ''}")
        num += 1
    
    data = task.run(
        # nornir_netmikoプラグイン内の設定変更用関数を指定
        task=netmiko_send_config,
        # 設定コマンド(list型)
        config_commands=ntp_command
        )

nr = InitNornir(config_file="config.yaml")

# 上記のカスタムタスクを実行し、結果をresultに格納
result = nr.run(
    task=configure_ntp_server
    )

# 実行結果を表示
print_result(result)
```

### 実行結果
例1と同様の結果になっています。

```
$ python nornir21.py
configure_ntp_server************************************************************
* iosvl2-0 ** changed : True ***************************************************
vvvv configure_ntp_server ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- netmiko_send_config ** changed : True ------------------------------------- INFO
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.
iosvl2-0(config)#ntp server 1.1.1.1 prefer
iosvl2-0(config)#ntp server 2.2.2.2
iosvl2-0(config)#end
iosvl2-0#
^^^^ END configure_ntp_server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* nxos-0 ** changed : True *****************************************************
vvvv configure_ntp_server ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- netmiko_send_config ** changed : True ------------------------------------- INFO
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.

nxos-0(config)# ntp server 1.1.1.1 prefer

nxos-0(config)# ntp server 2.2.2.2

nxos-0(config)# end

nxos-0# 
^^^^ END configure_ntp_server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

## 4-3. 例3: Jinja2によるConfig生成 + 設定変更 + 保存 + テスト
最後に、Jinja2でテンプレートファイルとNTPサーバアドレスをレンダリングしてConfigを動的に生成し、設定変更を行ってみます。その後設定保存と、投入したコマンドがshow runの中に含まれているか確認します。

タスクは以下の1～5で構成されています。
(1) NTPサーバ設定Configの生成
(2) NTPサーバ設定変更
(3) 設定保存
(4) 事後のConfig取得
(5) 事後ConfigにNTPコマンドが存在するか確認

(1)は`nornir_jinja2`プラグイン、(2)～(4)は`nornir_netmiko`プラグイン、(5)は自作した`validate_config`を使っています。Pythonそのものを使っているので、カスタムのしやすさも魅力だと思います。

Jinja2テンプレートファイル

```ntp_config.j2
ntp server {{ host["ntp_server"][0] }} prefer
{% for ip in host["ntp_server"][1:] %}
ntp server {{ ip }}
{% endfor %}
```

```python:nornir23.py
from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result
from nornir_netmiko.tasks import netmiko_send_command, netmiko_send_config, netmiko_save_config
from nornir_jinja2.plugins.tasks import template_file


# 事後のConfig内に、指定したコマンドが含まれているか判定
def validate_config(task: Task, intended_list: list, config: str) -> Result:
    result = ""
    for intended in intended_list:
        try:
            assert intended in config
            result += f"Assertion Success: '{intended}' exists in config\n"
        except AssertionError:
            result += f"Assertion Failure: '{intended}' not exists in config\n"
    return Result(host=task.host, result=result, changed=False)


def configure_ntp_server(task: Task) -> Result:
    data1 = task.run(
        # NTPサーバ設定Configの生成
        name="generate ntp config",
        # nornir_jinja2プラグイン内のJinja2テンプレートファイルからの設定変更用関数を指定
        task=template_file,
        # テンプレートファイル(str型)
        template="ntp_config.j2",
        path="./"
        )

    data2 = task.run(
        # NTPサーバ設定変更
        name="configure ntp server",
        # nornir_netmikoプラグイン内の設定変更用関数を指定
        task=netmiko_send_config,
        # 設定コマンド(list型)。1つ目のタスクで生成された改行付き文字列を、リスト形式に変換したもの
        config_commands=data1.result.splitlines()
        )
    
    data3 = task.run(
        # 設定保存
        name="save config",
        # nornir_netmikoプラグイン内の設定保存用関数を指定
        task=netmiko_save_config,
        cmd="copy run start",
        confirm="Destination filename",
        confirm_response="\n"
        )

    data4 = task.run(
        # 事後のConfig取得
        name="run show command",
        task=netmiko_send_command,
        enable=True,
        command_string="show run | inc ntp",
    )
    
    data5 = task.run(
        # 事後ConfigにNTPコマンドが存在するか確認
        name="confirm intended ntp commands exist",
        task=validate_config,
        intended_list=data1.result.splitlines(),
        config=data4.result
    )

nr = InitNornir(config_file="config.yaml")

# 上記のカスタムタスクを実行し、結果をresultに格納
result = nr.run(
    task=configure_ntp_server
    )

# 実行結果を表示
print_result(result)
```

#### 実行結果
タスク1～5ともに問題なく完了し、最後で`Assertion Success`と表示されています。

```
$ python nornir23.py
configure_ntp_server************************************************************
* iosvl2-0 ** changed : True ***************************************************
vvvv configure_ntp_server ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- generate ntp config ** changed : False ------------------------------------ INFO
ntp server 1.1.1.1 prefer
ntp server 2.2.2.2

---- configure ntp server ** changed : True ------------------------------------ INFO
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.
iosvl2-0(config)#ntp server 1.1.1.1 prefer
iosvl2-0(config)#ntp server 2.2.2.2
iosvl2-0(config)#end
iosvl2-0#
---- save config ** changed : True --------------------------------------------- INFO
copy run start
Destination filename [startup-config]? 
Building configuration...

---- run show command ** changed : False --------------------------------------- INFO
ntp server 1.1.1.1 prefer
ntp server 2.2.2.2
---- confirm intended ntp commands exist ** changed : False -------------------- INFO
Assertion Success: 'ntp server 1.1.1.1 prefer' exists in config
Assertion Success: 'ntp server 2.2.2.2' exists in config

^^^^ END configure_ntp_server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* nxos-0 ** changed : True *****************************************************
vvvv configure_ntp_server ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- generate ntp config ** changed : False ------------------------------------ INFO
ntp server 1.1.1.1 prefer
ntp server 2.2.2.2

---- configure ntp server ** changed : True ------------------------------------ INFO
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.

nxos-0(config)# ntp server 1.1.1.1 prefer

nxos-0(config)# ntp server 2.2.2.2

nxos-0(config)# end

nxos-0# 
---- save config ** changed : True --------------------------------------------- INFO
copy run start


---- run show command ** changed : False --------------------------------------- INFO
ntp server 1.1.1.1 prefer
ntp server 2.2.2.2

---- confirm intended ntp commands exist ** changed : False -------------------- INFO
Assertion Success: 'ntp server 1.1.1.1 prefer' exists in config
Assertion Success: 'ntp server 2.2.2.2' exists in config

^^^^ END configure_ntp_server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

# 最後に
他の強力な自動化ツールであるAnsibleでは、冪等性が担保されていたり、独自のJinja2フィルターがあったりするため、同様の機能まで作りこむとなると大変かと思います。ネットワーク作業で、冪等性を気にする必要が無ければ、Nornirも有効なツールかと思います。

# 追加参考URL
- 公式サイト
[GitHub - nornir_jinja2](https://github.com/nornir-automation/nornir_jinja2)