Privacy Policy DSGVO with sheets for categories, export-, check- and first erase-functions

This commit is contained in:
2026-08-08 10:16:36 +00:00
parent daac478714
commit daeb08c054
15 changed files with 162 additions and 526 deletions
+68 -18
View File
@@ -2,6 +2,7 @@ import json
import os
import re
import subprocess
import unicodedata
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
@@ -666,18 +667,49 @@ def map_device(raw: dict[str, Any], field_mappings: list[dict[str, Any]] | None
mapped[target] = value
return mapped
def _find_asset(db: Session, mapped: dict[str, Any], order: list[str]) -> tuple[Asset | None, str | None, str | None]:
def _normalize_manufacturer(value: Any) -> str:
"""Normalize manufacturer names for safe serial-number matching."""
if value in (None, ""):
return ""
text = unicodedata.normalize("NFKD", str(value)).casefold()
text = "".join(ch for ch in text if not unicodedata.combining(ch))
tokens = re.findall(r"[a-z0-9]+", text)
suffixes = {
"inc", "incorporated", "corp", "corporation", "company", "co",
"ltd", "limited", "llc", "gmbh", "ag", "kg", "se", "plc",
}
while tokens and tokens[-1] in suffixes:
tokens.pop()
return "".join(tokens)
def _find_asset(
db: Session,
mapped: dict[str, Any],
order: list[str],
*,
serial_match_manufacturer: bool = False,
) -> tuple[Asset | None, str | None, str | None]:
for method in order:
if method == "node_id" and mapped.get("mesh_node_id"):
matches = db.query(Asset).filter(Asset.mesh_node_id == mapped["mesh_node_id"]).all()
elif method == "serial_number" and mapped.get("serial_number"):
matches = db.query(Asset).filter(func.lower(Asset.serial_number) == mapped["serial_number"].lower()).all()
if serial_match_manufacturer:
incoming_manufacturer = _normalize_manufacturer(mapped.get("manufacturer"))
if not incoming_manufacturer:
return None, method, "Seriennummer stimmt möglicherweise überein, aber der Hersteller fehlt in den MeshCentral-Daten."
matches = [
asset for asset in matches
if _normalize_manufacturer(asset.manufacturer) == incoming_manufacturer
]
else:
continue
if len(matches) == 1:
return matches[0], method, None
if len(matches) > 1:
return None, method, f"Mehrere Assets stimmen über {method} überein."
detail = "Seriennummer und Hersteller" if method == "serial_number" and serial_match_manufacturer else method
return None, method, f"Mehrere Assets stimmen über {detail} überein."
return None, None, None
@@ -858,13 +890,14 @@ def _inventory_segments(raw: dict[str, Any]) -> tuple[dict[str, Any], list[Any]
software = []
return hardware, software
def synchronize(db: Session, log: Callable[[str, str], None] | None = None) -> SyncResult:
def synchronize(db: Session, log: Callable[[str, str], None] | None = None, *, dry_run: bool = False) -> SyncResult:
log = log or (lambda level, message: None)
cfg = load_config()["meshcentral"]
run = SyncRun(status="running")
db.add(run)
db.commit()
db.refresh(run)
run = SyncRun(status="running", devices_found=0, created_count=0, updated_count=0, conflict_count=0, error_count=0)
if not dry_run:
db.add(run)
db.commit()
db.refresh(run)
now = datetime.utcnow()
seen_node_ids: set[str] = set()
mapping_rows = db.query(MeshFieldMapping).filter(MeshFieldMapping.enabled.is_(True)).order_by(MeshFieldMapping.priority, MeshFieldMapping.id).all()
@@ -894,7 +927,10 @@ def synchronize(db: Session, log: Callable[[str, str], None] | None = None) -> S
for target in missing_columns:
log("error", f"Systemfeld '{target}' besitzt keine Asset-Datenbankspalte und wird übersprungen.")
run.error_count += 1
log("info", "Synchronisierung gestartet.")
log("info", "Dry-Run gestartet; es werden keine Datenbankänderungen gespeichert." if dry_run else "Synchronisierung gestartet.")
log("info", "Asset-Zuordnung: " + "".join(cfg.get("match_order", ["node_id", "serial_number"])))
if cfg.get("serial_match_manufacturer", False):
log("info", "Seriennummern-Abgleich erfordert zusätzlich einen normalisierten Herstellervergleich.")
devices = fetch_devices(log)
run.devices_found = len(devices)
log("info", f"Verarbeite {len(devices)} Gerät(e).")
@@ -918,7 +954,12 @@ def synchronize(db: Session, log: Callable[[str, str], None] | None = None) -> S
log("error", "Gerät ohne Node-ID wurde übersprungen.")
continue
seen_node_ids.add(node_id)
asset, match_method, match_error = _find_asset(db, mapped, cfg.get("match_order", ["node_id", "serial_number"]))
asset, match_method, match_error = _find_asset(
db,
mapped,
cfg.get("match_order", ["node_id", "serial_number"]),
serial_match_manufacturer=bool(cfg.get("serial_match_manufacturer", False)),
)
if match_error:
run.conflict_count += 1
log("warning", f"{mapped.get('name', node_id)}: {match_error}")
@@ -940,10 +981,11 @@ def synchronize(db: Session, log: Callable[[str, str], None] | None = None) -> S
asset = Asset(category_id=category.id, name=mapped.get("name", node_id), mesh_node_id=node_id)
db.add(asset)
run.created_count += 1
log("success", f"{mapped.get('name', node_id)}: neues Asset in Kategorie '{category.name}' (ID {category.id}) angelegt.")
log("success", f"{mapped.get('name', node_id)}: {'würde als neues Asset' if dry_run else 'neues Asset'} in Kategorie '{category.name}' (ID {category.id}) {'angelegt werden' if dry_run else 'angelegt'}." )
elif match_method == "serial_number" and not asset.mesh_node_id:
asset.mesh_node_id = node_id
log("info", f"{asset.name}: über Seriennummer zugeordnet.")
match_note = "über Seriennummer + Hersteller" if cfg.get("serial_match_manufacturer", False) else "über Seriennummer"
log("info", f"{asset.name}: {match_note} zugeordnet{' (Dry-Run)' if dry_run else ''}.")
conflicts: dict[str, Any] = {}
history_changes: dict[str, dict[str, str]] = {}
@@ -1034,7 +1076,7 @@ def synchronize(db: Session, log: Callable[[str, str], None] | None = None) -> S
asset.mesh_sync_message = "Über Seriennummer zugeordnet" if match_method == "serial_number" else "Synchronisiert"
if changed:
run.updated_count += 1
log("info", f"{asset.name}: Daten aktualisiert.")
log("info", f"{asset.name}: Daten {'würden aktualisiert' if dry_run else 'aktualisiert'}." )
else:
log("debug", f"{asset.name}: keine Änderungen.")
except Exception as item_error:
@@ -1052,17 +1094,25 @@ def synchronize(db: Session, log: Callable[[str, str], None] | None = None) -> S
run.status = "success" if run.error_count == 0 else "partial"
run.finished_at = datetime.utcnow()
db.commit()
log("success", f"Synchronisierung beendet: {run.created_count} neu, {run.updated_count} geändert, {run.conflict_count} Konflikte, {run.error_count} Fehler.")
if dry_run:
db.rollback()
log("success", f"Dry-Run beendet: {run.devices_found} gefunden, {run.created_count} würden neu angelegt, {run.updated_count} würden geändert, {run.conflict_count} Konflikte, {run.error_count} Fehler. Es wurden keine Änderungen gespeichert.")
else:
db.commit()
log("success", f"Synchronisierung beendet: {run.created_count} neu, {run.updated_count} geändert, {run.conflict_count} Konflikte, {run.error_count} Fehler.")
except Exception as error:
db.rollback()
run = db.query(SyncRun).filter(SyncRun.id == run.id).first()
if not dry_run and run.id is not None:
stored_run = db.query(SyncRun).filter(SyncRun.id == run.id).first()
if stored_run is not None:
run = stored_run
run.status = "error"
run.error_count += 1
run.error_count = int(run.error_count or 0) + 1
run.message = str(error)
run.finished_at = datetime.utcnow()
db.commit()
log("error", f"Synchronisierung abgebrochen: {error}")
if not dry_run and run.id is not None:
db.commit()
log("error", f"{'Dry-Run' if dry_run else 'Synchronisierung'} abgebrochen: {error}")
return SyncResult(run=run)
from .presence_state import extract_last_seen