てくなべ (tekunabe)

ansible / network / automation / StackStorm

【nornir】Python 製自動化フレームワーク「nornir」かんたんチュートリアル(Ansibleと比較しながら)

■ nornir とは

nornir は、Python 製の新しい自動化フレームワークです。バージョン 1.0.0 は2018年5月にリリースされました。 netmiko や NAPALM も取り込んでいるため、ネットワーク機器にも対応しています。 この記事では、ネットワーク機器を対象としたチュートリアルをご紹介します。

github.com

https://nornir.readthedocs.io/en/stable/index.html

Ansible との比較

Ansible や Salt は構成の定義を YAMLDSLという見方もできると思います)で記述します。 一方 nornir は純粋に Python のライブラリなので、実現したいことを Python でコーディングします。 Ansible で Playbook を書いていて、だんだん複雑になり「コードで書きたいな・・」と思ったときに、代替手段になるかもしれません。

netmiko や NAPALM との比較

ネットワーク機器への自動化といえば、netmikoNAPALMといった、Python ライブラリもあります。 nornir は これらのライブラリの機能に、インベントリや変数管理の仕組みをつけたフレームワークのようなものです。

requirements.txt を確認すると分かるように、nornir をインストールすると、netmiko や NAPALM もインストールされます。


■ インストールは pip install nornir

nornir のインストール手順は以下の通りです。

pip install nornir

必要に応じて virtualenv などを利用して、環境を分けてください。


チュートリアル: Junos の 2台 の show version 結果を表示

インストールができたところで、Juniper Junos の 2台の show version コマンド結果を表示するチュートリアルをはじめます。 説明のために、同じことを Ansible で実現する場合と比較しながら説明します。

インベントリと変数定義の準備

対象ホストを定義するインベントリと、それらに適用される変数定義のファイルを準備します。

nornir の場合

hosts.yaml(今回のコードの場合、ファイル名は固定)で、ホストの名前、所属させるグループ、実際に接続する IP アドレスを定義します。

vsrx1:
  groups:
    - junos
  nornir_host: 172.16.0.1
vsrx2:
  groups:
    - junos
  nornir_host: 172.16.0.2

続いて、groups.yaml(今回のコードの場合、ファイル名は固定)で、グループの名前、と利用する変数(ログイン情報など)を定義します。

junos:
  nornir_username: root
  nornir_password: pasword9999
  nornir_nos: junos

hosts.yaml 内の groups で指定した junos が、groups.yml 内の junos に対応します。

Ansible の場合

Ansible の場合は、以下のようなインベントリファイルと、グループ変数定義ファイルを用意します。

  • inventory.ini
[junos]
vsrx1 ansible_host=172.16.0.1
vsrx2 ansible_host=172.16.0.2
  • group_vars/junos.yaml
---
ansible_network_os: junos
ansible_connection: netconf   # 今回は network_cli でも可
ansible_user: root
ansible_password: pasword9999

実行ファイルの準備

nornir の場合

hosts.yamlgroups.yaml と同じディレクトに、以下の Python スクリプトを作成します。

  • show_version.py
from nornir.core import InitNornir
from nornir.plugins.functions.text import print_result
from nornir.plugins.tasks.networking import napalm_cli

# オブジェクトの初期化
nr = InitNornir()

# 実行
results = nr.run(
    # コマンドを直接実行するタスク napalm_cli を呼び出して、コマンドは show version を指定
    task=napalm_cli, commands=["show version"]  
)

# nornir 用の出力関数で結果を出力
print_result(results)

今回は、何も指定していませんが、InitNorir で初期化時に、パラメータを渡すことによって、ホストファイル名の指定などの設定ができます。詳細は API ドキュメントを参照してください。

Ansible の場合

以下の Playbook を作成します。

  • show_version.yml
- hosts: all
  gather_facts: no

  tasks:
    - name: show version
      junos_command:
        commands:
          - show version
      register: result

    - name: debug version
      debug:
        msg: "{{ result.stdout_lines[0] }}"

実行

nornir の場合

python コマンドで、先ほど作成したスクリプトを実行します。

$ python test.py
napalm_cli**********************************************************************
* vsrx1 ** changed : False *****************************************************
vvvv napalm_cli ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{ u'show version': u'\nHostname: vsrx1\nModel: firefly-perimeter\nJUNOS Software Release [12.1X47-D15.4]\n'}
^^^^ END napalm_cli ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* vsrx2 ** changed : False *****************************************************
vvvv napalm_cli ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{ u'show version': u'\nHostname: vsrx2\nModel: firefly-perimeter\nJUNOS Software Release [12.1X47-D15.4]\n'}
^^^^ END napalm_cli ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

nornir を利用したコードで、無事に show version コマンドの結果が表示されました。

以下のような色になります。 f:id:akira6592:20181016172526p:plain

Ansible の場合

$ ansible-playbook -i inventory.ini show_versoin.yml

PLAY [all] *******************************************************************

TASK [show version] **********************************************************
ok: [vsrx1]
ok: [vsrx2]

TASK [debug version] *********************************************************
ok: [vsrx1] => {
    "msg": [
        "Hostname: vsrx1",
        "Model: firefly-perimeter",
        "JUNOS Software Release [12.1X47-D15.4]"
    ]
}
ok: [vsrx2] => {
    "msg": [
        "Hostname: vsrx2",
        "Model: firefly-perimeter",
        "JUNOS Software Release [12.1X47-D15.4]"
    ]
}

PLAY RECAP *******************************************************************
vsrx1                      : ok=2    changed=0    unreachable=0    failed=0
vsrx2                      : ok=2    changed=0    unreachable=0    failed=0

こちらも無事に show version コマンドの結果が表示されました。


■ まとめ

今回は、簡単な show コマンド実行するだけのサンプルをご紹介しました。 他にも、設定系コマンドや、show コマンドの結果をパースして表示する機能などもあります。

「コードで処理を書きたい。でも Ansible のようなインベントリや変数管理の仕組みは欲しい。」という場合に、nornir が候補になるかもしれません。


■ 参考


Nornir - Python automation framework [Part 16] Network Programmability Stream

「インストラクショナルデザイン―教師のためのルールブック」個人的まとめ

教育資料や発表資料を、分かりやすく、効果的なものにするヒントになるかと思い「インストラクショナルデザイン―教師のためのルールブック」という本を読みました。

インストラクショナルデザイン―教師のためのルールブック

インストラクショナルデザイン―教師のためのルールブック

タイトルに「教師のための」とあるように、確かに読み手が教師であることを前提としている本ですが、そうでなくても、何かを伝える役割の人とにとっては得るものはあると思いました。

簡単ですが、本書で個人的にポイントだと思ったことをまとめます。

  • インストラクションとは、何らかの行動を引き出すための仕掛け。それにはデザインが必要。
  • 知能、技能、遂行に分けて考える
  • 分かりやすく教えるための3ステップ
    • (1) 説明する / 見せる
    • (2) 行動させる /練習させる
    • (3) 習得を確認させる
      • フィードバックすることで、自分で行動を評価できるようにする
      • あとどのくらいが学ぶべきことが残っているかも判斷でき、動機づけになる
  • 正答率が80%くらいの問題をを出し、100%になったら、少しレベルを上げて標的行動に近づけていく
    • スモールステップ
    • 「強化」の機会を増やせる
  • 自分の行動が変化していくことを学び手に分かるようにフィードバックしていくデザインが有効
    • 楽しさ
  • 分かったかどうか質問するのではなく、概念の例と例外を区別するような質問をする

ネットワーク自動化 Python ライブラリ NAPALM 2.3.3 リリース、公式 Docker イメージ追加など

■ NAPALM 2.3.3 リリース

github.com

2018/10/15 にネットワーク自動化 Python ライブラリである NAPALM 2.3.3がリリースされました。詳細は 2.3.3 CHNGELOG に記載があります。

この記事では、個人的に気になった Docker イメージの追加と、モックの利用法のドキュメント追加についてピックアップします。


■Docker イメージの追加

Dockerhub の napalmautomation/napalmに 「Official NAPALM Docker image」としてあがっています。 Dockerfile はこちらで、Debian ベースのようです。

さっそく、Junos を利用して簡単な動作確認をしてみます。

docker run/bin/bash を起動

$ sudo docker run -i -t napalmautomation/napalm /bin/bash
Unable to find image 'napalmautomation/napalm:latest' locallyTrying to pull repository docker.io/napalmautomation/napalm ...
latest: Pulling from docker.io/napalmautomation/napalm05d1a5232b46: Pull completed55900eb92a3: Pull completea30024124f3c: Pull complete
8743e06e9fcd: Pull completeDigest: sha256:ddf01b2ee4d2af5edbc3be4906e1f059410b8b776b519fe08d3230cad0ca7a0a] 37.02Status: Downloaded newer image for docker.io/napalmautomation/napalm:latest
root@58839899c7fd:/#  

python インタプリターを起動して、対象機器にログインセッションをオープン

root@58839899c7fd:/# python

>>> from pprint import pprint  # ディクショナリを見やすくするため
>>> from napalm import get_network_driver
>>> driver = get_network_driver('junos')
>>> device = driver('172.16.0.1', 'testuser', 'testpass')
>>> device.open()

デフォルトルートの情報を表示

>>> pprint(device.get_route_to('0.0.0.0'))
{u'0.0.0.0/0': [{'age': 507,
                 'current_active': True,
                 'inactive_reason': u'',
                 'last_active': True,
                 'next_hop': u'10.0.2.2',
                 'outgoing_interface': u'ge-0/0/0.0',
                 'preference': 12,
                 'protocol': u'Access-internal',
                 u'protocol_attributes': {},
                 'routing_table': u'inet.0',
                 'selected_next_hop': True}]}

show version コマンドの結果を表示

>>> pprint(device.cli(['show version']))
{u'show version': u'\nHostname: vsrx1\nModel: firefly-perimeter\nJUNOS Software Release [12.1X47-D15.4]\n'}

セッションをクローズ

>>> device.close()


docker run コマンド経由で napalm コマンドを実行する

今度は、 bash を起動せずに、docker run 経由で napalm というコマンドラインツールを使ってデフォルトルートの情報を表示します。 (ホスト側に NAPALM はインストール不要)

$ sudo docker run -i -t napalmautomation/napalm napalm --user testuser--password testpass--vendor junos 172.16.0.1 call get_route_to --method-kwargs "destination='0.0.0.0'"
{
    "0.0.0.0/0": [
        {
            "protocol": "Access-internal",
            "last_active": true,
            "outgoing_interface": "ge-0/0/0.0",
            "current_active": true,
            "routing_table": "inet.0",
            "next_hop": "10.0.2.2",
            "selected_next_hop": true,
            "preference": 12,
            "inactive_reason": "",
            "age": 2210,
            "protocol_attributes": {}
        }
    ]
}

簡単ですが動作確認できました。


■ Napalm mock driver を利用したチュートリアルの追加

単体テストをする際のモックの使い方に関するチュートリアルページが追加されました。 https://napalm.readthedocs.io/en/latest/tutorials/mock_driver.html

このモックを利用すると、ネットワーク機器を用意せずに単体テストができるようです。

通常であれば、iosjunos のようにネットワークOS名を指定する箇所に mock を指定して利用します。

import napalm
driver = napalm.get_network_driver('mock')


■ その他

他にも、バグフィックスなどがされているようですので、詳細は以下の CHENGELOG を参照してください。 github.com

#ssmjp 2018/10 で「Ansibleではじめるサーバー・ネットワークの自動化 (Ansible2.7情報つき)」という発表をしました

■ Ansibleではじめるサーバー・ネットワークの自動化 (Ansible2.7情報つき)

2018/10/12 開催の #ssmjp 2018/10 で「Ansibleではじめるサーバー・ネットワークの自動化 (Ansible2.7情報つき)」という発表をさせていだきました。

www.slideshare.net

別のイベントで発表した資料をベースにして、10/04 にリリースされた Ansible 2.7 のアップデート情報を追加しました。

https://image.slidesharecdn.com/201810ssmjpansible-181012101437/95/ansible-ansible2720181012-17-638.jpg?cb=1539422741

3 と 4 については、CHANGELOG にも掲載されていないので、別途ブログにもまとめておきました。

また、 Web サーバーのインストールとコンテンツのデプロイでは、Playbook を作成するところから、動作後の確認までをデモしました。 ちゃんと練習したおかげで、どうにか一発OKでした。


■ 全員 Ansible

今回の #ssmjp は「Ansible を語る回」なので、登壇者全員が Ansible についての発表でした。

AnsibleではじめるNW設定の自動化について - Cisco(VIRL)編 - (すがいさん)

www.slideshare.net

ネットワーク構成を変数で表現してなるべく同じPlaybookを再利用するのが興味深かったです。 あと、VIRL(自由度の高いネットワーク機器のシミュレーターソフト)欲しくなりました。 そして、資料内で私の記事をたくさん紹介していただきありがとうございました。残しておいてよかったです。

残念な現場にAnsibleを適応してみた過去と現在と未来 (@togakushiさん)

(資料の一般公開は無し)

大変苦労された貴重なお話を伺えました。


■ まとめ的リンク


■ さいごに

今回の登壇のお話は、実は4月に頂いていました。ssmjp は 基本的に毎月開催されている老舗イベントで予定がいっぱいという状態だったので、ようやく開催というかたちになりました。継続できるのは素晴らしいですね。 この度はこのような機会を頂きまことにありがとうございました。 参加していただいた皆まもありがとうございました!

ssmjp.connpass.com

ssm.pkan.org

【Ansible】データから「select * from users where name="hoge"」的な抽出ができる selectattr

SQL の where 句のように ディクショナリのリストから抽出するには selectattr を利用

以下のようなディクショナリのリストがあるとします。DBにおけるテーブルのデータのような構造です。

users:
  - name: yamada
    age: 42
  - name: tanaka
    age: 26
  - name: suzuki
    age: 32
  - name: sato
    age: 27

ここで、「 namesato であるディクショナリのリスト」を抽出したい場合は、 selectattr を利用して以下のようにフィルター(Jinja2の機能)します。

  • selectattr によるフィルタ
- debug:
    msg: "{{ users | selectattr('name', '==', 'sato') | list }}"
  • 結果
    "msg": [
        {
            "age": 27,
            "name": "sato"
        }
    ]

無地に1件が抽出されました。 SQL でいうと select * from users where name='sato' のようなイメージです。


■ 応用例: 部分一致、以上、以下など指定もできる

先ほど使用した == (または `qeualtoeq)では完全一致でしたが、他にもあります。

!= または ne: 〜ではない

- debug:
    msg: "{{ users | selectattr('name', '!=', 'sato') | list }}"
    "msg": [
        {
            "age": 42,
            "name": "yamada"
        },
        {
            "age": 26,
            "name": "tanaka"
        },
        {
            "age": 32,
            "name": "suzuki"
        }
    ]

in: 〜に含まれる

- debug:
    msg: "{{ users | selectattr('name', 'in', 'sssssatoooo') | list }}"
    "msg": [
        {
            "age": 27,
            "name": "sato"
        }
    ]

> または gt: 〜より大きい

- debug:
    msg: "{{ users | selectattr('age', '>', 27) | list }}"
    "msg": [
        {
            "age": 42,
            "name": "yamada"
        },
        {
            "age": 32,
            "name": "suzuki"
        }
    ]

>= または ge: 〜以上

- debug:
    msg: "{{ users | selectattr('age', '>=', 27) | list }}"
    "msg": [
        {
            "age": 42,
            "name": "yamada"
        },
        {
            "age": 32,
            "name": "suzuki"
        },
        {
            "age": 27,
            "name": "sato"
        }
    ]

< または lt: 〜以下

- debug:
    msg: "{{ users | selectattr('age', '<', 27) | list }}"
    "msg": [
        {
            "age": 26,
            "name": "tanaka"
        }
    ]

<= または le: 〜以下

- debug:
    msg: "{{ users | selectattr('age', '<=', 27) | list }}"
    "msg": [
        {
            "age": 26,
            "name": "tanaka"
        },
        {
            "age": 27,
            "name": "sato"
        }
    ]


■ 補足: ループと when の組み合わせよりとの比較

selectattr のように抽出してから何かを処理する方法の他にも、全件ループと when によって処理条件を指定する方法があります。 条件に会わなかった分のタスクは skipping になります。

  • Playbook: ループと when の組み合わせる場合
- debug:
    msg: "{{ item }}"
  when:
    - item.age <= 27
  loop: "{{ users }}"
  • 結果
skipping: [localhost] => (item={u'age': 42, u'name': u'yamada'})
ok: [localhost] => (item={u'age': 26, u'name': u'tanaka'}) => {
    "msg": {
        "age": 26,
        "name": "tanaka"
    }
}
skipping: [localhost] => (item={u'age': 32, u'name': u'suzuki'})
ok: [localhost] => (item={u'age': 27, u'name': u'sato'}) => {
    "msg": {
        "age": 27,
        "name": "sato"
    }
}

先に selectattr でフィルタしたほうがログがスッキリして、処理速度も早いかもしれません。

【Ansible】pip モジュールでバージョン指定しながら複数のパッケージを指定できるようになった(Ansible 2.7新機能)

■ Ansible 2.7 で pip モジュールの name オプションで、バージョン指定しながら複数のパッケージを指定できるように

Ansible の pip モジュールでは、これまで name オプションの値をリストで指定すると、pip install pkgA pkgB pkgC のように、複数のパッケージを扱うことができました。しかし、pkgA==1.0.0 のようなバージョン指定と、リストによる複数の指定は併用できませんでした。 Ansible 2.7 では、バージョン指定しながら複数のパッケージを指定できるようになりました。

Playbook

- name: pip test
  pip:
    name:
      - netmiko>=2.2.0
      - ncclient==0.6.0
      - requests

コマンドイメージ

pip install netmiko>=2.2.0 ncclient==0.6.0 requests

この機能については、CHANGELOGには掲載されていませんが、pip モジュールのドキュメントname のオプションの説明として、以下のように記載されています。

This can be a list (since 2.2) and contain version specifiers (since 2.7).

これに伴って、例も2つ追加されています。

# Install (bottle) python package with version specifiers
- pip:
    name: bottle>0.10,<0.20,!=0.11

# Install multi python packages with version specifiers
- pip:
    name:
      - django>1.11.0,<1.12.0
      - bottle>0.10,<0.20,!=0.11



■(補足)Ansible 2.6 までの場合

比較のために、Ansible 2.6 までの状況について説明します。(2.7でも引き続き利用できる方法です)

requirements で代替可能

必要なパッケージ名と(必要であれば)バージョンを定義したリストファイルを用意して、requirements オプションで指定することでも代替可能です。

Playbook

 - name: pip test
    pip:
      requirements: /vagrant/test/requirements.txt

requirements.txt

netmiko>=2.2.0
ncclient==0.6.0
requests

コマンドイメージ

 pip install -r requirements.txt

パッケージ名だけの複数指定はOK

バージョンの指定をしなければ、リストによるパッケージ名だけの複数指定はできます。

Playbook

- name: pip test
  pip:
    name:
      - netmiko
      - ncclient
      - request

コマンドイメージ

pip intall netmiko ncclient request

1つパッケージ名だけならバージョン指定OK

パッケージ名の指定が1つだけなら、バージョン指定ができます。

Playbook

- name: pip test
  pip:
    name: netmiko
    version: 2.2.0

コマンドイメージ

pip intall netmiko==2.0.0

ですが、バージョン決め打ちの指定しかできず、 version: <= 2.2.0 といった指定はできません。

ループで回せばバージョン指定しながら複数のパッケージを指定(制限あり)

今回 Ansible 2.7 でできるようになったことの近いことは、with_listloop によるループでもできます。ただし、以下のような制限があります。

  • 「1つパッケージ名だけならバージョン指定OK」で説明した事情と同じく、<<= などの不等号を含むバージョン指定はきない
  • バージョン指定をしない場合は | default(omit) のような工夫が必要
  • ループするのでやや遅い

Playbook

- name: pip test
  pip:
    name: "{{ item.name }}"
    version: "{{ item.version | default(omit) }}"
  loop:
    - { name: "netmiko", version: "2.2.2" }
    - { name: "ncclient", version: "0.6.0" }
    - { name: "requests" }

コマンドイメージ(pip コマンド自体が複数)

pip intall netmiko==2.2.0
pip install ncclient==0.6.0
pip install requests

おわりに

地味なアップデートですが、これがやりたかった!というからもいらっしゃるのではないでしょうか。 このように、Ansible のアップデートは、CHANGELOG にな掲載されないものもあります。 普段お使いのモジュールの説明ドキュメントを見ると、なにか新しい発見があるかもしれません。

モジュール一覧はこちら

「エンジニアの知的生産術」個人的まとめ

「エンジニアの知的生産術」という本を読みました。 各章ごとに自分がポイントだと思ったことをかなり雑ですが一言にしてまとめます。

第1章:新しいことを学ぶには

知りたいことから始める。

第2章:やる気を出すには

小さなタスク1つに絞る。

第3章:記憶を鍛えるには

期間をあけて、繰り返し、テストする。

第4章:効率的に読むには

読み返す。

第5章:考えをまとめるには

とにかく書き出すことから始める。

第6章:アイデアを思い付くには

言語化は質問で促す。

第7章:何を学ぶかを決めるには

悩むより手をつける。