注釈
この記事は、Qiitaの Ansible Advent Calendar 2019 の 9日目の記事です。[1]
今年の PyCon JP で 「Ansibleを通じて「べき等性」を理解してみよう」 というトークをしました。
このトークではAnsibeの冪等性担保のアプローチを yum
モジュールで理解したのですが、
「何であろうと ``yum`` コマンドを実行して、出力結果とリターンコードで判断する」
というものでした。
このときもらった質問+やり取りで、
Q: サーバー内でのファイルコピーとかどうしてるんでしょう?(意訳)
A: ローカルでファイルを保持して、 copy
モジュールとか使います(意訳)
というのがありました。
そこで今回はAdventCalendarに乗じて、
「 yum
ではああだったが、 copy
ではどうだろう? 」
というのを検証しようと思います。
ソースを読んで見る前に、ちょっと挙動をおさらいしてみます。
項目 |
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]
Ansibleモジュールの比較的基本となる原則として、
「 main()
を定義して、 __name__ == '__main__'
で呼ぶ、実行向けの構成を取る」
というのがあります。今回もそれに当てはまるので、まずは main()
関数を眺めてみます。
550 ),
551 add_file_common_args=True,
552 supports_check_mode=True,
553 )
554
555 if module.params.get("thirsty"):
556 module.deprecate(
557 'The alias "thirsty" has been deprecated and will be removed, use "force" instead',
558 version="2.13",
559 )
560
561 src = module.params["src"]
562 b_src = to_bytes(src, errors="surrogate_or_strict")
563 dest = module.params["dest"]
564 # Make sure we always have a directory component for later processing
引数の下処理の少し後 に、
転送予定ファイルのチェックサムを取得して checksum_src
に保存しいるところがあります。
すごくそれっぽいですね。
604 except ValueError:
605 md5sum_src = None
606
607 changed = False
608
609 if checksum and checksum_src != checksum:
610 module.fail_json(
611 msg="Copied file does not match the expected checksum. Transfer failed.",
更に読み進めると 、
転送先予定のパスにすでにファイルがある場合に、そのファイルのチェックサムを 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 group_changed = os.stat(dirpath).st_gid != gid
400 if group_changed is True:
401 changed = group_changed
402 for dir in [os.path.join(dirpath, d) for d in dirnames]:
403 group_changed = os.stat(dir).st_gid != gid
404 if group_changed is True:
405 changed = group_changed
406 for file in [os.path.join(dirpath, f) for f in filenames]:
407 group_changed = os.stat(file).st_gid != gid
408 if group_changed is True:
409 changed = group_changed
関数内にて
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
がやばめ。
といった違いがあります。 用途や原作によってモジュールもずいぶん違うのだなという感想で、 この記事は終了とさせていただきます。
注記
普段はQiitaで書きそうな記事ですが、ちょっと思いつきで自分のブログ行きになりました。
https://github.com/ansible/ansible/blob/v2.9.2/lib/ansible/modules/files/copy.py
2.9で若干修正が入っていますが、そこまで大きなな修正ではないものとします。
デフォルトでは no
となってます
None != None
は False
日報(2019-12-15)
日報(2019-12-07)