pytbox 0.0.1__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pytbox might be problematic. Click here for more details.
- pytbox/alert/alert_handler.py +139 -0
- pytbox/alert/ping.py +24 -0
- pytbox/alicloud/sls.py +9 -14
- pytbox/base.py +121 -0
- pytbox/categraf/build_config.py +143 -0
- pytbox/categraf/instances.toml +39 -0
- pytbox/categraf/jinja2/__init__.py +6 -0
- pytbox/categraf/jinja2/input.cpu/cpu.toml.j2 +5 -0
- pytbox/categraf/jinja2/input.disk/disk.toml.j2 +11 -0
- pytbox/categraf/jinja2/input.diskio/diskio.toml.j2 +6 -0
- pytbox/categraf/jinja2/input.dns_query/dns_query.toml.j2 +12 -0
- pytbox/categraf/jinja2/input.http_response/http_response.toml.j2 +9 -0
- pytbox/categraf/jinja2/input.mem/mem.toml.j2 +5 -0
- pytbox/categraf/jinja2/input.net/net.toml.j2 +11 -0
- pytbox/categraf/jinja2/input.net_response/net_response.toml.j2 +9 -0
- pytbox/categraf/jinja2/input.ping/ping.toml.j2 +11 -0
- pytbox/categraf/jinja2/input.prometheus/prometheus.toml.j2 +12 -0
- pytbox/categraf/jinja2/input.snmp/cisco_interface.toml.j2 +96 -0
- pytbox/categraf/jinja2/input.snmp/cisco_system.toml.j2 +41 -0
- pytbox/categraf/jinja2/input.snmp/h3c_interface.toml.j2 +96 -0
- pytbox/categraf/jinja2/input.snmp/h3c_system.toml.j2 +41 -0
- pytbox/categraf/jinja2/input.snmp/huawei_interface.toml.j2 +96 -0
- pytbox/categraf/jinja2/input.snmp/huawei_system.toml.j2 +41 -0
- pytbox/categraf/jinja2/input.snmp/ruijie_interface.toml.j2 +96 -0
- pytbox/categraf/jinja2/input.snmp/ruijie_system.toml.j2 +41 -0
- pytbox/categraf/jinja2/input.vsphere/vsphere.toml.j2 +211 -0
- pytbox/cli/__init__.py +7 -0
- pytbox/cli/categraf/__init__.py +7 -0
- pytbox/cli/categraf/commands.py +55 -0
- pytbox/cli/commands/vm.py +22 -0
- pytbox/cli/common/__init__.py +6 -0
- pytbox/cli/common/options.py +42 -0
- pytbox/cli/common/utils.py +269 -0
- pytbox/cli/formatters/__init__.py +7 -0
- pytbox/cli/formatters/output.py +155 -0
- pytbox/cli/main.py +24 -0
- pytbox/cli.py +9 -0
- pytbox/database/mongo.py +99 -0
- pytbox/database/victoriametrics.py +404 -0
- pytbox/dida365.py +11 -17
- pytbox/excel.py +64 -0
- pytbox/feishu/endpoints.py +12 -9
- pytbox/{logger.py → log/logger.py} +78 -30
- pytbox/{victorialog.py → log/victorialog.py} +2 -2
- pytbox/mail/alimail.py +142 -0
- pytbox/mail/client.py +171 -0
- pytbox/mail/mail_detail.py +30 -0
- pytbox/mingdao.py +164 -0
- pytbox/network/meraki.py +537 -0
- pytbox/notion.py +731 -0
- pytbox/pyjira.py +612 -0
- pytbox/utils/cronjob.py +79 -0
- pytbox/utils/env.py +2 -2
- pytbox/utils/load_config.py +132 -0
- pytbox/utils/load_vm_devfile.py +45 -0
- pytbox/utils/response.py +1 -1
- pytbox/utils/richutils.py +31 -0
- pytbox/utils/timeutils.py +479 -14
- pytbox/vmware.py +120 -0
- pytbox/win/ad.py +30 -0
- {pytbox-0.0.1.dist-info → pytbox-0.3.1.dist-info}/METADATA +13 -3
- pytbox-0.3.1.dist-info/RECORD +72 -0
- pytbox-0.3.1.dist-info/entry_points.txt +2 -0
- pytbox/common/base.py +0 -0
- pytbox/victoriametrics.py +0 -37
- pytbox-0.0.1.dist-info/RECORD +0 -21
- {pytbox-0.0.1.dist-info → pytbox-0.3.1.dist-info}/WHEEL +0 -0
- {pytbox-0.0.1.dist-info → pytbox-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
输出格式化器 - 支持多种格式和 rich 美化
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, Union
|
|
7
|
+
from ..common.utils import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OutputFormatter:
|
|
11
|
+
"""输出格式化器"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def format_data(data: Union[Dict[str, Any], list], format_type: str = 'toml') -> str:
|
|
15
|
+
"""格式化数据
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
data: 要格式化的数据
|
|
19
|
+
format_type: 输出格式 ('toml', 'json', 'yaml')
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
str: 格式化后的字符串
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ImportError: 缺少必要的依赖
|
|
26
|
+
ValueError: 不支持的格式
|
|
27
|
+
"""
|
|
28
|
+
logger.debug(f"格式化数据为 {format_type} 格式")
|
|
29
|
+
|
|
30
|
+
if format_type == 'json':
|
|
31
|
+
return OutputFormatter._format_json(data)
|
|
32
|
+
elif format_type == 'yaml':
|
|
33
|
+
return OutputFormatter._format_yaml(data)
|
|
34
|
+
elif format_type == 'toml':
|
|
35
|
+
return OutputFormatter._format_toml(data)
|
|
36
|
+
else:
|
|
37
|
+
raise ValueError(f"不支持的格式: {format_type}")
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _format_json(data: Any) -> str:
|
|
41
|
+
"""格式化为 JSON"""
|
|
42
|
+
try:
|
|
43
|
+
result = json.dumps(data, indent=2, ensure_ascii=False)
|
|
44
|
+
logger.debug(f"JSON 格式化完成,长度: {len(result)} 字符")
|
|
45
|
+
return result
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.error(f"JSON 格式化失败: {e}")
|
|
48
|
+
raise
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def _format_yaml(data: Any) -> str:
|
|
52
|
+
"""格式化为 YAML"""
|
|
53
|
+
try:
|
|
54
|
+
import yaml
|
|
55
|
+
result = yaml.dump(
|
|
56
|
+
data,
|
|
57
|
+
default_flow_style=False,
|
|
58
|
+
allow_unicode=True,
|
|
59
|
+
sort_keys=False,
|
|
60
|
+
indent=2
|
|
61
|
+
)
|
|
62
|
+
logger.debug(f"YAML 格式化完成,长度: {len(result)} 字符")
|
|
63
|
+
return result
|
|
64
|
+
except ImportError:
|
|
65
|
+
error_msg = "需要安装 pyyaml: pip install pyyaml"
|
|
66
|
+
logger.error(error_msg)
|
|
67
|
+
raise ImportError(error_msg)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"YAML 格式化失败: {e}")
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def _format_toml(data: Any) -> str:
|
|
74
|
+
"""格式化为 TOML"""
|
|
75
|
+
try:
|
|
76
|
+
import toml
|
|
77
|
+
result = toml.dumps(data)
|
|
78
|
+
logger.debug(f"TOML 格式化完成,长度: {len(result)} 字符")
|
|
79
|
+
return result
|
|
80
|
+
except ImportError:
|
|
81
|
+
try:
|
|
82
|
+
# Python 3.11+ 的 tomllib 只能读取,不能写入
|
|
83
|
+
import tomllib
|
|
84
|
+
error_msg = "需要安装 toml 库来支持 TOML 输出: pip install toml"
|
|
85
|
+
logger.error(error_msg)
|
|
86
|
+
raise ImportError(error_msg)
|
|
87
|
+
except ImportError:
|
|
88
|
+
error_msg = "需要安装 toml: pip install toml"
|
|
89
|
+
logger.error(error_msg)
|
|
90
|
+
raise ImportError(error_msg)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f"TOML 格式化失败: {e}")
|
|
93
|
+
raise
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def format_template_list(templates: list) -> str:
|
|
97
|
+
"""格式化模板列表"""
|
|
98
|
+
if not templates:
|
|
99
|
+
return "未找到模板文件"
|
|
100
|
+
|
|
101
|
+
logger.debug(f"格式化 {len(templates)} 个模板")
|
|
102
|
+
|
|
103
|
+
# 按文件类型分组
|
|
104
|
+
groups = {}
|
|
105
|
+
for template in templates:
|
|
106
|
+
if '.' in template:
|
|
107
|
+
ext = template.split('.')[-1]
|
|
108
|
+
if ext not in groups:
|
|
109
|
+
groups[ext] = []
|
|
110
|
+
groups[ext].append(template)
|
|
111
|
+
else:
|
|
112
|
+
if 'other' not in groups:
|
|
113
|
+
groups['other'] = []
|
|
114
|
+
groups['other'].append(template)
|
|
115
|
+
|
|
116
|
+
result = []
|
|
117
|
+
result.append("可用模板:")
|
|
118
|
+
|
|
119
|
+
for ext, files in sorted(groups.items()):
|
|
120
|
+
result.append(f"\n{ext.upper()} 模板:")
|
|
121
|
+
for template in sorted(files):
|
|
122
|
+
result.append(f" - {template}")
|
|
123
|
+
|
|
124
|
+
return "\n".join(result)
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def format_config_summary(config: Dict[str, Any]) -> str:
|
|
128
|
+
"""格式化配置摘要"""
|
|
129
|
+
logger.debug("生成配置摘要")
|
|
130
|
+
|
|
131
|
+
result = []
|
|
132
|
+
result.append("配置摘要:")
|
|
133
|
+
|
|
134
|
+
for service, service_config in config.items():
|
|
135
|
+
result.append(f"\n📊 {service.upper()} 服务:")
|
|
136
|
+
|
|
137
|
+
if isinstance(service_config, dict):
|
|
138
|
+
for key, value in service_config.items():
|
|
139
|
+
if isinstance(value, list):
|
|
140
|
+
result.append(f" {key}: {len(value)} 项")
|
|
141
|
+
# 显示前几个项目
|
|
142
|
+
for i, item in enumerate(value[:3]):
|
|
143
|
+
if isinstance(item, dict):
|
|
144
|
+
item_keys = list(item.keys())[:2] # 只显示前两个键
|
|
145
|
+
result.append(f" - 项目 {i+1}: {item_keys}")
|
|
146
|
+
else:
|
|
147
|
+
result.append(f" - {item}")
|
|
148
|
+
if len(value) > 3:
|
|
149
|
+
result.append(f" ... 还有 {len(value) - 3} 项")
|
|
150
|
+
else:
|
|
151
|
+
result.append(f" {key}: {value}")
|
|
152
|
+
else:
|
|
153
|
+
result.append(f" 值: {service_config}")
|
|
154
|
+
|
|
155
|
+
return "\n".join(result)
|
pytbox/cli/main.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Pytbox 主命令行入口
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from .categraf import categraf_group
|
|
8
|
+
from .commands.vm import vm_group
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
@click.version_option()
|
|
13
|
+
def main():
|
|
14
|
+
"""Pytbox 命令行工具集合"""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 注册子命令组
|
|
19
|
+
main.add_command(categraf_group, name='categraf')
|
|
20
|
+
main.add_command(vm_group, name='vm')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
pytbox/cli.py
ADDED
pytbox/database/mongo.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import pymongo
|
|
4
|
+
from ..utils.timeutils import TimeUtils
|
|
5
|
+
from pytbox.utils import timeutils
|
|
6
|
+
|
|
7
|
+
class Mongo:
|
|
8
|
+
'''
|
|
9
|
+
当前主要使用的类
|
|
10
|
+
'''
|
|
11
|
+
def __init__(self, host: str=None, port: int=27017, username: str=None, password: str=None, auto_source: str=None, db_name: str='automate', collection: str=None):
|
|
12
|
+
self.client = self._create_client(host, port, username, password, auto_source)
|
|
13
|
+
self.collection = self.client[db_name][collection]
|
|
14
|
+
|
|
15
|
+
def _create_client(self, host, port, username, password, auto_source):
|
|
16
|
+
'''
|
|
17
|
+
创建客户端
|
|
18
|
+
'''
|
|
19
|
+
return pymongo.MongoClient(host=host,
|
|
20
|
+
port=port,
|
|
21
|
+
username=username,
|
|
22
|
+
password=password,
|
|
23
|
+
authSource=auto_source)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check_alarm_exist(self, event_type, event_content) -> bool:
|
|
27
|
+
'''
|
|
28
|
+
_summary_
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
event_content (_type_): 告警内容
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
bool: 如果为 True, 表示允许插入告警
|
|
35
|
+
'''
|
|
36
|
+
if event_type == 'trigger':
|
|
37
|
+
query = { "event_content": event_content }
|
|
38
|
+
fields = {"event_name": 1, "event_time": 1, "resolved_time": 1}
|
|
39
|
+
result = self.collection.find(query, fields).sort({ "_id": pymongo.DESCENDING }).limit(1)
|
|
40
|
+
if self.collection.count_documents(query) == 0:
|
|
41
|
+
return True
|
|
42
|
+
else:
|
|
43
|
+
for doc in result:
|
|
44
|
+
if 'resolved_time' in doc:
|
|
45
|
+
# 当前没有告警, 可以插入数据
|
|
46
|
+
return True
|
|
47
|
+
elif event_type == 'resolved':
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
def query_alert_not_resolved(self, event_name: str=None):
|
|
51
|
+
query = {
|
|
52
|
+
"$or": [
|
|
53
|
+
{"resolved_time": { "$exists": False }}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
if event_name:
|
|
57
|
+
query['event_name'] = event_name
|
|
58
|
+
return self.collection.find(query)
|
|
59
|
+
|
|
60
|
+
def recent_alerts(self, event_content: str) -> str:
|
|
61
|
+
'''
|
|
62
|
+
获取最近 10 次告警
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
alarm_content (str): _description_
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
str: _description_
|
|
69
|
+
'''
|
|
70
|
+
|
|
71
|
+
query = {
|
|
72
|
+
"event_content": event_content,
|
|
73
|
+
'resolved_time': {
|
|
74
|
+
'$exists': True, # 字段必须存在
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
fields = {"_id": 0, 'event_time': 1, 'resolved_time': 1}
|
|
78
|
+
results = self.collection.find(query, fields).sort('event_time', -1)
|
|
79
|
+
|
|
80
|
+
alarm_list = []
|
|
81
|
+
for result in results:
|
|
82
|
+
duration_minute = '持续 ' + str(int((result['resolved_time'] - result['event_time']).total_seconds() / 60)) + ' 分钟'
|
|
83
|
+
alarm_list.append('触发时间: ' + TimeUtils.convert_timeobj_to_str(timeobj=result['event_time']) + ' ' + duration_minute)
|
|
84
|
+
|
|
85
|
+
alarm_str = '\n'.join(alarm_list)
|
|
86
|
+
|
|
87
|
+
alarm_str_display_threshold = 10
|
|
88
|
+
|
|
89
|
+
if len(alarm_list) > alarm_str_display_threshold:
|
|
90
|
+
# 如果告警超过 10 个
|
|
91
|
+
alarm_counter = alarm_str_display_threshold
|
|
92
|
+
alarm_str = '\n'.join(alarm_list[:alarm_str_display_threshold])
|
|
93
|
+
else:
|
|
94
|
+
# 如果不超过 10 个
|
|
95
|
+
alarm_counter = len(alarm_list)
|
|
96
|
+
alarm_str = '\n'.join(alarm_list)
|
|
97
|
+
|
|
98
|
+
return '该告警出现过' + str(len(alarm_list)) + f'次\n最近 {alarm_counter} 次告警如下: \n' + alarm_str
|
|
99
|
+
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
from typing import Literal, Optional, Dict, List
|
|
6
|
+
import requests
|
|
7
|
+
from ..utils.response import ReturnResponse
|
|
8
|
+
from ..utils.load_vm_devfile import load_dev_file
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VictoriaMetrics:
|
|
12
|
+
|
|
13
|
+
def __init__(self, url: str='', timeout: int=3, env: str='prod') -> None:
|
|
14
|
+
self.url = url
|
|
15
|
+
self.timeout = timeout
|
|
16
|
+
self.session = requests.Session()
|
|
17
|
+
self.session.headers.update({
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
'Accept': 'application/json'
|
|
20
|
+
})
|
|
21
|
+
self.env = env
|
|
22
|
+
|
|
23
|
+
def insert(self, metric_name: str = '', labels: Dict[str, str] = None,
|
|
24
|
+
value: List[float] = None, timestamp: int = None) -> ReturnResponse:
|
|
25
|
+
"""插入指标数据。
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
metric_name: 指标名称
|
|
29
|
+
labels: 标签字典
|
|
30
|
+
value: 值列表
|
|
31
|
+
timestamp: 时间戳(毫秒),默认为当前时间
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
requests.RequestException: 当请求失败时抛出
|
|
35
|
+
"""
|
|
36
|
+
if labels is None:
|
|
37
|
+
labels = {}
|
|
38
|
+
if value is None:
|
|
39
|
+
value = 1
|
|
40
|
+
if timestamp is None:
|
|
41
|
+
timestamp = int(time.time() * 1000)
|
|
42
|
+
|
|
43
|
+
url = f"{self.url}/api/v1/import"
|
|
44
|
+
data = {
|
|
45
|
+
"metric": {
|
|
46
|
+
"__name__": metric_name,
|
|
47
|
+
**labels
|
|
48
|
+
},
|
|
49
|
+
"values": [value],
|
|
50
|
+
"timestamps": [timestamp]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
response = requests.post(url, json=data, timeout=self.timeout)
|
|
55
|
+
return ReturnResponse(code=0, msg=f"数据插入成功,状态码: {response.status_code}, metric_name: {metric_name}, labels: {labels}, value: {value}, timestamp: {timestamp}")
|
|
56
|
+
except requests.RequestException as e:
|
|
57
|
+
return ReturnResponse(code=1, msg=f"数据插入失败: {e}")
|
|
58
|
+
|
|
59
|
+
def query(self, query: str=None, output_format: Literal['json']=None) -> ReturnResponse:
|
|
60
|
+
'''
|
|
61
|
+
查询指标数据
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
query (str): 查询语句
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
dict: 查询结果
|
|
68
|
+
'''
|
|
69
|
+
url = f"{self.url}/prometheus/api/v1/query"
|
|
70
|
+
r = requests.get(
|
|
71
|
+
url,
|
|
72
|
+
timeout=self.timeout,
|
|
73
|
+
params={"query": query}
|
|
74
|
+
)
|
|
75
|
+
res_json = r.json()
|
|
76
|
+
status = res_json.get("status")
|
|
77
|
+
result = res_json.get("data", {}).get("result", [])
|
|
78
|
+
is_json = output_format == 'json'
|
|
79
|
+
|
|
80
|
+
if status == "success":
|
|
81
|
+
if result:
|
|
82
|
+
code = 0
|
|
83
|
+
msg = f"[{query}] 查询成功!"
|
|
84
|
+
data = result
|
|
85
|
+
else:
|
|
86
|
+
code = 2
|
|
87
|
+
msg = f"[{query}] 没有查询到结果"
|
|
88
|
+
data = res_json
|
|
89
|
+
else:
|
|
90
|
+
code = 1
|
|
91
|
+
msg = f"[{query}] 查询失败: {res_json.get('error')}"
|
|
92
|
+
data = res_json
|
|
93
|
+
|
|
94
|
+
resp = ReturnResponse(code=code, msg=msg, data=data)
|
|
95
|
+
|
|
96
|
+
if is_json:
|
|
97
|
+
json_result = json.dumps(resp.__dict__, ensure_ascii=False)
|
|
98
|
+
return json_result
|
|
99
|
+
else:
|
|
100
|
+
return resp
|
|
101
|
+
|
|
102
|
+
def query_range(self, query):
|
|
103
|
+
'''
|
|
104
|
+
查询指标数据
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
query (str): 查询语句
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
dict: 查询结果
|
|
111
|
+
'''
|
|
112
|
+
url = f"{self.url}/prometheus/api/v1/query_range"
|
|
113
|
+
|
|
114
|
+
data = {
|
|
115
|
+
'query': query,
|
|
116
|
+
'start': '-1d',
|
|
117
|
+
'step': '1h'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
r = requests.post(url, data=data, timeout=self.timeout)
|
|
121
|
+
res_json = r.json()
|
|
122
|
+
print(res_json)
|
|
123
|
+
# status = res_json.get("status")
|
|
124
|
+
# result = res_json.get("data", {}).get("result", [])
|
|
125
|
+
# is_json = output_format == 'json'
|
|
126
|
+
|
|
127
|
+
# if status == "success":
|
|
128
|
+
# if result:
|
|
129
|
+
# code = 0
|
|
130
|
+
# msg = f"[{query}] 查询成功!"
|
|
131
|
+
# data = result
|
|
132
|
+
# else:
|
|
133
|
+
# code = 2
|
|
134
|
+
# msg = f"[{query}] 没有查询到结果"
|
|
135
|
+
# data = res_json
|
|
136
|
+
# else:
|
|
137
|
+
# code = 1
|
|
138
|
+
# msg = f"[{query}] 查询失败: {res_json.get('error')}"
|
|
139
|
+
# data = res_json
|
|
140
|
+
|
|
141
|
+
# resp = ReturnResponse(code=code, msg=msg, data=data)
|
|
142
|
+
|
|
143
|
+
# if is_json:
|
|
144
|
+
# json_result = json.dumps(resp.__dict__, ensure_ascii=False)
|
|
145
|
+
# return json_result
|
|
146
|
+
# else:
|
|
147
|
+
# return resp
|
|
148
|
+
def get_labels(self, metric_name: str) -> ReturnResponse:
|
|
149
|
+
url = f"{self.url}/api/v1/series?match[]={metric_name}"
|
|
150
|
+
response = requests.get(url, timeout=self.timeout)
|
|
151
|
+
results = response.json()
|
|
152
|
+
if results['status'] == 'success':
|
|
153
|
+
return ReturnResponse(code=0, msg=f"metric name: {metric_name} 获取到 {len(results['data'])} 条数据", data=results['data'])
|
|
154
|
+
else:
|
|
155
|
+
return ReturnResponse(code=1, msg=f"metric name: {metric_name} 查询失败")
|
|
156
|
+
|
|
157
|
+
def check_ping_result(self, target: str, last_minute: int=10, env: str='prod', dev_file: str='') -> ReturnResponse:
|
|
158
|
+
'''
|
|
159
|
+
检查ping结果
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
target (str): 目标地址
|
|
163
|
+
last_minute (int, optional): 最近多少分钟. Defaults to 10.
|
|
164
|
+
env (str, optional): 环境. Defaults to 'prod'.
|
|
165
|
+
dev_file (str, optional): 开发文件. Defaults to ''.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
ReturnResponse:
|
|
169
|
+
code = 0 正常, code = 1 异常, code = 2 没有查询到数据, 建议将其判断为正常
|
|
170
|
+
'''
|
|
171
|
+
query = f'min_over_time(ping_result_code{{target="{target}"}}[{last_minute}m])'
|
|
172
|
+
# query = f'avg_over_time((ping_result_code{{target="{target}"}})[{last_minute}m])'
|
|
173
|
+
if self.env == 'dev':
|
|
174
|
+
r = load_dev_file(dev_file)
|
|
175
|
+
else:
|
|
176
|
+
r = self.query(query=query)
|
|
177
|
+
if r.code == 0:
|
|
178
|
+
value = r.data[0]['value'][1]
|
|
179
|
+
if value == '0':
|
|
180
|
+
return ReturnResponse(code=0, msg=f"已检查 {target} 最近 {last_minute} 分钟是正常的!", data=r.data)
|
|
181
|
+
else:
|
|
182
|
+
return ReturnResponse(code=1, msg=f"已检查 {target} 最近 {last_minute} 分钟是异常的!", data=r.data)
|
|
183
|
+
else:
|
|
184
|
+
return r
|
|
185
|
+
|
|
186
|
+
def check_unreachable_ping_result(self, dev_file: str='') -> ReturnResponse:
|
|
187
|
+
'''
|
|
188
|
+
检查ping结果
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
target (str): 目标地址
|
|
192
|
+
last_minute (int, optional): 最近多少分钟. Defaults to 10.
|
|
193
|
+
env (str, optional): 环境. Defaults to 'prod'.
|
|
194
|
+
dev_file (str, optional): 开发文件. Defaults to ''.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
ReturnResponse:
|
|
198
|
+
code = 0 正常, code = 1 异常, code = 2 没有查询到数据, 建议将其判断为正常
|
|
199
|
+
'''
|
|
200
|
+
query = "ping_result_code == 1"
|
|
201
|
+
|
|
202
|
+
if self.env == 'dev':
|
|
203
|
+
r = load_dev_file(dev_file)
|
|
204
|
+
else:
|
|
205
|
+
r = self.query(query=query)
|
|
206
|
+
return r
|
|
207
|
+
|
|
208
|
+
def check_interface_rate(self,
|
|
209
|
+
direction: Literal['in', 'out'],
|
|
210
|
+
sysName: str,
|
|
211
|
+
ifName:str,
|
|
212
|
+
last_minutes: Optional[int] = None
|
|
213
|
+
) -> ReturnResponse:
|
|
214
|
+
"""查询指定设备的入方向总流量速率(bps)。
|
|
215
|
+
|
|
216
|
+
使用 PromQL 对 `snmp_interface_ifHCInOctets` 进行速率计算并聚合到设备级别,
|
|
217
|
+
将结果从字节每秒转换为比特每秒(乘以 8)。
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
sysName: 设备 `sysName` 标签值。
|
|
221
|
+
last_minutes: 计算速率的时间窗口(分钟)。未提供时默认使用 5 分钟窗口。
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
ReturnResponse: 查询结果包装。
|
|
225
|
+
"""
|
|
226
|
+
if direction == 'in':
|
|
227
|
+
query = f'(rate(snmp_interface_ifHCInOctets{{sysName="{sysName}", ifName="{ifName}"}}[{last_minutes}m])) * 8 / 1000000'
|
|
228
|
+
else:
|
|
229
|
+
query = f'(rate(snmp_interface_ifHCOutOctets{{sysName="{sysName}", ifName="{ifName}"}}[{last_minutes}m])) * 8 / 1000000'
|
|
230
|
+
r = self.query(query)
|
|
231
|
+
rate = r.data[0]['value'][1]
|
|
232
|
+
return int(float(rate))
|
|
233
|
+
|
|
234
|
+
def check_interface_avg_rate(self,
|
|
235
|
+
direction: Literal['in', 'out'],
|
|
236
|
+
sysname: str,
|
|
237
|
+
ifname:str,
|
|
238
|
+
last_hours: Optional[int] = 24,
|
|
239
|
+
last_minutes: Optional[int] = 5,
|
|
240
|
+
) -> ReturnResponse:
|
|
241
|
+
'''
|
|
242
|
+
_summary_
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
direction (Literal['in', 'out']): _description_
|
|
246
|
+
sysname (str): _description_
|
|
247
|
+
ifname (str): _description_
|
|
248
|
+
last_hours (Optional[int], optional): _description_. Defaults to 24.
|
|
249
|
+
last_minutes (Optional[int], optional): _description_. Defaults to 5.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
ReturnResponse: _description_
|
|
253
|
+
'''
|
|
254
|
+
if direction == 'in':
|
|
255
|
+
query = f'avg_over_time((rate(snmp_interface_ifHCInOctets{{sysName="{sysname}", ifName="{ifname}"}}[{last_minutes}m]) * 8) [{last_hours}h:]) / 1e6'
|
|
256
|
+
else:
|
|
257
|
+
query = f'avg_over_time((rate(snmp_interface_ifHCOutOctets{{sysName="{sysname}", ifName="{ifname}"}}[{last_minutes}m]) * 8) [{last_hours}h:]) / 1e6'
|
|
258
|
+
r = self.query(query)
|
|
259
|
+
try:
|
|
260
|
+
rate = r.data[0]['value'][1]
|
|
261
|
+
return ReturnResponse(code=0, msg=f"查询 {sysname} {ifname} 最近 {last_hours} 小时平均速率为 {round(float(rate), 2)} Mbit/s", data=round(float(rate), 2))
|
|
262
|
+
except KeyError:
|
|
263
|
+
return ReturnResponse(code=1, msg=f"查询 {sysname} {ifname} 最近 {last_hours} 小时平均速率为 0 Mbit/s")
|
|
264
|
+
|
|
265
|
+
def check_interface_max_rate(self,
|
|
266
|
+
direction: Literal['in', 'out'],
|
|
267
|
+
sysname: str,
|
|
268
|
+
ifname:str,
|
|
269
|
+
last_hours: Optional[int] = 24,
|
|
270
|
+
last_minutes: Optional[int] = 5,
|
|
271
|
+
) -> ReturnResponse:
|
|
272
|
+
'''
|
|
273
|
+
_summary_
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
direction (Literal['in', 'out']): _description_
|
|
277
|
+
sysname (str): _description_
|
|
278
|
+
ifname (str): _description_
|
|
279
|
+
last_hours (Optional[int], optional): _description_. Defaults to 24.
|
|
280
|
+
last_minutes (Optional[int], optional): _description_. Defaults to 5.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
ReturnResponse: _description_
|
|
284
|
+
'''
|
|
285
|
+
if direction == 'in':
|
|
286
|
+
query = f'max_over_time((rate(snmp_interface_ifHCInOctets{{sysName="{sysname}", ifName="{ifname}"}}[{last_minutes}m]) * 8) [{last_hours}h:]) / 1e6'
|
|
287
|
+
else:
|
|
288
|
+
query = f'max_over_time((rate(snmp_interface_ifHCOutOctets{{sysName="{sysname}", ifName="{ifname}"}}[{last_minutes}m]) * 8) [{last_hours}h:]) / 1e6'
|
|
289
|
+
r = self.query(query)
|
|
290
|
+
try:
|
|
291
|
+
rate = r.data[0]['value'][1]
|
|
292
|
+
return ReturnResponse(code=0, msg=f"查询 {sysname} {ifname} 最近 {last_hours} 小时最大速率为 {round(float(rate), 2)} Mbit/s", data=round(float(rate), 2))
|
|
293
|
+
except KeyError:
|
|
294
|
+
return ReturnResponse(code=1, msg=f"查询 {sysname} {ifname} 最近 {last_hours} 小时最大速率为 0 Mbit/s")
|
|
295
|
+
|
|
296
|
+
def check_snmp_port_status(self, sysname: str=None, if_name: str=None, last_minute: int=5, dev_file: str=None) -> ReturnResponse:
|
|
297
|
+
'''
|
|
298
|
+
查询端口状态
|
|
299
|
+
status code 可参考 SNMP 文件 https://mibbrowser.online/mibdb_search.php?mib=IF-MIB
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
sysname (_type_): 设备名称
|
|
303
|
+
if_name (_type_): _description_
|
|
304
|
+
last_minute (_type_): _description_
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
ReturnResponse:
|
|
308
|
+
code: 0, msg: , data: up,down
|
|
309
|
+
'''
|
|
310
|
+
q = f"""avg_over_time(snmp_interface_ifOperStatus{{sysName="{sysname}", ifName="{if_name}"}}[{last_minute}m])"""
|
|
311
|
+
if self.env == 'dev':
|
|
312
|
+
r = load_dev_file(dev_file)
|
|
313
|
+
else:
|
|
314
|
+
r = self.query(query=q)
|
|
315
|
+
if r.code == 0:
|
|
316
|
+
status_code = int(r.data[0]['value'][1])
|
|
317
|
+
if status_code == 1:
|
|
318
|
+
status = 'up'
|
|
319
|
+
else:
|
|
320
|
+
status = 'down'
|
|
321
|
+
return ReturnResponse(code=0, msg=f"{sysname} {if_name} 最近 {last_minute} 分钟端口状态为 {status}", data=status)
|
|
322
|
+
else:
|
|
323
|
+
return r
|
|
324
|
+
|
|
325
|
+
def insert_cronjob_run_status(self,
|
|
326
|
+
app_type: Literal['alert', 'meraki', 'other']='other',
|
|
327
|
+
app: str='',
|
|
328
|
+
status_code: Literal[0, 1]=1,
|
|
329
|
+
comment: str=None,
|
|
330
|
+
schedule_interval: str=None,
|
|
331
|
+
schedule_cron: str=None
|
|
332
|
+
) -> ReturnResponse:
|
|
333
|
+
labels = {
|
|
334
|
+
"app": app,
|
|
335
|
+
"env": self.env,
|
|
336
|
+
}
|
|
337
|
+
if app_type:
|
|
338
|
+
labels['app_type'] = app_type
|
|
339
|
+
if comment:
|
|
340
|
+
labels['comment'] = comment
|
|
341
|
+
|
|
342
|
+
if schedule_interval:
|
|
343
|
+
labels['schedule_type'] = 'interval'
|
|
344
|
+
labels['schedule_interval'] = schedule_interval
|
|
345
|
+
|
|
346
|
+
if schedule_cron:
|
|
347
|
+
labels['schedule_type'] = 'cron'
|
|
348
|
+
labels['schedule_cron'] = schedule_cron
|
|
349
|
+
|
|
350
|
+
r = self.insert(metric_name="cronjob_run_status", labels=labels, value=status_code)
|
|
351
|
+
return r
|
|
352
|
+
|
|
353
|
+
def insert_cronjob_duration_seconds(self,
|
|
354
|
+
app_type: Literal['alert', 'meraki', 'other']='other',
|
|
355
|
+
app: str='',
|
|
356
|
+
duration_seconds: float=None,
|
|
357
|
+
comment: str=None,
|
|
358
|
+
schedule_interval: str=None,
|
|
359
|
+
schedule_cron: str=None
|
|
360
|
+
) -> ReturnResponse:
|
|
361
|
+
labels = {
|
|
362
|
+
"app": app,
|
|
363
|
+
"env": self.env
|
|
364
|
+
}
|
|
365
|
+
if app_type:
|
|
366
|
+
labels['app_type'] = app_type
|
|
367
|
+
if comment:
|
|
368
|
+
labels['comment'] = comment
|
|
369
|
+
|
|
370
|
+
if schedule_interval:
|
|
371
|
+
labels['schedule_type'] = 'interval'
|
|
372
|
+
labels['schedule_interval'] = schedule_interval
|
|
373
|
+
|
|
374
|
+
if schedule_cron:
|
|
375
|
+
labels['schedule_type'] = 'cron'
|
|
376
|
+
labels['schedule_cron'] = schedule_cron
|
|
377
|
+
r = self.insert(metric_name="cronjob_run_duration_seconds", labels=labels, value=duration_seconds)
|
|
378
|
+
return r
|
|
379
|
+
|
|
380
|
+
def get_vmware_esxhostnames(self, vcenter: str=None) -> list:
|
|
381
|
+
'''
|
|
382
|
+
_summary_
|
|
383
|
+
'''
|
|
384
|
+
esxhostnames = []
|
|
385
|
+
query = f'vsphere_host_sys_uptime_latest{{vcenter="{vcenter}"}}'
|
|
386
|
+
metrics = self.query(query=query).data
|
|
387
|
+
for metric in metrics:
|
|
388
|
+
esxhostname = metric['metric']['esxhostname']
|
|
389
|
+
esxhostnames.append(esxhostname)
|
|
390
|
+
return esxhostnames
|
|
391
|
+
|
|
392
|
+
def get_vmware_cpu_usage(self, vcenter: str=None, esxhostname: str=None) -> float:
|
|
393
|
+
'''
|
|
394
|
+
_summary_
|
|
395
|
+
'''
|
|
396
|
+
query = f'vsphere_host_cpu_usage_average{{vcenter="{vcenter}", esxhostname="{esxhostname}"}}'
|
|
397
|
+
return self.query(query=query).data[0]['value'][1]
|
|
398
|
+
|
|
399
|
+
def get_vmware_memory_usage(self, vcenter: str=None, esxhostname: str=None) -> float:
|
|
400
|
+
'''
|
|
401
|
+
_summary_
|
|
402
|
+
'''
|
|
403
|
+
query = f'vsphere_host_mem_usage_average{{vcenter="{vcenter}", esxhostname="{esxhostname}"}}'
|
|
404
|
+
return self.query(query=query).data[0]['value'][1]
|