feat: Enhance plugin configurations and improve MAC normalization

This commit is contained in:
Jokob @NetAlertX
2026-01-21 01:58:52 +00:00
parent 3ee21ac830
commit 478b018fa5
34 changed files with 1117 additions and 63 deletions

View File

@@ -44,7 +44,7 @@ from models.user_events_queue_instance import UserEventsQueueInstance # noqa: E
from models.event_instance import EventInstance # noqa: E402 [flake8 lint suppression]
# Import tool logic from the MCP/tools module to reuse behavior (no blueprints)
from plugin_helper import is_mac # noqa: E402 [flake8 lint suppression]
from plugin_helper import is_mac, normalize_mac # noqa: E402 [flake8 lint suppression]
# is_mac is provided in mcp_endpoint and used by those handlers
# mcp_endpoint contains helper functions; routes moved into this module to keep a single place for routes
from messaging.in_app import ( # noqa: E402 [flake8 lint suppression]
@@ -469,33 +469,28 @@ def api_device_field_lock(mac, payload=None):
if not field_name:
return jsonify({"success": False, "error": "fieldName is required"}), 400
# Validate that the field can be locked
source_field = field_name + "Source"
allowed_tracked_fields = {
"devMac", "devName", "devLastIP", "devVendor", "devFQDN",
"devSSID", "devParentMAC", "devParentPort", "devParentRelType", "devVlan"
}
if field_name not in allowed_tracked_fields:
return jsonify({"success": False, "error": f"Field '{field_name}' cannot be locked"}), 400
device_handler = DeviceInstance()
normalized_mac = normalize_mac(mac)
try:
# When locking: set source to LOCKED
# When unlocking: check current value and let plugins take over
new_source = "LOCKED" if should_lock else "NEWDEV"
result = device_handler.updateDeviceColumn(mac, source_field, new_source)
if result.get("success"):
action = "locked" if should_lock else "unlocked"
return jsonify({
"success": True,
"message": f"Field {field_name} {action}",
"fieldName": field_name,
"locked": should_lock
})
if should_lock:
result = device_handler.lockDeviceField(normalized_mac, field_name)
action = "locked"
else:
return jsonify(result), 400
result = device_handler.unlockDeviceField(normalized_mac, field_name)
action = "unlocked"
response = dict(result)
response["fieldName"] = field_name
response["locked"] = should_lock
if response.get("success"):
response.setdefault("message", f"Field {field_name} {action}")
return jsonify(response)
if "does not support" in response.get("error", ""):
response["error"] = f"Field '{field_name}' cannot be {action}"
return jsonify(response), 400
except Exception as e:
mylog("none", f"Error locking field {field_name} for {mac}: {str(e)}")
return jsonify({"success": False, "error": str(e)}), 500

View File

@@ -152,12 +152,20 @@ def enforce_source_on_user_update(devMac, updates_dict, conn):
cur = conn.cursor()
# Check if field has a corresponding source and should be updated
cur = conn.cursor()
try:
cur.execute("PRAGMA table_info(Devices)")
device_columns = {row["name"] for row in cur.fetchall()}
except Exception:
device_columns = set()
updates_to_apply = {}
for field_name, new_value in updates_dict.items():
if field_name in FIELD_SOURCE_MAP:
source_field = FIELD_SOURCE_MAP[field_name]
# User is updating this field, so mark it as USER
updates_to_apply[source_field] = "USER"
if not device_columns or source_field in device_columns:
updates_to_apply[source_field] = "USER"
if not updates_to_apply:
return
@@ -198,6 +206,15 @@ def lock_field(devMac, field_name, conn):
source_field = FIELD_SOURCE_MAP[field_name]
cur = conn.cursor()
try:
cur.execute("PRAGMA table_info(Devices)")
device_columns = {row["name"] for row in cur.fetchall()}
except Exception:
device_columns = set()
if device_columns and source_field not in device_columns:
mylog("debug", [f"[lock_field] Source column {source_field} missing for {field_name}"])
return
sql = f"UPDATE Devices SET {source_field}='LOCKED' WHERE devMac = ?"
@@ -227,6 +244,15 @@ def unlock_field(devMac, field_name, conn):
source_field = FIELD_SOURCE_MAP[field_name]
cur = conn.cursor()
try:
cur.execute("PRAGMA table_info(Devices)")
device_columns = {row["name"] for row in cur.fetchall()}
except Exception:
device_columns = set()
if device_columns and source_field not in device_columns:
mylog("debug", [f"[unlock_field] Source column {source_field} missing for {field_name}"])
return
# Unlock by resetting to empty (allows overwrite)
sql = f"UPDATE Devices SET {source_field}='' WHERE devMac = ?"

View File

@@ -593,7 +593,6 @@ class DeviceInstance:
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute(sql, values)
conn.commit()
if data.get("createNew", False):
# Initialize source-tracking fields on device creation.
@@ -617,7 +616,6 @@ class DeviceInstance:
source_values.append(normalized_mac)
source_sql = f"UPDATE Devices SET {set_clause} WHERE devMac = ?"
cur.execute(source_sql, source_values)
conn.commit()
# Enforce source tracking on user updates
# User-updated fields should have their *Source set to "USER"
@@ -631,6 +629,8 @@ class DeviceInstance:
conn.close()
return {"success": False, "error": f"Source tracking failed: {e}"}
# Commit all changes atomically after all operations succeed
conn.commit()
conn.close()
mylog("debug", f"[DeviceInstance] setDeviceData SQL: {sql.strip()}")

View File

@@ -545,12 +545,13 @@ def create_new_devices(db):
# Derive primary IP family values
cur_IP = str(cur_IP).strip() if cur_IP else ""
cur_IP_normalized = check_IP_format(cur_IP) if ":" not in cur_IP else cur_IP
# Validate IPv6 addresses using format_ip_long for consistency
if ":" in cur_IP_normalized:
# Validate IPv6 addresses using format_ip_long for consistency (do not store integer result)
if cur_IP_normalized and ":" in cur_IP_normalized:
validated_ipv6 = format_ip_long(cur_IP_normalized)
cur_IP_normalized = validated_ipv6 if validated_ipv6 else ""
if validated_ipv6 is None or validated_ipv6 < 0:
cur_IP_normalized = ""
primary_ipv4 = cur_IP_normalized if cur_IP_normalized and ":" not in cur_IP_normalized else ""
primary_ipv6 = cur_IP_normalized if cur_IP_normalized and ":" in cur_IP_normalized else ""