Files
proxmox-snapshot-service/pvesnap/mail.py
T
duffyduckandClaude Opus 5.5 74be5aee59 Platz vor jedem Snapshot pruefen, bei Mangel alte loeschen, per Mail warnen
Volle Ceph-Pools legen alle VMs darauf lahm. Vor jedem Snapshot gilt jetzt
je Storage des Gastes: frei >= sofortiger Verbrauch + space_min_free
(Vorgabe 20 %). Die Reserve bleibt also auch nach dem Snapshot frei.

Reicht es nicht, wird der aelteste pvesnap-Snapshot des Gastes geloescht und
neu geprueft, bis es reicht. Sind nur noch die neuesten space_keep (Vorgabe
7) uebrig, entfaellt der Snapshot und der naechste Gast kommt dran.

Gezaehlt wird je Gast ueber alle Gruppen, anders als bei der Vorhaltezeit.
Auf dem Testhost haette eine Grenze je Gruppe nie gegriffen: keep_count = 5
liegt unter 7, und 13 GiB steckten in einer Gruppe, die es nicht mehr gibt.

Sofortiger Verbrauch: Dicke ZFS-Zvols muessen ihre Reservierung auch nach
dem Snapshot garantieren, das kostet alles seit dem letzten Snapshot
Geschriebene. Exakt ist das refreservation - usedbyrefreservation; die Werte
kommen per zfs get, bei anderen Nodes per ssh ueber die IP aus dem
Clusterstatus, weil Node-Namen nicht ueberall aufloesen. Dazu kommt der RAM
bei vmstate. --dry-run zeigt die Rechnung je Storage.

Ceph und ZFS geben Platz erst im Hintergrund frei. Deshalb wird nach jedem
Loeschen gewartet, solange der Platz waechst, und erst nach space_settle
(Vorgabe 30s) ohne Zuwachs weitergemacht. Ohne das wurde auf Ceph ein Snapshot
zu viel geloescht. Laesst sich der Platz nicht feststellen, wird nichts
geloescht und der Snapshot trotzdem versucht.

Neu ist [mail]: eine Alarm-Mail je Gruppe und Lauf, wenn ein Snapshot wegen
Platzmangels ausfaellt. STARTTLS oder SSL/TLS mit Zertifikatspruefung, ohne
Rueckfall auf Klartext. Einzurichten im Editor (Taste m) oder von Hand, Test
mit pvesnap mail-test. Liegt ein Passwort in der Datei, setzt der Editor die
Rechte auf 600, auch fuer die .bak.

Das Handbuch beschreibt, wie man Ceph (Ratios) und ZFS (Notreserve) frueher
"voll" sagen laesst und was im Notfall zu tun ist. Ausserdem war
"pvesnap run --dry-run" ueberall falsch: Die Option ist global und muss vor
dem Unterbefehl stehen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 10:27:21 +01:00

97 lines
3.4 KiB
Python

"""Alarm per Mail, wenn wegen Platzmangels ein Snapshot ausfaellt."""
from __future__ import annotations
import logging
import smtplib
import socket
import ssl
from email.message import EmailMessage
from email.utils import formatdate, make_msgid
log = logging.getLogger("pvesnap.mail")
SMTP_TIMEOUT = 30
class MailError(Exception):
"""Die Mail konnte nicht zugestellt werden."""
def _tls_context(mail):
context = ssl.create_default_context()
if not mail.tls_verify:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
return context
def _connect(mail):
if mail.security == "ssl":
return smtplib.SMTP_SSL(mail.server, mail.effective_port, timeout=SMTP_TIMEOUT,
context=_tls_context(mail))
client = smtplib.SMTP(mail.server, mail.effective_port, timeout=SMTP_TIMEOUT)
if mail.security == "starttls":
try:
# Bietet der Server kein STARTTLS an, schlaegt das fehl. Weiter im
# Klartext geht es dann bewusst nicht.
client.starttls(context=_tls_context(mail))
except Exception:
client.close()
raise
return client
def send_mail(mail, subject, body):
"""Verschickt eine Mail; jedes Scheitern wird zu einem MailError."""
message = EmailMessage()
message["Subject"] = subject
message["From"] = mail.sender
message["To"] = ", ".join(mail.recipients)
message["Date"] = formatdate(localtime=True)
message["Message-ID"] = make_msgid(domain=socket.getfqdn())
message.set_content(body)
try:
with _connect(mail) as client:
if mail.user:
client.login(mail.user, mail.password)
client.send_message(message)
except (OSError, smtplib.SMTPException, ValueError) as exc:
raise MailError("%s: %s" % (mail.server, exc))
def send_test_mail(mail):
host = socket.gethostname()
send_mail(mail, "[pvesnap] %s: Test-Mail" % host,
"Der Mail-Alarm von pvesnap auf %s funktioniert.\n" % host)
def send_space_alert(config, group_name, messages):
"""Meldet die ausgefallenen Snapshots eines Laufs in einer einzigen Mail.
Scheitert das, bleibt es bei einem Fehler im Protokoll: Eine kaputte Mail
darf die uebrigen Gruppen nicht aufhalten.
"""
mail = config.mail
if not mail.enabled:
return
host = socket.gethostname()
if config.globals.dry_run:
log.info("[TESTLAUF] wuerde Alarm-Mail an %s senden", ", ".join(mail.recipients))
return
subject = ("[pvesnap] %s: %d Snapshot(s) wegen Platzmangels ausgefallen"
% (host, len(messages)))
body = ("Auf %s konnte pvesnap in der Gruppe '%s' nicht genug Platz schaffen.\n"
"Fuer diese Gaeste wurde kein Snapshot angelegt:\n\n%s\n\n"
"Der Speicher wird knapp - laeuft er voll, stehen alle Maschinen darauf,\n"
"und auch das Aufraeumen kann dann scheitern. Bitte zeitnah pruefen:\n\n"
" ceph df (Ceph: MAX AVAIL)\n"
" pvesm status\n"
" pvesnap list -a (auch von Hand angelegte Snapshots)\n"
% (host, group_name, "\n".join(" - %s" % text for text in messages)))
try:
send_mail(mail, subject, body)
log.info("Alarm-Mail an %s gesendet", ", ".join(mail.recipients))
except MailError as exc:
log.error("Alarm-Mail nicht zustellbar: %s", exc)