終了コードに真実を語らせる
目標
シェルの1行の検査を「運用ツール」にします。終了コード0・1・2の約束、標準出力と標準エラー出力の分離、ログのレベル、JSON出力、インポートできるmain()です。
なぜ重要なのか
cronとCIは、画面を読まず、終了コードだけを見ます。診断のメッセージが標準出力に混ざると、パイプラインの次のステップが壊れます。トレースバックは、作った人には情報ですが、午前3時にアラートを受け取った人にはノイズです。このラボのツールは小さいですが、ここで身につけた形が、そのまま数百行の運用ツールの骨格になります。標準ライブラリ(argparse・logging・json・pathlib)だけを使います。
ステップ
/root/pyops/cli/dircheck.pyを作ってください。位置引数path、オプション--min-files(int、既定1)・--max-age-hours(float、既定24)・--json・-v/--verboseを、argparseで定義します。python3 dircheck.py --helpが、終了コード0で終わる必要があります。- ディレクトリの中のファイル数と、最も古いファイルの経過時間(時間)を測って、
files=<n> oldest_age_hours=<x> ok=<true|false>の1行を標準出力に出力してください。条件は、ファイル数 ≥--min-filesであり、最も古いファイルの経過時間 ≤--max-age-hoursです。 - 条件を満たせば終了コード0、違反なら1で終了してください。空のディレクトリは
files=0で、--min-files 1なら違反です。 - パスがないか、ディレクトリでなければ、トレースバックなしで標準エラー出力に1行を書き、終了コード2で終了してください。このとき、標準出力には何も書きません。
- loggingを付けてください。既定はINFO、
-vならDEBUGです。-vで実行すると、DEBUGを含む行が標準エラー出力にだけ出力され、標準出力には結果の1行だけが残る必要があります。 --jsonなら、標準出力にJSONオブジェクトを1つだけ出力してください。キーはpath・files・oldest_age_hours・okで、okは真偽値、filesは整数です。終了コードの約束は、そのままです。- パスを複数受け取ってください(
nargs="+")。ディレクトリごとに結果を1行(JSONなら配列を1つ)出力し、終了コードは最も悪いもの(2 > 1 > 0)で終了します。 main(argv=None) -> int関数に整理して、ファイルの一番下でだけsys.exit(main())を呼んでください。import dircheckだけでは何も実行されず、dircheck.main(["<디렉터리>"])(プレースホルダーはディレクトリです)が整数を返す必要があります。
参考
- 経過時間は、
time.time() - path.stat().st_mtimeを3600で割った値です。小数第3位までで十分です。 - テスト用のディレクトリは、自分で作ります:
mkdir -p /root/pyops/cli/samples/ok /root/pyops/cli/samples/empty && touch /root/pyops/cli/samples/ok/a.txt - よくあるミスは、診断を
print()で出力して標準出力を汚すこと、例外を捕まえておきながら0で終了すること、parse_args()をmain()の外(モジュールの最上部)で呼ぶことです。
引数の定義と--help
/root/pyops/cli/dircheck.pyを作って、path・--min-files・--max-age-hours・--json・-vをargparseで定義してください。--helpが終了コード0で終わります。
argparse.ArgumentParser()に、add_argumentを5回呼べば済みます。--helpはargparseが自動で作り、終了コード0で終了します。type=int、type=float、action="store_true"を区別してください。
ファイル数と、最も古い経過時間を測る
ディレクトリのファイル数と、最も古いファイルの経過時間(時間)を測って、files=<n> oldest_age_hours=<x> ok=<true|false>の1行を標準出力に出力してください。
Path(path).iterdir()から、is_file()のものだけを数え、経過時間は(time.time() - p.stat().st_mtime) / 3600です。空のディレクトリのmax()には、default=0.0を与えてください。
0と1の約束
条件を満たせば終了コード0、違反(ファイルが足りないか、古すぎる)なら1で終了してください。
main()が整数を返し、sys.exit(main())に渡してください。okがFalseなら1です。空のディレクトリに--min-files 1を付けたなら、違反です。
ツールのエラーは2で、トレースバックはなしで
存在しないパスか、ディレクトリではないパスなら、標準エラー出力に1行を書いて、終了コード2で終了してください。標準出力は空である必要があり、トレースバックが出てはいけません。
Path.is_dir()でまず除外し、stat()が投げるOSErrorを捕まえて、メッセージ1行に変えてください。メッセージは、print(..., file=sys.stderr)かloggingで出力します。
診断は標準エラー出力へ、-vでレベルを調整
logging.basicConfigで、既定はINFO、-vならDEBUGに設定してください。-vで実行すると、DEBUGの行が標準エラー出力にだけ出力され、標準出力には結果の1行だけが残ります。
basicConfigの既定の出力先は、sys.stderrです。formatに%(levelname)sを入れると、DEBUGという語が出力されます。log.debug()を、検査を開始する地点に1つ置いてください。
機械が読む結果はJSONで
--jsonなら、標準出力にJSONオブジェクトを1つだけ出力してください。キーはpath・files・oldest_age_hours・okで、okは真偽値、filesは整数です。終了コードの約束は維持します。
json.dumps(dict)をprintしてください。人が読む1行とJSONを同時に出力すると、パースが壊れます。どちらか一方だけにしてください。
複数のディレクトリ、最も悪いコード
pathをnargs="+"で複数受け取ってください。ディレクトリごとに1行ずつ(JSONなら配列)出力し、終了コードは最も悪いもの(2 > 1 > 0)で終了します。
worst = max(worst, code)を、ディレクトリごとに更新し、存在しないパス1つのために、残りの検査をスキップしないでください(continue)。
インポートしても実行されないツール
main(argv=None) -> intに整理して、ファイルの一番下のif __name__ == "__main__":の中でだけ、sys.exit(main())を呼んでください。import dircheckが何も実行せず、dircheck.main(["<ディレクトリ>"])が整数を返します。
parse_args(argv)にargvを渡せば、sys.argvを読みません。モジュールの最上部にparse_args()やsys.exit()が残っていると、インポートの瞬間に実行されます。