Compare commits
2 Commits
79829abd70
...
ffb81e8e3d
| Author | SHA1 | Date | |
|---|---|---|---|
| ffb81e8e3d | |||
| 166311da43 |
@ -1 +1 @@
|
|||||||
v20260109-09-ellipsis-reset-on-popup-close
|
v20260109-10-veeam-cloud-connect-report-parser
|
||||||
|
|||||||
@ -17,9 +17,105 @@ VEEAM_BACKUP_TYPES = [
|
|||||||
"Veeam Backup for Microsoft 365",
|
"Veeam Backup for Microsoft 365",
|
||||||
"Scale-out Backup Repository",
|
"Scale-out Backup Repository",
|
||||||
"Health Check",
|
"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:
|
def _strip_html_tags(value: str) -> str:
|
||||||
"""Very small helper to strip HTML tags from a string."""
|
"""Very small helper to strip HTML tags from a string."""
|
||||||
if not value:
|
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_body = _normalize_html(getattr(msg, "html_body", None) or "")
|
||||||
html_lower = html_body.lower()
|
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
|
# Special-case: Veeam Backup for Microsoft 365 mails can come without a
|
||||||
# subject marker. Detect via HTML and extract status from the banner.
|
# subject marker. Detect via HTML and extract status from the banner.
|
||||||
is_m365 = "veeam backup for microsoft 365" in html_lower
|
is_m365 = "veeam backup for microsoft 365" in html_lower
|
||||||
|
|
||||||
# If we cannot detect a status marker and this is not an M365 report,
|
# 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.
|
# 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()
|
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, {}, []
|
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:
|
if m_status:
|
||||||
status_word = m_status.group(1)
|
status_word = m_status.group(1)
|
||||||
rest = m_status.group(2)
|
rest = m_status.group(2)
|
||||||
|
|||||||
@ -71,6 +71,15 @@
|
|||||||
- Reset expanded ellipsis fields when a Bootstrap modal is shown or hidden, so expanded state does not persist between openings.
|
- Reset expanded ellipsis fields when a Bootstrap modal is shown or hidden, so expanded state does not persist between openings.
|
||||||
- Added the same reset behavior for Bootstrap offcanvas components to keep behavior consistent across popups.
|
- Added the same reset behavior for Bootstrap offcanvas components to keep behavior consistent across popups.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v20260109-10-veeam-cloud-connect-report-parser
|
||||||
|
|
||||||
|
- Updated the Veeam Cloud Connect Report parser to correctly handle object naming by combining User and Repository Name with a "|" separator.
|
||||||
|
- Excluded the row containing "TOTAL" from being processed as an object.
|
||||||
|
- Adjusted status detection so red rows are interpreted as Errors and yellow/orange rows as Warnings.
|
||||||
|
- Ensured the overall status is set to Error when one or more objects are detected as errors.
|
||||||
|
- Improved parsing logic to correctly classify mixed-status reports within a single mail.
|
||||||
================================================================================================================================================
|
================================================================================================================================================
|
||||||
## v0.1.19
|
## v0.1.19
|
||||||
This release delivers a broad set of improvements focused on reliability, transparency, and operational control across mail processing, administrative auditing, and Run Checks workflows. The changes aim to make message handling more robust, provide better insight for administrators, and give operators clearer and more flexible control when reviewing backup runs.
|
This release delivers a broad set of improvements focused on reliability, transparency, and operational control across mail processing, administrative auditing, and Run Checks workflows. The changes aim to make message handling more robust, provide better insight for administrators, and give operators clearer and more flexible control when reviewing backup runs.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user