Commvault Viết script Python hoặc PowerShell gọi REST API để kiểm tra trạng thái job hàng ngày

Commvault-Viết script Python hoặc PowerShell gọi REST API để kiểm tra trạng thái job hàng ngày.


Yêu cầu thực hành: Tiến hành viết Script Python gọi REST API để tổng hợp các Job trong 24h, sau đó xuất ra dưới dạng html và gửi tự động và Telegram hàng ngày

1. Tiến hành tạo Access Token trên Command Center

1788752420740.png

1788752420747.png


2. Script. Đây là Script mà tôi đã thực hiện, bạn có thể tham khảo

#!/usr/bin/env python3
import json
import ssl
import datetime
import urllib.request
# ==================== CẤU HÌNH ====================
URL = "https://hostnamecommserve/webconsole/api/Job?completedJobLookupTime=86400&jobCategory=All"
TOKEN = "QSDK + Access Token"

TELEGRAM_BOT_TOKEN = "YOUR_TELEGRAM_BOT_TOKEN"
TELEGRAM_CHAT_ID = "YOUR_TELEGRAM_CHAT_ID"
TELEGRAM_TOPIC_ID = YOUR_TOPIC_ID

context = ssl._create_unverified_context()

def format_size(bytes_val):
try:
b = float(bytes_val)
if b <= 0:
return "0 B"
units = ["B", "KB", "MB", "GB", "TB"]
i = 0
while b >= 1024 and i < len(units) - 1:
b /= 1024.0
i += 1
return f"{b:.2f} {units}"
except (ValueError, TypeError):
return "0 B"

def format_time(ts):
if not ts or ts <= 0:
return "N/A"
try:
return datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')
except Exception:
return "N/A"

def clean_error_desc(reason_str):
if not reason_str:
return "-"
return str(reason_str).replace("<br/>", " ").replace("\n", " ").replace("\r", " ").strip()

def send_telegram_file(file_path, caption):
if TELEGRAM_BOT_TOKEN == "YOUR_TELEGRAM_BOT_TOKEN":
print("[Telegram] Chưa cấu hình TELEGRAM_BOT_TOKEN, bỏ qua bước gửi Telegram.")
return

url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendDocument"
boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"

with open(file_path, "rb") as f:
file_content = f.read()

body = []
body.append(f"--{boundary}".encode())
body.append(f'Content-Disposition: form-data; name="chat_id"'.encode())
body.append(b"")
body.append(TELEGRAM_CHAT_ID.encode())

body.append(f"--{boundary}".encode())
body.append(f'Content-Disposition: form-data; name="caption"'.encode())
body.append(b"")
body.append(caption.encode())

body.append(f"--{boundary}".encode())
body.append(f'Content-Disposition: form-data; name="document"; filename="{file_path}"'.encode())
body.append(b"Content-Type: text/html")
body.append(b"")
body.append(file_content)
body.append(f"--{boundary}--".encode())
body.append(b"")

payload = b"\r\n".join(body)
headers = {"Content-Type": f"multipart/form-data; boundary={boundary}"}

req = urllib.request.Request(url, data=payload, headers=headers, method="POST")
try:
with urllib.request.urlopen(req) as resp:
print("[Telegram] Đã gửi thành công báo cáo HTML qua Telegram!")
except Exception as e:
print(f"[Telegram] Lỗi gửi file: {e}")

def get_job_history():
headers = {
"Authtoken": TOKEN,
"Accept": "application/json"
}

req = urllib.request.Request(URL, headers=headers, method="GET")

try:
with urllib.request.urlopen(req, context=context) as response:
res_data = json.loads(response.read().decode("utf-8"))
jobs = res_data.get("jobs", []) or res_data.get("jobsSummary", [])

html_rows = ""
total_jobs = len(jobs)
failed_count = 0

for item in jobs:
job = item.get("jobSummary", item)

job_id = str(job.get("jobId", "N/A"))
operation = str(job.get("localizedOperationName") or job.get("jobType") or job.get("appTypeName", "N/A"))
server = str(job.get("clientName") or job.get("destClientName", "N/A"))

# BỔ SUNG: Lấy tên Subclient từ Response API của Commvault
subclient = str(job.get("subclientName") or job.get("subclient", {}).get("subclientName") or "N/A")

size = format_size(job.get("sizeOfApplication", 0))
status = str(job.get("status") or job.get("localizedStatus", "N/A"))
end_time = format_time(job.get("jobEndTime") or job.get("transactionEndTime", 0))

err_code_val = job.get("errorCode", {}).get("errorCode") or job.get("errorCodeValue") or job.get("lastErrorCode")
err_code = str(err_code_val) if err_code_val and str(err_code_val) != "0" else "-"

raw_desc = job.get("pendingReason") or job.get("delayReason") or job.get("statusReason") or ""
err_desc = clean_error_desc(raw_desc) if "Completed" not in status or "errors" in status or "Failed" in status else "-"

status_color = "#28a745"
if "Failed" in status:
status_color = "#dc3545"
failed_count += 1
elif "errors" in status:
status_color = "#ffc107"
failed_count += 1

html_rows += f"""
<tr>
<td><b>{job_id}</b></td>
<td>{operation}</td>
<td>{server}</td>
<td>{subclient}</td>
<td>{size}</td>
<td>{end_time}</td>
<td style="color: {status_color}; font-weight: bold;">{status}</td>
<td>{err_code}</td>
<td class="err-desc">{err_desc}</td>
</tr>
"""

html_content = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Commvault Job History Report</title>
<!-- DataTables CDN -->
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<script type="text/javascript" src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>

<style>
body {{ font-family: Arial, sans-serif; margin: 20px; background-color: #f8f9fa; }}
h2 {{ color: #003366; margin-bottom: 5px; }}
.summary {{ margin-bottom: 20px; font-size: 14px; background: #fff; padding: 12px; border-radius: 5px; border-left: 4px solid #003366; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}

.table-container {{ width: 100%; overflow-x: auto; }}
table.dataTable {{ width: 100% !important; background-color: #ffffff; border-collapse: collapse !important; }}

table.dataTable thead th {{
background-color: #003366;
color: white;
padding: 10px 15px;
position: relative;
user-select: none;
vertical-align: middle;
}}

/* Thanh kéo mép cột */
.resizer {{
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 7px;
background: rgba(255, 255, 255, 0.3);
cursor: col-resize;
z-index: 10;
}}
.resizer:hover, .resizing {{
background: #ff9800 !important;
width: 8px;
}}

table.dataTable tbody td {{
padding: 8px 12px;
vertical-align: middle;
white-space: normal;
word-break: break-word;
}}
.err-desc {{ color: #d9534f; }}
</style>
</head>
<body>
<h2>COMMVAULT REST API - JOB HISTORY REPORT</h2>
<div class="summary">
<b>Thời gian xuất báo cáo:</b> {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}<br>
<b>Tổng số Jobs:</b> {total_jobs} | <b>Jobs Cảnh báo/Lỗi:</b> {failed_count}
</div>

<div class="table-container">
<table id="jobTable" class="display cell-border">
<thead>
<tr>
<th style="width: 80px;">Job ID</th>
<th style="width: 130px;">Operation</th>
<th style="width: 150px;">Server</th>
<th style="width: 150px;">Subclient</th>
<th style="width: 80px;">Size</th>
<th style="width: 150px;">End Time</th>
<th style="width: 100px;">Status</th>
<th style="width: 80px;">Err Code</th>
<th>Error Description (FULL)</th>
</tr>
</thead>
<tbody>
{html_rows}
</tbody>
</table>
</div>

<script>
$(document).ready(function() {{
var table = $('#jobTable').DataTable({{
"paging": true,
"pageLength": 25,
"searching": true,
"ordering": true,
"autoWidth": false,
"order": [[ 0, "desc" ]],
"language": {{
"search": "Tìm kiếm nhanh:",
"lengthMenu": "Hiển thị _MENU_ dòng",
"info": "Đang xem _START_ đến _END_ trong tổng số _TOTAL_ dòng",
"paginate": {{
"first": "Đầu",
"last": "Cuối",
"next": "Tiếp",
"previous": "Trước"
}}
}}
}});

function makeColumnsResizable() {{
const ths = document.querySelectorAll('#jobTable thead th');
ths.forEach(th => {{
if (th.querySelector('.resizer')) return;

const resizer = document.createElement('div');
resizer.classList.add('resizer');
th.appendChild(resizer);

let x = 0;
let w = 0;

const mouseDownHandler = function (e) {{
e.stopPropagation();
x = e.clientX;
const styles = window.getComputedStyle(th);
w = parseInt(styles.width, 10);

document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
resizer.classList.add('resizing');
}};

const mouseMoveHandler = function (e) {{
const dx = e.clientX - x;
th.style.width = `${{w + dx}}px`;
}};

const mouseUpHandler = function () {{
resizer.classList.remove('resizing');
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
}};

resizer.addEventListener('mousedown', mouseDownHandler);
}});
}}

makeColumnsResizable();
table.on('draw', function() {{
makeColumnsResizable();
}});
}});
</script>
</body>
</html>
"""

filename = "commvault_job_report.html"
with open(filename, "w", encoding="utf-8") as f:
f.write(html_content)

print(f"[OK] Đã xuất file báo cáo thành công: {filename}")

caption = f" *Commvault Job Report*\n• Total Jobs: {total_jobs}\n• Failed/Errors: {failed_count}\n• Time: {datetime.datetime.now().strftime('%d/%m/%Y %H:%M')}"
send_telegram_file(filename, caption)

except urllib.error.HTTPError as e:
print(f"Lỗi HTTP API ({e.code}): {e.reason}")
except Exception as e:
print(f"Lỗi xử lý API: {e}")

if __name__ == "__main__":
get_job_history()

3. Cấu hình Script gửi tự động gửi daily qua telegram: ví dụ: vào 14h05 hàng ngày

(crontab -l 2>/dev/null | grep -v "check_job_status.py"; echo "5 14 * * * /usr/bin/python3 /root/check_job_status.py >> /home/commvault/job_cron.log 2>&1") | crontab -
Lưu ý: Thay các đường dẫn tuỳ theo yêu cầu

4. Kiểm tra kết quả
  • Đã có thông báo gửi qua Telegram
1788752420752.png

  • Xem thông tin chi tiết report
1788752420760.png
 
Back
Top