zaki work log

作業ログやら生活ログやらなんやら

[Ansible] ini形式のインベントリファイルの行コメントには注意

本記事は「Ansible Advent Calendar 2022」の5日目のエントリとなります。

ちょっと前にやらかしたインベントリファイルの行中コメントについて、詳しく調べてみた。

結論から言うと、ini形式インベントリファイルのグループ変数定義部分の行コメントは、行の先頭のみしか認識せず、行中の行コメント文字はコメントとしては認識されない。
ちなみに急いでる人は「動作概要」だけ見ればOKです。

動作概要

[server]
host1  #ansible_user=nya   # サーバー1
host2  ansible_user=wan   # サーバー2

[server:vars]
# サーバーの設定
dst_addr=172.29.0.25 # サーバーアドレス
dst_port=25          # 接続先ポート

つい書いてしまいがちなこんな記述をすると、上でいうとdst_addrの値は「172.29.0.25」ではなく「172.29.0.25 # サーバーアドレス」になってしまう。インベントリファイル的には書式エラーではないため、Ansibleを実行しこの変数を参照するタスクが実行され(上の変数の用途的に)接続エラーになるまで問題が発覚しづらい。
# サーバーの設定 と書かれているコメント行はコメントとして有効。

なお、ホストの定義部分であれば、ホスト変数部分である「キー=値」の書式のparse処理が別途あるため、その際にコメント処理される。
前述の内容であれば、host2の「ansible_user=wan」は有効で、「host1ansible_user=nya」は無視される。

インベントリ変数がどう認識されるか確認するには

ansible-inventoryコマンドを使うと簡単。

ansible-inventory -i <インベントリファイル> --list --vars

ini形式インベントリファイルの読み込み実装を調査

ここから本編(?)です。

github.com

ini形式のインベントリファイルの変数部分をparseしているのは以下の_parse_variable_definition()メソッド内。(例によってprintデバッグで発掘)

lib/ansible/plugins/inventory/ini.pyのL.280-L.282

        if '=' in line:
            (k, v) = [e.strip() for e in line.split("=", 1)]
            return (k, self._parse_value(v))

ここの処理で、キーバリューの記述の=splitしているが、特に行末のコメント処理は行っていない。

returnで呼び出している_parse_value()についても、リストや辞書形式の記述をオブジェクト化するのみでコメント処理は行っていない

ではこの_parse_variable_definition()の呼び出し元はというと、同じクラスの_parse()メソッドから。

このメソッドは、以下のようにインベントリファイルの「コメント(;#)で始まる行」を除いた箇所を行ごと処理している。strip()は、行頭と行末のスペースを除いている。

lib/ansible/plugins/inventory/ini.pyのL.162-L.165

            line = line.strip()
            # Skip empty lines and comments
            if not line or line[0] in self._COMMENT_MARKERS:
                continue

で、処理対象の行の定義がhostchildrenvar部分かを判断しながら処理しており、変数の場合の処理で前述の_parse_variable_definition()を呼び出し、変数セットをインスタンスに保持している。

lib/ansible/plugins/inventory/ini.pyのL.220-L.222

            elif state == 'vars':
                (k, v) = self._parse_variable_definition(line)
                self.inventory.set_variable(groupname, k, v)

なお、使用可能なコメント文字は、以下の通り;#の2文字。どちらも同様に動作する。

lib/ansible/plugins/inventory/ini.pyのL.93

    _COMMENT_MARKERS = frozenset((u';', u'#'))

ということで、変数部分のコメント処理は、行頭の場合のみが有効だと実装からも確認できる。

iniファイルを処理するその他の実装

WindowsのGetPrivateProfileString()

WindowsではAPI関数のGetPrivateProfileString()を使ってiniファイルを読み込める。

learn.microsoft.com

作ったアプリはこちら。

github.com

[read]押下でiniから設定値を読み込み、[write]押下でフィールド入力中のテキストをiniへ書き込む、というもの。

このアプリを使って、コメント(;)を行中にいれて設定文字列をiniファイルへ書き込んで、

[read]押下するとこの通り、コメント以降も含めて読み込まれる。
コメント文字を#にしても動作は同じ。

分かりづらかったのでCLI

で、わざわざGUIアプリを作ってみたけど、ぶっちゃけ文章と図で状態の変化を見せるのには向いてないことに気付いた(笑)ので、ソース修正してGUI無しのmain()のみのCLI版も実装。

github.com

動作概要はこの通りで、実行時に引数があればiniへ書き込み、なければiniから読み込む、というもの。

zaki@wensley% ./cli.exe foobar
zaki@wensley% ./cli.exe
ini string: [foobar]
zaki@wensley% cat config.ini
[Section]
config=foobar

行中コメントを書き込んでみると、以下のようにコメント文字以降も設定値として有効な文字列として読み込まれる。

zaki@wensley% ./cli.exe "foobar ; comment"
zaki@wensley% ./cli.exe
ini string: [foobar ; comment]
zaki@wensley% cat config.ini
[Section]
config=foobar ; comment
zaki@wensley%
zaki@wensley% ./cli.exe "foobar # comment"
zaki@wensley% cat config.ini
[Section]
config=foobar # comment
zaki@wensley% ./cli.exe
ini string: [foobar # comment]

やはり作業とログはCLIに限るな。

PythonのConfigParser

Pythonでは標準で使えるini形式の設定ファイルを扱うConfigParserクラスがある。
(Ansibleでもこれ使ってるのかと思ってたけど、自前で実装しているのね)

docs.python.org

このクラスを使ったiniファイル操作も、Windowsと同様にデフォルトでは行中のコメントは認識されない。

import configparser

ini = configparser.ConfigParser()
ini.read('sample.ini')
interpreter = ini.get('defaults', 'interpreter_python')
ssh_config = ini.get('ssh_connection', 'ssh_args')

print(interpreter)
print(ssh_config)

こんなソースに対して、下記のようにansible.cfgっぽい内容のdefaultsセクションのinterpreter_pythonと、ssh_connectionセクションのssh_argsが定義された設定ファイルを読み込むと、これまでの例と同様にコメントを含めて読み込まれる。

(master %>) [zaki@cloud-dev2 configparser]$ cat sample.ini 
[defaults]
stdout_callback = yaml
interpreter_python = /usr/bin/python3 # ver3

[ssh_connection]
ssh_args = -o UserKnownHostsFile=/dev/null ; ssh
(master %>) [zaki@cloud-dev2 configparser]$ 
(master %>) [zaki@cloud-dev2 configparser]$ 
(master %>) [zaki@cloud-dev2 configparser]$ python sample.py 
/usr/bin/python3 # ver3
-o UserKnownHostsFile=/dev/null ; ssh

ただしConfigParserクラスにはinline_comment_prefixesオプションが用意されており、以下のようにコメント文字を指定することで行中のコメントを有効にする(コメントとして処理を無視させる)ことが可能。

configparser.ConfigParser(inline_comment_prefixes=(';', '#'))

インスタンス生成時にオプション指定しておくと、行中コメントを含むiniファイルの扱いは以下の通りで、行中コメントは無視される。

(master %>) [zaki@cloud-dev2 configparser]$ python sample.py 
/usr/bin/python3
-o UserKnownHostsFile=/dev/null

余談だが古いコード(Python2系)でインスタンス生成時にSafeConfigParserを使ってると、3.12で削除される警告が表示されるので要修正。

DeprecationWarning: The SafeConfigParser class has been renamed to ConfigParser in Python 3.2. This alias will be removed in Python 3.12. Use ConfigParser directly instead.

環境

確認に使ったCのソースコードはこちら

github.com

まとめ

ということで、Ansibleのini形式インベントリファイルに限らずiniファイルのコメントは、行頭以外には使わないようにしましょう。