pytbox 0.0.7__py3-none-any.whl → 0.3.2__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 +27 -7
- pytbox/alert/ping.py +0 -1
- pytbox/alicloud/sls.py +9 -6
- pytbox/base.py +74 -1
- pytbox/categraf/build_config.py +95 -32
- 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/commands/vm.py +22 -0
- pytbox/cli/main.py +2 -0
- pytbox/database/mongo.py +1 -1
- pytbox/database/victoriametrics.py +331 -40
- pytbox/excel.py +64 -0
- pytbox/feishu/endpoints.py +6 -6
- pytbox/log/logger.py +33 -13
- 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 +67 -21
- pytbox/utils/load_vm_devfile.py +45 -0
- pytbox/utils/richutils.py +11 -1
- pytbox/utils/timeutils.py +15 -57
- pytbox/vmware.py +120 -0
- pytbox/win/ad.py +30 -0
- {pytbox-0.0.7.dist-info → pytbox-0.3.2.dist-info}/METADATA +7 -4
- pytbox-0.3.2.dist-info/RECORD +72 -0
- pytbox/utils/ping_checker.py +0 -1
- pytbox-0.0.7.dist-info/RECORD +0 -39
- {pytbox-0.0.7.dist-info → pytbox-0.3.2.dist-info}/WHEEL +0 -0
- {pytbox-0.0.7.dist-info → pytbox-0.3.2.dist-info}/entry_points.txt +0 -0
- {pytbox-0.0.7.dist-info → pytbox-0.3.2.dist-info}/top_level.txt +0 -0
pytbox/cli/main.py
CHANGED
|
@@ -5,6 +5,7 @@ Pytbox 主命令行入口
|
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
7
|
from .categraf import categraf_group
|
|
8
|
+
from .commands.vm import vm_group
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@click.group()
|
|
@@ -16,6 +17,7 @@ def main():
|
|
|
16
17
|
|
|
17
18
|
# 注册子命令组
|
|
18
19
|
main.add_command(categraf_group, name='categraf')
|
|
20
|
+
main.add_command(vm_group, name='vm')
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
if __name__ == "__main__":
|
pytbox/database/mongo.py
CHANGED
|
@@ -80,7 +80,7 @@ class Mongo:
|
|
|
80
80
|
alarm_list = []
|
|
81
81
|
for result in results:
|
|
82
82
|
duration_minute = '持续 ' + str(int((result['resolved_time'] - result['event_time']).total_seconds() / 60)) + ' 分钟'
|
|
83
|
-
alarm_list.append('
|
|
83
|
+
alarm_list.append('触发时间: ' + TimeUtils.convert_timeobj_to_str(timeobj=result['event_time']) + ' ' + duration_minute)
|
|
84
84
|
|
|
85
85
|
alarm_str = '\n'.join(alarm_list)
|
|
86
86
|
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
from typing import Literal, Optional, Dict, List
|
|
4
6
|
import requests
|
|
5
7
|
from ..utils.response import ReturnResponse
|
|
8
|
+
from ..utils.load_vm_devfile import load_dev_file
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
class VictoriaMetrics:
|
|
9
12
|
|
|
10
|
-
def __init__(self, url: str='', timeout: int=3) -> None:
|
|
13
|
+
def __init__(self, url: str='', timeout: int=3, env: str='prod') -> None:
|
|
11
14
|
self.url = url
|
|
12
15
|
self.timeout = timeout
|
|
13
16
|
self.session = requests.Session()
|
|
@@ -15,14 +18,51 @@ class VictoriaMetrics:
|
|
|
15
18
|
'Content-Type': 'application/json',
|
|
16
19
|
'Accept': 'application/json'
|
|
17
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
|
+
"""插入指标数据。
|
|
18
26
|
|
|
19
|
-
|
|
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:
|
|
20
60
|
'''
|
|
21
61
|
查询指标数据
|
|
22
|
-
|
|
62
|
+
|
|
23
63
|
Args:
|
|
24
64
|
query (str): 查询语句
|
|
25
|
-
|
|
65
|
+
|
|
26
66
|
Returns:
|
|
27
67
|
dict: 查询结果
|
|
28
68
|
'''
|
|
@@ -32,14 +72,79 @@ class VictoriaMetrics:
|
|
|
32
72
|
timeout=self.timeout,
|
|
33
73
|
params={"query": query}
|
|
34
74
|
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
38
85
|
else:
|
|
39
|
-
|
|
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
|
|
40
99
|
else:
|
|
41
|
-
return
|
|
100
|
+
return resp
|
|
101
|
+
|
|
102
|
+
def query_range(self, query):
|
|
103
|
+
'''
|
|
104
|
+
查询指标数据
|
|
42
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
|
|
43
148
|
def get_labels(self, metric_name: str) -> ReturnResponse:
|
|
44
149
|
url = f"{self.url}/api/v1/series?match[]={metric_name}"
|
|
45
150
|
response = requests.get(url, timeout=self.timeout)
|
|
@@ -49,42 +154,56 @@ class VictoriaMetrics:
|
|
|
49
154
|
else:
|
|
50
155
|
return ReturnResponse(code=1, msg=f"metric name: {metric_name} 查询失败")
|
|
51
156
|
|
|
52
|
-
def check_ping_result(self, target: str, last_minute: int=10) -> ReturnResponse:
|
|
157
|
+
def check_ping_result(self, target: str, last_minute: int=10, env: str='prod', dev_file: str='') -> ReturnResponse:
|
|
53
158
|
'''
|
|
54
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 没有查询到数据, 建议将其判断为正常
|
|
55
170
|
'''
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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)
|
|
59
175
|
else:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if last_minute:
|
|
63
|
-
query = query + f"[{last_minute}m]"
|
|
64
|
-
|
|
65
|
-
r = self.query(query=query)
|
|
176
|
+
r = self.query(query=query)
|
|
66
177
|
if r.code == 0:
|
|
67
|
-
|
|
68
|
-
if
|
|
69
|
-
code =
|
|
70
|
-
msg = f"已检查 {target} 最近 {last_minute} 分钟是正常的!"
|
|
178
|
+
value = r.data[0]['value'][1]
|
|
179
|
+
if value == '0':
|
|
180
|
+
return ReturnResponse(code=0, msg=f"已检查 {target} 最近 {last_minute} 分钟是正常的!", data=r.data)
|
|
71
181
|
else:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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"
|
|
86
201
|
|
|
87
|
-
|
|
202
|
+
if self.env == 'dev':
|
|
203
|
+
r = load_dev_file(dev_file)
|
|
204
|
+
else:
|
|
205
|
+
r = self.query(query=query)
|
|
206
|
+
return r
|
|
88
207
|
|
|
89
208
|
def check_interface_rate(self,
|
|
90
209
|
direction: Literal['in', 'out'],
|
|
@@ -110,4 +229,176 @@ class VictoriaMetrics:
|
|
|
110
229
|
query = f'(rate(snmp_interface_ifHCOutOctets{{sysName="{sysName}", ifName="{ifName}"}}[{last_minutes}m])) * 8 / 1000000'
|
|
111
230
|
r = self.query(query)
|
|
112
231
|
rate = r.data[0]['value'][1]
|
|
113
|
-
return int(float(rate))
|
|
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]
|
pytbox/excel.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from openpyxl.styles import Alignment
|
|
4
|
+
from openpyxl.styles import PatternFill
|
|
5
|
+
from openpyxl.styles.differential import DifferentialStyle
|
|
6
|
+
from openpyxl.styles import Font
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ExcelFormat:
|
|
10
|
+
def __init__(self, ws):
|
|
11
|
+
self.ws = ws
|
|
12
|
+
|
|
13
|
+
def set_column(self):
|
|
14
|
+
for column in self.ws.columns:
|
|
15
|
+
max_length = 0
|
|
16
|
+
column = [cell for cell in column]
|
|
17
|
+
for cell in column:
|
|
18
|
+
try:
|
|
19
|
+
if len(str(cell.value)) > max_length:
|
|
20
|
+
max_length = len(cell.value)
|
|
21
|
+
except:
|
|
22
|
+
pass
|
|
23
|
+
adjusted_width = (max_length + 10)
|
|
24
|
+
self.ws.column_dimensions[column[0].column_letter].width = adjusted_width
|
|
25
|
+
|
|
26
|
+
def set_rows_center(self):
|
|
27
|
+
# 将所有单元格的文字居中
|
|
28
|
+
for row in self.ws.iter_rows():
|
|
29
|
+
|
|
30
|
+
for cell in row:
|
|
31
|
+
cell.alignment = Alignment(horizontal='center', vertical='center')
|
|
32
|
+
for row in self.ws.iter_rows():
|
|
33
|
+
self.ws.row_dimensions[row[0].row].height = 24
|
|
34
|
+
|
|
35
|
+
def set_freeze_first_row(self):
|
|
36
|
+
"""设置首行锁定/冻结首行"""
|
|
37
|
+
# 冻结首行,从第二行开始滚动
|
|
38
|
+
self.ws.freeze_panes = 'A2'
|
|
39
|
+
|
|
40
|
+
def set_freeze_first_column(self):
|
|
41
|
+
"""设置首列锁定/冻结首列"""
|
|
42
|
+
# 冻结首列,从第二列开始滚动
|
|
43
|
+
self.ws.freeze_panes = 'B1'
|
|
44
|
+
|
|
45
|
+
def set_first_row_bold_color(self, font_color='FF0000FF'):
|
|
46
|
+
"""设置首行字体加粗并改变颜色
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
font_color (str): 字体颜色的十六进制代码,默认为蓝色(FF0000FF)
|
|
50
|
+
格式:AARRGGBB 或 RRGGBB
|
|
51
|
+
例如:'FF0000FF'(蓝色), 'FFFF0000'(红色), 'FF008000'(绿色)
|
|
52
|
+
"""
|
|
53
|
+
# 遍历首行的所有单元格
|
|
54
|
+
for cell in self.ws[1]:
|
|
55
|
+
if cell.value is not None: # 只对有内容的单元格设置样式
|
|
56
|
+
# 设置字体为粗体并改变颜色
|
|
57
|
+
cell.font = Font(bold=True, color=font_color)
|
|
58
|
+
|
|
59
|
+
def set_freeze_first_row_and_column(self):
|
|
60
|
+
"""同时冻结首行和首列"""
|
|
61
|
+
# 冻结首行首列,从第二行第二列开始滚动
|
|
62
|
+
self.ws.freeze_panes = 'B2'
|
|
63
|
+
|
|
64
|
+
|
pytbox/feishu/endpoints.py
CHANGED
|
@@ -254,9 +254,9 @@ class MessageEndpoint(Endpoint):
|
|
|
254
254
|
body=payload
|
|
255
255
|
)
|
|
256
256
|
if r.code == 0:
|
|
257
|
-
return ReturnResponse(code=0,
|
|
257
|
+
return ReturnResponse(code=0, msg=f"{message_id} 回复 emoji [{emoji_type}] 成功")
|
|
258
258
|
else:
|
|
259
|
-
return ReturnResponse(code=1,
|
|
259
|
+
return ReturnResponse(code=1, msg=f"{message_id} 回复 emoji [{emoji_type}] 失败")
|
|
260
260
|
|
|
261
261
|
class BitableEndpoint(Endpoint):
|
|
262
262
|
|
|
@@ -432,10 +432,10 @@ class BitableEndpoint(Endpoint):
|
|
|
432
432
|
resp = self.parent.request(path=f'/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}',
|
|
433
433
|
method='PUT',
|
|
434
434
|
body=payload)
|
|
435
|
-
return ReturnResponse(code=resp.code,
|
|
435
|
+
return ReturnResponse(code=resp.code, msg=f"记录已存在, 进行更新", data=resp.data)
|
|
436
436
|
else:
|
|
437
437
|
resp = self.add_record(app_token, table_id, fields)
|
|
438
|
-
return ReturnResponse(code=resp.code,
|
|
438
|
+
return ReturnResponse(code=resp.code, msg=f"记录不存在, 进行创建", data=resp.data)
|
|
439
439
|
|
|
440
440
|
def query_name_by_record_id(self, app_token: str=None, table_id: str=None, field_names: list=None, record_id: str='', name: str=''):
|
|
441
441
|
response = self.query_record(app_token=app_token, table_id=table_id, field_names=field_names)
|
|
@@ -967,9 +967,9 @@ class ExtensionsEndpoint(Endpoint):
|
|
|
967
967
|
body=payload)
|
|
968
968
|
if response.code == 0:
|
|
969
969
|
if get == 'open_id':
|
|
970
|
-
return ReturnResponse(code=0,
|
|
970
|
+
return ReturnResponse(code=0, msg=f'根据用户输入的 {user_input}, 获取用户信息成功', data=response.data['user_list'][0]['user_id'])
|
|
971
971
|
else:
|
|
972
|
-
return ReturnResponse(code=response.code,
|
|
972
|
+
return ReturnResponse(code=response.code, msg=f"获取时失败, 报错请见 data 字段", data=response.data)
|
|
973
973
|
|
|
974
974
|
def format_rich_text(self, text: str, color: Literal['red', 'green', 'yellow', 'blue'], bold: bool=False):
|
|
975
975
|
if bold:
|
pytbox/log/logger.py
CHANGED
|
@@ -9,7 +9,7 @@ from ..database.mongo import Mongo
|
|
|
9
9
|
from ..feishu.client import Client as FeishuClient
|
|
10
10
|
from ..utils.timeutils import TimeUtils
|
|
11
11
|
from ..dida365 import Dida365
|
|
12
|
-
|
|
12
|
+
from ..alicloud.sls import AliCloudSls
|
|
13
13
|
|
|
14
14
|
logger.remove()
|
|
15
15
|
logger.add(sys.stdout, colorize=True, format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <level>{message}</level>")
|
|
@@ -31,7 +31,6 @@ class AppLogger:
|
|
|
31
31
|
feishu: FeishuClient=None,
|
|
32
32
|
dida: Dida365=None,
|
|
33
33
|
enable_sls: bool=False,
|
|
34
|
-
sls_url: str=None,
|
|
35
34
|
sls_access_key_id: str=None,
|
|
36
35
|
sls_access_key_secret: str=None,
|
|
37
36
|
sls_project: str=None,
|
|
@@ -49,10 +48,17 @@ class AppLogger:
|
|
|
49
48
|
self.stream = stream
|
|
50
49
|
self.victorialog = Victorialog(url=victorialog_url)
|
|
51
50
|
self.enable_victorialog = enable_victorialog
|
|
51
|
+
self.enable_sls = enable_sls
|
|
52
52
|
self.mongo = mongo
|
|
53
53
|
self.feishu = feishu
|
|
54
54
|
self.dida = dida
|
|
55
|
-
|
|
55
|
+
self.sls = AliCloudSls(
|
|
56
|
+
access_key_id=sls_access_key_id,
|
|
57
|
+
access_key_secret=sls_access_key_secret,
|
|
58
|
+
project=sls_project,
|
|
59
|
+
logstore=sls_logstore
|
|
60
|
+
)
|
|
61
|
+
|
|
56
62
|
def _get_caller_info(self) -> tuple[str, int, str]:
|
|
57
63
|
"""
|
|
58
64
|
获取调用者信息
|
|
@@ -78,6 +84,9 @@ class AppLogger:
|
|
|
78
84
|
logger.debug(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
|
|
79
85
|
if self.enable_victorialog:
|
|
80
86
|
self.victorialog.send_program_log(stream=self.stream, level="DEBUG", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
|
|
87
|
+
if self.enable_sls:
|
|
88
|
+
self.sls.put_logs(level="DEBUG", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
|
|
89
|
+
|
|
81
90
|
|
|
82
91
|
def info(self, message: str='', feishu_notify: bool=False):
|
|
83
92
|
"""记录信息级别日志"""
|
|
@@ -85,22 +94,31 @@ class AppLogger:
|
|
|
85
94
|
logger.info(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
|
|
86
95
|
if self.enable_victorialog:
|
|
87
96
|
r = self.victorialog.send_program_log(stream=self.stream, level="INFO", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
|
|
97
|
+
if self.enable_sls:
|
|
98
|
+
self.sls.put_logs(level="INFO", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
|
|
88
99
|
if feishu_notify:
|
|
89
|
-
self.feishu(
|
|
90
|
-
|
|
100
|
+
self.feishu.extensions.send_message_notify(
|
|
101
|
+
title=f"自动化脚本告警: {self.app_name}",
|
|
102
|
+
content=message
|
|
103
|
+
)
|
|
104
|
+
|
|
91
105
|
def warning(self, message: str):
|
|
92
106
|
"""记录警告级别日志"""
|
|
93
107
|
caller_filename, caller_lineno, caller_function, call_full_filename = self._get_caller_info()
|
|
94
108
|
logger.warning(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
|
|
95
109
|
if self.enable_victorialog:
|
|
96
110
|
self.victorialog.send_program_log(stream=self.stream, level="WARN", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
|
|
97
|
-
|
|
111
|
+
if self.enable_sls:
|
|
112
|
+
self.sls.put_logs(level="WARN", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
|
|
113
|
+
|
|
98
114
|
def error(self, message: str):
|
|
99
115
|
"""记录错误级别日志"""
|
|
100
116
|
caller_filename, caller_lineno, caller_function, call_full_filename = self._get_caller_info()
|
|
101
117
|
logger.error(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
|
|
102
118
|
if self.enable_victorialog:
|
|
103
119
|
self.victorialog.send_program_log(stream=self.stream, level="ERROR", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
|
|
120
|
+
if self.enable_sls:
|
|
121
|
+
self.sls.put_logs(level="ERROR", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
|
|
104
122
|
|
|
105
123
|
if self.feishu:
|
|
106
124
|
existing_message = self.mongo.collection.find_one({"message": message}, sort=[("time", -1)])
|
|
@@ -137,12 +155,13 @@ class AppLogger:
|
|
|
137
155
|
f"**function_name**: {caller_function}"
|
|
138
156
|
]
|
|
139
157
|
|
|
140
|
-
self.dida
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
if self.dida:
|
|
159
|
+
self.dida.task_create(
|
|
160
|
+
project_id="65e87d2b3e73517c2cdd9d63",
|
|
161
|
+
title=f"自动化脚本告警: {self.app_name}",
|
|
162
|
+
content="\n".join(dida_content_list),
|
|
163
|
+
tags=['L-程序告警', 't-问题处理']
|
|
164
|
+
)
|
|
146
165
|
|
|
147
166
|
def critical(self, message: str):
|
|
148
167
|
"""记录严重错误级别日志"""
|
|
@@ -150,7 +169,8 @@ class AppLogger:
|
|
|
150
169
|
logger.critical(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
|
|
151
170
|
if self.enable_victorialog:
|
|
152
171
|
self.victorialog.send_program_log(stream=self.stream, level="CRITICAL", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
|
|
153
|
-
|
|
172
|
+
if self.enable_sls:
|
|
173
|
+
self.sls.put_logs(level="CRITICAL", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
|
|
154
174
|
|
|
155
175
|
# 使用示例
|
|
156
176
|
if __name__ == "__main__":
|