Linux Prometheus Prometheus监控体系:内网node_exporter自动发现与配置更新脚本使用指南 墨颜丶 2023-01-02 2025-08-27 一、文档概述 本文档介绍一款用于内网 node_exporter(9100 端口)自动化管理 的 Python 脚本,核心功能包括:扫描指定内网网段的 9100 端口(node_exporter 默认端口)、检查 Prometheus 配置中是否已添加目标实例、自动补充未配置的实例、触发 Prometheus 配置热重载。脚本适配 Python 3.6 及以上版本,支持多线程扫描以提升效率,适用于内网监控场景下 node_exporter 实例的批量发现与配置。
二、核心功能清单 功能模块 具体说明 多线程端口扫描 基于线程池扫描指定内网网段,快速检测 9100 端口开放状态,支持多子网范围配置 Prometheus 配置读取 解析 Prometheus.yml,提取目标 Job 下已配置的 node_exporter 实例 配置差异对比 对比扫描结果与现有配置,筛选出未添加的新实例 配置自动更新 将新实例以标准格式写入 Prometheus.yml 的目标 Job 中 配置热重载 通过 curl 命令触发 Prometheus 配置重载(/-/reload
接口),无需重启服务
三、环境依赖 1. 运行环境 Python 版本:3.6 及以上(兼容低版本 Python 的subprocess
调用) 系统依赖:需安装curl
工具(用于触发 Prometheus 重载) 第三方库:需安装pyyaml
(用于解析 / 写入 YAML 格式的 Prometheus 配置) 2. 目标环境 内网环境:需与扫描的网段(如172.17.x.x
)互通 Prometheus:需启用--web.enable-lifecycle
参数(允许通过 API 重载配置)示例启动命令:prometheus --config.file=prometheus.yml --web.enable-lifecycle
写在启动文件里:/etc/systemd/system/prometheus.service
四、脚本配置说明 脚本开头的配置参数区 支持自定义调整,所有核心配置均集中在此处,无需修改代码逻辑,配置项说明如下:
配置项名称 类型 默认值 / 示例 说明 NETWORK_PREFIX
字符串 "172.17."
内网网段前缀,如"192.168.1."
表示扫描192.168.1.x
网段 SCAN_SUBNET_RANGES
列表(元组) [(0, 0)]
扫描的子网范围,格式为[(起始子网, 结束子网)]
,如[(0,2), (5,7)]
表示扫描172.17.0.x~172.17.2.x
和172.17.5.x~172.17.7.x
SCAN_PORT
整数 9100
扫描的端口号,固定为 node_exporter 默认端口9100
PROMETHEUS_CONFIG_PATH
字符串 "prometheus.yml"
Prometheus 配置文件的路径(绝对路径或相对路径,需确保脚本有读写权限) RELOAD_URL
字符串 "http://localhost:9090/-/reload"
Prometheus 重载接口地址,需与 Prometheus 实际地址 / 端口匹配 TARGET_JOB_NAME
字符串 "xnyh_mini"
Prometheus 中目标 Job 的名称(需与配置文件中job_name
一致) DEFAULT_TIMEOUT
整数 1
端口连接超时时间(秒),建议保持 1~3 秒以平衡速度与准确性 DEFAULT_MAX_WORKERS
整数 64
线程池最大线程数,控制扫描并发度(根据服务器性能调整,建议≤128)
定时执行:
1 30 03 * * * /usr/bin/python3 /data/scripts/Autodiscover_prot_prometheus.py >> /data/add_promttheus.log 2>&1
脚本内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 import socketimport sysimport yamlimport subprocessfrom concurrent.futures import ThreadPoolExecutor, as_completedNETWORK_PREFIX = "172.17." SCAN_SUBNET_RANGES = [(0 , 2 )] SCAN_PORT = 9100 PROMETHEUS_CONFIG_PATH = "/opt/prometheus/prometheus/prometheus.yml" RELOAD_URL = "http://localhost:9090/-/reload" TARGET_JOB_NAME = "xnyh_mini" DEFAULT_TIMEOUT = 1 DEFAULT_MAX_WORKERS = 64 def check_port (ip, port, timeout ): """检查指定IP的指定端口是否开放""" try : with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.settimeout(timeout) result = sock.connect_ex((ip, port)) return result == 0 except socket.error: return False def scan_subnets (): """扫描配置中定义的所有子网段落的指定端口""" open_targets = [] with ThreadPoolExecutor(max_workers=DEFAULT_MAX_WORKERS) as executor: for subnet in SCAN_SUBNET_RANGES: start_subnet, end_subnet = subnet for current_subnet in range (start_subnet, end_subnet + 1 ): current_prefix = f"{NETWORK_PREFIX} {current_subnet} ." future_to_ip = { executor.submit(check_port, f"{current_prefix} {i} " , SCAN_PORT, DEFAULT_TIMEOUT): f"{current_prefix} {i} :{SCAN_PORT} " for i in range (1 , 255 ) } for future in as_completed(future_to_ip): target = future_to_ip[future] try : if future.result(): print (f"发现开放端口: {target} " ) open_targets.append(target) except Exception as exc: print (f"扫描{target} 时出错: {exc} " ) return open_targets def get_existing_targets (): """从prometheus配置文件中获取已存在的targets""" try : with open (PROMETHEUS_CONFIG_PATH, 'r' ) as f: config = yaml.safe_load(f) for job in config.get('scrape_configs' , []): if job.get('job_name' ) == TARGET_JOB_NAME: existing = set () for static_config in job.get('static_configs' , []): existing.update(static_config.get('targets' , [])) return existing return set () except Exception as e: print (f"读取prometheus配置失败: {e} " ) sys.exit(1 ) def add_new_targets (new_targets ): """向prometheus配置添加新的targets""" if not new_targets: return False try : with open (PROMETHEUS_CONFIG_PATH, 'r' ) as f: config = yaml.safe_load(f) for job in config.get('scrape_configs' , []): if job.get('job_name' ) == TARGET_JOB_NAME: for target in new_targets: job['static_configs' ].append({'targets' : [target]}) print (f"已添加新目标: {target} " ) with open (PROMETHEUS_CONFIG_PATH, 'w' ) as f: yaml.dump(config, f, sort_keys=False , default_flow_style=False , allow_unicode=True ) return True return False except Exception as e: print (f"更新prometheus配置失败: {e} " ) return False def reload_prometheus (): """发送重新加载请求到prometheus""" try : result = subprocess.run( ['curl' , '-X' , 'POST' , RELOAD_URL], check=True , stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) if result.returncode == 0 : print ("Prometheus配置已成功重新加载" ) else : print ("重新加载Prometheus失败:" , result.stderr) return False return True except subprocess.CalledProcessError as e: print (f"重新加载Prometheus失败: {e.stderr} " ) return False def main (): print (f"开始扫描网段 {NETWORK_PREFIX} 下的子网 {SCAN_SUBNET_RANGES} 的 {SCAN_PORT} 端口...\n" ) open_targets = scan_subnets() if not open_targets: print ("未发现开放的9100端口" ) return existing_targets = get_existing_targets() print (f"\n已存在的目标数量: {len (existing_targets)} " ) new_targets = [t for t in open_targets if t not in existing_targets] if not new_targets: print ("没有需要添加的新目标" ) return print (f"\n需要添加的新目标数量: {len (new_targets)} " ) if add_new_targets(new_targets): reload_prometheus() if __name__ == "__main__" : main()
五、脚本执行流程 1. 执行命令 脚本无需命令行参数(扫描范围已在配置中定义),直接运行即可:
2. 详细流程 初始化配置 :加载开头定义的网段、端口、Job 名称等配置参数。多线程端口扫描:按SCAN_SUBNET_RANGES
遍历子网,生成172.17.x.1~172.17.x.254
的 IP 列表。 线程池提交check_port
任务,检测每个 IP 的 9100 端口是否开放,收集开放的IP:端口
列表。 读取现有配置:解析prometheus.yml
,找到job_name=TARGET_JOB_NAME
的 Job。 提取该 Job 下static_configs
中的所有targets
,去重后生成现有实例集合。 筛选新实例 :对比扫描结果与现有实例,得到需新增的目标列表。更新配置文件:若存在新实例,以- targets: ['IP:端口']
格式写入 Prometheus.yml 的目标 Job 中。 保持 YAML 格式规范(禁用流式风格、保留原有键顺序)。 触发配置重载 :通过curl -X POST RELOAD_URL
调用 Prometheus 接口,热重载配置。六、常见问题与解决方案 1. 报错:TypeError: __init__() got an unexpected keyword argument 'capture_output'
原因 :Python 3.6 及以下版本不支持subprocess.run()
的capture_output
参数。解决方案 :使用stdout=subprocess.PIPE
和stderr=subprocess.PIPE
替代,脚本已默认适配(见reload_prometheus
函数)。2. 报错:读取prometheus配置失败: [Errno 13] Permission denied
原因 :脚本对PROMETHEUS_CONFIG_PATH
指向的文件无读写权限。解决方案 :执行chmod +rwx prometheus.yml
(或对应路径)赋予权限,或使用 sudo 运行脚本。3. 扫描到实例但未添加到配置 原因 1:Prometheus.yml 中未找到job_name=TARGET_JOB_NAME
的 Job。解决方案:检查TARGET_JOB_NAME
是否与 Prometheus 配置中的 Job 名称完全一致(区分大小写)。 原因 2:实例已存在于配置中(可能格式不同,如IP:9100
与IP: 9100
)。解决方案:脚本会自动去重,无需手动处理,若仍有问题可检查配置文件中的目标格式。 4. 重载 Prometheus 失败:curl: (7) Failed to connect to localhost port 9090: Connection refused
原因 1:Prometheus 未启动或端口错误。解决方案:检查 Prometheus 进程状态(ps -ef | grep prometheus
),确认端口与RELOAD_URL
一致。 原因 2:Prometheus 未启用--web.enable-lifecycle
参数。解决方案:重启 Prometheus 并添加该参数,确保重载接口可用。 七、注意事项 配置备份 :脚本更新 Prometheus.yml 时会移除原有注释(YAML 解析器限制),建议首次运行前备份配置文件(如cp prometheus.yml prometheus.yml.bak
)。网段规划 :避免扫描过大的网段(如[(0,255)]
),建议按业务分区扫描(如[(0,10), (20,30)]
),防止并发过高导致网络拥堵。权限控制 :脚本需读写 Prometheus 配置文件、执行 curl 命令,建议使用非 root 用户运行(需确保对应权限),避免权限过高带来安全风险。日志记录 :脚本仅在控制台输出信息,若需持久化日志,可将执行结果重定向到文件(如python 脚本名.py > scan_log.txt 2>&1
)。八、触类旁通-端口扫描 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 import socketimport sysfrom concurrent.futures import ThreadPoolExecutor, as_completedNETWORK_PREFIX = "172.17." DEFAULT_PORT = 22 DEFAULT_TIMEOUT = 1 DEFAULT_MAX_WORKERS = 64 def check_port (ip, port, timeout ): """ 检查指定IP的指定端口是否开放。 :param ip: 要检查的IP地址 :param port: 要检查的端口号 :param timeout: 连接超时时间 :return: 如果端口开放,返回True,否则返回False """ try : with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.settimeout(timeout) result = sock.connect_ex((ip, port)) if result == 0 : return True except socket.error: pass return False def scan_subnet (network_prefix, subnet_range, port, timeout, max_workers ): """ 扫描指定范围内的多个子网中指定端口的开放情况。 :param network_prefix: 网段前缀,例如 "10.4." :param subnet_range: 子网范围,例如 (0, 99) :param port: 要扫描的端口号 :param timeout: 连接超时时间 :param max_workers: 最大线程数 """ open_ips = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: for subnet in range (subnet_range[0 ], subnet_range[1 ] + 1 ): current_prefix = f"{network_prefix} {subnet} ." future_to_ip = {executor.submit(check_port, f"{current_prefix} {i} " , port, timeout): f"{current_prefix} {i} " for i in range (1 , 255 )} for future in as_completed(future_to_ip): ip = future_to_ip[future] try : if future.result(): print (f"端口 {port} 在 {ip} 上是开放的。" ) open_ips.append(ip) except Exception as exc: print (f"{ip} 生成异常: {exc} " ) print ("\n扫描完成。以下IP的端口开放:" ) for ip in open_ips: print (ip) def parse_arguments (): """ 解析命令行参数,获取子网范围。 :return: 起始子网和结束子网 """ if len (sys.argv) != 3 : print ("使用方法: python port_scan.py 起始子网 结束子网" ) print ("例如: python port_scan.py 0 99" ) sys.exit(1 ) try : start = int (sys.argv[1 ]) end = int (sys.argv[2 ]) if start < 0 or end > 255 or start > end: raise ValueError return (start, end) except ValueError: print ("子网范围必须是0到255之间的整数,且起始子网不大于结束子网。" ) sys.exit(1 ) if __name__ == "__main__" : subnet_range = parse_arguments() NETWORK_PREFIX = NETWORK_PREFIX PORT = DEFAULT_PORT TIMEOUT = DEFAULT_TIMEOUT MAX_WORKERS = DEFAULT_MAX_WORKERS print (f"开始扫描网段 {NETWORK_PREFIX} {subnet_range[0 ]} .0/24 到 {NETWORK_PREFIX} {subnet_range[1 ]} .0/24 的端口 {PORT} 。\n" ) scan_subnet(NETWORK_PREFIX, subnet_range, PORT, TIMEOUT, MAX_WORKERS)
执行命令: