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.

Files changed (68) hide show
  1. pytbox/alert/alert_handler.py +139 -0
  2. pytbox/alert/ping.py +24 -0
  3. pytbox/alicloud/sls.py +9 -14
  4. pytbox/base.py +121 -0
  5. pytbox/categraf/build_config.py +143 -0
  6. pytbox/categraf/instances.toml +39 -0
  7. pytbox/categraf/jinja2/__init__.py +6 -0
  8. pytbox/categraf/jinja2/input.cpu/cpu.toml.j2 +5 -0
  9. pytbox/categraf/jinja2/input.disk/disk.toml.j2 +11 -0
  10. pytbox/categraf/jinja2/input.diskio/diskio.toml.j2 +6 -0
  11. pytbox/categraf/jinja2/input.dns_query/dns_query.toml.j2 +12 -0
  12. pytbox/categraf/jinja2/input.http_response/http_response.toml.j2 +9 -0
  13. pytbox/categraf/jinja2/input.mem/mem.toml.j2 +5 -0
  14. pytbox/categraf/jinja2/input.net/net.toml.j2 +11 -0
  15. pytbox/categraf/jinja2/input.net_response/net_response.toml.j2 +9 -0
  16. pytbox/categraf/jinja2/input.ping/ping.toml.j2 +11 -0
  17. pytbox/categraf/jinja2/input.prometheus/prometheus.toml.j2 +12 -0
  18. pytbox/categraf/jinja2/input.snmp/cisco_interface.toml.j2 +96 -0
  19. pytbox/categraf/jinja2/input.snmp/cisco_system.toml.j2 +41 -0
  20. pytbox/categraf/jinja2/input.snmp/h3c_interface.toml.j2 +96 -0
  21. pytbox/categraf/jinja2/input.snmp/h3c_system.toml.j2 +41 -0
  22. pytbox/categraf/jinja2/input.snmp/huawei_interface.toml.j2 +96 -0
  23. pytbox/categraf/jinja2/input.snmp/huawei_system.toml.j2 +41 -0
  24. pytbox/categraf/jinja2/input.snmp/ruijie_interface.toml.j2 +96 -0
  25. pytbox/categraf/jinja2/input.snmp/ruijie_system.toml.j2 +41 -0
  26. pytbox/categraf/jinja2/input.vsphere/vsphere.toml.j2 +211 -0
  27. pytbox/cli/__init__.py +7 -0
  28. pytbox/cli/categraf/__init__.py +7 -0
  29. pytbox/cli/categraf/commands.py +55 -0
  30. pytbox/cli/commands/vm.py +22 -0
  31. pytbox/cli/common/__init__.py +6 -0
  32. pytbox/cli/common/options.py +42 -0
  33. pytbox/cli/common/utils.py +269 -0
  34. pytbox/cli/formatters/__init__.py +7 -0
  35. pytbox/cli/formatters/output.py +155 -0
  36. pytbox/cli/main.py +24 -0
  37. pytbox/cli.py +9 -0
  38. pytbox/database/mongo.py +99 -0
  39. pytbox/database/victoriametrics.py +404 -0
  40. pytbox/dida365.py +11 -17
  41. pytbox/excel.py +64 -0
  42. pytbox/feishu/endpoints.py +12 -9
  43. pytbox/{logger.py → log/logger.py} +78 -30
  44. pytbox/{victorialog.py → log/victorialog.py} +2 -2
  45. pytbox/mail/alimail.py +142 -0
  46. pytbox/mail/client.py +171 -0
  47. pytbox/mail/mail_detail.py +30 -0
  48. pytbox/mingdao.py +164 -0
  49. pytbox/network/meraki.py +537 -0
  50. pytbox/notion.py +731 -0
  51. pytbox/pyjira.py +612 -0
  52. pytbox/utils/cronjob.py +79 -0
  53. pytbox/utils/env.py +2 -2
  54. pytbox/utils/load_config.py +132 -0
  55. pytbox/utils/load_vm_devfile.py +45 -0
  56. pytbox/utils/response.py +1 -1
  57. pytbox/utils/richutils.py +31 -0
  58. pytbox/utils/timeutils.py +479 -14
  59. pytbox/vmware.py +120 -0
  60. pytbox/win/ad.py +30 -0
  61. {pytbox-0.0.1.dist-info → pytbox-0.3.1.dist-info}/METADATA +13 -3
  62. pytbox-0.3.1.dist-info/RECORD +72 -0
  63. pytbox-0.3.1.dist-info/entry_points.txt +2 -0
  64. pytbox/common/base.py +0 -0
  65. pytbox/victoriametrics.py +0 -37
  66. pytbox-0.0.1.dist-info/RECORD +0 -21
  67. {pytbox-0.0.1.dist-info → pytbox-0.3.1.dist-info}/WHEEL +0 -0
  68. {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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pytbox 命令行工具 - 入口文件(保持向后兼容)
4
+ """
5
+
6
+ from pytbox.cli import main
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -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]