|
|
|
|
@ -17,9 +17,105 @@ VEEAM_BACKUP_TYPES = [
|
|
|
|
|
"Veeam Backup for Microsoft 365",
|
|
|
|
|
"Scale-out Backup Repository",
|
|
|
|
|
"Health Check",
|
|
|
|
|
"Cloud Connect Report",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_cloud_connect_report_from_html(html: str) -> Tuple[List[Dict], str]:
|
|
|
|
|
"""Parse Veeam Cloud Connect daily report (provider) HTML.
|
|
|
|
|
|
|
|
|
|
The report contains a "Backup" table with columns including:
|
|
|
|
|
User | Repository Name | ...
|
|
|
|
|
|
|
|
|
|
Objects in our system are a combination of the "User" and "Repository Name"
|
|
|
|
|
columns, separated by " | ".
|
|
|
|
|
|
|
|
|
|
Row background colour indicates status:
|
|
|
|
|
- Red/pink rows: Failed/Error
|
|
|
|
|
- Yellow/orange rows: Warning
|
|
|
|
|
- White rows: Success
|
|
|
|
|
|
|
|
|
|
The row where the first cell is "TOTAL" is a summary row and is not an object.
|
|
|
|
|
|
|
|
|
|
Returns: (objects, overall_status)
|
|
|
|
|
"""
|
|
|
|
|
html = _normalize_html(html)
|
|
|
|
|
if not html:
|
|
|
|
|
return [], "Success"
|
|
|
|
|
|
|
|
|
|
# Find the Backup table block.
|
|
|
|
|
m_table = re.search(r"(?is)<p[^>]*>\s*Backup\s*</p>\s*<table.*?</table>", html)
|
|
|
|
|
if not m_table:
|
|
|
|
|
return [], "Success"
|
|
|
|
|
|
|
|
|
|
table_html = m_table.group(0)
|
|
|
|
|
|
|
|
|
|
# Extract rows.
|
|
|
|
|
row_pattern = re.compile(r"(?is)<tr([^>]*)>(.*?)</tr>")
|
|
|
|
|
cell_pattern = re.compile(r"(?is)<t[dh][^>]*>(.*?)</t[dh]>")
|
|
|
|
|
|
|
|
|
|
objects: List[Dict] = []
|
|
|
|
|
saw_failed = False
|
|
|
|
|
saw_warning = False
|
|
|
|
|
|
|
|
|
|
for row_attr, row_inner in row_pattern.findall(table_html):
|
|
|
|
|
cells = cell_pattern.findall(row_inner)
|
|
|
|
|
if len(cells) < 3:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Convert cells to plain text.
|
|
|
|
|
plain = [_strip_html_tags(c).strip() for c in cells]
|
|
|
|
|
if not plain:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Skip header row.
|
|
|
|
|
if plain[0].strip().lower() == "user":
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
user = (plain[0] or "").strip()
|
|
|
|
|
repo_name = (plain[2] or "").strip()
|
|
|
|
|
|
|
|
|
|
# Skip summary row.
|
|
|
|
|
if user.upper() == "TOTAL":
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if not user and not repo_name:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Determine status based on background colour.
|
|
|
|
|
# Veeam uses inline styles like: background-color: #fb9895 (error)
|
|
|
|
|
# and background-color: #ffd96c (warning).
|
|
|
|
|
row_style = (row_attr or "")
|
|
|
|
|
m_bg = re.search(r"(?i)background-color\s*:\s*([^;\"\s]+)", row_style)
|
|
|
|
|
bg = (m_bg.group(1).strip().lower() if m_bg else "")
|
|
|
|
|
|
|
|
|
|
status = "Success"
|
|
|
|
|
if bg in {"#fb9895", "#ff9999", "#f4cccc", "#ffb3b3"}:
|
|
|
|
|
status = "Failed"
|
|
|
|
|
saw_failed = True
|
|
|
|
|
elif bg in {"#ffd96c", "#fff2cc", "#ffe599", "#f9cb9c"}:
|
|
|
|
|
status = "Warning"
|
|
|
|
|
saw_warning = True
|
|
|
|
|
|
|
|
|
|
name = f"{user} | {repo_name}".strip(" |")
|
|
|
|
|
objects.append(
|
|
|
|
|
{
|
|
|
|
|
"name": name,
|
|
|
|
|
"type": "Repository",
|
|
|
|
|
"status": status,
|
|
|
|
|
"error_message": None,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
overall_status = "Success"
|
|
|
|
|
if saw_failed:
|
|
|
|
|
overall_status = "Failed"
|
|
|
|
|
elif saw_warning:
|
|
|
|
|
overall_status = "Warning"
|
|
|
|
|
|
|
|
|
|
return objects, overall_status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _strip_html_tags(value: str) -> str:
|
|
|
|
|
"""Very small helper to strip HTML tags from a string."""
|
|
|
|
|
if not value:
|
|
|
|
|
@ -769,17 +865,46 @@ def try_parse_veeam(msg: MailMessage) -> Tuple[bool, Dict, List[Dict]]:
|
|
|
|
|
html_body = _normalize_html(getattr(msg, "html_body", None) or "")
|
|
|
|
|
html_lower = html_body.lower()
|
|
|
|
|
|
|
|
|
|
# Veeam Cloud Connect provider daily report (no [Success]/[Warning] marker).
|
|
|
|
|
is_cloud_connect_report = (
|
|
|
|
|
"veeam cloud connect" in subject.lower()
|
|
|
|
|
and "daily report" in subject.lower()
|
|
|
|
|
and "repository name" in html_lower
|
|
|
|
|
and "infrastructure status" in html_lower
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Special-case: Veeam Backup for Microsoft 365 mails can come without a
|
|
|
|
|
# subject marker. Detect via HTML and extract status from the banner.
|
|
|
|
|
is_m365 = "veeam backup for microsoft 365" in html_lower
|
|
|
|
|
|
|
|
|
|
# If we cannot detect a status marker and this is not an M365 report,
|
|
|
|
|
# we still try to parse when the subject strongly indicates a Veeam report.
|
|
|
|
|
if not m_status and not m_finished and not is_m365:
|
|
|
|
|
if not m_status and not m_finished and not is_m365 and not is_cloud_connect_report:
|
|
|
|
|
lowered = subject.lower()
|
|
|
|
|
if not any(k in lowered for k in ["veeam", "backup job", "backup copy job", "replica job", "configuration backup", "health check"]):
|
|
|
|
|
if not any(k in lowered for k in ["veeam", "cloud connect", "backup job", "backup copy job", "replica job", "configuration backup", "health check"]):
|
|
|
|
|
return False, {}, []
|
|
|
|
|
|
|
|
|
|
# Handle Cloud Connect daily report early: overall status is derived from row colours.
|
|
|
|
|
if is_cloud_connect_report:
|
|
|
|
|
objects, overall_status = _parse_cloud_connect_report_from_html(html_body)
|
|
|
|
|
|
|
|
|
|
overall_message = None
|
|
|
|
|
# Use the short subject summary when present, e.g. ": 2 Errors, 1 Warnings, 49 Successes".
|
|
|
|
|
m_sum = re.search(r"(?i)daily\s+report\s*:\s*(.+)$", subject)
|
|
|
|
|
if m_sum:
|
|
|
|
|
overall_message = (m_sum.group(1) or "").strip() or None
|
|
|
|
|
|
|
|
|
|
result = {
|
|
|
|
|
"backup_software": "Veeam",
|
|
|
|
|
"backup_type": "Cloud Connect Report",
|
|
|
|
|
"job_name": "Daily report",
|
|
|
|
|
"overall_status": overall_status,
|
|
|
|
|
}
|
|
|
|
|
if overall_message:
|
|
|
|
|
result["overall_message"] = overall_message
|
|
|
|
|
|
|
|
|
|
return True, result, objects
|
|
|
|
|
|
|
|
|
|
if m_status:
|
|
|
|
|
status_word = m_status.group(1)
|
|
|
|
|
rest = m_status.group(2)
|
|
|
|
|
|