Privacy Policy DSGVO with sheets for categories, export-, check- and first erase-functions
This commit is contained in:
+68
-18
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user