Добавить 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