fmeshsync fix with synology,vm's and lxc

This commit is contained in:
root
2026-08-08 19:44:27 +02:00
parent 2786f12ab6
commit 5e32be7d0c
41 changed files with 1335 additions and 1674 deletions
+113 -26
View File
@@ -9,6 +9,7 @@ import shutil
import io
import json
import logging
from logging.handlers import WatchedFileHandler
import traceback
import hashlib
import hmac
@@ -192,7 +193,7 @@ def _software_debug_tail(max_lines: int = 400) -> str:
logger = logging.getLogger("assetmanager")
logger.setLevel(logging.INFO)
if not logger.handlers:
file_handler = logging.FileHandler(APP_LOG_DIR / "errors.log", encoding="utf-8")
file_handler = WatchedFileHandler(APP_LOG_DIR / "errors.log", encoding="utf-8")
file_handler.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s | %(message)s"))
logger.addHandler(file_handler)
@@ -200,7 +201,7 @@ ldap_logger = logging.getLogger("assetmanager.ldap")
ldap_logger.setLevel(logging.DEBUG)
ldap_logger.propagate = False
if not ldap_logger.handlers:
ldap_file_handler = logging.FileHandler(APP_LOG_DIR / "ldap.log", encoding="utf-8")
ldap_file_handler = WatchedFileHandler(APP_LOG_DIR / "ldap.log", encoding="utf-8")
ldap_file_handler.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s | %(message)s"))
ldap_logger.addHandler(ldap_file_handler)
ldap_console_handler = logging.StreamHandler()
@@ -6023,6 +6024,39 @@ def _decode_software_callback_body(raw_body: bytes) -> tuple[str, str, list[str]
return decoded_body, detected_encoding, decode_errors
def _sanitize_callback_value(value: Any) -> Any:
"""Remove PostgreSQL-incompatible NUL characters from callback values.
Windows registry and uninstall metadata can occasionally contain embedded
NUL characters. PostgreSQL text and JSON values reject those characters,
so callback payloads are sanitized before they are written to the database.
Other characters and the original data structure are preserved.
"""
if isinstance(value, str):
return value.replace("\x00", "")
if isinstance(value, list):
return [_sanitize_callback_value(item) for item in value]
if isinstance(value, tuple):
return tuple(_sanitize_callback_value(item) for item in value)
if isinstance(value, dict):
return {
_sanitize_callback_value(key) if isinstance(key, str) else key: _sanitize_callback_value(item)
for key, item in value.items()
}
return value
def _count_nul_characters(value: Any) -> int:
"""Count embedded NUL characters recursively for diagnostics only."""
if isinstance(value, str):
return value.count("\x00")
if isinstance(value, (list, tuple)):
return sum(_count_nul_characters(item) for item in value)
if isinstance(value, dict):
return sum(_count_nul_characters(key) + _count_nul_characters(item) for key, item in value.items())
return 0
def _process_software_job_callback(
job_id: int,
token: str,
@@ -6101,6 +6135,14 @@ def _process_software_job_callback(
_software_debug_log(f"CALLBACK JSON TYPE ERROR | job={job_id} | root={type(payload).__name__}")
raise HTTPException(400, "JSON-Objekt erwartet")
nul_character_count = _count_nul_characters(payload)
if nul_character_count:
payload = _sanitize_callback_value(payload)
_software_debug_log(
f"CALLBACK sanitized PostgreSQL-incompatible NUL characters | "
f"job={job_id} | removed={nul_character_count}"
)
_software_debug_log(
f"CALLBACK JSON parsed | job={job_id} | keys={','.join(sorted(str(key) for key in payload.keys()))}"
)
@@ -7910,15 +7952,6 @@ async def settings_save(
success_color: str = Form("#198754"),
warning_color: str = Form("#d97706"),
danger_color: str = Form("#b42318"),
mesh_fallback_category_id: int = Form(1),
mesh_category_1: int | None = Form(None),
mesh_category_2: int | None = Form(None),
mesh_category_3: int | None = Form(None),
mesh_category_4: int | None = Form(None),
mesh_category_5: int | None = Form(None),
mesh_category_6: int | None = Form(None),
mesh_category_7: int | None = Form(None),
mesh_category_8: int | None = Form(None),
auth_mode: str = Form("none"),
ldap_server: str = Form(""),
ldap_port: int = Form(389),
@@ -8012,19 +8045,7 @@ async def settings_save(
"debug_logging": ldap_debug_logging == "on",
})
authentication["ldap"] = ldap
meshcentral = dict(current.get("meshcentral", {}))
meshcentral["fallback_category_id"] = int(mesh_fallback_category_id or 1)
meshcentral["category_mapping"] = {
"1": mesh_category_1,
"2": mesh_category_2,
"3": mesh_category_3,
"4": mesh_category_4,
"5": mesh_category_5,
"6": mesh_category_6,
"7": mesh_category_7,
"8": mesh_category_8,
}
save_config({"general": general, "authentication": authentication, "meshcentral": meshcentral})
save_config({"general": general, "authentication": authentication})
return RedirectResponse("/settings?toast_success=Einstellungen gespeichert", status_code=303)
@@ -8037,11 +8058,13 @@ def meshcentral_sync_page(request: Request, db: Session = Depends(get_db)):
missing = db.query(Asset).filter(Asset.mesh_sync_status == "missing").count()
definitions = db.query(FieldDefinition).filter(FieldDefinition.is_active.is_(True)).order_by(FieldDefinition.sort_order, FieldDefinition.label).all()
mappings = db.query(MeshFieldMapping).order_by(MeshFieldMapping.priority, MeshFieldMapping.id).all()
categories = db.query(Category).order_by(Category.name.asc()).all()
return templates.TemplateResponse("meshcentral_sync.html", {
"request": request, "runs": runs, "config": public_config(),
"linked": linked, "conflicts": conflicts, "missing": missing,
"definitions": definitions,
"mappings": mappings,
"categories": categories,
"transform_options": [
"raw", "string", "integer", "decimal", "boolean", "json",
"normalize_serial", "normalize_mac", "active_ipv4_address", "active_ipv4_mac",
@@ -8198,8 +8221,34 @@ async def meshcentral_settings_save(request: Request, db: Session = Depends(get_
'store_source_json': form.get('store_source_json') == 'on',
'serial_match_manufacturer': form.get('serial_match_manufacturer') == 'on',
})
submitted_match_order = [value for value in form.getlist('match_order') if value in {'node_id', 'serial_number'}]
mesh['match_order'] = submitted_match_order or ['node_id', 'serial_number']
invalid_serial_values = []
for line in str(form.get('invalid_serial_values') or '').splitlines():
value = line.strip()
if value and value.casefold() not in {item.casefold() for item in invalid_serial_values}:
invalid_serial_values.append(value)
mesh['invalid_serial_values'] = invalid_serial_values
manufacturer_rules: dict[str, list[str]] = {}
for line in str(form.get('manufacturer_invalid_serial_rules') or '').splitlines():
line = line.strip()
if not line or line.startswith('#'):
continue
if '|' not in line:
continue
manufacturer, serial = (part.strip() for part in line.split('|', 1))
if not manufacturer or not serial:
continue
values = manufacturer_rules.setdefault(manufacturer, [])
if serial.casefold() not in {item.casefold() for item in values}:
values.append(serial)
mesh['manufacturer_invalid_serial_rules'] = [
{'manufacturer': manufacturer, 'values': values}
for manufacturer, values in manufacturer_rules.items()
]
submitted_match_order = [value for value in form.getlist('match_order') if value in {'node_id', 'mac_address', 'serial_number'}]
mesh['match_order'] = submitted_match_order or ['node_id', 'mac_address', 'serial_number']
# Do not retain plaintext passwords from older versions.
mesh.pop('password', None)
mesh.pop('field_mappings', None)
@@ -8208,6 +8257,44 @@ async def meshcentral_settings_save(request: Request, db: Session = Depends(get_
return RedirectResponse('/sync/meshcentral?toast_success=MeshCentral-Einstellungen gespeichert', status_code=303)
@app.post('/sync/meshcentral/categories/save')
async def meshcentral_category_mapping_save(request: Request, db: Session = Depends(get_db)):
_require_admin(request)
form = await request.form()
categories = db.query(Category).all()
valid_category_ids = {int(category.id) for category in categories}
if not valid_category_ids:
raise HTTPException(400, 'No asset categories are available.')
try:
fallback_category_id = int(form.get('mesh_fallback_category_id') or 0)
except (TypeError, ValueError):
fallback_category_id = 0
if fallback_category_id not in valid_category_ids:
raise HTTPException(400, 'Invalid fallback category.')
category_mapping = {}
for type_id in range(1, 9):
raw_value = str(form.get(f'mesh_category_{type_id}') or '').strip()
if not raw_value:
category_mapping[str(type_id)] = None
continue
try:
category_id = int(raw_value)
except ValueError:
raise HTTPException(400, f'Invalid category mapping for MeshCentral type {type_id}.')
if category_id not in valid_category_ids:
raise HTTPException(400, f'Unknown category for MeshCentral type {type_id}.')
category_mapping[str(type_id)] = category_id
current = load_config()
mesh = dict(current.get('meshcentral', {}))
mesh['fallback_category_id'] = fallback_category_id
mesh['category_mapping'] = category_mapping
save_config({'meshcentral': mesh})
return RedirectResponse('/sync/meshcentral?toast_success=' + quote(_translate_request(request, 'settings.mesh_mapping_saved', 'MeshCentral category mapping saved')), status_code=303)
@app.post('/sync/meshcentral/mappings/save')
async def meshcentral_mappings_save(request: Request, db: Session = Depends(get_db)):
_require_admin(request)