feat: alternative fingerprint rules count once (4.8.0)
CI / test (3.10) (push) Successful in 28s
CI / test (3.11) (push) Successful in 25s
CI / test (3.12) (push) Successful in 27s
CI / test (3.13) (push) Successful in 26s

Discovery compares drivers by score divided by the most a driver could
score, and that maximum summed every positive rule. Rules that never
match together, such as one vendor's landing page or another's, made a
driver that knows several vendors lose to one that knows none:
napalm-redfish's Supermicro landing page scored 6 of 41 and lost to a
bare "linux" in the same BMC's SNMP sysDescr, 5 of 18.

FingerprintRule gets an optional group. rules_max_weight() sums the
positive rules and counts each group once, with its strongest rule.
Existing rules have no group and count as before.
This commit is contained in:
christianmanivong committed 2026-10-13 10:19:54 +02:00
1 parent 7afa590c0c
commit a1da0337e2
5 files changed
+97 -2

No files matched your search

+2 -1
View File
@@ -60,7 +60,7 @@ Introspection -- :func:`~napalm_device_types.roles.roles_of`,
:func:`~napalm_device_types.roles.primary_role_of`.
"""
from napalm_device_types.base import DeviceTypeDriver, FingerprintRule, PortSpec
from napalm_device_types.base import DeviceTypeDriver, FingerprintRule, PortSpec, rules_max_weight
from napalm_device_types.access_point import AccessPointDriver
from napalm_device_types.channel import (
ByteStream,
@@ -156,6 +156,7 @@ __all__ = [
"DeviceTypeDriver",
"DhcpServerMixin",
"FingerprintRule",
"rules_max_weight",
"FirewallDriver",
"FirewallRuleMixin",
"HealthMetricsMixin",
+25
View File
@@ -8,6 +8,7 @@ live connection.
from __future__ import annotations
from collections.abc import Iterable
from typing import NamedTuple
from napalm.base import NetworkDriver
@@ -24,12 +25,36 @@ class FingerprintRule(NamedTuple):
weight — evidence added to the driver's score on match
mandatory — pattern absent → driver immediately disqualified
negative — pattern present → weight subtracted instead of added
group — rules sharing a group are alternatives that never match
together (one vendor's landing page *or* another's); only the
strongest of them counts towards the most a driver can score
"""
pattern: str
weight: float = 1.0
mandatory: bool = False
negative: bool = False
group: str | None = None
def rules_max_weight(rules: Iterable[FingerprintRule]) -> float:
"""The most evidence *rules* can add together.
Every positive rule counts, except that a group of alternatives counts once,
with its strongest rule. Discovery divides a driver's score by its maximum to
compare drivers; summing alternatives would make a driver that recognises
three vendors' pages lose to one that recognises none.
"""
total = 0.0
group_best: dict[str, float] = {}
for rule in rules:
if rule.negative or rule.weight <= 0:
continue
if rule.group is None:
total += rule.weight
else:
group_best[rule.group] = max(group_best.get(rule.group, 0.0), rule.weight)
return total + sum(group_best.values())
class PortSpec(NamedTuple):