てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible] OSPF 設定後にネイバーが張られるまで待つ(until の活用)

はじめに

Ansible には、指定した条件を満たすまでタスクを繰り返す until というループ機能があります。

until を利用すると、ネットワーク機器に設定を投入した後に、期待した状態になるまで待つ、といった処理を Playbook で実現できます。

この記事では、Cisco IOS の機器に OSPF の設定をして、ネイバーが張られるまで待つ Playbook をご紹介します。

検証環境


■ 想定環境

以下のように、2 台の Cisco IOS のネットワーク機器の環境を想定します。 2台はお互いに GigabitEthernet2 で接続されています。インターフェースの IP アドレスは設定済みですが、OSPF はまだ設定していません。

f:id:akira6592:20200209113500p:plain
2台のCisco IOS でOSPFを設定する


■ 環境の準備

ネイバーが張られるまで待つには、show ip ospf neighbor コマンドの結果を確認します。コマンドの結果を正規表現などでマッチさせる方法では少々危ういので、結果をパースして確認することにします。

パースするために TextFSM というライブラリをインストールします。

  • TextFSM のインストール
pip intall textfsm

また、TextFSM ではパースするために各コマンドごとにテンプレート準備する必要があります。 ありがたいことに、networktocode/ntc-templates というリポジトリで、様々なベンダーのコマンドのテンプレートが作られています。今回は、Cisco IOSshow ip ospf neighbor コマンドの結果をパースしたいので、cisco_ios_show_ip_ospf_neighbor.textfsm を利用します。ダウンロードして templates ディレクトリに配置します。

(もちろんnetworktocode/ntc-templatesリポジトリをまるごとcloneなどしても構いません。今回は上記の構成を前提に説明します。)

ファイル構成

ファイルの構成は以下のようにします。

.
├── configure_ospf.yml # Playbook (後述)
├── group_vars
│   └── ios.yml   # 接続情報を定義(詳細省略)
├── inventory.ini # 2台をグループ ios で定義(詳細省略)
└── templates
    └── cisco_ios_show_ip_ospf_neighbor.textfsm # 先程ダウンロードしたもの


■ サンプル Playbook の作成

処理を定義する Playbook を以下の通り作成します(説明は後述)。

- hosts: ios
  gather_facts: no

  tasks:
    - name: 1. configure OSPF
      ios_config:
        lines:
          - network 10.0.1.0 0.0.0.255 area 0
        parents:
          - router ospf 1

    - name: 2. check neighbor
      ios_command:
        commands:
          - show ip ospf neighbor
      register: result
      vars:
        parsed: "{{ result.stdout[0] | parse_cli_textfsm('./templates/cisco_ios_show_ip_ospf_neighbor.textfsm') }}" 
      until: "'FULL' in (parsed | selectattr('INTERFACE', '==', 'GigabitEthernet2') | list | first ).STATE"
      delay: 10
      retries: 6

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

以下、各タスクについて説明します。

1. configure OSPF

ios_config モジュールを利用して、OSPF のコンフィグを投入します。

具体的には、parentslines オプションの指定により以下のコンフィグを投入します。

router ospf 1
  network 10.0.1.0 0.0.0.255 area 0

2. check neighbor

このタスクが一番のポイントです。ネイバーが張られるまで待ちます。少し複雑ですが、以下の流れで処理します。

(1) show コマンドの実行

ios_command モジュールを利用して、show ip ospf neighbor コマンドを実行し、結果を変数 result に格納します。 この時点での result.stdout[0] の内容は以下の通りです。(ios1 側の例)

Neighbor ID     Pri   State           Dead Time   Address         Interface
2.2.2.2           1   2WAY/DROTHER    00:00:39    10.0.1.2        GigabitEthernet2

(2) コマンド実行結果のパース

vars ディレクティブで、コマンド結果変数 result をパースして、変数 parsed に格納する。パースは、内部で TextFSM を呼び出す parse_cli_textfsm フィルター を利用します。対応するパーステンプレートファイルは、「環境の準備」でダウンロードしたものを指定します。

この時点での parsed の内容は以下の通りです。(ios1 側の例)

[
 {
     "ADDRESS": "10.0.1.2",
     "DEAD_TIME": "00:00:38",
     "INTERFACE": "GigabitEthernet2",
     "NEIGHBOR_ID": "2.2.2.2",
     "PRIORITY": "1",
     "STATE": "2WAY/DROTHER"
  }
]

なお、このタスクの vars ディレクティブで定義した変数 parsed のスコープは、あくまでこのタスクの範囲なのでご注意ください。

参考:

ネットワーク機器のコマンド結果をパースする parse_cli_textfsm フィルタープラグインを試す (Ansible 2.4新機能) - てくなべ (tekunabe)

(3) 繰り返し条件

until で、このタスクの終了条件を指定します。ここでは、パースしたコマンド実行結果である parsed 内の STATEFULL という文字列が含まれていたら終了とします。

今回の場合はネイバーは1つだけなので、以下のように「parsed の最初の要素の STATE」という指定方法でも構いません。

      until: "'FULL' in (parsed.0.STATE)"

ですが、拡張性を持たせるため、INTERFACEGigabitEthernet2 であるという条件にして、対象のネイバーを抽出します。

      until: "'FULL' in (parsed | selectattr('INTERFACE', '==', 'GigabitEthernet2') | list | first ).STATE"

until の動作を決めるリトライ間隔は delay 、リトライ回数を retries で指定します。終了条件を満たすまで(1)に戻って繰り返しこのタスクを実行します。一定回数繰り返しているうちにネイバーが張られ、STATEFULL/DRFULL/BDR などに変わります。そして、ループを終了し、次のタスクに移ります。

3. debug status

デバッグとして、show ip ospf neighbor コマンドの実行結果を表示します。

Playbook の各タスクの説明は以上です。次は Playbook の実行です。


■ Playbook 実行

Playbook を実行します。

$ ansible-playbook -i inventory.ini configure_ospf.yml 
PLAY [ios] *******************************************************************************************************

TASK [1. configure OSPF] *****************************************************************************************
changed: [ios1]
changed: [ios2]

TASK [2. check neighbor] *****************************************************************************************
FAILED - RETRYING: 2. check neighbor (6 retries left).  # 10秒ごとに繰り返しチェック中
FAILED - RETRYING: 2. check neighbor (6 retries left).
FAILED - RETRYING: 2. check neighbor (5 retries left).
FAILED - RETRYING: 2. check neighbor (5 retries left).
FAILED - RETRYING: 2. check neighbor (4 retries left).
FAILED - RETRYING: 2. check neighbor (4 retries left).
FAILED - RETRYING: 2. check neighbor (3 retries left).
FAILED - RETRYING: 2. check neighbor (3 retries left).
ok: [ios1]    # ネイバーが張れた
ok: [ios2]    # ネイバーが張れた

TASK [3. debug status] *******************************************************************************************
ok: [ios2] => {
    "msg": [
        "Neighbor ID     Pri   State           Dead Time   Address         Interface",
        "1.1.1.1           1   FULL/BDR        00:00:38    10.0.1.1        GigabitEthernet2"
    ]
}
ok: [ios1] => {
    "msg": [
        "Neighbor ID     Pri   State           Dead Time   Address         Interface",
        "2.2.2.2           1   FULL/DR         00:00:37    10.0.1.2        GigabitEthernet2"
    ]
}

PLAY RECAP *******************************************************************************************************
ios1                       : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ios2                       : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

ネイバーが張られるまでタスク 2. check neighbor を繰り返し実行し、張られたら次のたタスク3. debug status に進んだことが分かります。

ネイバーがはれないと・・

何らかの問題があり、OSPF のネイバーが張れないと、タスク 2. check neighbor がエラーになって終了します。


■ さいごに

この記事では、Cisco IOS の機器に OSPF の設定をして、ネイバーが張られるまで待つ Playbook をご紹介しました。

このように、show コマンドを実効するモジュールと、until を組み合わせると、期待した状態まで待つ、といったことが Playbook で実現できます。

ネットワーク機器への作業は、OSPF の他にも設定してから待つ必要がある作業もあるので、until は有効ではないでしょうか。

[Ansible] 変数名を変数で指定する

はじめに

vars lookup pluginは、変数名を引数にして変数の値を取得できます。

この性質を利用すると、変数名を変数で指定して、その値を取得できます。

簡単な例でご紹介します。

サンプル Playbook

値を取得したい変数名を、var_name 変数の値で指定します。(以下の場合 var1

1つめのタスクでは、単純に var1 の値を表示します。 2つめのタスクでは、loop で変数名の一部を可変にして、var1, var2, var3 の値を表示します。

- hosts: localhost
  gather_facts: no

  vars:
    var_name: var1  # 変数名を指定する変数
    var1: value1    # 取得対象の変数その1
    var2: value2    # 取得対象の変数その2
    var3: value3    # 取得対象の変数その3
    
  tasks:
    - name: lookup plugin
      debug:
        msg: "{{ lookup('vars', var_name) }}"   # 変数 var1 の指定と等価

    - name: using loop
      debug:
        msg: "{{ lookup('vars', 'var' + (item | string)) }}" # ループを利用して変数 var1, var2, var3 を指定 
      loop:
        - 1
        - 2
        - 3

Playbook 実行

Playbook を実行します。

$ ansible-playbook -i localhost, var.yml 

PLAY [localhost] **************************************************************************************************

TASK [lookup plugin] **********************************************************************************************
ok: [localhost] => {
    "msg": "value1"
}

TASK [using loop] *************************************************************************************************
ok: [localhost] => (item=1) => {
    "msg": "value1"
}
ok: [localhost] => (item=2) => {
    "msg": "value2"
}
ok: [localhost] => (item=3) => {
    "msg": "value3"
}

PLAY RECAP ********************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

参考

When should I use {{ }}? Also, how to interpolate variables or dynamic variable names

docs.ansible.com

別解

How do I access a variable of the first host in a group?

docs.ansible.com

[Ansible] ネットワーク機器のコンフィグ生成ツールとしての Ansible

はじめに

Ansible にはネットワークモジュールが多数用意されていて、ネットワーク機器に接続してコマンドを実行できます。

では、機器に接続できない環境で投入コンフィグを準備するにはどのようにすればよいでしょうか。 それには template モジュールが利用できます。

ネットワーク機器そのもとは直接関係なく汎用的なものなので、テンプレートの書き方次第で Ansible が対応していないネットワーク機器のコンフィグも生成できます。

この記事では、コンフィグテンプレートと、(YAMLではなくあえて)CSVファイルに定義したパラメータを利用して、簡単なコンフィグファイルを生成する方法をご紹介します。


準備するもの

├─ configs               生成コンフィグを出力先ディレクトリ
├─ templates             コンフィグテンプレート格納ディレクトリ
│ └─ config_template.j2  コンフィグテンプレート
├─ params.csv            パラメーターCSVファイル
└─ gen_config.yml        Playbook

インベントリファイルは不要です。

CSV ファイル (params.csv

ホスト名と、VLAN、IPアドレスを定義する CSV ファイルです。1行目はヘッダーです。

hostname,vlan,address,mask
sw101,10,10.0.10.254,255.255.255.0
sw201,20,10.0.20.254,255.255.255.0

コンフィグテンプレート (templates/config_template.j2)

Ansible ではテンプレートエンジンとして Jinja2 を利用しているため Jinja2 形式で書きます。 ホスト名と vlan に IP アドレスを設定するだけの部分的なコンフィグのテンプレートです。

hostname {{ item.hostname }}

interface vlan{{ item.vlan }}
  ip address {{ item.address }} {{ item.mask }}

後述の Playbook の loop で呼び出されるため、具体的なパラメーターは item の中を指定します。

Playbook (gen_config.yml)

Playbook は以下の通りです。read_csv モジュールで、CSV ファイルの内容を読み取って変数化して、template モジュールでコンフィグファイルを生成します。 生成するコンフィグのファイル名には、CSV ファイルの hostname 列の値を利用します。

- hosts: localhost
  gather_facts: no
  connection: local

  tasks:
    - name: read csv
      read_csv:
        path: params.csv   # パラメーターCSVファイル
      register: params

    - name: generate config
      template:
        src: config_template.j2    # コンフィグテンプレートの指定
        dest: "configs/{{ item.hostname }}_config.txt"  # 出力先
      loop: "{{ params.list }}"   # CSVフィアルの行数分をループ(ヘッダー除く)


Playbook 実行

Playbook を実行します。

$ ansible-playbook -i localhost, gen_config.yml 

PLAY [localhost] *****************************************************************************************************

TASK [read csv] ******************************************************************************************************

ok: [localhost]

TASK [generate config] ***********************************************************************************************
changed: [localhost] => (item={'hostname': 'sw101', 'mask': '255.255.255.0', 'vlan': '10', 'address': '10.0.10.254'})
changed: [localhost] => (item={'hostname': 'sw201', 'mask': '255.255.255.0', 'vlan': '20', 'address': '10.0.20.254'})

PLAY RECAP ***********************************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   


確認

生成されたファイルを確認します。

$ cat configs/sw101_config.txt 
hostname sw101

interface vlan10
  ip address 10.0.10.254 255.255.255.0
$ cat configs/sw201_config.txt 
hostname sw201

interface vlan20
  ip address 10.0.20.254 255.255.255.0

無事にホストごとにパラメータが異なるコンフィグが生成されました。


さいごに

ごく簡単な例でしたが、いろいろ応用はできるかと思います。 Excel のシートと VBA などでコンフィグを生成していて、他の方法を探している場合に参考になればと思います。

ケーブルの接続先を追う時に面ファスナー(マジックテープ/ベルクロ)を使うと便利

はじめに

ケーブルを追っていく方法を紹介した以下ツイートの補足です。

写真は自宅の機器で、説明用に適当に取り繕ったものなのでご了承ください。

手で追うと間違えそうになったり、手が離せなくなったりするので。

あと、いわゆるねじりっこは使わないようにします。

途中でケーブルが束ねられていたら・・・

そのままではなかなか通しにくいですね。

束ねられている両端を手で引っ張るなどして、束ねた反対側のケーブルを特定したらそちら側に面ファスナーを移す(または追加で付ける)、のように、合わせ技を使うようかなと思います。

ラックや床や天井を経由したら難しいのでは?

こちらも難しいですね。。

個人的には、一番よく使うケースは電源ケーブルを追うときなので、基本的にはラック内で収まってます。

そもそもタグや管理表で管理されているべきでは?

本来はそうです。ただ現実としては、タグが付いていない場面に遭遇することもあるので、そのような場合は追う必要があります。

あとは、タグや管理票があっても信用できないというか、より慎重を要する場合は、現場の現状の確認したほうが良いと思います。

LAN ケーブルはちゃんとしていても、電源ケーブルはタグがなかったり、どのタップのどの口に接続されているかまで管理されていないこともあるかと思います。

参考

参考なったツイートをご紹介します。

リング通しと呼ぶ

研修やマニュアルで教わる

別のもので代用

知恵

https://twitter.com/tanke25616429/status/1223619432083148802

[Ansible] httpd.conf を編集後にファイルの検証処理を挟む方法

はじめに

Ansible の lineinfilereplace などのファイルを編集するモジュールには validate オプションを備えているものがあります。

validate オプションには編集後のファイルの妥当性を検証するコマンドを指定します。 これにより、編集に問題がなければ処理を継続、問題があれば処理を停止、のようにワンクッション挟めるため安全性を高められます。

公式ドキュメントの lineinfile モジュールの Example には、/etc/sudoers 編集後に実行する validate オプションの例があります。

この記事では、 Apache httpd の設定ファイルである、httpd.conf を validete する Playbook の例をご紹介します。


サンプル Playbook の作成

以下のような簡単な Playbook を利用します。

- hosts: web
  gather_facts: no
  become: yes

  tasks:
    - name: configure Listen Port
      lineinfile:
        path: /etc/httpd/conf/httpd.conf
        regexp: '^Listen 80$'
        line: 'Listen 8080'
        validate: httpd -tf %s    # point
      notify:
        - restart httpd

  handlers:
    - name: restart httpd
      service:
        name: httpd
        state: restarted
        enabled: yes

処理は以下のとおりです。

  • lineinfile モジュールによりhttpd.conf^Listen 80$Listen 8080 に置換
    • 変更なしなら、終了
    • 変更ありなら、次へ
  • httpd -tf コマンドにて validate する
    • 問題ありなら、エラーで終了
    • 問題なしなら、次へ

validate: httpd -tf %s について補足

validate オプションには httpd -tf %s を指定しています。

httpd コマンドの -t オプションによって、httpd.conf に問題がないか検証できます。-f オプションを併用することで検証対象のファイルを明示指定できます。

%s は Ansible 側の仕様によるものです。lineinfile などのモジュールでは、編集対象のファイルを直接編集せず、編集用に一時ファイルを作成します。その一時ファイルのパスが %s に格納されます。

そのため、httpd -t のみの指定の場合、Ansible が編集した一時ファイルを検証の対象にしないでご注意ください。


Playbook の実行

validete が OK なパターンと NG なパターンそれぞれ試します。

OK パターン

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

PLAY [localhost] ****************************************************************************************************

TASK [configure Listen Port] ****************************************************************************************
changed: [localhost]

RUNNING HANDLER [restart httpd] *************************************************************************************
changed: [localhost]

PLAY RECAP **********************************************************************************************************
localhost                  : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

validete に問題がなかったので、処理が継続されました。

NG パターン

Playbook で指定した置換後文字列の

        line: 'Listen 8080'

        line: 'Listen XXX'

にして試します。 httpd.conf の Listen は待受ポート番号を指定する項目のため、文字列が指定されると httpd -tf %s の validate によって Syntax Check になります。

$ ansible-playbook -i  inventory.ini web.yml
PLAY [localhost] ****************************************************************************************************

TASK [configure Listen Port] ****************************************************************************************
fatal: [localhost]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": false, "msg": "failed to validate: rc:1 error:AH00526: Syntax error on line 43 of /tmp/tmpbcDcMD:\nPort must be specified\n"}

PLAY RECAP **********************************************************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   

failed to validate: rc:1 error:AH00526: Syntax error on line 43 of /tmp/tmpbcDcMD:\nPort must be specified\n" というエラーメッセージとともに validate が NG となり、処理がエラーで停止されました。

手動で validate した場合は、以下の実行イメージです。

$ httpd -tf <対象httpd.conf>
AH00526: Syntax error on line 43 of <対象httpd.conf>:
Port must be specified


さいごに

もし validate オプションを指定しなかった場合、httpd.conf に問題があっても、ハンドラー restart httpd が呼ばれてしまい、再起動を試みたところで失敗し、httpd が停止したままになってしまいます。

validate オプションを有効に使うことで、このような自体を未然に防げます。

参考

[Ansible] lookup plugin 名は大文字小文字を意識しておいた方が良い(2.10から区別される)

はじめに

Ansible には lookup plugin という仕組みがあり、ファイルの内容を取得したり、指定したURLの情報を取得したりできます。(一覧はこちら

たとえば、file という lookup plugin であれば、 lookup("file", "ファイル名") のような形式で指定します。

現状、lookup plugin 一覧に掲載されている lookup plugin 名は全て小文字です。Ansible 2.9 ままでは、大文字でも小文字でも関係なく呼び出せるのですが、開発中のバージョンで大文字小文字の区別をするようになりました。Ansible 2.10 に反映される予定で、Ansible 2.10 の Porting Guide にも追記されました。

挙動の確認のために試してみましたので、結果をまとめます。

動作確認環境(比較用に2つ用意)


検証用 Playbook

file lookup plugin を利用する以下の Playbook を利用します。

- hosts: localhost
  gather_facts: no
  connection: local

  tasks:
    - name: 1 all lowercase
      debug:
        msg: '{{ lookup("file", "test.txt") }}'   # すべて小文字(通常)

    - name: 2 start uppercase
      debug:
        msg: '{{ lookup("File", "test.txt") }}'   # 大文字はじめ


Playbook 実行

Ansible 2.9.4 の場合

Ansible 2.9.4 で Playbook を実行します。

$ ansible-playbook -i localhost, lookup.yml

PLAY [localhost] ***********************************************************************************************

TASK [1 all lowercase] *****************************************************************************************
ok: [localhost] => {
    "msg": "Hello, Ansible!"
}

TASK [2 start uppercase] ***************************************************************************************
ok: [localhost] => {
    "msg": "Hello, Ansible!"
}

PLAY RECAP *****************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

file でも File でも正常に file lookup plugin を呼び出せて、ファイルの内容が表示されました。

Ansible 2.10.0.dev0(開発中)の場合

Ansible 2.10.0.dev0(開発中) で同じ Playbook を実行します。

$ ansible-playbook -i localhost, lookup.yml

PLAY [localhost] ***********************************************************************************************

TASK [1 all lowercase] *****************************************************************************************
ok: [localhost] => {
    "msg": "Hello, Ansible!"
}

TASK [2 start uppercase] ***************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "lookup plugin (File) not found"}

PLAY RECAP *****************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

2つめのタスクの、大文字からはじめた File という指定は "lookup plugin (File) not found" というエラーになってしまいました。


さいごに

Ansible 2.10 で、lookup plugin 名の指定は、大文字小文字が区別されるようになることを確認しました。

2.9 などの stable バージョンでは区別されないので、あわてて対応する必要はありませんが、将来に備えて意識しておいたほうがよさそうです。感覚としては正しい仕様になったのかなと思います。

interface description はメタデータになり得るか

description に何をどう書いてる?

ネットワーク機器の物理や論理インターフェースの descripion にどんなことを書いているでしょうか?

  • 自/対向ホスト名
  • 自/対向システム名
  • 大文字?小文字?
  • # は使ってる?
    • 自動化時にプロンプトと誤認知してコケやすい
  • デリミタは - ? _ ?

などなど、いろいろお作法はあるかと思います。

有効活用している例

discription の書き方を工夫して、メタデータとして有効活用している例を目にしたことがありますので、ご紹介します。

みなさんがどうしてるのか気になる

こうなってくると、みなさんがどういう description を書いてるのか気になってきます。ネットワーク機器のコンフィグの中でも自由に書ける数少ない項目なので、いろいろ工夫されているのではないかと思います。

とはいえ、description にはホスト名やシステム名など、固有名詞が書かれることもあり、オープンにしづらい点もあります。

少しこじんまりと共有するために、「JANOGで description BoF(座談会のようなもの)やるのとかどうでしょう」なんて話も聞きました。なにか機会があれば聞いてみたいです。

妄想

JSONのような構造化データの形式で書いたら、活用方法広がるかな?と妄想しました。

interface Loopback99
 description {"from": "rt1", "to": "rt2"}
end