Ansibleのcopyモジュールはどうやって冪等性を担保しているのか¶
この記事は、Qiitaの Ansible Advent Calendar 2019 の 9日目の記事です。1
前書き¶
今年の PyCon JP で 「Ansibleを通じて「べき等性」を理解してみよう」 というトークをしました。
このトークではAnsibeの冪等性担保のアプローチを yum
モジュールで理解したのですが、
「何であろうと ``yum`` コマンドを実行して、出力結果とリターンコードで判断する」
というものでした。
このときもらった質問+やり取りで、
Q: サーバー内でのファイルコピーとかどうしてるんでしょう?(意訳)
A: ローカルでファイルを保持して、
copy
モジュールとか使います(意訳)
というのがありました。
そこで今回はAdventCalendarに乗じて、
「 yum
ではああだったが、 copy
ではどうだろう? 」
というのを検証しようと思います。
copy
と yum
を見比べる¶
ソースを読んで見る前に、ちょっと挙動をおさらいしてみます。
項目 |
yum |
copy |
---|---|---|
何をするか
|
yumコマンドを操作する
|
ローカルからファイルを転送する
|
Changed条件
|
パッケージの有無に変化があった
or
パッケージを更新した
|
ファイルの有無に変化があった
or
ファイルを更新した
|
やってそうなこと
|
* インストール状態の把握
* 必要に応じてyumコマンドの実行
|
* 何かしらでファイルの新旧比較
* 必要に応じてファイルの転送
|
※ yum
については、文字通りPyCon発表以前に想像した「やっそうなこと」で、
実際の動作は前述のスライドのとおりです。
気になるのは copy
モジュールの場合このあたりの動作はどう変わってくるか。
yum
と同じく「とりあえずファイルを転送」というのは考えづらいです。
というのも、
stat
コマンドを使っても、ファイルサイズまでしかわからないcp
コマンドの挙動的に、inodeは変化しない
ファイルの内容にかかわらずChangedは変化する
ファイルの内容変化はわからない
copy
モジュールにはbackup
パラメーターが存在する
となるため、「なにかしらの判定を挟まないと、OK/Changedが判定できない」と考えられそうです。
$ date > test.txt && cp test.txt var && stat var/test.txt
File: var/test.txt
Size: 32 Blocks: 8 IO Block: 4096 regular file
Device: 10305h/66309d Inode: 446998103 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ attakei) Gid: ( 985/ users)
Access: 2019-12-08 23:39:01.628030154 +0900
Modify: 2019-12-08 23:42:18.812931334 +0900
Change: 2019-12-08 23:42:18.812931334 +0900
Birth: 2019-12-08 23:39:01.628030154 +0900
$ date > test.txt && cp test.txt var && stat var/test.txt
File: var/test.txt
Size: 32 Blocks: 8 IO Block: 4096 regular file
Device: 10305h/66309d Inode: 446998103 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ attakei) Gid: ( 985/ users)
Access: 2019-12-08 23:39:01.628030154 +0900
Modify: 2019-12-08 23:42:25.529653015 +0900
Change: 2019-12-08 23:42:25.529653015 +0900
Birth: 2019-12-08 23:39:01.628030154 +0900
$ date >> test.txt && cp test.txt var && stat var/test.txt
File: var/test.txt
Size: 64 Blocks: 8 IO Block: 4096 regular file
Device: 10305h/66309d Inode: 446998103 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ attakei) Gid: ( 985/ users)
Access: 2019-12-08 23:39:01.628030154 +0900
Modify: 2019-12-08 23:42:31.103031998 +0900
Change: 2019-12-08 23:42:31.103031998 +0900
Birth: 2019-12-08 23:39:01.628030154 +0900
$ rm test.txt
$ date > test.txt && cp test.txt var && stat var/test.txt
File: var/test.txt
Size: 32 Blocks: 8 IO Block: 4096 regular file
Device: 10305h/66309d Inode: 446998103 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ attakei) Gid: ( 985/ users)
Access: 2019-12-08 23:42:34.633060910 +0900
Modify: 2019-12-08 23:43:51.577024235 +0900
Change: 2019-12-08 23:43:51.577024235 +0900
Birth: 2019-12-08 23:39:01.628030154 +0900
実際に調べてみる¶
というわけで、ここからはコードリーディングタイムです。
なお、今回はリリースしたばかりの v2.9.2
で挙動を確認してみます。 2 3
まずは main()
から¶
Ansibleモジュールの比較的基本となる原則として、
「 main()
を定義して、 __name__ == '__main__'
で呼ぶ、実行向けの構成を取る」
というのがあります。今回もそれに当てはまるので、まずは main()
関数を眺めてみます。
チェックサムを調べている¶
550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 | checksum_dest = None
if os.path.isfile(src):
checksum_src = module.sha1(src)
else:
checksum_src = None
# Backwards compat only. This will be None in FIPS mode
try:
if os.path.isfile(src):
md5sum_src = module.md5(src)
else:
md5sum_src = None
except ValueError:
md5sum_src = None
|
引数の下処理の少し後 に、
転送予定ファイルのチェックサムを取得して checksum_src
に保存しいるところがあります。
すごくそれっぽいですね。
604 605 606 607 608 609 610 611 | if os.path.exists(b_dest):
if os.path.islink(b_dest) and follow:
b_dest = os.path.realpath(b_dest)
dest = to_native(b_dest, errors='surrogate_or_strict')
if not force:
module.exit_json(msg="file already exists", src=src, dest=dest, changed=False)
if os.access(b_dest, os.R_OK) and os.path.isfile(b_dest):
checksum_dest = module.sha1(dest)
|
更に読み進めると 、
転送先予定のパスにすでにファイルがある場合に、そのファイルのチェックサムを checksum_dest
に保存しています。
だんだん答えが見えてきました。
チェックサムを比較している¶
if checksum_src != checksum_dest or os.path.islink(b_dest):
if not module.check_mode:
# 状況に応じた様々な処理
try:
if backup:
pass
except (IOError, OSError):
module.fail_json(msg="failed to copy: %s to %s" % (src, dest), traceback=traceback.format_exc())
changed = True
else:
changed = False
終盤に入ると 、
checksum_dest
と checksum_src
の状況に応じて、条件分岐するようになっています。
上記の抜粋ではかなり省略してますが、重要なのは else
の方で、
この記述を持って
「両者のチェックサムが一致した場合は、変更を加えずに changed = False
とする」
実装となっているのが見て取れます。
フォルダごとの転送でも頑張る¶
チェックサムの確認は、いずれも os.path.isfile
が True
のときのみ行われます。
src
, dest
どちらもがフォルダの場合は結果が None
になるので、上記の転送処理に入りません。4
ただし、 remote_src=yes
の場合に限り、後続処理が定義されています。5
この場合は、 copy_diff_files()
関数を用いて内部で、ファイルのdiffを取って判定しています。
399 400 401 402 403 404 405 406 407 408 409 | def copy_diff_files(src, dest, module):
changed = False
owner = module.params['owner']
group = module.params['group']
local_follow = module.params['local_follow']
diff_files = filecmp.dircmp(src, dest).diff_files
if len(diff_files):
changed = True
if not module.check_mode:
for item in diff_files:
src_item_path = os.path.join(src, item)
|
関数内にて
filecmp.dircmp(src, dest).diff_files
が実行されて、差分が存在するファイルをリスト化を行います。
そして1ファイルでも存在すれば changed = True
となるように実装されてます。
もちろん差分がなければ changed = False
で何もしません。
結論¶
copy
モジュールは、
ローカルtoリモートでのファイルコピー時には、
sha1
でのチェックサムを比較して上書き要否を判定するリモート上でのファイルコピー時には、
filecmp
モジュールを利用して上書き要否を判定する
という挙動を取って冪等性を担保しているようでした。
まぁ yum
モジュールよりは混みいった実装をしないと難しいようです。
余談¶
ちなみに、複雑度計測ツールの lizard
で2個のモジュールを比較してると、
% lizard
================================================
NLOC CCN token PARAM length location
------------------------------------------------
2 1 12 2 2 __init__@285-286@./ansible-copy-module.py
7 3 87 1 8 clear_facls@293-300@./ansible-copy-module.py
13 4 100 1 16 split_pre_existing_dir@303-318@./ansible-copy-module.py
7 2 66 5 11 adjust_recursive_directory_permissions@321-331@./ansible-copy-module.py
61 37 544 2 63 chown_recursive@334-396@./ansible-copy-module.py
25 8 189 3 26 copy_diff_files@399-424@./ansible-copy-module.py
40 24 403 3 47 copy_left_only@427-473@./ansible-copy-module.py
14 5 133 3 16 copy_common_dirs@476-491@./ansible-copy-module.py
225 95 2067 0 293 main@494-786@./ansible-copy-module.py
4 1 28 2 18 __init__@376-393@./ansible-yum-module.py
18 7 122 2 19 _enablerepos_with_error_checking@395-413@./ansible-yum-module.py
28 8 179 1 41 is_lockfile_pid_valid@415-455@./ansible-yum-module.py
24 8 179 1 28 yum_base@457-484@./ansible-yum-module.py
4 2 43 2 5 po_to_envra@486-490@./ansible-yum-module.py
17 9 144 2 23 is_group_env_installed@492-514@./ansible-yum-module.py
45 23 426 5 55 is_installed@516-570@./ansible-yum-module.py
28 9 238 4 36 is_available@572-607@./ansible-yum-module.py
31 11 276 4 41 is_update@609-649@./ansible-yum-module.py
45 13 386 4 57 what_provides@651-707@./ansible-yum-module.py
18 9 124 2 36 transaction_exists@709-744@./ansible-yum-module.py
17 4 103 2 20 local_envra@746-765@./ansible-yum-module.py
36 15 285 1 40 set_env_proxy@768-807@./ansible-yum-module.py
19 3 104 2 22 pkg_to_dict@809-830@./ansible-yum-module.py
7 4 66 3 7 repolist@832-838@./ansible-yum-module.py
25 20 310 3 32 list_stuff@840-871@./ansible-yum-module.py
27 13 260 5 45 exec_install@873-917@./ansible-yum-module.py
110 41 828 3 172 install@919-1090@./ansible-yum-module.py
44 13 306 3 66 remove@1092-1157@./ansible-yum-module.py
3 1 31 1 4 run_check_update@1159-1162@./ansible-yum-module.py
18 6 195 1 45 parse_check_update@1165-1209@./ansible-yum-module.py
156 48 1129 3 203 latest@1211-1413@./ansible-yum-module.py
99 33 604 2 120 ensure@1415-1534@./ansible-yum-module.py
2 1 6 0 2 has_yum@1537-1538@./ansible-yum-module.py
72 24 507 1 102 run@1540-1641@./ansible-yum-module.py
7 1 48 0 20 main@1644-1663@./ansible-yum-module.py
2 file analyzed.
==============================================================
NLOC Avg.NLOC AvgCCN Avg.token function_cnt file
--------------------------------------------------------------
662 43.8 19.9 400.1 9 ./ansible-copy-module.py
1262 34.8 12.6 266.4 26 ./ansible-yum-module.py
こんな感じになります。意外なことに copy
モジュールのほうが行数が少ないようです。
意外なことに、
copy
のほうが行数が少ないCNN(複雑度)は、
copy
のほうが高いyum
は処理を細かく散らしているに対して、copy
は少数に加えてmain
がやばめ
といった違いがあります。 用途や原作によってモジュールもずいぶん違うのだなという感想で、 この記事は終了とさせていただきます。