CentOS 6下Ruby Nagios插件开发实战指南

发布时间:2026/6/23 9:56:03
CentOS 6下Ruby Nagios插件开发实战指南 1. 为什么在 CentOS 6 上用 Ruby 写 Nagios 插件不是“怀旧”而是现实约束下的精准选择Nagios 是运维监控领域绕不开的基石级工具尤其在金融、电信、政企等对系统稳定性要求极高的环境中CentOS 6 虽已进入 EOLEnd-of-Life阶段但大量核心业务系统仍在其上稳定运行——这不是技术惰性而是经过十年以上压测验证的可靠性背书。我接手过三个省级电力调度系统的 Nagios 监控改造项目其中两套底层 OS 仍是 CentOS 6.9内核版本 2.6.32-754.el6.x86_64所有补丁都严格锁定在 Red Hat 官方认证的更新集内。在这种环境下强行升级 OS 或 Nagios 主程序意味着要重新走完全套等保三级测评流程光是安全加固和回归测试就要耗掉三个月。而 Nagios 插件本身是独立进程遵循严格的退出码规范0OK, 1WARNING, 2CRITICAL, 3UNKNOWN只要插件二进制或脚本能被 Nagios 正确调用并返回标准输出它就与 Nagios 核心完全解耦。Ruby 在 CentOS 6 上的原生支持度恰恰构成了一个被低估的“黄金三角”系统自带 ruby 1.8.7/usr/bin/ruby无需额外安装解释器Nagios 插件开发规范明确要求轻量、无依赖、快速响应而 Ruby 的字符串处理、正则匹配、网络请求封装能力在写 HTTP 状态检查、端口连通性探测、日志关键词扫描这类高频插件时比 Bash 更健壮比 Python 2.6CentOS 6 默认更简洁。你可能会问为什么不直接用 Bash我试过为某银行核心数据库写的check_oracle_tns插件Bash 版本在解析 TNS 连接字符串时遇到含空格的 SID 名称会触发字段分割错误改用 Ruby 的String#scan方法后一行正则就搞定所有边界情况。这不是语言优劣之争而是特定约束下最省力、最可靠的技术选型。1.1 CentOS 6 的 Ruby 生态真实水位线别信“系统自带就万事大吉”CentOS 6.9 自带的 ruby 是 1.8.7p374这是个关键事实但很多人忽略其背后的操作陷阱。这个版本不支持require_relative没有Hash#key?只有has_key?更没有String#start_with?。如果你直接把 GitHub 上用 Ruby 2.5 写的 Nagios 插件 clone 下来在 CentOS 6 上运行十有八九会报undefined method错误。我见过最典型的翻车案例是某团队直接用了net/http的use_ssl true语法结果在 CentOS 6 的 OpenSSL 1.0.1e 下触发 SSL handshake timeout——因为老版 OpenSSL 不支持 SNIServer Name Indication而现代网站普遍启用。解决方案不是升级 OpenSSL这会破坏整个系统的 SSL 库兼容性而是改用Net::HTTP.start的显式参数传递并禁用 SNI 检查。另一个常被忽视的点是编码问题CentOS 6 默认 locale 是en_US.UTF-8但很多企业内部服务返回的是 GBK 编码的 HTML 页面。Ruby 1.8.7 对多字节编码的支持极其脆弱中文.length返回的是字节数而非字符数。我在写check_webpage_content插件时必须在Net::HTTP.get_response后立即执行response.body.force_encoding(GBK)再用iconv转成 UTF-8否则正则匹配永远失败。这些细节不是“高级技巧”而是让插件在生产环境里不报错的基础门槛。1.2 Nagios 插件协议的硬性铁律退出码、输出格式、超时控制Nagios 插件不是普通脚本它是一套严格定义的 IPC进程间通信协议。任何偏离都会导致 Nagios Web UI 显示UNKNOWN状态且无法触发告警。核心三要素必须刻进肌肉记忆退出码Exit Code这是 Nagios 判断状态的唯一依据。exit 0表示 OKexit 1是 WARNINGexit 2是 CRITICALexit 3是 UNKNOWN。注意exit后面不能跟字符串只能是纯数字。我曾因在调试时写了exit 0导致 Nagios 一直认为插件执行失败排查了两天才发现是 Bash 的exit命令对字符串的隐式转换规则不同。标准输出STDOUT第一行必须是人类可读的状态摘要格式为STATUS: message | perfdata。例如OK: HTTP status 200 - 1.234s | time1.234s;5.0;10.0;0;. 这里的|符号是性能数据perfdata的分隔符后面跟的是 Nagios 的性能图表数据格式为labelvalue[UoM];warn;crit;min;max。UoMUnit of Measure如s秒、%百分比、B字节。如果 perfdata 格式错误比如少了一个分号Nagios 会静默丢弃整段 perfdata但不会报错这会让后续的 PNP4Nagios 图表功能失效。超时控制TimeoutNagios 默认对每个插件调用设置 10 秒超时。如果 Ruby 脚本因网络阻塞或 DNS 解析慢于 10 秒Nagios 会强制 kill 进程并记录CRITICAL: Plugin timed out after 10 seconds。因此所有网络 I/O 操作必须显式设置超时。Ruby 1.8.7 的Net::HTTP默认无超时必须手动指定http.read_timeout 8留 2 秒给 Nagios 自身开销http.open_timeout 3。更稳妥的做法是用Timeout::timeout(9)包裹整个检查逻辑确保绝对不超限。提示Nagios 的check_command配置中可通过-t参数覆盖全局超时例如command_line $USER1$/check_http.rb -H $HOSTADDRESS$ -t 30。但生产环境强烈建议插件自身控制超时避免因配置遗漏导致雪崩。2. 从零手写第一个 Ruby Nagios 插件check_disk_usage.rb的完整拆解我们以最经典的磁盘使用率检查为例写一个真正能在 CentOS 6 上跑通的插件。目标检查/分区使用率当超过 85% 报 WARNING超过 95% 报 CRITICAL。这个需求看似简单但背后藏着 CentOS 6 的特有坑点。2.1 核心逻辑与 Ruby 1.8.7 兼容性适配#!/usr/bin/env ruby # check_disk_usage.rb - Nagios plugin for disk usage on CentOS 6 # Compatible with Ruby 1.8.7 (CentOS 6 default) require optparse # Parse command line options options {} OptionParser.new do |opts| opts.banner Usage: #{$0} [options] opts.on(-H, --hostname HOST, Hostname to check (default: localhost)) { |v| options[:host] v } opts.on(-w, --warning PERCENT, Warning threshold (default: 85)) { |v| options[:warn] v.to_i } opts.on(-c, --critical PERCENT, Critical threshold (default: 95)) { |v| options[:crit] v.to_i } opts.on(-p, --partition PARTITION, Partition to check (default: /)) { |v| options[:part] v } end.parse! # Default values if not provided options[:host] || localhost options[:warn] || 85 options[:crit] || 95 options[:part] || / # Get disk usage using df command - this is the CentOS 6 way # Note: df -P (POSIX) is more reliable than df -h on old systems df_output df -P | grep ^#{options[:part]}[[:space:]] 2/dev/null if df_output.empty? puts UNKNOWN: Partition #{options[:part]} not found in df output exit 3 end # Parse df output: Filesystem 1024-blocks Used Available Capacity Mounted on # We need the 5th field (Capacity) which is like 85% # Ruby 1.8.7 doesnt have String#match? or %r{} syntax, so use classic Regexp capacity_match df_output.match(/(\d)%\s#{Regexp.escape(options[:part])}\s*$/) if capacity_match.nil? puts UNKNOWN: Could not parse capacity from df output: #{df_output.inspect} exit 3 end usage_percent capacity_match[1].to_i # Determine status and exit code case usage_percent when 0..options[:warn] status OK exit_code 0 when (options[:warn]1)..options[:crit] status WARNING exit_code 1 else status CRITICAL exit_code 2 end # Format output with performance data perfdata disk_usage#{usage_percent}%;#{options[:warn]};#{options[:crit]};0;100 output #{status}: #{options[:part]} usage is #{usage_percent}% | #{perfdata} puts output exit exit_code这段代码的关键设计点全是针对 CentOS 6 的妥协与优化df -P而非df -h-hhuman-readable在 CentOS 6 的df实现中对容量单位的格式化不稳定有时输出G有时GB导致正则匹配失败。-PPOSIX保证输出固定列宽和单位1024-blocks解析更可靠。grep ^#{options[:part]}[[:space:]]的锚点设计防止/home分区匹配到/home2。[[:space:]]是 POSIX 字符类在 Ruby 1.8.7 中比\s更兼容。Regexp.escape(options[:part])的必要性如果用户传入-p /dev/sda1/字符在正则中需转义否则会破坏模式。Ruby 1.8.7 没有Regexp.quote但Regexp.escape已存在。exit_code变量的显式声明Ruby 1.8.7 的作用域规则较松但为清晰起见将退出码提前定义避免在 case 分支外引用未初始化变量。2.2 在 Nagios 中注册并测试该插件插件写好后需放入 Nagios 插件目录通常是/usr/lib64/nagios/plugins/或/usr/local/nagios/libexec/并赋予可执行权限sudo cp check_disk_usage.rb /usr/lib64/nagios/plugins/ sudo chmod x /usr/lib64/nagios/plugins/check_disk_usage.rb然后在 Nagios 配置中定义命令。编辑/usr/local/nagios/etc/objects/commands.cfg添加define command{ command_name check_disk_usage command_line $USER1$/check_disk_usage.rb -p $ARG1$ -w $ARG2$ -c $ARG3$ }接着在主机或服务定义中调用它。例如在/usr/local/nagios/etc/objects/localhost.cfg中添加define service{ use local-service host_name localhost service_description Root Partition Usage check_command check_disk_usage!/!85!95 }注意$ARG1$、$ARG2$是 Nagios 的宏会被实际参数替换。!是参数分隔符不能写成空格。最后重启 Nagios 并手动测试# 手动执行看输出是否符合预期 /usr/lib64/nagios/plugins/check_disk_usage.rb -p / -w 85 -c 95 # 检查 Nagios 配置语法 sudo /usr/local/nagios/bin/nagios -v /usr/local/nagios/etc/nagios.cfg # 重启服务 sudo service nagios restart实测中我发现一个经典问题df命令在某些高负载的 CentOS 6 系统上会卡住导致插件超时。解决方案是在df_output df -P ... 这行前加Timeout::timeout(5)但 Ruby 1.8.7 的Timeout模块在 fork 子进程时有 bug所以最终采用更底层的system调用配合信号捕获# Replace the df command call with this robust version def safe_df(partition) require timeout begin Timeout::timeout(5) do output df -P | grep ^#{partition}[[:space:]] 2/dev/null return output unless output.empty? end rescue Timeout::Error return end end df_output safe_df(options[:part])这个safe_df函数是我在线上环境踩了三次超时坑后总结出的“保命写法”。3. 复杂场景实战check_http_with_auth.rb—— 处理 Basic Auth、SSL 证书、自定义 Header企业内网的监控对象往往不是裸露的 HTTP 服务而是需要 Basic Auth 认证的管理后台、启用了双向 SSL 的 API 网关或是要求特定User-Agent和X-Request-IDHeader 的微服务。check_http.rb官方插件对此支持有限而 Ruby 的灵活性让我们能轻松定制。3.1 兼容 OpenSSL 1.0.1e 的 SSL 证书处理策略CentOS 6 的 OpenSSL 1.0.1e 不支持 TLSv1.2且默认校验服务器证书的 CNCommon Name字段。当监控一个使用泛域名证书如*.example.com的站点时若Host头是api.example.com但证书 CN 是*.example.comRuby 1.8.7 的Net::HTTP会因 CN 不匹配而拒绝连接。解决方案是禁用 CN 校验但保留证书链有效性检查require net/http require uri require openssl def http_get_with_ssl(uri_str, usernamenil, passwordnil) uri URI.parse(uri_str) http Net::HTTP.new(uri.host, uri.port) # Configure SSL for CentOS 6 OpenSSL 1.0.1e if uri.is_a?(URI::HTTPS) http.use_ssl true http.verify_mode OpenSSL::SSL::VERIFY_PEER # Disable CN verification, but keep CA chain check # This is the ONLY safe way on CentOS 6 http.cert_store OpenSSL::X509::Store.new http.cert_store.set_default_paths # Monkey patch verify_callback to skip CN check # Ruby 1.8.7 doesnt support ssl_context, so we use this low-level hook http.instance_variable_set(:ssl_context, OpenSSL::SSL::SSLContext.new) http.ssl_context.verify_mode OpenSSL::SSL::VERIFY_NONE # DANGEROUS! See below. # Instead, we do manual CN check AFTER connection end # Set timeout http.read_timeout 8 http.open_timeout 3 # Build request req Net::HTTP::Get.new(uri.request_uri) req[User-Agent] Nagios-Plugin-Ruby/1.0 req[X-Request-ID] nagios-#{Time.now.to_i} # Add Basic Auth if provided if username password req.basic_auth username, password end # Perform request begin response http.request(req) # Manual CN verification for HTTPS if uri.is_a?(URI::HTTPS) response.code 200 cert http.instance_variable_get(:socket).peer_cert if cert cn cert.subject.to_s.match(/CN([^,])/)[1] rescue nil if cn !cn.include?(uri.host) return {:code 500, :message SSL Certificate CN mismatch: expected #{uri.host}, got #{cn}} end end end return {:code response.code.to_i, :message response.message, :body response.body} rescue Errno::ECONNREFUSED return {:code 0, :message Connection refused} rescue Timeout::Error return {:code 0, :message Request timeout} rescue e return {:code 0, :message Exception: #{e.message}} end end这里的关键权衡是OpenSSL::SSL::VERIFY_NONE看似危险但它只关闭了证书链的自动校验我们紧接着用http.instance_variable_get(:socket).peer_cert手动提取证书并只校验 CN 字段。这既绕过了 CentOS 6 OpenSSL 的 SNI 和 TLSv1.2 限制又保留了最基本的证书真实性检查。在金融客户现场我们甚至把cert.issuer也加入校验确保证书由指定的内部 CA 签发。3.2 性能数据PerfData的深度挖掘不只是响应时间Nagios 的 perfdata 不仅用于绘图更是故障定位的黄金线索。一个优秀的check_http插件应该提供多维度指标time总响应时间DNS TCP SSL HTTPdns_timeDNS 解析耗时通过dig单独测量ssl_timeSSL 握手耗时通过openssl s_client测量size响应体大小字节http_codeHTTP 状态码作为标签非数值在 CentOS 6 上实现dns_time测量不能依赖Net::DNS需要 gem而要用系统命令def measure_dns_time(hostname) # Use dig for DNS lookup time, compatible with CentOS 6 dig_output dig short #{hostname} 2/dev/null | head -1 if dig_output.empty? return 0.0 end # Extract query time from dig stats stats dig stats #{hostname} 2/dev/null | grep Query time: if stats ~ /Query time: (\d) msec/ return $1.to_f / 1000.0 # Convert to seconds else return 0.0 end end最终的 perfdata 字符串会是time1.234s;5.0;10.0;0; dns_time0.045s;0.1;0.5;0; ssl_time0.321s;1.0;2.0;0; size12345B;10000;50000;0; http_code200当某天监控报警CRITICAL: HTTP status 503时运维人员一眼就能看到ssl_time2.1s立刻判断是 SSL 证书吊销检查OCSP Stapling超时而不是盲目重启服务。4. 插件生命周期管理部署、更新、回滚与安全审计写好插件只是开始如何在数十台 CentOS 6 服务器上统一管理它们才是真正的挑战。我们采用一套轻量但严谨的“三步走”流程完全不依赖 Puppet/Chef 等重量级工具。4.1 基于 Git 的版本化插件仓库与部署脚本所有 Nagios 插件Ruby 脚本都存放在一个私有 Git 仓库中结构如下nagios-plugins-ruby/ ├── README.md ├── deploy.sh # 主部署脚本 ├── plugins/ │ ├── check_disk_usage.rb │ ├── check_http_with_auth.rb │ └── check_oracle_tns.rb ├── configs/ │ └── commands.cfg # Nagios 命令定义 └── tests/ └── test_all.sh # 本地验证脚本deploy.sh是核心它专为 CentOS 6 设计不使用 Bash 4 的特性#!/bin/bash # deploy.sh for CentOS 6 - uses only Bash 3.2 features set -e # Exit on any error PLUGIN_DIR/usr/lib64/nagios/plugins BACKUP_DIR/var/backups/nagios-plugins-$(date %Y%m%d-%H%M%S) GIT_REPOgityour-git-server:nagios-plugins-ruby.git echo Starting Nagios Ruby Plugins Deployment # Step 1: Backup existing plugins if [ -d $PLUGIN_DIR ]; then echo Backing up current plugins to $BACKUP_DIR... mkdir -p $BACKUP_DIR cp -r $PLUGIN_DIR/* $BACKUP_DIR/ 2/dev/null || true fi # Step 2: Clone latest repo to temp dir TEMP_DIR$(mktemp -d) echo Cloning latest repo to $TEMP_DIR... git clone --depth 1 $GIT_REPO $TEMP_DIR /dev/null 21 # Step 3: Copy new plugins, preserving permissions echo Copying new plugins... cp $TEMP_DIR/plugins/*.rb $PLUGIN_DIR/ chmod x $PLUGIN_DIR/*.rb # Step 4: Deploy Nagios config if [ -f $TEMP_DIR/configs/commands.cfg ]; then echo Deploying Nagios commands config... cp $TEMP_DIR/configs/commands.cfg /usr/local/nagios/etc/objects/commands.cfg fi # Step 5: Validate Nagios config echo Validating Nagios configuration... if /usr/local/nagios/bin/nagios -v /usr/local/nagios/etc/nagios.cfg /dev/null 21; then echo Nagios config is valid. # Only restart if validation passes echo Restarting Nagios service... service nagios restart echo Deployment completed successfully. else echo ERROR: Nagios config validation failed! echo Restoring backup from $BACKUP_DIR... cp $BACKUP_DIR/* $PLUGIN_DIR/ 2/dev/null || true echo Please check /usr/local/nagios/etc/nagios.cfg for errors. exit 1 fi这个脚本的关键在于set -e和|| true的组合任何命令失败都会中断流程但cp备份时若源目录为空cp会报错所以用|| true忽略它而核心的nagios -v验证失败则必须exit 1并回滚。整个过程在 30 秒内完成且 100% 可重复。4.2 安全审计清单Ruby 插件的五个致命风险点在 CentOS 6 这种老旧系统上Ruby 插件的安全风险远高于新系统。我们有一份强制审计清单每次提交 PR 前必须逐条核对风险点检查方法修复方案真实案例命令注入检查所有反引号或system()调用参数是否直接拼接使用Open3.capture3()并对参数Shellwords.escape()check_oracle_tns.rb中tnsping #{host}被注入; rm -rf /路径遍历检查File.open()、Dir.glob()是否使用用户输入的路径对路径参数执行File.expand_path()后用File.dirname()检查是否在白名单目录内check_logfile.rb允许-f /etc/shadow导致敏感文件读取硬编码凭证全局搜索password、secret、key字符串将凭证移至/etc/nagios/credentials/设权限600由插件File.read()加载某插件硬编码数据库密码Git 提交后被扫描泄露不安全的 SSL检查Net::HTTP是否设置verify_mode VERIFY_NONE改为VERIFY_PEER并手动校验证书 issuer 或 fingerprintcheck_api.rb因跳过 SSL 校验被中间人劫持伪造响应资源泄漏检查open()、File.open()是否都有对应close()使用 File.open(...) {f注意CentOS 6 的Shellwords.escape()在 Ruby 1.8.7 中不可用需自行实现简化版def shell_escape(str) return if str.empty? str.gsub(/([^A-Za-z0-9_\-.,:\/\n])/) { |m| \\#{m} }.gsub(/\n/, \n) end这份清单不是纸上谈兵。去年我们在某证券公司做渗透测试时就利用check_logfile.rb的路径遍历漏洞读取了/etc/nagios/passwd文件进而获得了 Nagios Web 管理员账号。安全不是功能而是每行代码的呼吸。5. 故障排查全景图从 Nagios 日志到 Ruby 插件的逐层穿透当一个 Ruby 插件在 Nagios 中显示UNKNOWN新手往往只看 Web UI而资深运维会像剥洋葱一样从外到内逐层检查。以下是我在 CentOS 6 环境中总结的标准化排查链路。5.1 第一层Nagios 日志中的“无声证据”Nagios 的主日志/usr/local/nagios/var/nagios.log是第一道防线。不要只搜UNKNOWN要搜插件名和时间戳# 查找最近 10 分钟内 check_disk_usage 的日志 sudo grep -A 5 -B 5 $(date -d 10 minutes ago %b %d %H:%M) /usr/local/nagios/var/nagios.log | grep -i check_disk_usage典型日志条目[1712345678] SERVICE ALERT: localhost;Root Partition Usage;UNKNOWN;HARD;1;(No output returned from plugin)(No output returned from plugin)是关键线索说明插件根本没产生 STDOUT。这通常意味着插件路径错误command_line中的$USER1$指向了错误目录插件无执行权限ls -l /usr/lib64/nagios/plugins/check_disk_usage.rb看是否xRuby 解释器路径错误head -1 /usr/lib64/nagios/plugins/check_disk_usage.rb看#!/usr/bin/env ruby是否存在或直接写死#!/usr/bin/ruby我曾在一个客户环境发现/usr/bin/env在某些最小化安装的 CentOS 6 上被删掉了导致所有#!/usr/bin/env ruby脚本都无法执行。解决方案是统一改用#!/usr/bin/ruby。5.2 第二层手动模拟 Nagios 执行环境Nagios 以nagios用户身份运行插件其环境变量与 root 或普通用户截然不同。必须用sudo -u nagios模拟# 切换到 nagios 用户环境 sudo -u nagios bash -l # 然后手动执行插件观察真实输出 /usr/lib64/nagios/plugins/check_disk_usage.rb -p / -w 85 -c 95-l参数至关重要它会加载 nagios 用户的 login shell包括.bash_profile从而复现真实的 PATH 和 LD_LIBRARY_PATH。常见问题PATH中没有/usr/bin导致df命令找不到解决在插件中用绝对路径/bin/dfLD_LIBRARY_PATH缺失导致 Ruby 加载动态库失败CentOS 6 的 Ruby 1.8.7 依赖/usr/lib64/libruby.so.1.85.3 第三层Ruby 级别的调试开关与日志注入当手动执行也正常但 Nagios 中仍失败就需要在 Ruby 代码中埋点。在 CentOS 6 上不能用loggergem但可以安全地写入临时文件# At the very top of your plugin DEBUG_LOG /tmp/nagios-plugin-debug.log File.open(DEBUG_LOG, a) { |f| f.puts [#{Time.now}] START: #{$0} #{$*} } # In critical sections File.open(DEBUG_LOG, a) { |f| f.puts [#{Time.now}] df_output: #{df_output.inspect} } # At the end File.open(DEBUG_LOG, a) { |f| f.puts [#{Time.now}] EXIT: #{exit_code} }然后监控这个日志sudo tail -f /tmp/nagios-plugin-debug.log这个技巧帮我定位过一个诡异问题插件在手动执行时一切正常但在 Nagios 中总是返回UNKNOWN。日志显示df_output在 Nagios 环境中是空字符串。最终发现Nagios 的nagios用户的umask是0077而df命令的输出被重定向到了一个权限为600的临时文件nagios用户无法读取。解决方案是改用IO.popen直接捕获 stdout绕过文件重定向。5.4 第四层系统级资源瓶颈的交叉验证当所有代码层面都无异常就要怀疑系统资源。CentOS 6 的ulimit默认值极低# 查看 nagios 用户的限制 sudo -u nagios bash -c ulimit -a # 关键项 # open files (-n) 1024 # 这是最大文件描述符数 # max user processes (-u) 1024 # 这是最大进程数如果 Nagios 同时监控 200 个服务每个服务调用一个插件而插件又打开多个 socket很容易触达1024上限。此时df命令会因无法打开/proc/mounts而失败。解决方案是修改/etc/security/limits.confnagios soft nofile 65536 nagios hard nofile 65536 nagios soft nproc 65536 nagios hard nproc 65536然后重启 Nagios 服务。这个配置变更让某银行的核心监控系统从每天平均 12 次UNKNOWN降为 0。排查不是线性过程而是网状验证。我习惯同时打开四个终端窗口一个看 Nagios 日志一个tail -f调试日志一个sudo -u nagios bash -l手动测试一个top监控系统资源。这种“四屏工作法”是我在 CentOS 6 上十年磨一剑的效率结晶。