Добавить docs/services/lxc208-manage/alert/pbs.py
This commit is contained in:
parent
c9ef477b3d
commit
cfcfad24ac
226
docs/services/lxc208-manage/alert/pbs.py
Normal file
226
docs/services/lxc208-manage/alert/pbs.py
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import re
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, Dict, Tuple
|
||||||
|
|
||||||
|
from alerts.utils import escape_html, format_size
|
||||||
|
from alerts.alerts import ALERTS_CONFIG
|
||||||
|
from alerts.formatters import get_formatter
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BACKUP_CHANNEL = "backup"
|
||||||
|
|
||||||
|
PBS_REQUIRED_FIELDS = {"backup_size", "guest", "timestamp"}
|
||||||
|
|
||||||
|
|
||||||
|
class BackupSource(Enum):
|
||||||
|
PVE = "pve"
|
||||||
|
PBS = "pbs"
|
||||||
|
|
||||||
|
|
||||||
|
def detect_backup_source(obj: dict) -> Optional[BackupSource]:
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
text = (
|
||||||
|
str(obj.get("title", "")) +
|
||||||
|
"\n" +
|
||||||
|
str(obj.get("message", ""))
|
||||||
|
).lower()
|
||||||
|
|
||||||
|
if "vzdump" in text:
|
||||||
|
logger.debug("Detected PVE backup source (vzdump in text)")
|
||||||
|
return BackupSource.PVE
|
||||||
|
|
||||||
|
if "proxmox backup" in text:
|
||||||
|
logger.debug("Detected PBS backup source (proxmox backup in text)")
|
||||||
|
return BackupSource.PBS
|
||||||
|
|
||||||
|
if "backup" in text:
|
||||||
|
pbs_keys = PBS_REQUIRED_FIELDS & set(obj.keys())
|
||||||
|
if pbs_keys:
|
||||||
|
logger.debug("Detected PBS backup source (keys: %s)", pbs_keys)
|
||||||
|
return BackupSource.PBS
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_size_to_bytes(size_str: str) -> Optional[float]:
|
||||||
|
try:
|
||||||
|
parts = size_str.strip().split()
|
||||||
|
if len(parts) != 2:
|
||||||
|
return None
|
||||||
|
value = float(parts[0])
|
||||||
|
unit = parts[1].upper()
|
||||||
|
multipliers = {
|
||||||
|
"B": 1,
|
||||||
|
"KB": 1024,
|
||||||
|
"KIB": 1024,
|
||||||
|
"MB": 1024 ** 2,
|
||||||
|
"MIB": 1024 ** 2,
|
||||||
|
"GB": 1024 ** 3,
|
||||||
|
"GIB": 1024 ** 3,
|
||||||
|
"TB": 1024 ** 4,
|
||||||
|
"TIB": 1024 ** 4,
|
||||||
|
"PB": 1024 ** 5,
|
||||||
|
"PIB": 1024 ** 5,
|
||||||
|
}
|
||||||
|
multiplier = multipliers.get(unit)
|
||||||
|
if multiplier is None:
|
||||||
|
return None
|
||||||
|
return value * multiplier
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to parse size")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_pve_backup_table(message: str) -> Optional[Dict]:
|
||||||
|
table_pattern = r'^(\d+)\s+(\S+)\s+(ok|error|failed)\s+(\d+(?:m\s+\d+)?s)\s+([\d.]+\s+\S+)\s+((?:ct|vm)/\S+)'
|
||||||
|
matches = re.findall(table_pattern, message, re.MULTILINE)
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
return None
|
||||||
|
|
||||||
|
backups = []
|
||||||
|
total_size_bytes = 0
|
||||||
|
|
||||||
|
for match in matches:
|
||||||
|
vmid, name, status, duration, size_str, filename = match
|
||||||
|
|
||||||
|
prefix = "ct" if filename.startswith("ct/") else "vm"
|
||||||
|
backup_id = f"{prefix}-{vmid}"
|
||||||
|
size_bytes = parse_size_to_bytes(size_str)
|
||||||
|
if size_bytes is not None:
|
||||||
|
total_size_bytes += size_bytes
|
||||||
|
|
||||||
|
backups.append({
|
||||||
|
"backup_id": backup_id,
|
||||||
|
"name": name,
|
||||||
|
"status": status,
|
||||||
|
"duration": duration,
|
||||||
|
"size": size_str,
|
||||||
|
"size_bytes": size_bytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
time_match = re.search(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', message)
|
||||||
|
time_str = time_match.group(1) if time_match else datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
all_success = all(b["status"] == "ok" for b in backups)
|
||||||
|
has_errors = any(b["status"] in ("error", "failed") for b in backups)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"backups": backups,
|
||||||
|
"time": time_str,
|
||||||
|
"total_size": format_size(total_size_bytes),
|
||||||
|
"total_size_bytes": total_size_bytes,
|
||||||
|
"success": all_success and not has_errors,
|
||||||
|
"has_errors": has_errors,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_pbs_backup(obj: dict) -> Optional[Dict]:
|
||||||
|
node = obj.get("node", "")
|
||||||
|
vmid = obj.get("vmid", "") or obj.get("guest", "")
|
||||||
|
timestamp = obj.get("time") or obj.get("timestamp")
|
||||||
|
size = obj.get("size") or obj.get("backup_size")
|
||||||
|
status = str(obj.get("status", "unknown")).lower()
|
||||||
|
|
||||||
|
if not vmid and "backup" in obj:
|
||||||
|
match = re.search(r"(vm|ct)-(\d+)", obj.get("backup", ""))
|
||||||
|
if match:
|
||||||
|
vmid = f"{match.group(1)}-{match.group(2)}"
|
||||||
|
|
||||||
|
if not vmid:
|
||||||
|
return None
|
||||||
|
|
||||||
|
backup_id = f"{node}:{vmid}" if node else vmid
|
||||||
|
|
||||||
|
if timestamp:
|
||||||
|
try:
|
||||||
|
if isinstance(timestamp, (int, float)):
|
||||||
|
dt = datetime.fromtimestamp(timestamp)
|
||||||
|
else:
|
||||||
|
dt = datetime.fromisoformat(str(timestamp).replace("Z", "+00:00"))
|
||||||
|
time_str = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Failed to parse timestamp: %s", timestamp)
|
||||||
|
time_str = str(timestamp)
|
||||||
|
else:
|
||||||
|
time_str = "Unknown"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"backups": [{
|
||||||
|
"backup_id": backup_id,
|
||||||
|
"name": "",
|
||||||
|
"status": status,
|
||||||
|
"duration": "",
|
||||||
|
"size": format_size(size),
|
||||||
|
"size_bytes": 0,
|
||||||
|
}],
|
||||||
|
"time": time_str,
|
||||||
|
"total_size": format_size(size),
|
||||||
|
"total_size_bytes": 0,
|
||||||
|
"success": status in ("ok", "success"),
|
||||||
|
"has_errors": status not in ("ok", "success"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_backup(obj: dict, source: Optional[BackupSource] = None) -> Optional[Dict]:
|
||||||
|
if not obj:
|
||||||
|
return None
|
||||||
|
if isinstance(obj, str):
|
||||||
|
try:
|
||||||
|
obj = json.loads(obj)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if source is None:
|
||||||
|
source = detect_backup_source(obj)
|
||||||
|
|
||||||
|
if source == BackupSource.PVE:
|
||||||
|
return parse_pve_backup_table(obj.get("message", ""))
|
||||||
|
if source == BackupSource.PBS:
|
||||||
|
return parse_pbs_backup(obj)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_backup_message(info: Dict) -> Tuple[str, str]:
|
||||||
|
alertname = "Backup Success" if info.get("success") else "Backup Failed"
|
||||||
|
config = ALERTS_CONFIG.get(alertname)
|
||||||
|
|
||||||
|
if not config:
|
||||||
|
return (
|
||||||
|
"<b>Backup notification received</b><br><br><pre>" +
|
||||||
|
escape_html(json.dumps(info, ensure_ascii=False, indent=2)) +
|
||||||
|
"</pre>",
|
||||||
|
BACKUP_CHANNEL
|
||||||
|
)
|
||||||
|
|
||||||
|
formatter = get_formatter(config.get("formatter"))
|
||||||
|
text = f"<b>{config['title']}</b>\n\n"
|
||||||
|
|
||||||
|
if formatter:
|
||||||
|
text += formatter(info)
|
||||||
|
|
||||||
|
room = config.get("room", BACKUP_CHANNEL)
|
||||||
|
return text, room
|
||||||
|
|
||||||
|
|
||||||
|
def process_backup(obj: dict, send_func, source: Optional[BackupSource] = None) -> None:
|
||||||
|
info = parse_backup(obj, source)
|
||||||
|
|
||||||
|
if not info:
|
||||||
|
send_func(
|
||||||
|
"<b>Backup notification received</b><br><br><pre>" +
|
||||||
|
escape_html(json.dumps(obj, ensure_ascii=False, indent=2)) +
|
||||||
|
"</pre>",
|
||||||
|
BACKUP_CHANNEL
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
message, room = build_backup_message(info)
|
||||||
|
send_func(message, room)
|
||||||
Loading…
Reference in New Issue
Block a user