From 88231d97c8b12ea8f2f0be12b53aeed505d7a7ed Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Fri, 22 May 2026 22:50:33 +0000 Subject: [PATCH] Enhance documentation and implement SET_ALWAYS functionality for device name resolution #1650 --- docs/DEVICE_FIELD_LOCK.md | 2 + docs/DEVICE_SOURCE_FIELDS.md | 6 +++ docs/PERFORMANCE.md | 35 +++++++++++++++++ mkdocs.yml | 1 + server/models/device_instance.py | 9 +++++ server/scan/device_handling.py | 65 ++++++++++++++++++++++++++++++++ 6 files changed, 118 insertions(+) diff --git a/docs/DEVICE_FIELD_LOCK.md b/docs/DEVICE_FIELD_LOCK.md index 47369c57..b68709b7 100644 --- a/docs/DEVICE_FIELD_LOCK.md +++ b/docs/DEVICE_FIELD_LOCK.md @@ -1,5 +1,7 @@ # Quick Reference Guide - Device Field Lock/Unlock System +> For how scan overwrite rules (`SET_ALWAYS`, `SET_EMPTY`) and source tracking work under the hood, see [Device Source Fields](./DEVICE_SOURCE_FIELDS.md). + ## Overview ![Field source and locks](./img/DEVICE_MANAGEMENT/field_sources_and_locks.png) diff --git a/docs/DEVICE_SOURCE_FIELDS.md b/docs/DEVICE_SOURCE_FIELDS.md index 02ef5e46..7f2bdc52 100644 --- a/docs/DEVICE_SOURCE_FIELDS.md +++ b/docs/DEVICE_SOURCE_FIELDS.md @@ -1,5 +1,7 @@ # Understanding Device Source Fields and Field Updates +> For the UI guide on locking and unlocking individual fields, see [Device Field Lock/Unlock](./DEVICE_FIELD_LOCK.md). + When the system scans a network, it finds various details about devices (like names, IP addresses, and manufacturers). To ensure the data remains accurate without accidentally overwriting manual changes, the system uses a set of "Source Rules." ![Field source and locks](./img/DEVICE_MANAGEMENT/field_sources_and_locks.png) @@ -17,6 +19,8 @@ Every piece of information for a device has a **Source**. This source determines | **NEWDEV** | This value was initialized from `NEWDEV` plugin settings. | **Always** | | **(Plugin Name)** | The value was found by a specific scanner (e.g., `NBTSCAN`). | **Only if specific rules are met** | +> For how `USER` and `LOCKED` sources are set through the UI (lock/unlock buttons), see [Device Field Lock/Unlock](./DEVICE_FIELD_LOCK.md). + --- ## How Scans Update Information @@ -34,6 +38,8 @@ Some plugins are configured to be "authoritative." If a field is in the **SET_AL * The scanner will **always** overwrite the current value with the new one. * *Note: It will still never overwrite a `USER` or `LOCKED` field.* +> On large networks, enabling `SET_ALWAYS` on name-resolution fields (e.g., `devName`) widens the resolution scope and increases DNS query volume. See [Performance — Plugin Field Authority](./PERFORMANCE.md#plugin-field-authority-set_always-and-set_empty) for details. + ### 3. SET_EMPTY If a field is in the **SET_EMPTY** list: diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index e7fb0f4a..abf779db 100755 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -95,6 +95,41 @@ For example, the **ICMP plugin** allows scanning only IPs that match a specific --- +## Plugin Field Authority: `SET_ALWAYS` and `SET_EMPTY` + +Plugins can be configured to control how aggressively they overwrite existing device field values via two settings: + +| Setting | Behaviour | +|---|---| +| `_SET_ALWAYS` | Plugin always overwrites the field, as long as it can resolve a value and the field is not `USER`/`LOCKED` | +| `_SET_EMPTY` | Plugin only writes when the field is currently empty | + +Both settings accept a list of field names (e.g., `devName`, `devFQDN`). See [Name resolution](./NAME_RESOLUTION.md) and [Field locking](./DEVICE_SOURCE_FIELDS.md) docs for details. + +### Performance Impact of `SET_ALWAYS` on Name Resolution + +By default, name resolution (DIGSCAN, NBTSCAN, NSLOOKUP, AVAHISCAN) only runs against **devices that have no name yet**. This keeps DNS query volume proportional to new devices discovered, not the total inventory. + +When **any** name-resolution plugin has `devName` in its `SET_ALWAYS` list, the system additionally runs a second resolution pass against **all devices whose name is not `USER`/`LOCKED` protected**. This allows a higher-priority plugin (e.g., DIGSCAN) to overwrite names previously set by a lower-priority one (e.g., NBTSCAN). + +**Cost:** one DNS query per unprotected, already-named device per name-resolution cycle. + +| Scenario | Devices resolved per cycle | +|---|---| +| No `SET_ALWAYS` on `devName` | Only new/unknown devices | +| `SET_ALWAYS: devName` on any plugin | New/unknown devices **+** all unprotected named devices | + +> [!WARNING] +> On large installations (thousands of devices), enabling `SET_ALWAYS: devName` significantly increases DNS query volume and cycle duration. To mitigate: +> +> * Increase the scan interval of name-resolution plugins (`DIGSCAN_RUN_SCHD`, `NBTSCAN_RUN_SCHD`, etc.). +> * Mark devices whose name should never change as `USER` or `LOCKED` — they are excluded from the re-resolve pass entirely. +> * Use `SET_ALWAYS` only on the highest-priority plugin; leave lower-priority plugins without it. + +The actual number of DB rows updated is logged at `verbose` level under `[Update Device Name] SET_ALWAYS re-resolve - DB rows updated`. + +--- + ## Storing Temporary Files in Memory On devices with slower I/O, you can improve performance by storing temporary files (and optionally the database) in memory using `tmpfs`. diff --git a/mkdocs.yml b/mkdocs.yml index 8c9bd5ac..28d1ccc2 100755 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -79,6 +79,7 @@ nav: - Device Display Settings: DEVICE_DISPLAY_SETTINGS.md - Session Info: SESSION_INFO.md - Field Lock/Unlock: DEVICE_FIELD_LOCK.md + - Device Source Fields: DEVICE_SOURCE_FIELDS.md - Icons and Topology: - Icons: ICONS.md - Network Topology: NETWORK_TREE.md diff --git a/server/models/device_instance.py b/server/models/device_instance.py index cc8d9d89..10ba61cb 100755 --- a/server/models/device_instance.py +++ b/server/models/device_instance.py @@ -53,6 +53,15 @@ class DeviceInstance: WHERE devName IN ("(unknown)", "(name not found)", "") """) + def getResolvable(self): + """Return devices that have a name already set but are not USER/LOCKED protected. + Used by SET_ALWAYS name-resolution plugins to re-resolve existing names.""" + return self._fetchall(""" + SELECT * FROM Devices + WHERE devName NOT IN ("(unknown)", "(name not found)", "") + AND COALESCE(devNameSource, '') NOT IN ('USER', 'LOCKED') + """) + def getValueWithMac(self, column_name, devMac): row = self._fetchone(f""" SELECT {column_name} FROM Devices WHERE devMac = ? diff --git a/server/scan/device_handling.py b/server/scan/device_handling.py index 1029caa8..aaa17652 100755 --- a/server/scan/device_handling.py +++ b/server/scan/device_handling.py @@ -1090,6 +1090,71 @@ def update_devices_names(pm): plugin_records, ) + # --- Step 1b: Re-resolve already-named devices for SET_ALWAYS plugins --- + # If any name-resolution plugin declares devName in SET_ALWAYS, it should be + # able to overwrite names set by lower-priority plugins. Step 1 only covers + # unknown devices, so we run a second pass here limited to devices that: + # - already have a name (not unknown/empty), AND + # - are not USER/LOCKED protected + # recordsNotFound is intentionally discarded: if resolution fails, the + # existing name is kept as-is. + name_resolution_plugins = [label for _, label in strategies] + set_always_plugins = [ + p for p in name_resolution_plugins + if "devName" in get_plugin_authoritative_settings(p).get("set_always", []) + ] + + if not set_always_plugins: + mylog("debug", "[Update Device Name] SET_ALWAYS re-resolve: skipped (no name-resolution plugin has devName in SET_ALWAYS)") + else: + resolvableDevices = device_handler.getResolvable() + mylog("debug", f"[Update Device Name] SET_ALWAYS re-resolve: active plugins={set_always_plugins}, candidate devices={len(resolvableDevices)}") + + if resolvableDevices: + recordsToUpdate, _, fs, notFound = resolve_devices(resolvableDevices) + + res_string = f"{fs['DIGSCAN']}/{fs['AVAHISCAN']}/{fs['NSLOOKUP']}/{fs['NBTSCAN']}" + mylog("verbose", f"[Update Device Name] SET_ALWAYS re-resolve - Found (DIG/AVAHI/NSL/NBT): {len(recordsToUpdate)} ({res_string}), Not Found: {notFound}") + + records_by_plugin = {} + for entry in recordsToUpdate: + records_by_plugin.setdefault(entry[1], []).append(entry) + + total_updated = 0 + for plugin_label, plugin_records in records_by_plugin.items(): + plugin_settings = get_plugin_authoritative_settings(plugin_label) + name_clause = get_overwrite_sql_clause( + "devName", "devNameSource", plugin_settings + ) + fqdn_clause = get_overwrite_sql_clause( + "devFQDN", "devFQDNSource", plugin_settings + ) + + sql.executemany( + f"""UPDATE Devices + SET devName = CASE + WHEN {name_clause} THEN ? + ELSE devName + END, + devNameSource = CASE + WHEN {name_clause} THEN ? + ELSE devNameSource + END, + devFQDN = CASE + WHEN {fqdn_clause} THEN ? + ELSE devFQDN + END, + devFQDNSource = CASE + WHEN {fqdn_clause} THEN ? + ELSE devFQDNSource + END + WHERE devMac = ?""", + plugin_records, + ) + total_updated += sql.rowcount + + mylog("verbose", f"[Update Device Name] SET_ALWAYS re-resolve - DB rows updated: {total_updated}") + # --- Step 2: Optionally refresh FQDN for all devices --- if get_setting_value("REFRESH_FQDN"): allDevices = device_handler.getAll()