てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Batfish] JANOG43.5 で「ネットワークコンフィグ分析ツール Batfish との付き合い方」という発表をしてきました

■ はじめに

2019/04/26 に開催された JANOG43.5 Interim Meeting で「ネットワークコンフィグ分析ツール Batfish との付き合い方」という発表をさせていただきました。

janog.connpass.com

togetter.com

Batfish は、ネットワーク機器のコンフィグのさまざまな分析、検証ができるオープンソースのツールです。たとえば、経路やACL、NTP設定などの妥当性を確認できます。 コンフィグファイルを読み込んで処理するため、実機に接続する必要はありません。本発表では Batfish の概要と、ツール調査の中で見えてきた使いどころなど、Batfish との付き合い方をお伝えします。

JANOG と Batfish という点では、JANOG 43 で「Batfishというconfigテストツールの可能性」という発表がありました。今回私からは、少し違う視点でお話させていただきました。


■ 発表内容

前半で、Batfish はネットワーク機器のコンフィグの さまざまな分析、検証ができるツールであることをお伝えしました。検証のサンプルとして、ルーターのスタティックルートを削除する前後で、どのような到達性差分がでるかを検出数する例を取り上げました。

https://image.slidesharecdn.com/20190426janog43-190426053145/95/batfish-11-638.jpg?cb=1556284514

後半では、学習の仕方、バグと思った時の調べ方、使い所をお伝えしました。 使い所としては「特定の機能を、事前に、網羅的に」検証するという3つの観点でご紹介しました。

こちらのブログの内容からもヒントをいただきました。 www.intentionet.com

資料

発表に使用した資料はこちらです。

www.slideshare.net

togetter は 2019-04-26 15:18:06 から(ツイートありがとうございます!) togetter.com

動画

アーカイブ動画はこちら(1ヶ月程度の公開予定)です。私の発表は 20:15 頃からです。

www.youtube.com

いただいたフィードバック

発表直後の質疑応答の際に、以下のコメントをいただきました。(ありがとうございます!)

L2のコンフィグは試したことがなかったのでありがいコメントでした。

確かに、デフォルトだと各ノードのコンフィグのIFのL3情報をもとにして暗黙的な接続をしてトポロジを作るので、L2が入ると確かにどうなるんだろう、という思いがありました。 試せていませんが、layer1_topology.json という定義で明示的にトポロジを定義できるかも知れません。

tiwtter での反応 (ありがとうございます!)


■ 他の発表

他の方の発表も興味深いものばかりでした。

「RESTで休めない話!」にあった、自動化のユーザーインターフェースとして Google フォームを使っていて、GAS などでさまざまなことをキックする仕組みが印象てきでした。


JANOG 44は神戸

次回 JANOG 44 は、2019/07/24-2 に神戸で開催されます。次回も何かしらのかたちで参加する予定です。

https://www.janog.gr.jp/meeting/janog44/


参考: 私と JANOG

[Batfish] question で利用するノードやインターフェースなどの指定方法

question とは

ネットワークコンフィギュレーション分析ツール「Batfish」の Python ライブラリである pybatfishでは、question という形で、コンフィグに対してさまざまな検証を行います。question は Available questions というページにまとめられています。

公式チュートリアルJupyter Notebookを見ていくと、たとえば以下のような指定があります。

ert = bfq.traceroute(startLocation="as3core1[Loopback0]", headers=headers).answer().frame()

startLocation の指定は、雰囲気では、ノード as3core1 の インターフェース Loopback0 であろう、ということが読み取れますが、実際にどのような指定ができるのかは分かりません。 pybatfish のドキュメント(上記例の場合は、traceroute)を見ても、詳細は記載されていません。

Grammar for rich parameter types

これらの情報は、以下のページにまとめられています。

batfish/Parameters.md at master · batfish/batfish · GitHub

前述の例の startLocation の場合、Location Specifierを見ると、どのような指定ができるのかを確認できます。

参考

tekunabe.hatenablog.jp

[Ansible] 「ポート管理表+パラメータ表+Jinja2テンプレートから、L2SWのConfigを自動生成してみた」を Ansible で

■ はじめに

以下の記事(以下、元記事)を拝見し、Ansible でもできるかなと思って試してみました。

qiita.com

開発中の Ansible 2.8 で導入予定の read_csv モジュールを利用しています。現在(2019/04/02)安定版の Ansible 2.7 系では利用できませんのでご注意ください。read_csv モジュール を利用しない方法もできると思いますが、少し複雑になるでしょう。

  • Ansible devel バージョン (2019/04/02時点)


■ 用意するもの

ポート管理表

元記事と同じものです。

  • port_list_hqaccess1.csv
port_no,speed,duplex,mode,vlan,portfast,status,description
FastEthernet0/1,auto,auto,access,100,o,x,To PC1
FastEthernet0/2,auto,auto,access,100,o,x,To PC2
FastEthernet0/3,auto,auto,access,100,o,x,
FastEthernet0/4,auto,auto,access,100,o,x,
FastEthernet0/5,auto,auto,access,100,o,x,To PC3
FastEthernet0/6,auto,auto,access,100,o,x,To PC4
FastEthernet0/7,auto,auto,access,100,o,x,
FastEthernet0/8,auto,auto,access,100,o,x,
GigabitEthernet0/1,1000,full,access,100,o,x,To hqdist1 Gi0/0/1
GigabitEthernet0/1,1000,full,access,100,o,x,To hqdist2 Gi0/0/1

パラメータ表

元記事と同じものです。

  • parameter_list_hqaccess1.csv
hostname,hardware,secret,username,password,vlan_num,vlan_desc,ip_address,subnet,default_gw,ntp_server
hqdist1,catalyst2960,test,test,cisco,100,<< Server Segment >>,192.168.100.47,255.255.255.0,192.168.100.150,192.168.100.44

Jinja2

ほぼ、元記事と同じものです。

Ansibleで試すと改行が入ってほしいところに入らなかったので、少し調整しました。

  • catalyst2960_template.j2

クリックして展開する

!
no service pad
service timestamps debug datetime localtime
service timestamps log datetime localtime
service password-encryption
!
hostname {{ hostname }}
!
no logging console
enable secret {{ secret }}
!
username {{ username }} privilege 15 password {{ password }}
clock timezone JST 9
ip subnet-zero
no ip domain-lookup
ip domain-name {{ hostname }}
ip ssh version 2
!
spanning-tree mode pvst
no spanning-tree optimize bpdu transmission
spanning-tree extend system-id
!
!
{% for item in interfaces %}
interface {{ item.port_no }}
{% if item.description != '' %}
 description << {{ item.description }} >>
{% endif %}
{% if item.mode == 'access' %}
 switchport access {{ item.vlan }}
 switchport mode access
{% elif item.mode == 'trunk' %}
 switchport mode trunk
{% endif %}
{% if item.duplex != 'auto' %}
 duplex {{ item.duplex }}
{% endif %}
{% if item.speed != 'auto' %}
 speed {{ item.speed }}
{% endif %}
{% if item.status == 'x' %}
 shutdown
{% endif %}
{% if item.portfast == 'o' %}
 spanning-tree portfast
{% endif %}
!
{% endfor %}
!
interface Vlan1
 no ip address
 no ip route-cache
 shutdown
!
interface Vlan{{ vlan_num }}
 description {{ vlan_desc }}
 ip address {{ ip_address }} {{ subnet }}
 no ip route-cache
!
ip default-gateway {{ default_gw }}
no ip http server
no ip http secure-server
!
logging 192.168.100.107
snmp-server community C1sc0 RO
snmp-server host 192.168.100.107 C1sc0 
banner login ^C
============NOTICE==============
| This is test device for demo |
================================
^C
!
line con 0
line vty 0 4
 login local
line vty 5 15
 login local
!
ntp server {{ ntp_server }}
!
crypto key generate rsa modulus 2048
!
end

Playbook

元記事の config_generation.py に相当する Playbook です。

name: adjust..(略)... の2つのタスクは、元記事の Jinja2 テンプレートの変数構造に合わせるための調整です。テンプレート側を変更すれば、これらのタスクは不要になります。特に、adjust parameter variables のほうは、やや乱暴な調整をしているので、テンプレート側で調整したほうが良いかもしれません。

また、今回の要件では、Ansible を実行しているホストのローカルで動けばよいので、対象ホストは localhost、コネクション方式は local を指定しています。

  • build_template.yml
- hosts: localhost
  gather_facts: no
  connection: local
  
  vars:
    TEMPLATE: ./catalyst2960_template.j2
    PARAMETER_LIST: ./parameter_list_hqaccess1.csv
    PORT_LIST: ./port_list_hqaccess1.csv
    CONFIG_FILENAME: ./config_hqaccess1.txt
  
  tasks:
    - name: read interfaces
      read_csv:
        path: "{{ PORT_LIST }}"
      register: interfaces_temp
    
    - name: read params
      read_csv:
        path: "{{ PARAMETER_LIST }}"
      register: params_temp

    - name: adjust interface variable
      set_fact:
        interfaces: "{{ interfaces_temp.list }}"

    - name: adjust parameter variables
      set_fact:
        "{{ item.key }}": "{{ item.value }}"
      loop: "{{ lookup('dict', params_temp.list[0]) }}"

    - name: build template
      template:
        src: "{{ TEMPLATE }}"
        dest: "{{ CONFIG_FILENAME }}"


■ 実行

Playbookを実行します。インベントリファイルは用意せず、-i オプションでは -i localhost, のよううに host_list の形式で指定しています。

$ ansible-playbook -i localhost, build_template.yml

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

TASK [read  interfaces] ***********************************************************************************************************
ok: [localhost]

TASK [read params] ***********************************************************************************************************
ok: [localhost]

TASK [adjust interface variable] ***************************************************************************************
ok: [localhost]

TASK [adjust parameter variables] **************************************************************************************
ok: [localhost] => (item={'key': u'username', 'value': u'test'})
ok: [localhost] => (item={'key': u'subnet', 'value': u'255.255.255.0'})
ok: [localhost] => (item={'key': u'hostname', 'value': u'hqdist1'})
ok: [localhost] => (item={'key': u'secret', 'value': u'test'})
ok: [localhost] => (item={'key': u'default_gw', 'value': u'192.168.100.150'})
ok: [localhost] => (item={'key': u'hardware', 'value': u'catalyst2960'})
ok: [localhost] => (item={'key': u'vlan_num', 'value': u'100'})
ok: [localhost] => (item={'key': u'vlan_desc', 'value': u'<< Server Segment >>'})
ok: [localhost] => (item={'key': u'ntp_server', 'value': u'192.168.100.44'})
ok: [localhost] => (item={'key': u'password', 'value': u'cisco'})
ok: [localhost] => (item={'key': u'ip_address', 'value': u'192.168.100.47'})

TASK [build template] **************************************************************************************************
changed: [localhost]

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

できあがったコンフィグファイルです。

クリックして展開する

!
no service pad
service timestamps debug datetime localtime
service timestamps log datetime localtime
service password-encryption
!
hostname hqdist1
!
no logging console
enable secret test
!
username test privilege 15 password cisco
clock timezone JST 9
ip subnet-zero
no ip domain-lookup
ip domain-name hqdist1
ip ssh version 2
!
spanning-tree mode pvst
no spanning-tree optimize bpdu transmission
spanning-tree extend system-id
!
!
interface FastEthernet0/1
 description << To PC1 >>
 switchport access 100
 switchport mode access
 shutdown
 spanning-tree portfast
!
interface FastEthernet0/2
 description << To PC2 >>
 switchport access 100
 switchport mode access
 shutdown
 spanning-tree portfast
!
interface FastEthernet0/3
 switchport access 100
 switchport mode access
 shutdown
 spanning-tree portfast
!
interface FastEthernet0/4
 switchport access 100
 switchport mode access
 shutdown
 spanning-tree portfast
!
interface FastEthernet0/5
 description << To PC3 >>
 switchport access 100
 switchport mode access
 shutdown
 spanning-tree portfast
!
interface FastEthernet0/6
 description << To PC4 >>
 switchport access 100
 switchport mode access
 shutdown
 spanning-tree portfast
!
interface FastEthernet0/7
 switchport access 100
 switchport mode access
 shutdown
 spanning-tree portfast
!
interface FastEthernet0/8
 switchport access 100
 switchport mode access
 shutdown
 spanning-tree portfast
!
interface GigabitEthernet0/1
 description << To hqdist1 Gi0/0/1 >>
 switchport access 100
 switchport mode access
 duplex full
 speed 1000
 shutdown
 spanning-tree portfast
!
interface GigabitEthernet0/1
 description << To hqdist2 Gi0/0/1 >>
 switchport access 100
 switchport mode access
 duplex full
 speed 1000
 shutdown
 spanning-tree portfast
!
!
interface Vlan1
 no ip address
 no ip route-cache
 shutdown
!
interface Vlan100
 description << Server Segment >>
 ip address 192.168.100.47 255.255.255.0
 no ip route-cache
!
ip default-gateway 192.168.100.150
no ip http server
no ip http secure-server
!
logging 192.168.100.107
snmp-server community C1sc0 RO
snmp-server host 192.168.100.107 C1sc0 
banner login ^C
============NOTICE==============
| This is test device for demo |
================================
^C
!
line con 0
line vty 0 4
 login local
line vty 5 15
 login local
!
ntp server 192.168.100.44
!
crypto key generate rsa modulus 2048
!
end


■ まとめ

Ansible と Jinja2 を利用して、パラメータとなる CSV からコンフィグを生成させることができました。 Ansible が備える変数管理の仕組みを活用すれば、もっと応用ができるようになると思います。

今回のように、Jinja2 内の制御文があまり複雑でない場合や、他の処理を組み合わせなくて良い場合は Ansible でもよさそうです。一方で、もっと準備に複雑な処理を必要とする場合は、元記事のように Python などで書くのが良いと思います。

[Ansible] read_csv モジュールの基本的な使い方(CSVファイルをリストやディクショナリとして読み込む)

■ はじめに

2019年5月リリース予定の Ansible 2.8 では、CSV ファイルをリストやディクショナリとして読み込む read_csv モジュール が導入されます。カンマ区切りのファイルだけでなく、別のデリミタ(;など)を指定することもできます。

この記事では、 read_csv モジュールの公式ドキュメントに記載されている使用例をベースにして、使い方を説明します。

なお、公式ドキュメントの使用例は、Playbook 単位ではなくtask 単位で記載されています。この記事では Playbook 単位で例示します。

似たものとして、csvfile lookup プラグインがあります。csvfile lookup プラグインが「ローカルのCSVファイル」の「指定条件の値を取得」するのに対して、read_csv モジュールは、「リモート(またはローカル)のCSVファイル」の「全体をリストまたはディクショナリとして取得」します。

動作確認環境

目次


■ 単純にCSVファイルを読み込んでリストにする

CSV ファイル

以下のヘッダー付き CSV ファイルを利用します。

name,uid,gid
dag,500,500
jeroen,501,500

Playbook

単純にCSVファイルを読み込んでリストにするシンプルな Playbook です。

- hosts: testsv
  gather_facts: no

  tasks:
    - name: Read users from CSV file and return a list
      read_csv:
        path: users.csv
      register: users
      delegate_to: localhost  # ここを省略するとリモートのファイルを参照

    - name: debug csv
      debug:
        msg: "{{ users }}"
  • read_csv タスク
    • path オプション
      • 読み込む CSV ファイルのパスを指定します。(必須)
    • delegate_to ディレクティブ
      • delegate_to: localhost という指定で、リモートではなくローカル(ansible-playbook コマンド自体を実行しているマシン)上の path を参照します。省略すると、リモートの path を参照します。
  • debug タスク
    • msg オプション
      • リストとして取得した CSV ファイルの内容を単純に全部出力します。

実行ログ

Playbook を実行します。

$  ansible-playbook -i ../inventory csv_simple.yml 

PLAY [testsv] *******************************************************************************************

TASK [Read users from CSV file and return a list] *******************************************************
ok: [testsv]

TASK [debug csv] ****************************************************************************************
ok: [testsv] => {
    "msg": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "dict": {}, 
        "failed": false, 
        "list": [
            {
                "gid": "500", 
                "name": "dag", 
                "uid": "500"
            }, 
            {
                "gid": "500", 
                "name": "jeroen", 
                "uid": "501"
            }
        ]
    }
}

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

debug タスクの出力により、CSV ファイルの内容がリストとして、users.list に格納されていることが分かります。各リスト内の要素は CSV ファイルの1行目のヘッダー gid,name,uid が、キーとなるディクショナリの構造になります。

特定の値を取得する場合、例えば、1番目(0オリジン)の name であれば、"{{ users.list.1.name }}" と指定すすると、jeroen という値を取得できます。

他、SQL の where 句のような条件で抽出するには selectattrを使用します。例えば、gid500 以上の name のリストであれば、

"{{ `users.list | selectattr('gid', '==', '500') | map(attribute='name') | list }}"

と指定すると ["dag", "jeroen"]という値を取得できます。


CSV ファイルを読み込んでディクショナリにする

CSV ファイル

先ほどと同じく、以下のヘッダー付き CSV ファイルを利用します。

name,uid,gid
dag,500,500
jeroen,501,500

Playbook

CSV ファイルを読み込んでディクショナリにする Playbook です。

- hosts: testsv
  gather_facts: no

  tasks:
    - name: Read users from CSV file and return a list
      read_csv:
        path: users.csv
        key: name
      register: users
      delegate_to: localhost  # ここを省略するとリモートのファイルを参照

    - name: debug csv
      debug:
        msg: "{{ users }}"
  • read_csv タスク
    • key オプション
      • ディクショナリとして取得する際のキーを指定します。
  • debug タスク
    • msg オプション
      • ディクショナリとして取得した CSV ファイルの内容を単純に全部出力します。

実行ログ

Playbook を実行します。

$  ansible-playbook -i ../inventory csv_dict.yml 

PLAY [testsv] *******************************************************************************************

TASK [Read users from CSV file and return a list] *******************************************************
ok: [testsv]

TASK [debug csv] ****************************************************************************************
ok: [testsv] => {
    "msg": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "dict": {
            "dag": {
                "gid": "500", 
                "name": "dag", 
                "uid": "500"
            }, 
            "jeroen": {
                "gid": "500", 
                "name": "jeroen", 
                "uid": "501"
            }
        }, 
        "failed": false, 
        "list": []
    }
}

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

debug タスクの出力により、CSV ファイルの内容がディクショナリとして、users.dict に格納されていることが分かります。key オプションで指定した name にあたる値がキーとなる構造になります。

特定の値を取得する場合、例えば namedaguid の場合、"{{ users.dict.dag.uid }}" と指定すると、500 という値を取得できます。


■ ヘッダーのないセミコロン区切りのファイルを読み込んでリストにする

カンマでなく、別のデリミタの場合です。

ファイル

以下のヘッダーのないセミコロン区切りのファイルを利用します。

dag;500;500
jeroen;501;500

Playbook

ヘッダーのないセミコロン区切りのファイルを読み込んでリストにする Playbook です。

- hosts: testsv
  gather_facts: no

  tasks:
    - name: Read users from CSV file and return a list
      read_csv:
        path: users_wo_headers.csv
        fieldnames: name,uid,gid
        delimiter: ';'
      register: users
      delegate_to: localhost  # ここを省略するとリモートのファイルを参照

    - name: debug csv
      debug:
        msg: "{{ users }}"
  • read_csv タスク
    • fieldnames オプション
      • ヘッダーのないファイルの場合にそれぞれのカラムにつけヘッダー名です。
    • delimiter オプション
      • 区切り文字となるデリミタを指定します。今回の読み込みファイルは ; 区切りなので、delimiter: ';' を指定します。
  • debug タスク
    • msg オプション
      • リストとして取得したファイルの内容を単純に全部出力します。

実行ログ

Playbook を実行します。

$  ansible-playbook -i ../inventory csv_3.yml 

PLAY [testsv] *******************************************************************************************

TASK [Read users from CSV file and return a list] *******************************************************
ok: [testsv]

TASK [debug csv] ****************************************************************************************
ok: [testsv] => {
    "msg": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "dict": {}, 
        "failed": false, 
        "list": [
            {
                "gid": "500", 
                "name": "dag", 
                "uid": "500"
            }, 
            {
                "gid": "500", 
                "name": "jeroen", 
                "uid": "501"
            }
        ]
    }
}

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


debug タスクの出力により、ヘッダーのない ; 区切りのファイルの内容がリストとして、users.list に格納されていることが分かります。各リスト内の要素は fieldnames オプションで指定した、gid,name,uid が、キーとなるディクショナリの構造になります。


■ まとめ

公式ドキュメントの使用例をベースにしてread_csv モジュール の使い方を説明しました。

他に「こんなことできるかな?」と気になる事がありましたら、公式ドキュメントで詳細をご確認ください。

docs.ansible.com

また、read_csv モジュールは Files modules に分類されています。Files modules には、他にも、指定した正規表現にマッチするすべての文字列を置換する replace や、行ではなくブロック単位で編集する blockinfile などのモジュールがあります。詳細は Files modules の一覧からご確認ください。

[Ansible] ファイルや変数内の改行ごとに分割してリストにする splitlines() の使い方

■ はじめに

ファイルの内容や変数に改行が含まれている場合、1行ごとにリストにしてループなどの処理をしたい場合があります。そんなときに利用できるのが、 splitlines() です。split() が引数で指定した文字で分割するのに対して、splitlines() は改行で分割します。

Python をご存知の方であれば、str.splitlines()をイメージしていただけるとわかりやすいと思います。

この記事では簡単な例をもとに使い方をご紹介します。

  • 動作確認環境: Ansible 2.3.0, 2.7.8


準備

読み込む利用するテキストファイル

今回は、以下のテキストファイルを利用します。改行コードは LF です。CR/LFでも問題ありません。

  • local.txt
111
222
333

Playbook

以下のような Playbook を利用します。file lookup pluginで、ローカル(リモートではなく)のファイルをの内容を取得し、splitlines() で改行ごとのリストに変換しています。そのリストでループして、1行ごとに debug モジュールで標準出力させています。

- hosts: testsv
  gather_facts: no

  tasks:
    - name: local file debug
      debug: 
        msg: "{{ item }}"
      with_list: "{{ lookup('file', '/vagrant/local.txt').splitlines() }}"
  • 応用 Tips
    • ローカルではなく、リモートのテキストファイルの内容を利用したい場合は slurp モジュールを利用してください。
    • テキストファイルの内容ではなく、変数の値を利用したい場合は with_list: "{{ myvars.splitlines() }}" などに読み替えてください。
    • Ansible 2.5 以降では、with_list の代わりに loop キーワードも利用できます。

実行結果

それでは Playbook を実行します。

$ ansible-playbook -i inventory splitlinestest.yml 

PLAY [testsv] ******************************************************************

TASK [local file debug] ********************************************************
ok: [testsv] => (item=111) => {
    "msg": "111"
}
ok: [testsv] => (item=222) => {
    "msg": "222"
}
ok: [testsv] => (item=333) => {
    "msg": "333"
}

PLAY RECAP *********************************************************************
testsv                     : ok=1    changed=0    unreachable=0    failed=0 

無事に local.txt の1行ごとのループができました。

■ まとめ

splitlines() で、改行ごとに分割してリストにできることを確認しました。

[Ansible] グループAとBの両方に所属しているホストを対象にする方法

■ はじめに

Ansible では、管理対象のホストをグループ化してインベントリファイルに定義できます。Playbook 内にグループを指定すると、指定したグループに所属するホストのみが対象になります。

では「グループAとBに両方に所属しているホスト」を対象にしたい場合はどのようにしたらよいでしょうか。 例えば以下のようなインベントリファイルがある場合、host02host03 を対象にしたい、というケースです。

[group_a]
host01
host02
host03

[group_b]
host02
host03
host04

この記事では、両方に所属しているホストを対象にする2つの方法を、簡単な例とともにご紹介します。

  • 動作確認環境: Ansible 2.3.0, 2.7.9


■ 方法1: play の hosts ディレクティブにグループを and 条件で指定する

Pkabbook内(のplay)の hosts ディレクティブには、複数の対象を and 条件で指定できます。この機能を利用する方法です。

参考: https://docs.ansible.com/ansible/latest/user_guide/intro_patterns.html

  • playbook
- hosts: group_a:&group_b
  gather_facts: no

  tasks:
    - name: debug test
      debug:
        msg: "I am {{ inventory_hostname }}."

上記のように group_a:&group_b と指定することで、group_a に所属、かつ group_b にも所属、という指定になります。

  • 実行例
$ ansible-playbook -i inventory test.yml

PLAY [group_a:&group_b] ****************************************************************

TASK [debug test] **********************************************************************
ok: [host02] => {
    "msg": "I am host02."
}
ok: [host03] => {
    "msg": "I am host03."
}

PLAY RECAP ******************************************************************************
host02                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0
host03                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0

両グループに所属している、host02host03 が対象となりました。

なお、--list-hosts オプションを付けると実処理をせずに対象ホストを確認できます。

$ ansible-playbook -i inventory test.yml --list-hosts

playbook: test.yml

  play #1 (group_a:&group_b): group_a:&group_b  TAGS: []
    pattern: [u'group_a:&group_b']
    hosts (2):
      host02
      host03


■ 方法2: -l オプションと組み合わせる

ansible-playbook コマンドには、対象ホストを絞り込む -l または --limit オプションがあります。この機能と、hosts ディレクティブを併用する方法です。

参考: https://docs.ansible.com/ansible/latest/cli/ansible-playbook.html#cmdoption-ansible-playbook-l

  • playbook
- hosts: group_a
  gather_facts: no

  tasks:
    - name: debug test
      debug:
        msg: "I am {{ inventory_hostname }}."

Playbook としては group_a を対象としています。

  • 実行例
$ ansible-playbook -i inventory test.yml -l group_b

PLAY [group_a] ***************************************************************************

TASK [debug test] ************************************************************************
ok: [host02] => {
    "msg": "I am host02."
}
ok: [host03] => {
    "msg": "I am host03."
}

PLAY RECAP *******************************************************************************
host02                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0
host03                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0

-l group_b で絞ることによって、結果的に両グループに所属している host02host03 が対象となりました。

--list-hosts オプションでも確認してみます。

$ ansible-playbook -i inventory test.yml -l group_b --list-hosts

playbook: test.yml

  play #1 (group_a): group_a    TAGS: []
    pattern: [u'group_a']
    hosts (2):
      host02
      host03


■ 方法3: when で group_names で判断する [2019/10/16 追記]

Play 単位ではなく、when が効く ロールや block 、タスクなどで使える方法です。

- hosts: all
  roles:
    - role: test
      when: "'group_a' in group_names and 'group_b' in group_names"
  tasks:
    - debug:
msg: In group_a and group_b
      when: "'group_a' in group_names and 'group_b' in group_names"

@satoh_fumiyasu さんにアイディアいただきました。ありがとうございます。


■ まとめ

複数のグループに所属しているホストを対象にする方法を2つご紹介しました。 ただ、インベントリとして新たにグループを定義したほうが、メンテナンス性が良いケースもあるかもしれません。必要に応じて使い分けるのが良いと思います。

[Ansible] Playbook(YAML)のための vim のインデント関連設定

■ はじめに

Ansible の Playbook を書いて試すときは、普段 VSCode を使っているため、あまり vim は使っていません。ですが、vim しか使えない環境に置かれたときのために vim で Playbook (YAML) を書くときに便利そうな設定を調べました。

f:id:akira6592:20190321135658g:plain
インデント関連設定をした vim で Playbook をかく

せっかくですのでまとめておきます。


■ .vimrc の中身

今のところ、 .vimrc の中身は以下のようにしています。

set expandtab
set softtabstop=2
set shiftwidth=2
set autoindent

それぞれの設定について簡単に説明します。


set expandtab: Tab キーを押したときに Tab の代わりにスペースを入力する

省略系: set et

通常、Tab キーを押すと tab が入力されます。YAML のインデントには tab ではなくスペースを利用します。 そこで、set expandtab で、Tab キーを押したときに tab の代わりにスペースを入力されうように設定します。


set softtabstop=2: softtab(スペース)の幅をスペース2個にする

省略形: set sts

前述の set expandtab で有効にした Tab キーによるスペース入力の、スペースの数を指定します。 Playbook (YAML)としては、スペースが2個でも4個でも階層的に矛盾がなければ構わないのですが、私はいつもスペース2個なので set softtabstop=2 を設定します。


set shiftwidth=2: インデントレベルの変更時のスペースを2個にする

省略形: set sw=2

>><< などによるインデントレベルの変更時に使うスペースの数です。softtabstop の数と合わせて、set shiftwidth=2を設定します。

f:id:akira6592:20190321135112g:plain
インデントレベルの調整


set autoindent: 自動でインデントする

省略系: set ai

インデントされた状態の行をにカーソルがある状態で改行したときに、次の行で同じインデント位置を保つようにする設定です。

f:id:akira6592:20190321135042g:plain
自動インデント

この設定は少し注意が必要です。手入力時は便利ですが、すでにインデントされたテキストをコピペすると余計なインデントが入ってしまいます。

例えば以下のインデントされたテキストの場合。

- hosts: eos
  gather_facts: no

  tasks:
    - name: test
      eos_command:
        commands:
          - show running-config
      register: result

ペーストすると以下のように余計なインデントが入ってしまいます。これは正しいPlyabookではありません。

- hosts: eos
  gather_facts: no

    tasks:
        - name: test
              eos_command:
                      commands:
                                - show running-config
                                      register: result                                                             

対策は以下のとおりです。(ほかにもあるかもしれません。)

対策1. 自動インデントが不要なときだけ無効(set noautoindent / set noai)に設定する。 対策3. set paste でペーストモードにしてからペーストする。 対策3. そもそも普段は自動インデント無効にしておき、必要な時だけ有効(set autoindent / set ni)に設定する


■ まとめ

vim で Playbook (YAML)を書くときに便利そうな設定をまとめました。YAML では、インデントの数が情報構造に強く関わっているので、特にインデント周りの設定を好みに設定しておくと良いと思いました。

参考

参考にさせていただきました。ありがとうございます。 hatakazu.hatenablog.com

Ansible 公式ドキュメントでは、Ansible vim というプラグインが紹介されています。

docs.ansible.com