fmeshsync fix with synology,vm's and lxc
This commit is contained in:
+113
-26
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user