Codex 实战:从一句需求到可验收的 Linux 主机巡检脚本

用 Codex 编写并验收 Linux Bash 主机巡检脚本,覆盖 CPU、内存、磁盘、inode、systemd、日志、OOM、僵尸进程及监听端口。

运维脚本最危险的状态,不是"不会运行",而是"看起来能运行"。

一个脚本可以顺利输出 CPU、内存和磁盘信息,却可能在生产环境里隐藏很多问题:命令缺失时仍然返回成功、日志没有读取权限却显示"0 条错误"、磁盘和 inode 只检查其一、阈值写死、输出无法被监控系统消费,甚至把巡检和自动修复混在一起。

这篇文章不讨论"让 AI 随便写个 Shell 脚本"。我会用一个完整案例,演示如何让 Codex 参与需求拆解、Bash 实现、安全审查和验收测试,最终得到一套默认只读、可配置、可生成文本和 JSON 报告,并且有明确退出码与自测模式的 Linux 主机健康巡检脚本。

本文代码不包含任何真实服务器地址、用户名、业务名称、内网路径、密钥或生产日志。演示结果全部来自脚本内置的匿名数据。

一、为什么选择 Linux 主机巡检

"用 AI 写运维脚本"这个主题很容易写成几个零散示例:清理日志、批量 SSH、检查 Nginx。问题是,示例能运行,不等于具备运维价值。

Linux 主机巡检更适合作为完整案例,因为它同时涉及:

  1. 多种系统信息源,例如 /proc、df、ps、systemctl、journalctl 和 ss。

  2. 不同 Linux 发行版和运行环境的兼容性。

  3. 命令缺失、权限不足、systemd 不可用等异常分支。

  4. 阈值、退出码和机器可读报告等自动化接口。

  5. 最重要的安全边界:巡检脚本默认不应该修改系统。

这个案例也能体现 Codex 和普通"代码补全"的区别。真正有价值的不是一次生成多少行代码,而是能否把模糊需求逐步收敛为可执行、可测试、可审查的工程结果。OpenAI 官方的 Codex 用例也强调了自动化、代码、测试和构建可复用 CLI 这类工作流。

二、先定义验收标准,再让 Codex 写代码

如果只给 Codex 一句话:

帮我写一个 Linux 巡检脚本。

它大概率会生成一个能显示 top、free 和 df 的脚本,但这还不够。

我把需求改写成下面这份提示词:

你是一名资深 Linux SRE。请编写一个 Bash 4+ 主机健康巡检脚本,目标系统包括 Ubuntu、Debian、Rocky Linux、AlmaLinux 和 CentOS Stream。

要求:

  1. 默认只读,不修改配置、不重启服务、不清理文件、不调用 sudo。

  2. 检查 1 分钟归一化负载、MemAvailable 内存使用率、磁盘空间、inode、失败的 systemd 单元、僵尸进程、最近 1 小时高优先级日志、最近 24 小时 OOM 和 TCP 监听端口数量。

  3. 磁盘、inode、内存、负载和错误日志阈值必须可通过命令行参数调整。

  4. 同时生成 report.txt 和 report.json。

  5. 退出码固定为:0 正常、1 告警、2 严重、3 脚本错误。

  6. 使用严格模式,并处理命令缺失、日志权限不足、systemd 不可用等情况,不能把"无法检查"误报为"正常"。

  7. 提供 --demo 匿名演示模式和 --self-test 自测模式。

  8. 参数必须校验;告警阈值必须小于严重阈值。

  9. JSON 必须正确转义,能被标准解析器读取。

  10. 给出 bash -n、内置自测、非法参数、演示报告和 JSON 解析的验收方法。

先解释设计,再生成代码。不要声称未经验证的发行版一定兼容。

这份提示词最关键的不是"角色设定",而是把运维要求写成了验收条件:输入是什么、输出是什么、允许做什么、禁止做什么、失败时怎样表达。

三、脚本架构:采集、判断、报告三层分离

最终脚本分成三层。

  1. 数据采集层

采集层只负责读取系统状态:

  1. /proc/loadavg:获取 1 分钟负载。

  2. /proc/meminfo:读取 MemTotal 和 MemAvailable。

  3. df -P:检查文件系统空间。

  4. df -Pi:检查 inode 使用率。

  5. systemctl --failed:列出失败的 systemd 单元。

  6. ps:统计僵尸进程。

  7. journalctl:统计高优先级日志和 OOM 记录。

  8. ss -lntH:统计 TCP 监听端口。

这里有一个容易写错的点:Linux 内存不能简单地用 MemFree / MemTotal 判断。内核文档将 MemAvailable 定义为"不发生交换的情况下,可供新应用使用的内存估计值",因此脚本使用:

内存使用率 = (MemTotal - MemAvailable) / MemTotal × 100%

它比只看 MemFree 更符合现代 Linux 的缓存回收机制。

  1. 状态判断层

所有百分比指标统一经过同一个判断函数:

status_for_value() {

local value=$1

local warn=$2

local crit=$3

if awk -v value="value" -v crit="crit" 'BEGIN { exit !(value >= crit) }'; then

printf 'CRITICAL'

elif awk -v value="value" -v warn="warn" 'BEGIN { exit !(value >= warn) }'; then

printf 'WARNING'

else

printf 'OK'

fi

}

这样可以避免 CPU、内存、磁盘分别维护一套互相不一致的阈值逻辑。

总体状态遵循"最严重状态优先":

只要有 CRITICAL → 总体 CRITICAL → 退出码 2

没有 CRITICAL,但有 WARNING → 总体 WARNING → 退出码 1

其余 → 总体 OK → 退出码 0

脚本自身错误 → 退出码 3

这个退出码设计可以直接接入 cron、CI、监控采集器或其他编排系统。

  1. 报告层

脚本同时输出两份报告:

  1. report.txt:便于人工查看和归档。

  2. report.json:便于监控平台、自动化任务或其他程序解析。

报告层不重新采集系统数据,只消费已经标准化的检查结果。这样文本和 JSON 不会出现同一次巡检结果不一致的问题。

四、安全设计:默认只读比"自动修复"更重要

这套脚本刻意没有加入自动清日志、重启服务、杀进程或修改防火墙。

原因很简单:

  1. 巡检与修复属于不同权限等级。

  2. 同一个现象可能有多种根因,自动修复容易扩大故障。

  3. 生产变更应该有审批、回滚和审计记录。

  4. AI 生成的脚本必须先经过人工复核和测试环境验证。

脚本使用:

set -Eeuo pipefail

其中 -u 可以发现未定义变量,pipefail 可以让管道中间命令的失败被传递出来,-e 和 -E 用于强化失败传播。不过严格模式不是魔法,某些 if、!、|| 和命令替换场景仍需要显式处理返回码。因此脚本对 systemctl 和 journalctl 采用了单独判断:读取失败时显示 INFO / N/A,而不是错误地显示 OK / 0。

这是 AI 生成运维代码时非常值得检查的一点:

"没有发现问题"和"没有权限检查问题"不是一回事。

五、关键实现解析

  1. 对负载进行 CPU 核数归一化

负载 4.0 在 2 核机器和 16 核机器上的含义完全不同。脚本把 1 分钟负载换算为单核百分比:

load_percent=(awk -v load="load1" -v cpu="$cpu_count" \

'BEGIN { printf "%.0f", load * 100 / cpu }')

默认 80% 为告警,120% 为严重,但可以根据业务特点调整。

  1. 同时检查磁盘空间和 inode

磁盘"还有很多 GB"不代表一定能继续写文件。大量小文件可能先耗尽 inode。因此脚本分别执行:

df -P

df -Pi

每个挂载点都会形成独立检查项,避免只看根分区而漏掉数据盘或日志盘。

  1. systemd 不可用时不误报

容器、WSL、精简系统或非 systemd 发行版中,systemctl 可能存在但无法连接 PID 1。脚本先检查命令,再检查执行结果:

if ! failed_output=$(systemctl --failed --no-legend --plain 2>/dev/null); then

add_check '失败的 systemd 单元' 'INFO' 'N/A' \

'systemctl 无法连接到 systemd,已跳过。'

return

fi

  1. 日志权限不足时返回 N/A

普通用户不一定能读取完整 journal。下面这种写法比直接把错误重定向到 /dev/null 更稳妥:

if ! error_output=$(journalctl -p 0..3 --since '1 hour ago' \

--no-pager -q 2>/dev/null); then

add_check '最近 1 小时错误日志' 'INFO' 'N/A' \

'journalctl 无法读取日志,可能需要调整权限。'

return

fi

journalctl -p 0..3 表示筛选 emerg、alert、crit 和 err 级别,--since 用于限定时间范围。

  1. 提供匿名演示和自测模式

运维文章不应该为了展示效果而公开真实主机名、业务服务和告警数据。因此脚本内置:

./linux-health-audit.sh --demo

它只使用匿名数据,生成稳定的文本和 JSON 报告。

自测模式:

./linux-health-audit.sh --self-test

它会验证阈值分类和 JSON 转义,不读取系统状态,也不生成报告。

六、完整脚本

下面是完整可复制版本。

#!/usr/bin/env bash

set -Eeuo pipefail

SCRIPT_VERSION="1.0.0"

DISK_WARN=80

DISK_CRIT=90

INODE_WARN=80

INODE_CRIT=90

MEM_WARN=80

MEM_CRIT=90

LOAD_WARN=80

LOAD_CRIT=120

ERROR_WARN=20

OUTPUT_DIR=""

DEMO_MODE=0

SELF_TEST=0

declare -a CHECK_NAMES=()

declare -a CHECK_STATUSES=()

declare -a CHECK_VALUES=()

declare -a CHECK_DETAILS=()

usage() {

cat <<'EOF'

用法:

./linux-health-audit.sh 选项

选项:

--output-dir PATH 指定报告目录

--disk-warn N 磁盘使用率告警阈值,默认 80

--disk-crit N 磁盘使用率严重阈值,默认 90

--inode-warn N inode 使用率告警阈值,默认 80

--inode-crit N inode 使用率严重阈值,默认 90

--mem-warn N 内存使用率告警阈值,默认 80

--mem-crit N 内存使用率严重阈值,默认 90

--load-warn N 单核归一化负载告警阈值,默认 80

--load-crit N 单核归一化负载严重阈值,默认 120

--error-warn N 最近 1 小时错误日志告警条数,默认 20

--demo 使用匿名演示数据生成报告

--self-test 运行内置单元测试,不读取系统状态

-h, --help 显示帮助

退出码:

0 全部正常

1 存在告警

2 存在严重问题

3 脚本运行错误

EOF

}

die() {

printf '错误:%s\n' "$*" >&2

exit 3

}

is_uint() {

\[ ${1:-} =\~ \^\[0-9\]+$ \]

}

require_value() {

local option=$1

local value=${2:-}

\[ -n $value ] || die "$option 缺少参数。"

}

while (($# > 0)); do

case "$1" in

--output-dir)

require_value "1" "{2:-}"

OUTPUT_DIR=$2

shift 2

;;

--disk-warn) DISK_WARN=${2:-}; shift 2 ;;

--disk-crit) DISK_CRIT=${2:-}; shift 2 ;;

--inode-warn) INODE_WARN=${2:-}; shift 2 ;;

--inode-crit) INODE_CRIT=${2:-}; shift 2 ;;

--mem-warn) MEM_WARN=${2:-}; shift 2 ;;

--mem-crit) MEM_CRIT=${2:-}; shift 2 ;;

--load-warn) LOAD_WARN=${2:-}; shift 2 ;;

--load-crit) LOAD_CRIT=${2:-}; shift 2 ;;

--error-warn) ERROR_WARN=${2:-}; shift 2 ;;

--demo) DEMO_MODE=1; shift ;;

--self-test) SELF_TEST=1; shift ;;

-h|--help) usage; exit 0 ;;

*) die "未知参数:$1" ;;

esac

done

for threshold in \

"DISK_WARN" "DISK_CRIT" "INODE_WARN" "INODE_CRIT" \

"MEM_WARN" "MEM_CRIT" "LOAD_WARN" "LOAD_CRIT" "$ERROR_WARN"; do

is_uint "threshold" \|\| die "阈值必须是非负整数:threshold"

done

((DISK_WARN < DISK_CRIT)) || die 'disk-warn 必须小于 disk-crit。'

((INODE_WARN < INODE_CRIT)) || die 'inode-warn 必须小于 inode-crit。'

((MEM_WARN < MEM_CRIT)) || die 'mem-warn 必须小于 mem-crit。'

((LOAD_WARN < LOAD_CRIT)) || die 'load-warn 必须小于 load-crit。'

status_for_value() {

local value=$1

local warn=$2

local crit=$3

if awk -v value="value" -v crit="crit" 'BEGIN { exit !(value >= crit) }'; then

printf 'CRITICAL'

elif awk -v value="value" -v warn="warn" 'BEGIN { exit !(value >= warn) }'; then

printf 'WARNING'

else

printf 'OK'

fi

}

add_check() {

CHECK_NAMES+=("$1")

CHECK_STATUSES+=("$2")

CHECK_VALUES+=("$3")

CHECK_DETAILS+=("$4")

}

json_escape() {

local value=${1-}

value=${value//\\/\\\\}

value=${value//\"/\\\"}

value={value//'\n'/\\n}

value={value//'\r'/\\r}

value={value//'\t'/\\t}

printf '%s' "$value"

}

run_self_test() {

local failures=0

\[ $(status_for_value 79 80 90) == OK ] || ((failures += 1))

\[ $(status_for_value 80 80 90) == WARNING ] || ((failures += 1))

\[ $(status_for_value 90 80 90) == CRITICAL ] || ((failures += 1))

\[ $(json_escape 'a"b\\c') == 'a\\"b\\\\c' ] || ((failures += 1))

if ((failures == 0)); then

printf 'SELF-TEST PASS:阈值分类与 JSON 转义测试通过。\n'

exit 0

fi

printf 'SELF-TEST FAIL:%d 项测试失败。\n' "$failures" >&2

exit 3

}

if ((SELF_TEST == 1)); then

run_self_test

fi

collect_demo_data() {

add_check 'CPU 归一化负载' 'OK' '42%' '1 分钟负载 1.68,CPU 逻辑核数 4。'

add_check '内存使用率' 'WARNING' '83%' '可用内存低于告警阈值。'

add_check '磁盘 /' 'OK' '61%' '根分区空间充足。'

add_check 'inode /' 'OK' '22%' '根分区 inode 充足。'

add_check '失败的 systemd 单元' 'CRITICAL' '1' 'demo-worker.service'

add_check '僵尸进程' 'OK' '0' '未发现僵尸进程。'

add_check '最近 1 小时错误日志' 'WARNING' '27' '超过告警阈值 20。'

add_check '最近 24 小时 OOM' 'OK' '0' '未发现 OOM 记录。'

add_check 'TCP 监听端口' 'INFO' '5' '仅记录数量,不修改防火墙。'

}

collect_load() {

local load1 cpu_count load_percent status

read -r load1 _ < /proc/loadavg

cpu_count=$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)

load_percent=(awk -v load="load1" -v cpu="$cpu_count" 'BEGIN { printf "%.0f", load * 100 / cpu }')

status=(status_for_value "load_percent" "LOAD_WARN" "LOAD_CRIT")

add_check 'CPU 归一化负载' "status" "{load_percent}%" "1 分钟负载 load1,CPU 逻辑核数 cpu_count。"

}

collect_memory() {

local total available used_percent status

read -r total available < <(

awk '

1 == "MemTotal:" { total=2 }

1 == "MemAvailable:" { available=2 }

END { print total, available }

' /proc/meminfo

)

\[ -n ${total:-} \&\& -n ${available:-} \&\& $total -gt 0 ] || die '无法解析 /proc/meminfo。'

used_percent=(awk -v total="total" -v available="$available" 'BEGIN { printf "%.0f", (total - available) * 100 / total }')

status=(status_for_value "used_percent" "MEM_WARN" "MEM_CRIT")

add_check '内存使用率' "status" "{used_percent}%" '基于 MemAvailable 计算,包含内核可回收缓存。'

}

collect_filesystems() {

local filesystem blocks used available capacity mount percent status

while read -r filesystem blocks used available capacity mount; do

\[ $filesystem == Filesystem ] && continue

percent=${capacity%%%}

is_uint "$percent" || continue

status=(status_for_value "percent" "DISK_WARN" "DISK_CRIT")

add_check "磁盘 mount" "status" "{percent}%" "文件系统:filesystem"

done < <(df -P -x tmpfs -x devtmpfs 2>/dev/null || df -P)

while read -r filesystem inodes iused ifree capacity mount; do

\[ $filesystem == Filesystem ] && continue

percent=${capacity%%%}

is_uint "$percent" || continue

status=(status_for_value "percent" "INODE_WARN" "INODE_CRIT")

add_check "inode mount" "status" "{percent}%" "文件系统:filesystem"

done < <(df -Pi -x tmpfs -x devtmpfs 2>/dev/null || df -Pi)

}

collect_systemd() {

if ! command -v systemctl >/dev/null 2>&1; then

add_check '失败的 systemd 单元' 'INFO' 'N/A' '系统未提供 systemctl,已跳过。'

return

fi

local failed_output failed_count detail status

if ! failed_output=$(systemctl --failed --no-legend --plain 2>/dev/null); then

add_check '失败的 systemd 单元' 'INFO' 'N/A' 'systemctl 无法连接到 systemd,已跳过。'

return

fi

failed_count=(awk 'NF { count++ } END { print count+0 }' \<\<\<"failed_output")

if ((failed_count > 0)); then

status='CRITICAL'

detail=(awk 'NF { print 1 }' <<<"$failed_output" | head -n 5 | paste -sd ', ' -)

else

status='OK'

detail='未发现失败的 systemd 单元。'

fi

add_check '失败的 systemd 单元' "status" "failed_count" "$detail"

}

collect_processes() {

local zombie_count status

zombie_count=(ps -eo stat= 2\>/dev/null \| awk '1 ~ /^Z/ { count++ } END { print count+0 }')

status=(\[\[ zombie_count -gt 0 ]] && printf 'WARNING' || printf 'OK')

add_check '僵尸进程' "status" "zombie_count" "(\[\[ zombie_count -gt 0 ]] && printf '需要定位父进程。' || printf '未发现僵尸进程。')"

}

collect_journal() {

if ! command -v journalctl >/dev/null 2>&1; then

add_check '最近 1 小时错误日志' 'INFO' 'N/A' '系统未提供 journalctl,已跳过。'

add_check '最近 24 小时 OOM' 'INFO' 'N/A' '系统未提供 journalctl,已跳过。'

return

fi

local error_output error_count error_status kernel_output oom_count oom_status

if ! error_output=$(journalctl -p 0..3 --since '1 hour ago' --no-pager -q 2>/dev/null); then

add_check '最近 1 小时错误日志' 'INFO' 'N/A' 'journalctl 无法读取日志,可能需要调整权限。'

add_check '最近 24 小时 OOM' 'INFO' 'N/A' 'journalctl 无法读取日志,可能需要调整权限。'

return

fi

error_count=(awk 'NF { count++ } END { print count+0 }' \<\<\<"error_output")

error_status=(\[\[ error_count -ge $ERROR_WARN ]] && printf 'WARNING' || printf 'OK')

add_check '最近 1 小时错误日志' "error_status" "error_count" "告警阈值:$ERROR_WARN。"

if ! kernel_output=$(journalctl -k --since '24 hours ago' --no-pager -q 2>/dev/null); then

add_check '最近 24 小时 OOM' 'INFO' 'N/A' 'journalctl 无法读取内核日志,可能需要调整权限。'

return

fi

oom_count=(grep -Eic 'out of memory\|oom-killer\|killed process' \<\<\<"kernel_output" || true)

oom_status=(\[\[ oom_count -gt 0 ]] && printf 'CRITICAL' || printf 'OK')

add_check '最近 24 小时 OOM' "oom_status" "oom_count" "(\[\[ oom_count -gt 0 ]] && printf '发现内存耗尽相关内核日志。' || printf '未发现 OOM 记录。')"

}

collect_ports() {

if ! command -v ss >/dev/null 2>&1; then

add_check 'TCP 监听端口' 'INFO' 'N/A' '系统未提供 ss,已跳过。'

return

fi

local port_count

port_count=$(ss -lntH 2>/dev/null | awk 'NF { count++ } END { print count+0 }')

add_check 'TCP 监听端口' 'INFO' "$port_count" '仅记录监听数量,不修改防火墙。'

}

overall_status() {

local status has_warning=0

for status in "${CHECK_STATUSES@}"; do

\[ $status == CRITICAL ] && { printf 'CRITICAL'; return; }

\[ $status == WARNING ] && has_warning=1

done

((has_warning == 1)) && printf 'WARNING' || printf 'OK'

}

exit_code_for_status() {

case "$1" in

OK) printf '0' ;;

WARNING) printf '1' ;;

CRITICAL) printf '2' ;;

*) printf '3' ;;

esac

}

write_text_report() {

local file=$1

local generated_at=$2

local host_name=$3

local overall=$4

local index

{

printf 'Linux 主机健康巡检报告\n'

printf '生成时间:%s\n' "$generated_at"

printf '主机:%s\n' "$host_name"

printf '脚本版本:%s\n' "$SCRIPT_VERSION"

printf '总体状态:%s\n\n' "$overall"

printf '%-28s %-10s %-10s %s\n' '检查项' '状态' '数值' '说明'

printf '%-28s %-10s %-10s %s\n' '----------------------------' '----------' '----------' '------------------------------'

for index in "${!CHECK_NAMES@}"; do

printf '%-28s %-10s %-10s %s\n' \

"{CHECK_NAMES\[index]}" \

"{CHECK_STATUSES\[index]}" \

"{CHECK_VALUES\[index]}" \

"{CHECK_DETAILS\[index]}"

done

} > "$file"

}

write_json_report() {

local file=$1

local generated_at=$2

local host_name=$3

local overall=$4

local index comma=''

{

printf '{\n'

printf ' "generated_at": "%s",\n' "(json_escape "generated_at")"

printf ' "hostname": "%s",\n' "(json_escape "host_name")"

printf ' "script_version": "%s",\n' "$SCRIPT_VERSION"

printf ' "overall_status": "%s",\n' "$overall"

printf ' "checks": [\n'

for index in "${!CHECK_NAMES@}"; do

printf '%s {"name":"%s","status":"%s","value":"%s","detail":"%s"}' \

"$comma" \

"(json_escape "{CHECK_NAMES$index}")" \

"(json_escape "{CHECK_STATUSES$index}")" \

"(json_escape "{CHECK_VALUES$index}")" \

"(json_escape "{CHECK_DETAILS$index}")"

comma=$',\n'

done

printf '\n ]\n}\n'

} > "$file"

}

if ((DEMO_MODE == 1)); then

collect_demo_data

HOST_NAME='demo-linux-host'

else

\[ -r /proc/loadavg \&\& -r /proc/meminfo ] || die '缺少 Linux /proc 接口;请在 Linux 主机运行。'

command -v awk >/dev/null 2>&1 || die '缺少 awk。'

command -v df >/dev/null 2>&1 || die '缺少 df。'

command -v ps >/dev/null 2>&1 || die '缺少 ps。'

collect_load

collect_memory

collect_filesystems

collect_systemd

collect_processes

collect_journal

collect_ports

HOST_NAME=$(hostname -f 2>/dev/null || hostname)

fi

GENERATED_AT=$(date -u '+%Y-%m-%dT%H:%M:%SZ')

OVERALL_STATUS=$(overall_status)

REPORT_EXIT_CODE=(exit_code_for_status "OVERALL_STATUS")

if \[ -z $OUTPUT_DIR ]; then

OUTPUT_DIR="./linux-health-report-$(date '+%Y%m%d-%H%M%S')"

fi

if \[ ! -d $OUTPUT_DIR ]; then

mkdir -p -- "$OUTPUT_DIR"

fi

TEXT_REPORT="$OUTPUT_DIR/report.txt"

JSON_REPORT="$OUTPUT_DIR/report.json"

write_text_report "TEXT_REPORT" "GENERATED_AT" "HOST_NAME" "OVERALL_STATUS"

write_json_report "JSON_REPORT" "GENERATED_AT" "HOST_NAME" "OVERALL_STATUS"

printf '巡检完成:%s\n' "$OVERALL_STATUS"

printf '文本报告:%s\n' "$TEXT_REPORT"

printf 'JSON 报告:%s\n' "$JSON_REPORT"

printf '退出码:%s\n' "$REPORT_EXIT_CODE"

exit "$REPORT_EXIT_CODE"

七、运行方法

  1. 保存并授权

chmod 750 linux-health-audit.sh

  1. 先运行语法检查

bash -n linux-health-audit.sh

没有输出并返回 0,表示 Bash 语法检查通过。

  1. 运行内置自测

./linux-health-audit.sh --self-test

预期输出:

SELF-TEST PASS:阈值分类与 JSON 转义测试通过。

  1. 先用匿名演示模式查看报告格式

./linux-health-audit.sh --demo --output-dir ./demo-report

演示数据故意包含一个严重状态,所以退出码应为 2。

  1. 在测试机执行真实巡检

./linux-health-audit.sh --output-dir ./health-report

自定义阈值示例:

./linux-health-audit.sh \

--disk-warn 75 \

--disk-crit 88 \

--mem-warn 85 \

--mem-crit 95 \

--error-warn 10 \

--output-dir ./health-report

不要一开始就用 root 运行。先用普通运维账号执行,确认哪些检查需要额外日志权限,再按最小权限原则授权。

八、验收测试结果

本地完成了以下验证:

验收项:`bash -n` 语法检查;结果:通过;说明:返回 0

验收项:内置 `--self-test`;结果:通过;说明:阈值分类与 JSON 转义通过

验收项:非法阈值;结果:通过;说明:`--disk-warn 95 --disk-crit 90` 返回 3

验收项:匿名 `--demo`;结果:通过;说明:总体状态 CRITICAL,返回 2

验收项:文本报告;结果:通过;说明:成功生成 `report.txt`

验收项:JSON 报告;结果:通过;说明:标准解析器可读取,包含 9 个检查项

需要明确的是:当前验证覆盖脚本语法、核心逻辑、参数校验、报告生成和 JSON 解析,不等于已经覆盖所有 Linux 发行版和生产环境。正式使用前仍应在目标发行版的测试机验证:

  1. Bash 版本是否为 4 或以上。

  2. df、ps、awk、systemctl、journalctl 和 ss 的可用性。

  3. 运维账号对 systemd journal 的读取权限。

  4. 容器、云主机和特殊挂载点是否需要排除规则。

  5. 阈值是否符合当前业务基线,而不是照搬示例值。

九、Codex 在这个案例里真正解决了什么

Codex 的价值不是替运维人员做决定,而是缩短"需求---实现---测试---修订"的循环。

在这个案例中,它适合承担:

  1. 把自然语言需求拆成参数、函数和退出码。

  2. 生成重复但容易出错的 Bash 代码。

  3. 检查异常分支和兼容性问题。

  4. 补充 --demo、--self-test 和验收命令。

  5. 根据测试结果修改脚本,而不是只给一次性答案。

人仍然需要负责:

  1. 确认指标是否有业务意义。

  2. 决定阈值、权限和执行频率。

  3. 审查命令是否会修改系统。

  4. 在目标发行版和测试环境中验证。

  5. 对生产变更、回滚和事故结果负责。

最稳妥的合作模式不是"AI 写完,我直接执行",而是:

人定义边界 → Codex 生成 → 静态检查 → 匿名演示 → 测试机验证 → 人工审批 → 生产只读运行

十、如何接入 cron 和监控系统

脚本退出码已经标准化,可以被外部系统直接判断:

./linux-health-audit.sh --output-dir /var/tmp/linux-health

case $? in

  1. echo 'OK' ;;

  2. echo 'WARNING' ;;

  3. echo 'CRITICAL' ;;

*) echo 'SCRIPT_ERROR' ;;

esac

cron 示例:

15 * * * * /opt/ops/linux-health-audit.sh --output-dir /var/tmp/linux-health >/var/log/linux-health-audit.log 2>&1

这里仍然建议先做两项改造:

  1. 报告目录增加保留策略,避免报告自身占满磁盘。

  2. 使用监控平台或日志系统收集结果,而不是只依赖本机文件。

脚本本身没有实现通知发送,因为邮件、Webhook 和监控接口通常涉及凭据。凭据应该由环境变量、受控配置文件或密钥管理系统提供,不应硬编码在脚本或文章中。

十一、总结

用 Codex 写运维脚本,最重要的不是提示词有多华丽,而是有没有把需求变成可以验收的工程约束。

这次实践得到的经验可以浓缩成五句话:

  1. 先定义只读边界,再写采集逻辑。

  2. "无法检查"必须和"检查正常"分开。

  3. 人看的文本报告和机器读的 JSON 报告都要有。

  4. 退出码、参数校验和自测决定脚本能否进入自动化流程。

  5. AI 生成代码必须经过静态检查、匿名演示和目标环境验证。

真正专业的 AI 运维,不是让模型获得更大的权限,而是让每一步都更可控、更可查、更容易回滚。

参考资料

  1. OpenAI 官方 Codex 用例:https://learn.chatgpt.com/use-cases

  2. GNU Bash Reference Manual:https://www.gnu.org/software/bash/manual/bash.html

  3. systemd journalctl 官方手册:https://www.freedesktop.org/software/systemd/man/latest/journalctl.html

  4. Linux man-pages:/proc/meminfo:https://www.kernel.org/doc/man-pages/

#Codex #Linux #Bash #运维 #DevOps #Shell #AI编程

相关推荐
2601_9622184732 分钟前
万象生鲜系统冷链物联网接入技术实现生鲜企业温控管理数字化
大数据·运维·微服务·云原生·架构
Best-Wishes1 小时前
BurpSuite Pro教育版在linux系统中配置
linux·运维·服务器·网络安全·burpsuite
志栋智能1 小时前
从关联到因果:智能超自动化如何实现真正的根因分析定界
运维·自动化
虎王物联1 小时前
RK3568嵌入式Linux跑Docker:内核配置排查到AIoT容器化的完整路径
linux·运维·docker·rk3568·aiot·嵌入式linux
Java后端的Ai之路2 小时前
14、Python - 责任链模式
服务器·开发语言·人工智能·python·责任链模式
江安下小雨2 小时前
muduo网络库(十六):新增连接池模块
网络·c++
aiot189189352183 小时前
机场候机大厅高空场景技术红线!蓝牙AOA不能做手机导航??!!
大数据·网络·人工智能·蓝牙aoa
从入门到退休3 小时前
企业远程控制选型:向日葵SDK vs RustDesk自建,谁是更务实的选择?
运维·服务器·网络·远程工作·远程控制
fb_123454 小时前
网络技术基础
网络