Jenkins设备监控系统教程
1. 设备监控的目的
在持续集成和自动化测试环境中,确保测试设备的可用性至关重要。设备监控的主要目的包括:
- 提高测试可靠性:及时发现离线或故障设备,避免无效测试运行
- 减少人工干预:自动化监控减少手动检查设备状态的需求
- 快速故障响应:第一时间发现问题并通知相关人员
- 资源优化利用:合理分配在线设备,提高测试效率
- 提升CI/CD流程稳定性:确保构建和测试任务在健康的设备上执行
2. 整体方案实现思路
2.1 系统架构
设备监控系统基于Jenkins构建,采用分布式架构:
┌─────────────────┐ HTTP API ┌──────────────────┐
│ 中心节点 │◄───────────────┤ Jenkins Master │
└─────────┬───────┘ └──────────────────┘
│
│ 查询节点信息
▼
┌─────────────────┐ 远程执行 ┌──────────────────┐
│ 监控脚本 │───────────────►│ Jenkins Node1 │
└─────────┬───────┘ └─────────┬────────┘
│ │ 检查设备状态
│ 通知结果 ▼
▼ ┌──────────────────┐
┌─────────────────┐ │ Android/iOS设备 │
│ 钉钉通知 │ └──────────────────┘
└─────────────────┘
2.2 核心组件
- Jenkins Master:提供API接口和任务调度
- Jenkins Nodes:实际连接设备的执行节点
- 监控脚本:负责检查各节点设备状态
- 通知系统:通过钉钉机器人发送警报
2.3 工作流程
- 脚本通过Jenkins API获取所有在线节点信息
- 解析各节点的环境变量,确定连接的设备
- 通过远程执行方式在各节点上检查设备状态
- 汇总检查结果,对离线设备发出警报
3. 脚本介绍
3.1 脚本功能概述
设备监控脚本(check_devices.py)具备以下核心功能:
- 节点发现:自动发现所有在线Jenkins节点
- 设备识别:解析节点环境变量识别连接的设备
- 状态检查:针对不同类型设备执行相应的检查命令
- 结果汇总:统计所有离线设备并生成报告
- 通知发送:通过钉钉机器人发送设备状态报告
3.2 支持的设备类型
- Android设备 :通过
adb devices命令检查 - iOS设备 :通过
idevice_id -l命令检查 - NG设备:通过Python脚本和SDS库检查
- Dilu设备:通过串口存在性检查
3.3 关键技术实现
3.3.1 Jenkins API交互
使用requests库与Jenkins API交互,获取节点信息:
python
# 获取在线节点
api_url = f"{JENKINS_URL}/computer/api/json"
response = requests.get(api_url, auth=(JENKINS_USER, JENKINS_TOKEN))
3.3.2 远程命令执行
通过Jenkins的script接口在远程节点执行命令:
python
# 构造Groovy脚本在远程节点执行命令
groovy_script = f"""
def sout = new StringBuffer(), serr = new StringBuffer()
def proc = ['sh', '-c', '{command}'].execute()
proc.consumeProcessOutput(sout, serr)
proc.waitForOrKill(30000)
println "STDOUT: " + sout.toString()
println "STDERR: " + serr.toString()
println "EXIT_CODE: " + proc.exitValue()
"""
3.3.3 跨平台兼容
根据不同操作系统类型选择合适的命令执行方式:
python
if is_windows:
# Windows系统使用批处理命令
groovy_script = f"""
def sout = new StringBuffer(), serr = new StringBuffer()
def proc = ['cmd', '/c', '{command}'].execute()
// ...
"""
else:
# Unix-like系统使用shell命令
groovy_script = f"""
def sout = new StringBuffer(), serr = new StringBuffer()
def proc = ['sh', '-c', '{command}'].execute()
// ...
"""
3.4 设备检查实现
python
import requests
import re
from requests.auth import HTTPBasicAuth
# Jenkins配置
JENKINS_URL = "http://"
JENKINS_USER = "admin"
JENKINS_TOKEN = "admin123" # 密码即可
node_env_vars_type = ["android_devices_id", "ios_devices_id", "ng_devices_id", "dilu_port"]
# node_env_vars_type = ["ng_devices_id"]
# 获取Jenkins CSRF保护所需的crumb
def get_crumb(session):
crumb_url = f"{JENKINS_URL}/crumbIssuer/api/json"
try:
response = session.get(crumb_url, timeout=10)
if response.status_code == 200:
crumb_data = response.json()
return crumb_data["crumbRequestField"], crumb_data["crumb"]
else:
print(f"⚠️ 获取crumb失败,状态码:{response.status_code}")
return "", ""
except Exception as e:
print(f"⚠️ 获取crumb异常:{str(e)[:50]}")
return "", ""
# 在远程节点上执行命令
def execute_remote_command(node_name, command, is_windows=False):
# 创建会话并设置认证信息
session = requests.Session()
session.auth = HTTPBasicAuth(JENKINS_USER, JENKINS_TOKEN)
# 获取CSRF Token
crumb_header, crumb_value = get_crumb(session)
# 设置请求头部
headers = {"Content-Type": "application/x-www-form-urlencoded"}
if crumb_header and crumb_value:
headers[crumb_header] = crumb_value
# 构造远程执行命令的URL
exec_url = f"{JENKINS_URL}/computer/{node_name}/scriptText"
# 根据操作系统类型选择执行方式
if is_windows:
# Windows系统使用批处理命令
groovy_script = f"""
def sout = new StringBuffer(), serr = new StringBuffer()
def proc = ['cmd', '/c', '{command}'].execute()
proc.consumeProcessOutput(sout, serr)
proc.waitForOrKill(30000)
println "STDOUT: " + sout.toString()
println "STDERR: " + serr.toString()
println "EXIT_CODE: " + proc.exitValue()
"""
else:
# Unix-like系统(macOS/Linux)使用shell命令
groovy_script = f"""
def sout = new StringBuffer(), serr = new StringBuffer()
def proc = ['sh', '-c', '{command}'].execute()
proc.consumeProcessOutput(sout, serr)
proc.waitForOrKill(30000)
println "STDOUT: " + sout.toString()
println "STDERR: " + serr.toString()
println "EXIT_CODE: " + proc.exitValue()
"""
data = {
"script": groovy_script
}
try:
response = session.post(
exec_url,
headers=headers,
data=data,
timeout=30
)
response.raise_for_status()
return response.text
except Exception as e:
print(f"❌ 节点[{node_name}]执行命令失败:{command} → 错误:{str(e)[:50]}")
return f"ERROR: {str(e)[:50]}"
# 获取所有在线节点
def get_online_nodes():
# 创建 Jenkins API 请求
api_url = f"{JENKINS_URL}/computer/api/json"
# 使用 Token 进行认证
response = requests.get(api_url, auth=(JENKINS_USER, JENKINS_TOKEN))
if response.status_code != 200:
raise Exception(f"无法连接到 Jenkins: {response.status_code} - {response.text}")
data = response.json()
online_nodes = []
# 遍历在线节点
for computer in data.get("computer", []):
# 跳过离线节点
if computer.get("offline", True):
continue
node_name = computer.get("displayName")
# 获取节点详细配置
config_url = f"{JENKINS_URL}/computer/{node_name}/config.xml"
config_response = requests.get(config_url, auth=(JENKINS_USER, JENKINS_TOKEN))
if config_response.status_code != 200:
continue
config_xml = config_response.text
# 解析节点标签
node_labels = []
label_match = re.search(r"<label>(.*?)</label>", config_xml, re.DOTALL)
if label_match:
node_labels = label_match.group(1).strip().split()
# 解析节点环境变量 (根据你提供的XML结构调整解析方法)
env_vars = {}
if "<envVars" in config_xml and "</envVars>" in config_xml:
# 查找环境变量部分
env_vars_section = config_xml.split("<envVars")[1].split("</envVars>")[0]
# 使用正则表达式查找所有的键值对
# 在你提供的XML中,键值对是这样组织的:
# <string>android_devices_id</string>
# <string>10CF4809DD002B4</string>
strings = re.findall(r"<string>(.*?)</string>", env_vars_section)
# 成对处理这些字符串作为键值对
for i in range(0, len(strings), 2):
if i + 1 < len(strings):
key = strings[i].strip().lower()
value = strings[i+1].strip()
env_vars[key] = value
# 判断节点环境变量是否有node_env_vars_type里的值,如果没有则跳过
has_required_env_var = any(env_var in env_vars for env_var in node_env_vars_type)
if not has_required_env_var:
continue
# 判断节点是否为Windows系统(通过节点名称判断)
if "mac" in node_name:
is_windows = False
else:
is_windows = True
# 如果有,则获取节点环境变量值并判断是否在线
node_info = {
"name": node_name,
"labels": node_labels,
"env_vars": env_vars,
"is_windows": is_windows
}
online_nodes.append(node_info)
return online_nodes
def check_devices(node_name, devices_type, devices_id, is_windows=False):
if devices_type=="android_devices_id":
# 使用adb命令检查设备是否在线
try:
command = f"adb devices"
result = execute_remote_command(node_name, command, is_windows)
if devices_id in result:
return True
else:
return False
except Exception:
return False
if devices_type=="ios_devices_id":
# 使用idevice_id命令检查iOS设备是否在线
try:
if is_windows:
# Windows上不支持iOS设备
command = f"echo iOS devices not supported on Windows"
result = execute_remote_command(node_name, command, is_windows)
return False
else:
# 在Unix-like系统上使用idevice_id检查iOS设备
# 使用完整路径或者source profile来确保环境变量正确
command = f"source ~/.bash_profile 2>/dev/null || source ~/.zshrc 2>/dev/null; which idevice_id || echo \"idevice_id not found\""
check_result = execute_remote_command(node_name, command, is_windows)
if "idevice_id not found" in check_result:
# 尝试使用常见路径
command = f"/usr/local/bin/idevice_id -l 2>/dev/null | grep \"{devices_id}\" || /opt/homebrew/bin/idevice_id -l 2>/dev/null | grep \"{devices_id}\" || echo \"Device not found\""
result = execute_remote_command(node_name, command, is_windows)
return devices_id in result and "Device not found" not in result
else:
# 如果which找到了idevice_id,则正常使用
command = f"source ~/.bash_profile 2>/dev/null || source ~/.zshrc 2>/dev/null; idevice_id -l | grep \"{devices_id}\" || echo \"Device not found\""
result = execute_remote_command(node_name, command, is_windows)
return devices_id in result and "Device not found" not in result
except Exception as e:
print(f" iOS设备检查异常: {str(e)}")
return False
if devices_type=="ng_devices_id":
# NG设备检查需要使用Python脚本来检查
try:
sds_file_name = "SDSApplicationServer"
if is_windows:
sds_file_name = sds_file_name + ".exe"
# 根据进程名称杀死SDS然后等待10s
# 使用webtool命令查找ng_devices
kill_cmd = f"taskkill /F /IM {sds_file_name} >nul 2>&1 & timeout /t 10 >nul"
result1 = execute_remote_command(node_name, kill_cmd, is_windows)
# 执行wbtool info命令,需要先确保环境变量正确
wbtool_cmd = f"where wbtool >nul 2>&1 && wbtool info || echo \"wbtool not found\""
result2 = execute_remote_command(node_name, wbtool_cmd, is_windows)
if devices_id in result2 and "Device not found" not in result2:
return True
else:
return False
else:
# mac逻辑
kill_cmd = f"pkill -f {sds_file_name} >/dev/null 2>&1; sleep 10"
result1 = execute_remote_command(node_name, kill_cmd, is_windows)
# 执行wbtool info命令,需要先确保环境变量正确
wbtool_cmd = f"source ~/.bash_profile 2>/dev/null || source ~/.zshrc 2>/dev/null; which wbtool || echo \"wbtool not found\""
check_result = execute_remote_command(node_name, wbtool_cmd, is_windows)
if "wbtool not found" in check_result:
# 尝试使用常见路径
wbtool_cmd = f"source ~/.bash_profile 2>/dev/null || source ~/.zshrc 2>/dev/null; /usr/local/bin/wbtool info 2>/dev/null | grep \"{devices_id}\" || /opt/homebrew/bin/wbtool info 2>/dev/null | grep \"{devices_id}\" || echo \"Device not found\""
result2 = execute_remote_command(node_name, wbtool_cmd, is_windows)
else:
# 如果which找到了wbtool,则正常使用
wbtool_cmd = f"source ~/.bash_profile 2>/dev/null || source ~/.zshrc 2>/dev/null; wbtool info | grep \"{devices_id}\" || echo \"Device not found\""
result2 = execute_remote_command(node_name, wbtool_cmd, is_windows)
if devices_id in result2 and "Device not found" not in result2:
return True
else:
return False
except Exception as e:
print(f" NG设备检查异常: {str(e)}")
return False
if devices_type=="dilu_port":
try:
# 检查串口是否存在
if is_windows: # Windows
command = f"mode"
result = execute_remote_command(node_name, command, is_windows)
if devices_id.upper() in result.upper():
return True
else: # Unix-like systems
command = f"ls /dev/{devices_id}"
result = execute_remote_command(node_name, command, is_windows)
# 如果命令成功执行且没有错误信息,则设备存在
return "No such file" not in result and "ERROR" not in result
return False
except Exception:
return False
# 发送报告
def send_report(offline_devices):
if not offline_devices:
print("所有设备均在线")
return
# 构建报告内容
title = "【Device Check Report】"
text = "以下设备不在线:\n"
for device in offline_devices:
text += f"- 节点名称: {device['node_name']}, 设备类型: {device['device_type']}, 设备ID: {device['device_id']}\n"
print(text)
# 发送钉钉通知
try:
# 钉钉配置机器人获取
webhook_url = 'https://oapi.dingtalk.com/robot/send?access_token=666'
# 构建钉钉消息格式
message = {
"msgtype": "markdown",
"markdown": {
"title": title,
"text": text
},
"at": {
"atMobiles": [],
"isAtAll": False
}
}
# 发送POST请求
response = requests.post(webhook_url, json=message)
if response.status_code == 200:
print("钉钉通知发送成功")
else:
print(f"钉钉通知发送失败,状态码: {response.status_code}")
except Exception as e:
print(f"发送钉钉通知时发生异常: {e}")
def main():
"""
主函数,执行设备检查流程
"""
offline_devices = []
# 获取所有在线节点
try:
nodes = get_online_nodes()
print(f"发现 {len(nodes)} 个在线节点")
except Exception as e:
print(f"获取节点信息失败: {e}")
return
# 遍历每个节点,检查其设备状态
for node in nodes:
node_name = node["name"]
env_vars = node["env_vars"]
is_windows = node["is_windows"]
print(f"检查节点: {node_name}")
# 检查每种类型的设备
for device_type in node_env_vars_type:
if device_type in env_vars:
device_id = env_vars[device_type]
print(f" 检查设备类型: {device_type}, ID: {device_id}")
is_online = check_devices(node_name, device_type, device_id, is_windows)
if not is_online:
offline_devices.append({
"node_name": node_name,
"device_type": device_type,
"device_id": device_id
})
print(f" 设备不在线!")
else:
print(f" 设备在线")
# 发送报告
send_report(offline_devices)
if __name__ == "__main__":
main()
4. Jenkins定时触发配置
4.1 创建定时任务
- 登录Jenkins管理界面
- 点击"新建任务"
- 输入任务名称,例如"Device-Monitor"
- 选择"构建一个自由风格的软件项目"
- 点击"确定"
4.2 配置定时触发
在任务配置页面:
- 找到"构建触发器"部分
- 勾选"定时构建"
- 在日程表中输入定时表达式:
常用表达式示例:
H/30 * * * *:每30分钟执行一次H 8 * * *:每天早上8点执行一次H 8 * * 1-5:周一至周五每天早上8点执行一次H H * * *:每天随机时间执行一次
4.3 配置构建步骤
- 在"构建"部分点击"增加构建步骤"
- 选择"执行Windows批处理命令"或"Execute shell"(根据主节点系统类型)
- 输入执行脚本的命令:
bash
python D:\project\SuuntoTest\jenkins\check_devices.py
4.4 配置环境变量
在"常规"部分勾选"参数化构建过程",添加以下参数:
- JENKINS_URL: Jenkins服务器地址
- JENKINS_USER: Jenkins用户名
- JENKINS_TOKEN: Jenkins API token
或者在"构建环境"中配置相关环境变量。