Merge pull request 'Auto-commit local changes before build (2026-01-12 11:13:43)' (#92) from v20260112-05-qnap-firmware-update-info-parser into main
Reviewed-on: #92
This commit is contained in:
commit
14d2422a1f
@ -1 +1 @@
|
|||||||
v20260112-04-remove-runchecks-success-override-button
|
v20260112-05-qnap-firmware-update-info-parser
|
||||||
|
|||||||
@ -360,6 +360,10 @@ def build_report_job_filters_meta():
|
|||||||
if (bs_val or "").strip().lower() == "synology" and bt_val.lower() == "updates":
|
if (bs_val or "").strip().lower() == "synology" and bt_val.lower() == "updates":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# QNAP firmware update notifications are informational and should never appear in reports.
|
||||||
|
if (bs_val or "").strip().lower() == "qnap" and bt_val.lower() == "firmware update":
|
||||||
|
continue
|
||||||
|
|
||||||
if bs_val:
|
if bs_val:
|
||||||
backup_softwares_set.add(bs_val)
|
backup_softwares_set.add(bs_val)
|
||||||
if bt_val:
|
if bt_val:
|
||||||
@ -381,7 +385,7 @@ def build_report_job_filters_meta():
|
|||||||
"backup_softwares": backup_softwares,
|
"backup_softwares": backup_softwares,
|
||||||
"backup_types": backup_types,
|
"backup_types": backup_types,
|
||||||
"by_backup_software": by_backup_software_out,
|
"by_backup_software": by_backup_software_out,
|
||||||
"excluded_backup_types": ["License Key", "Updates (Synology)"],
|
"excluded_backup_types": ["License Key", "Updates (Synology)", "Firmware Update (QNAP)"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -507,6 +511,7 @@ def api_reports_generate(report_id: int):
|
|||||||
where_filters = (
|
where_filters = (
|
||||||
" AND COALESCE(j.backup_type,'') NOT ILIKE 'license key' "
|
" AND COALESCE(j.backup_type,'') NOT ILIKE 'license key' "
|
||||||
" AND NOT (LOWER(COALESCE(j.backup_software,'')) = 'synology' AND LOWER(COALESCE(j.backup_type,'')) = 'updates') "
|
" AND NOT (LOWER(COALESCE(j.backup_software,'')) = 'synology' AND LOWER(COALESCE(j.backup_type,'')) = 'updates') "
|
||||||
|
" AND NOT (LOWER(COALESCE(j.backup_software,'')) = 'qnap' AND LOWER(COALESCE(j.backup_type,'')) = 'firmware update') "
|
||||||
)
|
)
|
||||||
rc = _safe_json_dict(getattr(report, "report_config", None))
|
rc = _safe_json_dict(getattr(report, "report_config", None))
|
||||||
filters = rc.get("filters") if isinstance(rc, dict) else None
|
filters = rc.get("filters") if isinstance(rc, dict) else None
|
||||||
|
|||||||
@ -637,6 +637,8 @@ def _infer_schedule_map_from_runs(job_id: int):
|
|||||||
return schedule
|
return schedule
|
||||||
if bs == 'synology' and bt == 'updates':
|
if bs == 'synology' and bt == 'updates':
|
||||||
return schedule
|
return schedule
|
||||||
|
if bs == 'qnap' and bt == 'firmware update':
|
||||||
|
return schedule
|
||||||
if bs == 'syncovery' and bt == 'syncovery':
|
if bs == 'syncovery' and bt == 'syncovery':
|
||||||
return schedule
|
return schedule
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@ -14,6 +14,7 @@ from .veeam import try_parse_veeam
|
|||||||
from .rdrive import try_parse_rdrive
|
from .rdrive import try_parse_rdrive
|
||||||
from .syncovery import try_parse_syncovery
|
from .syncovery import try_parse_syncovery
|
||||||
from .ntfs_auditing import try_parse_ntfs_auditing
|
from .ntfs_auditing import try_parse_ntfs_auditing
|
||||||
|
from .qnap import try_parse_qnap
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_text(value: object) -> object:
|
def _sanitize_text(value: object) -> object:
|
||||||
@ -106,6 +107,8 @@ def parse_mail_message(msg: MailMessage) -> None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
handled, result, objects = try_parse_3cx(msg)
|
handled, result, objects = try_parse_3cx(msg)
|
||||||
|
if not handled:
|
||||||
|
handled, result, objects = try_parse_qnap(msg)
|
||||||
if not handled:
|
if not handled:
|
||||||
handled, result, objects = try_parse_synology(msg)
|
handled, result, objects = try_parse_synology(msg)
|
||||||
if not handled:
|
if not handled:
|
||||||
|
|||||||
100
containers/backupchecks/src/backend/app/parsers/qnap.py
Normal file
100
containers/backupchecks/src/backend/app/parsers/qnap.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import html
|
||||||
|
import re
|
||||||
|
from typing import Dict, Tuple, List
|
||||||
|
|
||||||
|
from ..models import MailMessage
|
||||||
|
|
||||||
|
|
||||||
|
_SUBJECT_RE = re.compile(
|
||||||
|
r"^\[(?P<severity>info|warning|error)\]\s*\[\s*firmware\s+update\s*\]\s*notification\s+from\s+your\s+device\s*:\s*(?P<host>.+)$",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
|
||||||
|
_NAS_NAME_RE = re.compile(r"\bNAS\s*Name\s*:\s*(?P<host>[^\n<]+)", re.I)
|
||||||
|
_APP_NAME_RE = re.compile(r"\bApp\s*Name\s*:\s*(?P<app>[^\n<]+)", re.I)
|
||||||
|
_CATEGORY_RE = re.compile(r"\bCategory\s*:\s*(?P<cat>[^\n<]+)", re.I)
|
||||||
|
_MESSAGE_RE = re.compile(r"\bMessage\s*:\s*(?P<msg>.+)$", re.I | re.M)
|
||||||
|
|
||||||
|
_BR_RE = re.compile(r"<\s*br\s*/?\s*>", re.I)
|
||||||
|
_TAG_RE = re.compile(r"<[^>]+>")
|
||||||
|
_WS_RE = re.compile(r"[\t\r\f\v ]+")
|
||||||
|
|
||||||
|
|
||||||
|
def _html_to_text(value: str) -> str:
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
s = value
|
||||||
|
s = _BR_RE.sub("\n", s)
|
||||||
|
s = _TAG_RE.sub("", s)
|
||||||
|
s = html.unescape(s)
|
||||||
|
s = s.replace("\u00a0", " ")
|
||||||
|
# keep newlines, but normalize whitespace on each line
|
||||||
|
lines = [(_WS_RE.sub(" ", ln)).strip() for ln in s.split("\n")]
|
||||||
|
return "\n".join([ln for ln in lines if ln]).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def try_parse_qnap(msg: MailMessage) -> Tuple[bool, Dict, List[Dict]]:
|
||||||
|
"""Parse QNAP Notification Center e-mails.
|
||||||
|
|
||||||
|
Supported (informational):
|
||||||
|
- Firmware Update notifications
|
||||||
|
Subject: [Info][Firmware Update] Notification from your device: <HOST>
|
||||||
|
|
||||||
|
These notifications are informational: they should be visible in Run Checks,
|
||||||
|
but they must not participate in schedule inference, missed/expected logic,
|
||||||
|
or reporting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
subject = (getattr(msg, "subject", None) or "").strip()
|
||||||
|
if not subject:
|
||||||
|
return False, {}, []
|
||||||
|
|
||||||
|
m = _SUBJECT_RE.match(subject)
|
||||||
|
if not m:
|
||||||
|
return False, {}, []
|
||||||
|
|
||||||
|
host = (m.group("host") or "").strip()
|
||||||
|
|
||||||
|
html_body = getattr(msg, "html_body", None) or ""
|
||||||
|
text_body = getattr(msg, "text_body", None) or getattr(msg, "body", None) or ""
|
||||||
|
text = _html_to_text(html_body) if html_body else (text_body or "")
|
||||||
|
|
||||||
|
if text:
|
||||||
|
m_host = _NAS_NAME_RE.search(text)
|
||||||
|
if m_host:
|
||||||
|
host = (m_host.group("host") or "").strip() or host
|
||||||
|
|
||||||
|
# Prefer the detailed 'Message:' line from the body.
|
||||||
|
overall_message = None
|
||||||
|
if text:
|
||||||
|
m_msg = _MESSAGE_RE.search(text)
|
||||||
|
if m_msg:
|
||||||
|
overall_message = (m_msg.group("msg") or "").strip() or None
|
||||||
|
|
||||||
|
# If the body doesn't contain a dedicated message line, derive one.
|
||||||
|
if not overall_message and text:
|
||||||
|
parts: List[str] = []
|
||||||
|
m_app = _APP_NAME_RE.search(text)
|
||||||
|
if m_app:
|
||||||
|
parts.append((m_app.group("app") or "").strip())
|
||||||
|
m_cat = _CATEGORY_RE.search(text)
|
||||||
|
if m_cat:
|
||||||
|
parts.append((m_cat.group("cat") or "").strip())
|
||||||
|
if parts:
|
||||||
|
overall_message = " / ".join([p for p in parts if p]) or None
|
||||||
|
|
||||||
|
result: Dict = {
|
||||||
|
"backup_software": "QNAP",
|
||||||
|
"backup_type": "Firmware Update",
|
||||||
|
"job_name": "Firmware Update",
|
||||||
|
"overall_status": "Warning",
|
||||||
|
"overall_message": overall_message,
|
||||||
|
}
|
||||||
|
|
||||||
|
objects: List[Dict] = []
|
||||||
|
if host:
|
||||||
|
objects.append({"name": host, "status": "Warning"})
|
||||||
|
|
||||||
|
return True, result, objects
|
||||||
@ -61,6 +61,32 @@ PARSER_DEFINITIONS = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "qnap_firmware_update",
|
||||||
|
"backup_software": "QNAP",
|
||||||
|
"backup_types": ["Firmware Update"],
|
||||||
|
"order": 235,
|
||||||
|
"enabled": True,
|
||||||
|
"match": {
|
||||||
|
"from_contains": "notifications@",
|
||||||
|
"subject_contains": "Firmware Update",
|
||||||
|
},
|
||||||
|
"description": "Parses QNAP Notification Center firmware update notifications (informational; excluded from reporting and missing logic).",
|
||||||
|
"example": {
|
||||||
|
"subject": "[Info][Firmware Update] Notification from your device: BETSIES-NAS01",
|
||||||
|
"from_address": "notifications@customer.tld",
|
||||||
|
"body_snippet": "NAS Name: BETSIES-NAS01\n...\nMessage: ...",
|
||||||
|
"parsed_result": {
|
||||||
|
"backup_software": "QNAP",
|
||||||
|
"backup_type": "Firmware Update",
|
||||||
|
"job_name": "Firmware Update",
|
||||||
|
"overall_status": "Warning",
|
||||||
|
"objects": [
|
||||||
|
{"name": "BETSIES-NAS01", "status": "Warning", "error_message": None}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "veeam_replication_job",
|
"name": "veeam_replication_job",
|
||||||
"backup_software": "Veeam",
|
"backup_software": "Veeam",
|
||||||
|
|||||||
@ -133,6 +133,24 @@
|
|||||||
- Set overall status to Warning when the subject indicates any detected changes (↑/↓ counts > 0); otherwise Success.
|
- Set overall status to Warning when the subject indicates any detected changes (↑/↓ counts > 0); otherwise Success.
|
||||||
- Ensured job name is consistently generated as "<hostname> file audits" for all supported senders.
|
- Ensured job name is consistently generated as "<hostname> file audits" for all supported senders.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v20260112-04-remove-runchecks-success-override-button
|
||||||
|
|
||||||
|
- Removed the “Mark success (override)” button from the Run Checks popup.
|
||||||
|
- Prevented creation of actual overrides when marking individual runs as success.
|
||||||
|
- Simplified override overview by ensuring Run Checks actions no longer affect override administration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v20260112-05-qnap-firmware-update-info-parser
|
||||||
|
|
||||||
|
- Added parser support for QNAP Firmware Update notifications.
|
||||||
|
- Classified firmware update messages as informational warnings.
|
||||||
|
- Excluded QNAP firmware update messages from missing-run detection logic.
|
||||||
|
- Excluded QNAP firmware update messages from reports and scheduling.
|
||||||
|
- Ensured affected NAS devices are only shown in Run Checks when the message occurs.
|
||||||
|
|
||||||
================================================================================================================================================
|
================================================================================================================================================
|
||||||
## 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