pytbox 0.2.5__py3-none-any.whl → 0.2.9__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.
- pytbox/database/victoriametrics.py +29 -2
- pytbox/mingdao.py +3 -2
- pytbox/network/meraki.py +364 -8
- pytbox/pyjira.py +66 -19
- {pytbox-0.2.5.dist-info → pytbox-0.2.9.dist-info}/METADATA +1 -1
- {pytbox-0.2.5.dist-info → pytbox-0.2.9.dist-info}/RECORD +9 -9
- {pytbox-0.2.5.dist-info → pytbox-0.2.9.dist-info}/WHEEL +0 -0
- {pytbox-0.2.5.dist-info → pytbox-0.2.9.dist-info}/entry_points.txt +0 -0
- {pytbox-0.2.5.dist-info → pytbox-0.2.9.dist-info}/top_level.txt +0 -0
|
@@ -168,7 +168,8 @@ class VictoriaMetrics:
|
|
|
168
168
|
ReturnResponse:
|
|
169
169
|
code = 0 正常, code = 1 异常, code = 2 没有查询到数据, 建议将其判断为正常
|
|
170
170
|
'''
|
|
171
|
-
query = f'
|
|
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])'
|
|
172
173
|
if self.env == 'dev':
|
|
173
174
|
r = load_dev_file(dev_file)
|
|
174
175
|
else:
|
|
@@ -374,4 +375,30 @@ class VictoriaMetrics:
|
|
|
374
375
|
labels['schedule_type'] = 'cron'
|
|
375
376
|
labels['schedule_cron'] = schedule_cron
|
|
376
377
|
r = self.insert(metric_name="cronjob_run_duration_seconds", labels=labels, value=duration_seconds)
|
|
377
|
-
return r
|
|
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/mingdao.py
CHANGED
|
@@ -12,7 +12,7 @@ class Mingdao:
|
|
|
12
12
|
_summary_
|
|
13
13
|
'''
|
|
14
14
|
def __init__(self, app_key: str=None, sign: str=None, timeout: int=5):
|
|
15
|
-
self.base_url = "https://api.mingdao.com
|
|
15
|
+
self.base_url = "https://api.mingdao.com"
|
|
16
16
|
self.headers = {
|
|
17
17
|
'Content-Type': 'application/json;charset=UTF-8',
|
|
18
18
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36'
|
|
@@ -20,7 +20,7 @@ class Mingdao:
|
|
|
20
20
|
self.timeout = timeout
|
|
21
21
|
self.app_key = app_key
|
|
22
22
|
self.sign = sign
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
def _build_api_request(self, api_url: str, method: Literal['GET', 'POST'], params: dict=None, body: dict=None, api_version: Literal['v1', 'v2']='v2'):
|
|
25
25
|
body['appKey'] = self.app_key
|
|
26
26
|
body['sign'] = self.sign
|
|
@@ -116,6 +116,7 @@ class Mingdao:
|
|
|
116
116
|
parse_control_id: bool=False,
|
|
117
117
|
page_size: int=100,
|
|
118
118
|
):
|
|
119
|
+
|
|
119
120
|
filters = []
|
|
120
121
|
if project_value:
|
|
121
122
|
filters.append({
|
pytbox/network/meraki.py
CHANGED
|
@@ -4,16 +4,21 @@ from typing import Any, Literal
|
|
|
4
4
|
import requests
|
|
5
5
|
from ..utils.response import ReturnResponse
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
class Meraki:
|
|
8
9
|
'''
|
|
9
10
|
Meraki Client
|
|
10
11
|
'''
|
|
11
|
-
def __init__(self, api_key: str=None, organization_id: str=None, timeout: int=10):
|
|
12
|
+
def __init__(self, api_key: str=None, organization_id: str=None, timeout: int=10, region: Literal['global', 'china']='china'):
|
|
12
13
|
if not api_key:
|
|
13
14
|
raise ValueError("api_key is required")
|
|
14
15
|
if not organization_id:
|
|
15
16
|
raise ValueError("organization_id is required")
|
|
16
|
-
|
|
17
|
+
if region == 'china':
|
|
18
|
+
self.base_url = 'https://api.meraki.cn/api/v1'
|
|
19
|
+
else:
|
|
20
|
+
self.base_url = 'https://api.meraki.com/api/v1'
|
|
21
|
+
|
|
17
22
|
self.headers = {
|
|
18
23
|
"Authorization": f"Bearer {api_key}",
|
|
19
24
|
"Accept": "application/json"
|
|
@@ -21,6 +26,34 @@ class Meraki:
|
|
|
21
26
|
self.organization_id = organization_id
|
|
22
27
|
self.timeout = timeout
|
|
23
28
|
|
|
29
|
+
def get_organizations(self) -> ReturnResponse:
|
|
30
|
+
'''
|
|
31
|
+
https://developer.cisco.com/meraki/api-v1/get-organizations/
|
|
32
|
+
'''
|
|
33
|
+
r = requests.get(
|
|
34
|
+
f"{self.base_url}/organizations",
|
|
35
|
+
headers=self.headers,
|
|
36
|
+
timeout=self.timeout
|
|
37
|
+
)
|
|
38
|
+
if r.status_code == 200:
|
|
39
|
+
return ReturnResponse(code=0, msg=f"获取组织成功", data=r.json())
|
|
40
|
+
return ReturnResponse(code=1, msg=f"获取组织失败: {r.status_code} {r.text}")
|
|
41
|
+
|
|
42
|
+
def get_api_requests(self, timespan: int=5*60) -> ReturnResponse:
|
|
43
|
+
|
|
44
|
+
params = {}
|
|
45
|
+
params['timespan'] = timespan
|
|
46
|
+
|
|
47
|
+
r = requests.get(
|
|
48
|
+
url=f"{self.base_url}/organizations/{self.organization_id}/apiRequests",
|
|
49
|
+
headers=self.headers,
|
|
50
|
+
params=params,
|
|
51
|
+
timeout=self.timeout
|
|
52
|
+
)
|
|
53
|
+
if r.status_code == 200:
|
|
54
|
+
return ReturnResponse(code=0, msg='获取 API 请求数量成功', data=r.json())
|
|
55
|
+
return ReturnResponse(code=1, msg=f"获取 API 请求失败: {r.status_code} - {r.text}", data=None)
|
|
56
|
+
|
|
24
57
|
def get_networks(self, tags: list[str]=None) -> ReturnResponse:
|
|
25
58
|
'''
|
|
26
59
|
https://developer.cisco.com/meraki/api-v1/get-organization-networks/
|
|
@@ -146,24 +179,347 @@ class Meraki:
|
|
|
146
179
|
return ReturnResponse(code=3, msg=f"设备 {serial} 还未添加过", data=None)
|
|
147
180
|
return ReturnResponse(code=1, msg=f"获取设备详情失败: {r.status_code} - {r.text}", data=None)
|
|
148
181
|
|
|
149
|
-
def
|
|
182
|
+
def get_device_availability(self, network_id: list=None,
|
|
150
183
|
status: Literal['online', 'offline', 'dormant', 'alerting']=None,
|
|
151
|
-
serial: str=None
|
|
184
|
+
serial: str=None,
|
|
185
|
+
tags: list=None,
|
|
186
|
+
get_all: bool=False) -> ReturnResponse:
|
|
187
|
+
'''
|
|
188
|
+
https://developer.cisco.com/meraki/api-v1/get-organization-devices-availabilities/
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
network_id (str, optional): 如果是列表, 不能传太多 network_id
|
|
192
|
+
status (Literal['online', 'offline', 'dormant', 'alerting'], optional): _description_. Defaults to None.
|
|
193
|
+
serial (str, optional): _description_. Defaults to None.
|
|
194
|
+
get_all (bool, optional): _description_. Defaults to False.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
ReturnResponse: _description_
|
|
198
|
+
'''
|
|
152
199
|
params = {}
|
|
153
200
|
|
|
154
201
|
if status:
|
|
155
202
|
params["statuses[]"] = status
|
|
156
203
|
|
|
157
204
|
if serial:
|
|
158
|
-
|
|
205
|
+
if isinstance(serial, str):
|
|
206
|
+
params["serials[]"] = [serial]
|
|
207
|
+
else:
|
|
208
|
+
params["serials[]"] = serial
|
|
209
|
+
|
|
210
|
+
if network_id:
|
|
211
|
+
if isinstance(network_id, str):
|
|
212
|
+
params["networkIds[]"] = [network_id]
|
|
213
|
+
else:
|
|
214
|
+
params["networkIds[]"] = network_id
|
|
215
|
+
|
|
216
|
+
if tags:
|
|
217
|
+
params["tags[]"] = tags
|
|
218
|
+
|
|
219
|
+
# 如果需要获取所有数据,设置每页最大数量
|
|
220
|
+
if get_all:
|
|
221
|
+
params['perPage'] = 1000
|
|
222
|
+
|
|
223
|
+
all_data = []
|
|
224
|
+
url = f"{self.base_url}/organizations/{self.organization_id}/devices/availabilities"
|
|
159
225
|
|
|
226
|
+
while url:
|
|
227
|
+
r = requests.get(
|
|
228
|
+
url=url,
|
|
229
|
+
headers=self.headers,
|
|
230
|
+
params=params if url == f"{self.base_url}/organizations/{self.organization_id}/devices/availabilities" else {},
|
|
231
|
+
timeout=self.timeout
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if r.status_code != 200:
|
|
235
|
+
return ReturnResponse(code=1, msg=f"获取设备健康状态失败: {r.status_code} - {r.text}", data=None)
|
|
236
|
+
|
|
237
|
+
data = r.json()
|
|
238
|
+
all_data.extend(data)
|
|
239
|
+
|
|
240
|
+
# 如果不需要获取所有数据,只返回第一页
|
|
241
|
+
if not get_all:
|
|
242
|
+
return ReturnResponse(code=0, msg=f"获取设备健康状态成功,共 {len(data)} 条", data=data)
|
|
243
|
+
|
|
244
|
+
# 解析 Link header 获取下一页 URL
|
|
245
|
+
url = None
|
|
246
|
+
link_header = r.headers.get('Link', '')
|
|
247
|
+
if link_header:
|
|
248
|
+
# 解析 Link header,格式如: '<url>; rel=next, <url>; rel=prev'
|
|
249
|
+
for link in link_header.split(','):
|
|
250
|
+
link = link.strip()
|
|
251
|
+
if 'rel=next' in link or 'rel="next"' in link:
|
|
252
|
+
# 提取 URL (在 < > 之间)
|
|
253
|
+
url = link.split(';')[0].strip('<> ')
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
return ReturnResponse(code=0, msg=f"获取设备健康状态成功,共 {len(all_data)} 条", data=all_data)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def get_device_availabilities_change_history(self, network_id: str=None, serial: str=None) -> ReturnResponse:
|
|
260
|
+
'''
|
|
261
|
+
https://developer.cisco.com/meraki/api-v1/get-organization-devices-availabilities/
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
network_id (str, optional): _description_. Defaults to None.
|
|
265
|
+
serial (str, optional): _description_. Defaults to None.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
ReturnResponse: _description_
|
|
269
|
+
'''
|
|
270
|
+
params = {}
|
|
160
271
|
if network_id:
|
|
161
|
-
params[
|
|
272
|
+
params['networkId'] = network_id
|
|
273
|
+
if serial:
|
|
274
|
+
params['serial'] = serial
|
|
162
275
|
|
|
163
276
|
r = requests.get(
|
|
164
|
-
url=f"{self.base_url}/organizations/{self.organization_id}/devices/availabilities",
|
|
277
|
+
url=f"{self.base_url}/organizations/{self.organization_id}/devices/availabilities/changeHistory",
|
|
165
278
|
headers=self.headers,
|
|
166
279
|
params=params,
|
|
167
280
|
timeout=self.timeout
|
|
168
281
|
)
|
|
169
|
-
|
|
282
|
+
if r.status_code == 200:
|
|
283
|
+
return ReturnResponse(code=0, msg=f"获取设备健康状态变化历史成功", data=r.json())
|
|
284
|
+
return ReturnResponse(code=1, msg=f"获取设备健康状态变化历史失败: {r.status_code} - {r.text}", data=None)
|
|
285
|
+
|
|
286
|
+
def reboot_device(self, serial: str) -> ReturnResponse:
|
|
287
|
+
'''
|
|
288
|
+
该接口 60s 只能执行一次
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
serial (str): _description_
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
ReturnResponse: _description_
|
|
295
|
+
'''
|
|
296
|
+
r = requests.post(
|
|
297
|
+
url=f"{self.base_url}/devices/{serial}/reboot",
|
|
298
|
+
headers=self.headers,
|
|
299
|
+
timeout=self.timeout
|
|
300
|
+
)
|
|
301
|
+
if r.status_code == 202 and r.json()['success'] == True:
|
|
302
|
+
return ReturnResponse(code=0, msg=f"重启 {serial} 成功", data=r.json())
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
error_msg = r.json()['error']
|
|
306
|
+
except KeyError:
|
|
307
|
+
error_msg = r.json()
|
|
308
|
+
return ReturnResponse(code=1, msg=f"重启 {serial} 失败, 报错 {error_msg}", data=None)
|
|
309
|
+
|
|
310
|
+
def get_alerts(self):
|
|
311
|
+
# from datetime import datetime, timedelta
|
|
312
|
+
params = {}
|
|
313
|
+
params['tsStart'] = "2025-10-20T00:00:00Z"
|
|
314
|
+
params['tsEnd'] = "2025-10-30T00:00:00Z"
|
|
315
|
+
# # 获取昨天0:00的时间戳(秒)
|
|
316
|
+
# yesterday = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=1)
|
|
317
|
+
# ts_start = int(yesterday.timestamp()) * 1000
|
|
318
|
+
# params['tsStart'] = str(ts_start)
|
|
319
|
+
# print(params)
|
|
320
|
+
r = requests.get(
|
|
321
|
+
url=f"{self.base_url}/organizations/{self.organization_id}/assurance/alerts",
|
|
322
|
+
headers=self.headers,
|
|
323
|
+
timeout=self.timeout,
|
|
324
|
+
params=params
|
|
325
|
+
)
|
|
326
|
+
for i in r.json():
|
|
327
|
+
print(i)
|
|
328
|
+
# return ReturnResponse(code=0, msg="获取告警成功", data=r.json())
|
|
329
|
+
|
|
330
|
+
def get_network_events(self, network_id):
|
|
331
|
+
params = {}
|
|
332
|
+
params['productType'] = "wireless"
|
|
333
|
+
|
|
334
|
+
print(params)
|
|
335
|
+
r = requests.get(
|
|
336
|
+
url=f"{self.base_url}/networks/{network_id}/events",
|
|
337
|
+
headers=self.headers,
|
|
338
|
+
timeout=self.timeout,
|
|
339
|
+
params=params
|
|
340
|
+
)
|
|
341
|
+
if r.status_code == 200:
|
|
342
|
+
return ReturnResponse(code=0, msg=f"获取网络事件成功", data=r.json())
|
|
343
|
+
return ReturnResponse(code=1, msg=f"获取网络事件失败: {r.status_code} - {r.text}", data=None)
|
|
344
|
+
|
|
345
|
+
def get_wireless_failcounter(self, network_id: str, timespan: int=5*60, serial: str=None):
|
|
346
|
+
'''
|
|
347
|
+
https://developer.cisco.com/meraki/api-v1/get-network-wireless-failed-connections/
|
|
348
|
+
'''
|
|
349
|
+
params = {}
|
|
350
|
+
params['timespan'] = timespan
|
|
351
|
+
if serial:
|
|
352
|
+
params['serial'] = serial
|
|
353
|
+
|
|
354
|
+
r = requests.get(
|
|
355
|
+
url=f"{self.base_url}/networks/{network_id}/wireless/failedConnections",
|
|
356
|
+
headers=self.headers,
|
|
357
|
+
timeout=self.timeout,
|
|
358
|
+
params=params
|
|
359
|
+
)
|
|
360
|
+
if r.status_code == 200:
|
|
361
|
+
return ReturnResponse(code=0, msg=f"获取无线失败连接成功", data=r.json())
|
|
362
|
+
return ReturnResponse(code=1, msg=f"获取无线失败连接失败: {r.status_code} - {r.text}", data=None)
|
|
363
|
+
|
|
364
|
+
def claim_network_devices(self, network_id: str, serials: list[str]) -> ReturnResponse:
|
|
365
|
+
'''
|
|
366
|
+
https://developer.cisco.com/meraki/api-v1/claim-network-devices/
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
network_id (_type_): _description_
|
|
370
|
+
serials (list): _description_
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
ReturnResponse: _description_
|
|
374
|
+
'''
|
|
375
|
+
new_serials = []
|
|
376
|
+
already_claimed_serials = []
|
|
377
|
+
|
|
378
|
+
for serial in serials:
|
|
379
|
+
r = self.get_device_detail(serial=serial)
|
|
380
|
+
if r.code == 0:
|
|
381
|
+
already_claimed_serials.append(serial)
|
|
382
|
+
elif r.code == 3:
|
|
383
|
+
new_serials.append(serial)
|
|
384
|
+
else:
|
|
385
|
+
new_serials.append(serial)
|
|
386
|
+
|
|
387
|
+
body = {
|
|
388
|
+
"serials": new_serials,
|
|
389
|
+
"addAtomically": True
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
r = requests.post(
|
|
393
|
+
url=f"{self.base_url}/networks/{network_id}/devices/claim",
|
|
394
|
+
headers=self.headers,
|
|
395
|
+
json=body,
|
|
396
|
+
timeout=self.timeout + 10
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if len(already_claimed_serials) == len(serials):
|
|
400
|
+
code = 0
|
|
401
|
+
msg = f"All {len(already_claimed_serials)} devices are already claimed"
|
|
402
|
+
elif len(already_claimed_serials) > 0:
|
|
403
|
+
code = 0
|
|
404
|
+
msg = f"Some {len(already_claimed_serials)} devices are already claimed"
|
|
405
|
+
else:
|
|
406
|
+
code = 0
|
|
407
|
+
msg = f"Claim network devices successfully, claimed {len(new_serials)} devices"
|
|
408
|
+
|
|
409
|
+
return ReturnResponse(code=code, msg=msg)
|
|
410
|
+
|
|
411
|
+
def update_device(self, serial: str, name: str=None, tags: list=None, address: str=None, lat: float=None, lng: float=None) -> ReturnResponse:
|
|
412
|
+
'''
|
|
413
|
+
https://developer.cisco.com/meraki/api-v1/update-device/
|
|
414
|
+
'''
|
|
415
|
+
body = {}
|
|
416
|
+
if name:
|
|
417
|
+
body['name'] = name
|
|
418
|
+
if tags:
|
|
419
|
+
body['tags'] = tags
|
|
420
|
+
if address:
|
|
421
|
+
body['address'] = address
|
|
422
|
+
if lat:
|
|
423
|
+
body['lat'] = lat
|
|
424
|
+
if lng:
|
|
425
|
+
body['lng'] = lng
|
|
426
|
+
r = requests.put(
|
|
427
|
+
url=f"{self.base_url}/devices/{serial}",
|
|
428
|
+
headers=self.headers,
|
|
429
|
+
json=body,
|
|
430
|
+
timeout=self.timeout
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def update_device(self,
|
|
435
|
+
config_template_id: str=None,
|
|
436
|
+
serial: str=None,
|
|
437
|
+
name: str=None,
|
|
438
|
+
tags: list=None,
|
|
439
|
+
address: str=None,
|
|
440
|
+
lat: float=None,
|
|
441
|
+
lng: float=None,
|
|
442
|
+
switch_profile_id: str=None
|
|
443
|
+
) -> ReturnResponse:
|
|
444
|
+
|
|
445
|
+
body = {
|
|
446
|
+
"name": name,
|
|
447
|
+
"tags": tags,
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if address:
|
|
451
|
+
body['address'] = address
|
|
452
|
+
body["moveMapMarker"] = True
|
|
453
|
+
|
|
454
|
+
if lat:
|
|
455
|
+
body['Lat'] = lat
|
|
456
|
+
|
|
457
|
+
if lng:
|
|
458
|
+
body['Lng'] = lng
|
|
459
|
+
|
|
460
|
+
if not switch_profile_id:
|
|
461
|
+
model = self.get_device_detail(serial=serial).data.get('model')
|
|
462
|
+
for switch_profile in self.get_switch_profiles(config_template_id=config_template_id).data:
|
|
463
|
+
if switch_profile.get('model') == model:
|
|
464
|
+
switch_profile_id = switch_profile.get('switchProfileId')
|
|
465
|
+
body['switchProfileId'] = switch_profile_id
|
|
466
|
+
else:
|
|
467
|
+
body['switchProfileId'] = switch_profile_id
|
|
468
|
+
|
|
469
|
+
response = requests.put(
|
|
470
|
+
url=f"{self.base_url}/devices/{serial}",
|
|
471
|
+
headers=self.headers,
|
|
472
|
+
json=body,
|
|
473
|
+
timeout=3
|
|
474
|
+
)
|
|
475
|
+
if response.status_code == 200:
|
|
476
|
+
return ReturnResponse(code=0, msg=f"更新设备 {serial} 成功", data=response.json())
|
|
477
|
+
else:
|
|
478
|
+
return ReturnResponse(code=1, msg=f"更新设备 {serial} 失败: {response.status_code} - {response.text}", data=None)
|
|
479
|
+
|
|
480
|
+
def get_switch_ports(self, serial: str) -> ReturnResponse:
|
|
481
|
+
'''
|
|
482
|
+
https://developer.cisco.com/meraki/api-v1/get-device-switch-ports/
|
|
483
|
+
'''
|
|
484
|
+
r = requests.get(
|
|
485
|
+
url=f"{self.base_url}/devices/{serial}/switch/ports/statuses",
|
|
486
|
+
headers=self.headers,
|
|
487
|
+
timeout=self.timeout
|
|
488
|
+
)
|
|
489
|
+
if r.status_code == 200:
|
|
490
|
+
return ReturnResponse(code=0, msg=f"获取交换机端口状态成功", data=r.json())
|
|
491
|
+
return ReturnResponse(code=1, msg=f"获取交换机端口状态失败: {r.status_code} - {r.text}", data=None)
|
|
492
|
+
|
|
493
|
+
def get_ssids(self, network_id):
|
|
494
|
+
'''
|
|
495
|
+
https://developer.cisco.com/meraki/api-v1/get-network-wireless-ssids/
|
|
496
|
+
'''
|
|
497
|
+
r = requests.get(
|
|
498
|
+
url=f"{self.base_url}/networks/{network_id}/wireless/ssids",
|
|
499
|
+
headers=self.headers,
|
|
500
|
+
timeout=self.timeout
|
|
501
|
+
)
|
|
502
|
+
if r.status_code == 200:
|
|
503
|
+
return ReturnResponse(code=0, msg=f"获取 SSID 成功", data=r.json())
|
|
504
|
+
return ReturnResponse(code=1, msg=f"获取 SSID 失败: {r.status_code} - {r.text}", data=None)
|
|
505
|
+
|
|
506
|
+
def update_ssid(self, network_id, ssid_number, body):
|
|
507
|
+
'''
|
|
508
|
+
https://developer.cisco.com/meraki/api-v1/update-network-wireless-ssid/
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
network_id (_type_): _description_
|
|
512
|
+
ssid_number (_type_): _description_
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
_type_: _description_
|
|
516
|
+
'''
|
|
517
|
+
r = requests.put(
|
|
518
|
+
url=f"{self.base_url}/networks/{network_id}/wireless/ssids/{ssid_number}",
|
|
519
|
+
headers=self.headers,
|
|
520
|
+
timeout=self.timeout,
|
|
521
|
+
json=body
|
|
522
|
+
)
|
|
523
|
+
if r.status_code == 200:
|
|
524
|
+
return ReturnResponse(code=0, msg=f"更新 SSID 成功", data=r.json())
|
|
525
|
+
return ReturnResponse(code=1, msg=f"更新 SSID 失败: {r.status_code} - {r.text}", data=None)
|
pytbox/pyjira.py
CHANGED
|
@@ -314,13 +314,13 @@ class PyJira:
|
|
|
314
314
|
else:
|
|
315
315
|
return ReturnResponse(code=1, msg=f'添加评论 [{comment}] 失败, 返回值: {r.text}', data=r.json())
|
|
316
316
|
|
|
317
|
-
def issue_search(self, jql: str, max_results: int =
|
|
318
|
-
"""使用JQL搜索JIRA
|
|
317
|
+
def issue_search(self, jql: str, max_results: int = 1000, fields: Optional[List[str]] = None) -> ReturnResponse:
|
|
318
|
+
"""使用JQL搜索JIRA任务(支持自动分页获取所有结果)
|
|
319
319
|
https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-jql-get
|
|
320
320
|
|
|
321
321
|
Args:
|
|
322
322
|
jql: JQL查询字符串
|
|
323
|
-
max_results: 最大返回结果数,默认
|
|
323
|
+
max_results: 最大返回结果数,默认1000。会自动分页获取
|
|
324
324
|
fields: 需要返回的字段列表,默认返回所有字段
|
|
325
325
|
|
|
326
326
|
Returns:
|
|
@@ -328,25 +328,72 @@ class PyJira:
|
|
|
328
328
|
"""
|
|
329
329
|
url = f"{self.base_url}/rest/api/3/search/jql"
|
|
330
330
|
|
|
331
|
-
#
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
331
|
+
# Jira API 单次请求最多返回100条,需要分页
|
|
332
|
+
page_size = 100
|
|
333
|
+
all_issues = []
|
|
334
|
+
seen_keys = set() # 用于去重
|
|
335
|
+
next_page_token = None
|
|
336
|
+
page_count = 0
|
|
337
337
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
params
|
|
338
|
+
while len(all_issues) < max_results:
|
|
339
|
+
page_count += 1
|
|
340
|
+
|
|
341
|
+
# 构建查询参数
|
|
342
|
+
params = {
|
|
343
|
+
"jql": jql,
|
|
344
|
+
"maxResults": page_size
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# 如果有下一页的 token,添加到参数中(按文档使用 nextPageToken 参数名)
|
|
348
|
+
if next_page_token:
|
|
349
|
+
params["nextPageToken"] = next_page_token
|
|
350
|
+
|
|
351
|
+
# 如果指定了字段,添加到查询参数中
|
|
352
|
+
if isinstance(fields, list):
|
|
353
|
+
params["fields"] = ",".join(fields)
|
|
354
|
+
else:
|
|
355
|
+
params["fields"] = fields
|
|
356
|
+
|
|
357
|
+
r = self.session.get(url, headers=self.headers, params=params, timeout=self.timeout)
|
|
358
|
+
|
|
359
|
+
if r.status_code != 200:
|
|
360
|
+
return ReturnResponse(code=1, msg=f'获取 issue 失败, status code: {r.status_code}, 报错: {r.text}')
|
|
361
|
+
|
|
362
|
+
data = r.json()
|
|
363
|
+
issues = data.get('issues', [])
|
|
364
|
+
|
|
365
|
+
if not issues:
|
|
366
|
+
break
|
|
367
|
+
|
|
368
|
+
# 去重:只添加未见过的 issue
|
|
369
|
+
new_issues_count = 0
|
|
370
|
+
for issue in issues:
|
|
371
|
+
issue_key = issue.get('key')
|
|
372
|
+
if issue_key and issue_key not in seen_keys:
|
|
373
|
+
all_issues.append(issue)
|
|
374
|
+
seen_keys.add(issue_key)
|
|
375
|
+
new_issues_count += 1
|
|
376
|
+
|
|
377
|
+
# 检查是否是最后一页
|
|
378
|
+
is_last = data.get('isLast', True)
|
|
379
|
+
next_page_token = data.get('nextPageToken')
|
|
380
|
+
|
|
381
|
+
# 如果是最后一页或没有新数据,退出循环
|
|
382
|
+
if is_last or new_issues_count == 0:
|
|
383
|
+
break
|
|
343
384
|
|
|
344
|
-
|
|
385
|
+
# 如果获取的数据超过 max_results,截断到指定数量
|
|
386
|
+
if len(all_issues) > max_results:
|
|
387
|
+
all_issues = all_issues[:max_results]
|
|
345
388
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
389
|
+
# 返回合并后的结果
|
|
390
|
+
result_data = {
|
|
391
|
+
'issues': all_issues,
|
|
392
|
+
'total': len(all_issues),
|
|
393
|
+
'startAt': 0
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return ReturnResponse(code=0, msg=f'成功获取 {len(all_issues)} 个唯一 issue(共请求 {page_count} 页)', data=result_data)
|
|
350
397
|
|
|
351
398
|
|
|
352
399
|
def get_boards(self) -> ReturnResponse:
|
|
@@ -2,11 +2,11 @@ pytbox/base.py,sha256=WC_p_PYMMpNq-5JRLyZoD74k-d2EQVmO4VjTvnI7VqE,4541
|
|
|
2
2
|
pytbox/cli.py,sha256=N775a0GK80IT2lQC2KRYtkZpIiu9UjavZmaxgNUgJhQ,160
|
|
3
3
|
pytbox/dida365.py,sha256=pUMPB9AyLZpTTbaz2LbtzdEpyjvuGf4YlRrCvM5sbJo,10545
|
|
4
4
|
pytbox/excel.py,sha256=f5XBLCeJbGgxytoSwVhbk03WLTzz8Q3IJ_RZ2-r_w6A,2334
|
|
5
|
-
pytbox/mingdao.py,sha256=
|
|
5
|
+
pytbox/mingdao.py,sha256=afEFJ9NKPdsmAZ4trBEJKl66fMj3Z8TWfaOcomNGhzw,6042
|
|
6
6
|
pytbox/notion.py,sha256=GRPdZAtyG2I6M6pCFbdrTWDACaPsp1RAXrY_RpWYKus,26572
|
|
7
7
|
pytbox/onepassword_connect.py,sha256=nD3xTl1ykQ4ct_dCRRF138gXCtk-phPfKYXuOn-P7Z8,3064
|
|
8
8
|
pytbox/onepassword_sa.py,sha256=08iUcYud3aEHuQcUsem9bWNxdXKgaxFbMy9yvtr-DZQ,6995
|
|
9
|
-
pytbox/pyjira.py,sha256=
|
|
9
|
+
pytbox/pyjira.py,sha256=Str9f7qTeBuy8XtJq2bXBI9ib79aQYCHBlJP1QKtZeo,24605
|
|
10
10
|
pytbox/vmware.py,sha256=WiH67_3-VCBjXJuh3UueOc31BdZDItiZhkeuPzoRhw4,3975
|
|
11
11
|
pytbox/alert/alert_handler.py,sha256=WCn4cKahv5G5BFGgmc7dX7BQ38h2kxTgxfRVTwc1O2M,6579
|
|
12
12
|
pytbox/alert/ping.py,sha256=KEnnXdIRJHvR_rEHPWLBt0wz4cGwmA29Lenlak3Z_1Y,778
|
|
@@ -45,7 +45,7 @@ pytbox/cli/formatters/__init__.py,sha256=4o85w4j-A-O1oBLvuE9q8AFiJ2C9rvB3MIKsy5V
|
|
|
45
45
|
pytbox/cli/formatters/output.py,sha256=h5WhZlQk1rjmxEj88Jy5ODLcv6L5zfGUhks_3AWIkKU,5455
|
|
46
46
|
pytbox/common/__init__.py,sha256=3JWfgCQZKZuSH5NCE7OCzKwq82pkyop9l7sH5YSNyfU,122
|
|
47
47
|
pytbox/database/mongo.py,sha256=AhJ9nCAQHKrrcL-ujeonOwEf3x2QkmT2VhoCdglqJmU,3478
|
|
48
|
-
pytbox/database/victoriametrics.py,sha256
|
|
48
|
+
pytbox/database/victoriametrics.py,sha256=iSgYLcINpWK3ry1fWoL32k65j91Ag49kNBWXYxW3NKA,15750
|
|
49
49
|
pytbox/feishu/client.py,sha256=kwGLseGT_iQUFmSqpuS2_77WmxtHstD64nXvktuQ3B4,5865
|
|
50
50
|
pytbox/feishu/endpoints.py,sha256=z3nPOZPC2JGDJlO7SusWBpRA33hZZ4Z-GBhI6F8L_u4,40240
|
|
51
51
|
pytbox/feishu/errors.py,sha256=79qFAHZw7jDj3gnWAjI1-W4tB0q1_aSfdjee4xzXeuI,1179
|
|
@@ -56,7 +56,7 @@ pytbox/log/victorialog.py,sha256=gffEiq38adv9sC5oZeMcyKghd3SGfRuqtZOFuqHQF6E,413
|
|
|
56
56
|
pytbox/mail/alimail.py,sha256=njKA3PUbIaiKFaxKvUObmklmEEHg2YA-O5rpgsgT5_w,5147
|
|
57
57
|
pytbox/mail/client.py,sha256=RNgWhhTXFTpD43U4p7hbmnfRdmltuZmbm890gaZTzhI,6278
|
|
58
58
|
pytbox/mail/mail_detail.py,sha256=6u8DK-7WzYPSuX6TdicSCh2Os_9Ou6Rn9xc6WRvv85M,699
|
|
59
|
-
pytbox/network/meraki.py,sha256=
|
|
59
|
+
pytbox/network/meraki.py,sha256=CgwIXZMjkQXBVpp-kpCUsalXL-dw0YstycByb7Odpv4,19635
|
|
60
60
|
pytbox/utils/cronjob.py,sha256=b17CY1fmaFTdQojicXAXHliov_JZdmT7cZhtO4v5sCo,3080
|
|
61
61
|
pytbox/utils/env.py,sha256=gD2-NyL3K3Vg1B1eGeD1hRtlSHPGgF8Oi9mchuQL6_o,646
|
|
62
62
|
pytbox/utils/load_config.py,sha256=R4pGerBinbewsym41hQ8Z-I5I7gepuEKODjIrli4C08,5043
|
|
@@ -65,8 +65,8 @@ pytbox/utils/response.py,sha256=kXjlwt0WVmLRam2eu1shzX2cQ7ux4cCQryaPGYwle5g,1247
|
|
|
65
65
|
pytbox/utils/richutils.py,sha256=OT9_q2Q1bthzB0g1GlhZVvM4ZAepJRKL6a_Vsr6vEqo,487
|
|
66
66
|
pytbox/utils/timeutils.py,sha256=uSKgwt20mVcgIGKLsH2tNum8v3rcpzgmBibPvyPQFgM,20433
|
|
67
67
|
pytbox/win/ad.py,sha256=-3pWfL3dElz-XoO4j4M9lrgu3KJtlhrS9gCWJBpafAU,1147
|
|
68
|
-
pytbox-0.2.
|
|
69
|
-
pytbox-0.2.
|
|
70
|
-
pytbox-0.2.
|
|
71
|
-
pytbox-0.2.
|
|
72
|
-
pytbox-0.2.
|
|
68
|
+
pytbox-0.2.9.dist-info/METADATA,sha256=-1h0V2CZvPPtUYHMGpQ-vAWlusDx06lJVhHDSxPAGCg,6319
|
|
69
|
+
pytbox-0.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
70
|
+
pytbox-0.2.9.dist-info/entry_points.txt,sha256=YaTOJ2oPjOiv2SZwY0UC-UA9QS2phRH1oMvxGnxO0Js,43
|
|
71
|
+
pytbox-0.2.9.dist-info/top_level.txt,sha256=YADgWue-Oe128ptN3J2hS3GB0Ncc5uZaSUM3e1rwswE,7
|
|
72
|
+
pytbox-0.2.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|