Enhance documentation and implement SET_ALWAYS functionality for device name resolution #1650

This commit is contained in:
Jokob @NetAlertX
2026-05-22 22:50:33 +00:00
parent 821594f617
commit 88231d97c8
6 changed files with 118 additions and 0 deletions

View File

@@ -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)

View File

@@ -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:

View File

@@ -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 |
|---|---|
| `<PLUGIN>_SET_ALWAYS` | Plugin always overwrites the field, as long as it can resolve a value and the field is not `USER`/`LOCKED` |
| `<PLUGIN>_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`.

View File

@@ -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

View File

@@ -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 = ?

View File

@@ -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()