てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible] Ansible 2.8 リリース、便利機能や注意点まとめ

f:id:akira6592:20190517162018p:plain:w400

■ はじめに

2019/05/16 に Ansible 2.8.0 がリリースされました。多数のモジュールの追加や、機能追加、バグ修正機能削除機能の非推奨化、仕様変更が含まれています。

大きめなトピックとしては以下のものがあります。

この記事では、CHANGELOGPorting Guide を中心に、気になった点と関連URLをまとめます。 特性が分かるように、主観ですが「便利」「地味に便利」「ちょっと注意」というタグのようなものをつけています。

また、★ 印があるものは、2019/05/31 開催予定の Ansible Night in Osaka 2019.05でもご紹介する予定です。リモート参加枠もありますので、よろしければご参加ください。

[2019/06/01追記] 当日の発表資料です。

www.slideshare.net

特に CHANGELOG は、それでだけ見てもどういったことが分かりにくいものもありますので、関連URLも参考にしていただくと良いかと思います。Ansible 2.8 へのアップデート判断の材料になれば幸いです。

■ 変更

全体

権限昇格

ansible-galaxy

制御 / jinja2 / 変数

インベントリ

環境

ファイル

システム

Windows

ネットワーク


■ 新規プラグイン

コネクションプラグイン

インベントリプラグイン

Lookup


■ 新規モジュール

Cloud

Files

Monitoring

Net Tools

Network

Notification

Source Control

System


■ まとめ

Ansible 2.8.0 、CHANGELOGPorting Guide を中心に、気になった点と関連URLをまとめました。

基本的には便利になりますが、非推奨として残しておいたものの削除や、paramiko の非同梱化など、ちょっとした注意点もありました。

[Ansible] restconf_get モジュールで Cisco IOS XE のインターフェース情報を取得してみる

■ はじめに

Ansible 2.8 では、RESTCONF でネットワーク機器の情報を取得したり、設定を変更したりできる RESTCONF モジュールが導入されます。(本記事執筆時現在 RC段階)

この記事では restconf_get モジュールを利用して、IOS-XE へ RESTCONF でアクセスしてインターフェース情報の取得を試してみます。(後述しますが、そのままではうまくいかなかったので、暫定対処として一部コードの修正をして試しました → Ansible 2.8.2 でバグ解消済み)


■ 環境

  • Cisco IOS-XE (16.8)
    • Cisco DevNet SandBox
      • NETCONF-YANG and RESTCONF Always-On
      • netconf-yangrestconf コマンド有効
  • Ansible 2.8.0rc1
    • 本記事執筆時現在 RC段階のため、pip install ansible==2.8.0rc1 でインストール

■ 準備

Playook 実行に必要なファイルや Playbook を作成していきます。

インベントリファイル

ホスト情報を示すインベントリファイルを作成します。

  • inventory
[ios]
iosao1 ansible_host=ios-xe-mgmt.cisco.com

変数定義ファイル

インベントリファイルで定義したグループ ios が利用する変数を定義するファイルを作成します。

  • group_vars/ios.yml
ansible_connection: httpapi
ansible_httpapi_port: 9443
ansible_network_os: restconf
ansible_user: root
ansible_password: p@ss9999
ansible_httpapi_use_ssl: yes
ansible_httpapi_validate_certs: no

変数の説明

変数が多数登場しますのでそれぞれ説明します。

  • ansible_connection
  • ansible_httpapi_port
    • 利用するポートを指定します。SSH 接続では ansible_portansible_ssh_port 変数でポート番号を指定しますが、ansible_httpapi_port であることにご注意ください。
    • デフォルトは、変数 ansible_httpapi_use_ssl の値によって変わります。yes の場合は 443no の場合は 80 です。
    • ここでは、環境の都合により 9443 を指定しています。
  • ansible_network_os
    • ネットワークOSを指定します。
    • ios_configjunos_config モジュールなどのベンダー個別モジュールを利用する場合はこの変数は iosjunos といった値を指定しますが、RESTCONF モジュールを利用する場合は、このように restconf を利用します。こうすることで、restconf という httpapi プラグインが利用されます。
    • もし、例えば ios を指定した場合は unable to load API plugin for network_os ios というエラーが表示されます。
  • ansible_user
    • ユーザー名を指定します。
  • ansible_password
    • パスワードを指定します。ここではダミーの値を記載しています。必要に応じて ansible-vault で暗号化します。
  • ansible_httpapi_use_ssl
  • ansible_httpapi_validate_certs
    • SSL/TLS 接続する場合に証明書をかどうかを指定します。デフォルトは yes です。
    • ここでは、環境の都合により no を指定しています。

Playbook

処理内容を記載する Playbook を作成します。

  • restconf.yml
- hosts: ios
  gather_facts: no

  tasks:
    - name: restconf test
      restconf_get:
        path: /data/ietf-interfaces:interfaces
      register: result

    - name: debug result
      debug:
        msg: "{{ result }}"

path は Cisco DevNet Learning Labs の「Exploring IOS XE YANG Data Models with RESTCONF」や、こちらのサンプルに出てきたものを利用します。

restconf モジュールでは、root_path としてデフォルトで /restconf が定義されているため、実際には、/restconf 以降を path に指定します。


■ 実行(失敗)

それでは、Playbook を実行します。が、エラーになってしまいます。

$ ansible-playbook -i inventory restconf.yml

(...略...)
fatal: [iosao1]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": false, "code": 406, "msg": "HTTP Error 406: Not Acceptable"}
(...略...)

HTTP Error 406: Not Acceptable とのことなので、HTTP の Acceppt フィールドまわりを調査しました。

原因調査と暫定対処

Ansible ではなく Postman で同様の GET を試すと以下の結果になりました。

  • Accept: application/yang-data+json では正常に json で情報が取得できた
  • Accept: では 406: Not Acceptable になった
  • Accept フィールド自体を指定しない場合は、XML で情報が取得できた

また、lib/ansible/module_utils/network/restconf/restconf.py を見ると、以下のようになっていました。

    accept = None
    if output == 'xml':
        accept = 'application/yang.data+xml'

    connection = Connection(module._socket_path)
    return connection.send_request(None, path=path, method='GET', accept=accept)

どうやら、Accept フィールドが空になってしまっていたようです。

暫定対処として、

    accept = None

の箇所を

    accept = 'application/yang-data+json'

に修正しました。


■ 再実行(成功)

暫定対処したところで、再度同じ Playbook を実行します。

$ ansible-playbook -i inventory restconf.yml 

PLAY [ios] ***********************************************************************************************************************

TASK [restconf test] *************************************************************************************************************
 [WARNING]: Platform darwin on host iosao1 is using the discovered Python interpreter at /usr/bin/python, but future installation
of another Python interpreter could change this. See
https://docs.ansible.com/ansible/2.8/reference_appendices/interpreter_discovery.html for more information.

ok: [iosao1]

TASK [debug result] **************************************************************************************************************
ok: [iosao1] => {
    "msg": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "failed": false, 
        "response": {
            "ietf-interfaces:interfaces": {
                "interface": [
                    {
                        "description": "DON'T TOUCH ME", 
                        "enabled": true, 
                        "ietf-ip:ipv4": {
                            "address": [
                                {
                                    "ip": "10.10.20.48", 
                                    "netmask": "255.255.255.0"
                                }
                            ]
                        }, 
                        "ietf-ip:ipv6": {}, 
                        "name": "GigabitEthernet1", 
                        "type": "iana-if-type:ethernetCsmacd"
                    }, 
                    {
                        "enabled": true, 
                        "ietf-ip:ipv4": {}, 
                        "ietf-ip:ipv6": {}, 
                        "name": "GigabitEthernet2", 
                        "type": "iana-if-type:ethernetCsmacd"
                    }, 
                    {
                        "enabled": false, 
                        "ietf-ip:ipv4": {}, 
                        "ietf-ip:ipv6": {}, 
                        "name": "GigabitEthernet3", 
                        "type": "iana-if-type:ethernetCsmacd"
                    }
                ]
            }
        }, 
        "warnings": [
            "Platform darwin on host iosao1 is using the discovered Python interpreter at /usr/bin/python, but future installation of another Python interpreter could change this. See https://docs.ansible.com/ansible/2.8/reference_appendices/interpreter_discovery.html for more information."
        ]
    }
}

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

こんどは正常にインターフェース情報を取得できました。

※ちらっと表示されている Python interpreter についての WARNING の詳細は こちらをご参照ください。


■ まとめ

Ansible 2.8 で導入される restconf_get モジュールで、Cisco IOS-XE のインターフェース情報を取得してみました。 (一部コード修正が必要だった点は、他の対応方法があるのかなどの点は現時点では不明です。)

RESTCONF が有効であれば、他のベンダーの機器にも使える雰囲気がします。ベンダーごとにモジュールを使い分けなくて良い点は便利そうだと感じました。

また、Cisco IOS-XE では、RESTCONF が使えるようになってきているようなので、今後 RESTCONF は自動化の手段の選択肢になり得るのではないかと思います。

[2019/05/27 追記] 少しに気なる issue を見つけました。

restconf httpapi plugin Media Type · Issue #56680 · ansible/ansible · GitHub

[2019/09/06 追記] Ansible 2.8.2 で修正されていることを確認しました。 ansible/CHANGELOG-v2.8.rst at stable-2.8 · ansible/ansible · GitHub

Fix media type of RESTCONF requests.

[2019/09/09 追記] 設定変更編を投稿しました。

tekunabe.hatenablog.jp

[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() で、改行ごとに分割してリストにできることを確認しました。