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 checksum_dest = None
551
552 if os.path.isfile(src):
553 checksum_src = module.sha1(src)
554 else:
555 checksum_src = None
556
557 # Backwards compat only. This will be None in FIPS mode
558 try:
559 if os.path.isfile(src):
560 md5sum_src = module.md5(src)
561 else:
562 md5sum_src = None
563 except ValueError:
564 md5sum_src = None
引数の下処理の少し後 に、
転送予定ファイルのチェックサムを取得して checksum_src
に保存しいるところがあります。
すごくそれっぽいですね。
604 if os.path.exists(b_dest):
605 if os.path.islink(b_dest) and follow:
606 b_dest = os.path.realpath(b_dest)
607 dest = to_native(b_dest, errors='surrogate_or_strict')
608 if not force:
609 module.exit_json(msg="file already exists", src=src, dest=dest, changed=False)
610 if os.access(b_dest, os.R_OK) and os.path.isfile(b_dest):
611 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を取って判定しています。
399def copy_diff_files(src, dest, module):
400 changed = False
401 owner = module.params['owner']
402 group = module.params['group']
403 local_follow = module.params['local_follow']
404 diff_files = filecmp.dircmp(src, dest).diff_files
405 if len(diff_files):
406 changed = True
407 if not module.check_mode:
408 for item in diff_files:
409 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
がやばめ
といった違いがあります。 用途や原作によってモジュールもずいぶん違うのだなという感想で、 この記事は終了とさせていただきます。
※脚注