让退出码说真话
目标
把 shell 里一行的检查做成“运维工具”——退出码 0、1、2 的约定,标准输出与标准错误的分离,日志级别,JSON 输出,可导入的 main()。
为什么重要
cron 和 CI 不读屏幕,只看退出码。如果诊断信息混入标准输出,流水线的下一个步骤就会出错。traceback 对创建者来说是信息,对凌晨收到告警的人来说却是噪音。本实验的工具虽小,但在这里学到的形态,会原样成为几百行运维工具的骨架。只使用标准库(argparse、logging、json、pathlib)。
步骤
- 创建
/root/pyops/cli/dircheck.py。用 argparse 定义位置参数path,以及选项--min-files(int,默认 1)、--max-age-hours(float,默认 24)、--json、-v/--verbose。python3 dircheck.py --help必须以退出码 0 结束。 - 测量目录中的文件数和最旧文件的年龄(小时),向标准输出输出一行
files=<n> oldest_age_hours=<x> ok=<true|false>(占位符依次为文件数、年龄、是否通过)。条件是文件数 ≥--min-files,并且最旧文件的年龄 ≤--max-age-hours。 - 满足条件时以退出码 0 结束,违反时以 1 结束。空目录是
files=0,在--min-files 1的情况下就属于违反。 - 如果路径不存在或不是目录,不要输出 traceback,而是往标准错误写一行,并以退出码 2 结束。这时不要往标准输出写任何内容。
- 加上 logging。默认为 INFO,有
-v时为 DEBUG。用-v运行时,包含DEBUG的行只应出现在标准错误,标准输出只应留下一行结果。 - 有
--json时,只向标准输出输出一个 JSON 对象。键是path、files、oldest_age_hours、ok,ok是布尔值,files是整数。退出码约定保持不变。 - 接收多个路径(
nargs="+")。对每个目录输出一行结果(有 JSON 时输出一个数组),退出码以最坏的那个(2 > 1 > 0)结束。 - 整理成
main(argv=None) -> int函数,只在文件最底部调用sys.exit(main())。仅import dircheck不应该执行任何东西,并且dircheck.main(["<디렉터리>"])(占位符为目录)必须返回整数。
参考
- 年龄是
time.time() - path.stat().st_mtime除以 3600 的值。精确到小数点后第三位就足够了。 - 测试用的目录自己创建:
mkdir -p /root/pyops/cli/samples/ok /root/pyops/cli/samples/empty && touch /root/pyops/cli/samples/ok/a.txt - 常见错误:用
print()打印诊断信息而弄脏标准输出,捕获了异常却以 0 结束,在main()之外(模块最顶层)调用parse_args()。
参数定义与 --help
创建 /root/pyops/cli/dircheck.py,用 argparse 定义 path、--min-files、--max-age-hours、--json、-v。--help 以退出码 0 结束。
对 argparse.ArgumentParser() 调用五次 add_argument 就行。--help 由 argparse 自动生成,并以退出码 0 结束。请区分 type=int、type=float、action="store_true"。
测量文件数和最旧的年龄
测量目录中的文件数和最旧文件的年龄(小时),向标准输出输出一行 files=<n> oldest_age_hours=<x> ok=<true|false>(占位符依次为文件数、年龄、是否通过)。
对 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,而且不输出 traceback
如果路径不存在或不是目录,就往标准错误写一行,并以退出码 2 结束。标准输出必须为空,也不能出现 traceback。
先用 Path.is_dir() 过滤,再捕获 stat() 抛出的 OSError,把它变成一行信息。信息用 print(..., file=sys.stderr) 或 logging 输出。
诊断走标准错误,用 -v 调节级别
用 logging.basicConfig 设置默认 INFO,有 -v 时为 DEBUG。用 -v 运行时,DEBUG 行只出现在标准错误,标准输出只留下一行结果。
basicConfig 的默认目的地是 sys.stderr。在 format 中加入 %(levelname)s,就会打印出 DEBUG 这个词。请在检查开始的位置放一个 log.debug()。
供机器读取的结果用 JSON
有 --json 时,只向标准输出输出一个 JSON 对象。键是 path、files、oldest_age_hours、ok,ok 是布尔值,files 是整数。保持退出码约定。
把 json.dumps(dict) 用 print 输出。如果同时输出给人看的一行和 JSON,解析就会出错——只能二选一。
多个目录,取最坏的退出码
用 nargs="+" 接收多个 path。每个目录输出一行(有 JSON 时输出数组),退出码以最坏的那个(2 > 1 > 0)结束。
对每个目录更新 worst = max(worst, code),并且不要因为一个不存在的路径就跳过其余的检查(continue)。
即使被导入也不会执行的工具
整理成 main(argv=None) -> int,只在文件最底部的 if __name__ == "__main__": 之内调用 sys.exit(main())。import dircheck 不执行任何东西,并且 dircheck.main(["<目录>"]) 返回整数。
把 argv 传给 parse_args(argv),它就不会读取 sys.argv。如果模块最顶层还留着 parse_args() 或 sys.exit(),导入的那一刻就会被执行。