first commit
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
"""Kernlogik: Gaeste auswaehlen, Snapshots anlegen, alte Snapshots aufraeumen."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
from .naming import build_name, effective_slug, parse_name, render_description
|
||||
from .proxmox import ProxmoxError
|
||||
|
||||
log = logging.getLogger("pvesnap.engine")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auswahl der Gaeste
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _matches_any_glob(text, patterns):
|
||||
lowered = (text or "").lower()
|
||||
return any(fnmatch.fnmatch(lowered, pattern.lower()) for pattern in patterns)
|
||||
|
||||
|
||||
def select_guests(group, guests):
|
||||
"""Alle Gaeste, auf die die Auswahlregeln der Gruppe zutreffen."""
|
||||
wanted_vmids = set(group.vmids)
|
||||
wanted_tags = set(group.tags)
|
||||
wanted_pools = set(p.lower() for p in group.pools)
|
||||
|
||||
excluded_vmids = set(group.exclude_vmids)
|
||||
excluded_tags = set(group.exclude_tags)
|
||||
|
||||
selected = []
|
||||
for guest in guests:
|
||||
if group.types and guest.type not in group.types:
|
||||
continue
|
||||
|
||||
if group.all:
|
||||
hit = True
|
||||
else:
|
||||
hit = (guest.vmid in wanted_vmids
|
||||
or (group.names and _matches_any_glob(guest.name, group.names))
|
||||
or (wanted_tags and wanted_tags.intersection(guest.tags))
|
||||
or (wanted_pools and guest.pool.lower() in wanted_pools))
|
||||
if not hit:
|
||||
continue
|
||||
|
||||
if guest.vmid in excluded_vmids:
|
||||
continue
|
||||
if group.exclude_names and _matches_any_glob(guest.name, group.exclude_names):
|
||||
continue
|
||||
if excluded_tags and excluded_tags.intersection(guest.tags):
|
||||
continue
|
||||
|
||||
selected.append(guest)
|
||||
return selected
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vorhaltezeit
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def managed_snapshots(prefix, group, snapshots):
|
||||
"""Nur die Snapshots, die pvesnap fuer *diese* Gruppe angelegt hat."""
|
||||
slug = effective_slug(prefix, group.slug)
|
||||
result = []
|
||||
for snap in snapshots:
|
||||
parsed = parse_name(prefix, snap.name)
|
||||
if not parsed or parsed["slug"] != slug:
|
||||
continue
|
||||
created = (datetime.fromtimestamp(snap.snaptime) if snap.snaptime
|
||||
else parsed["created"])
|
||||
result.append((snap, created))
|
||||
result.sort(key=lambda item: item[1], reverse=True) # neueste zuerst
|
||||
return result
|
||||
|
||||
|
||||
def plan_prune(prefix, group, snapshots, now):
|
||||
"""Welche Snapshots sollen weg? Gibt eine Liste von (Snapshot, Grund) zurueck."""
|
||||
doomed = []
|
||||
for index, (snap, created) in enumerate(managed_snapshots(prefix, group, snapshots)):
|
||||
if index < group.keep_min:
|
||||
continue
|
||||
age = (now - created).total_seconds()
|
||||
if group.keep_count > 0 and index >= group.keep_count:
|
||||
doomed.append((snap, "Anzahl > %d" % group.keep_count))
|
||||
elif group.keep_time > 0 and age > group.keep_time:
|
||||
doomed.append((snap, "aelter als Vorhaltezeit"))
|
||||
return doomed
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ausfuehrung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class GroupResult:
|
||||
group: str
|
||||
matched: int = 0
|
||||
created: list = field(default_factory=list)
|
||||
deleted: list = field(default_factory=list)
|
||||
skipped: list = field(default_factory=list)
|
||||
errors: list = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def ok(self):
|
||||
return not self.errors
|
||||
|
||||
def summary(self):
|
||||
return ("Gruppe '%s': %d Gast/Gaeste, %d Snapshot(s) angelegt, "
|
||||
"%d geloescht, %d Fehler"
|
||||
% (self.group, self.matched, len(self.created),
|
||||
len(self.deleted), len(self.errors)))
|
||||
|
||||
|
||||
def run_group(proxmox, config, group, now=None, create=True, prune=True, guests=None):
|
||||
"""Legt fuer eine Gruppe Snapshots an und raeumt alte weg."""
|
||||
now = now or datetime.now()
|
||||
prefix = config.globals.prefix
|
||||
result = GroupResult(group=group.name)
|
||||
|
||||
if guests is None:
|
||||
guests = proxmox.inventory()
|
||||
selected = select_guests(group, guests)
|
||||
result.matched = len(selected)
|
||||
|
||||
if not selected:
|
||||
log.warning("Gruppe '%s': keine passenden Gaeste gefunden", group.name)
|
||||
return result
|
||||
|
||||
snapshot_name = build_name(prefix, group.slug, now)
|
||||
template = group.description or config.globals.description
|
||||
|
||||
for guest in selected:
|
||||
if group.skip_stopped and not guest.running:
|
||||
result.skipped.append("%s (gestoppt)" % guest.label)
|
||||
log.info("Gruppe '%s': %s uebersprungen (gestoppt)", group.name, guest.label)
|
||||
continue
|
||||
|
||||
if create:
|
||||
description = render_description(template, group, guest, now, prefix)
|
||||
try:
|
||||
proxmox.create_snapshot(
|
||||
guest, snapshot_name, description,
|
||||
vmstate=group.vmstate and guest.running,
|
||||
)
|
||||
result.created.append("%s:%s" % (guest.vmid, snapshot_name))
|
||||
log.info("Gruppe '%s': Snapshot '%s' fuer %s angelegt",
|
||||
group.name, snapshot_name, guest.label)
|
||||
except ProxmoxError as exc:
|
||||
message = "%s: Snapshot fehlgeschlagen: %s" % (guest.label, exc)
|
||||
result.errors.append(message)
|
||||
log.error("Gruppe '%s': %s", group.name, message)
|
||||
continue # ohne neuen Snapshot nicht aufraeumen
|
||||
|
||||
if prune:
|
||||
try:
|
||||
_prune_guest(proxmox, config, group, guest, now, result)
|
||||
except ProxmoxError as exc:
|
||||
message = "%s: Aufraeumen fehlgeschlagen: %s" % (guest.label, exc)
|
||||
result.errors.append(message)
|
||||
log.error("Gruppe '%s': %s", group.name, message)
|
||||
|
||||
log.info(result.summary())
|
||||
return result
|
||||
|
||||
|
||||
def _prune_guest(proxmox, config, group, guest, now, result):
|
||||
prefix = config.globals.prefix
|
||||
snapshots = proxmox.list_snapshots(guest)
|
||||
for snap, reason in plan_prune(prefix, group, snapshots, now):
|
||||
try:
|
||||
proxmox.delete_snapshot(guest, snap.name)
|
||||
result.deleted.append("%s:%s" % (guest.vmid, snap.name))
|
||||
log.info("Gruppe '%s': Snapshot '%s' von %s geloescht (%s)",
|
||||
group.name, snap.name, guest.label, reason)
|
||||
except ProxmoxError as exc:
|
||||
message = "%s: '%s' nicht loeschbar: %s" % (guest.label, snap.name, exc)
|
||||
result.errors.append(message)
|
||||
log.error("Gruppe '%s': %s", group.name, message)
|
||||
Reference in New Issue
Block a user