TT Lab
はじめる
学ぶ 学習パス コース

失敗したのに終了コードは 0 だった

終了コードに真実を語らせる

TT Labで続きを見る

目標

シェルの1行の検査を「運用ツール」にします。終了コード0・1・2の約束、標準出力と標準エラー出力の分離、ログのレベル、JSON出力、インポートできるmain()です。

なぜ重要なのか

cronとCIは、画面を読まず、終了コードだけを見ます。診断のメッセージが標準出力に混ざると、パイプラインの次のステップが壊れます。トレースバックは、作った人には情報ですが、午前3時にアラートを受け取った人にはノイズです。このラボのツールは小さいですが、ここで身につけた形が、そのまま数百行の運用ツールの骨格になります。標準ライブラリ(argparse・logging・json・pathlib)だけを使います。

ステップ

  1. /root/pyops/cli/dircheck.pyを作ってください。位置引数path、オプション--min-files(int、既定1)・--max-age-hours(float、既定24)・--json・-v/--verboseを、argparseで定義します。python3 dircheck.py --helpが、終了コード0で終わる必要があります。
  2. ディレクトリの中のファイル数と、最も古いファイルの経過時間(時間)を測って、files=<n> oldest_age_hours=<x> ok=<true|false>の1行を標準出力に出力してください。条件は、ファイル数 ≥ --min-filesであり、最も古いファイルの経過時間 ≤ --max-age-hoursです。
  3. 条件を満たせば終了コード0、違反なら1で終了してください。空のディレクトリはfiles=0で、--min-files 1なら違反です。
  4. パスがないか、ディレクトリでなければ、トレースバックなしで標準エラー出力に1行を書き、終了コード2で終了してください。このとき、標準出力には何も書きません。
  5. loggingを付けてください。既定はINFO、-vならDEBUGです。-vで実行すると、DEBUGを含む行が標準エラー出力にだけ出力され、標準出力には結果の1行だけが残る必要があります。
  6. --jsonなら、標準出力にJSONオブジェクトを1つだけ出力してください。キーはpath・files・oldest_age_hours・okで、okは真偽値、filesは整数です。終了コードの約束は、そのままです。
  7. パスを複数受け取ってください(nargs="+")。ディレクトリごとに結果を1行(JSONなら配列を1つ)出力し、終了コードは最も悪いもの(2 > 1 > 0)で終了します。
  8. main(argv=None) -> int関数に整理して、ファイルの一番下でだけsys.exit(main())を呼んでください。import dircheckだけでは何も実行されず、dircheck.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()が残っていると、インポートの瞬間に実行されます。