feat: alternative fingerprint rules count once (4.8.0)
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:
1 parent
7afa590c0c
commit
a1da0337e2
5 files changed
+97
-2
No files matched your search
@@ -285,6 +285,27 @@ since both commonly run the DHCP server for their networks).
|
||||
`ConfigLifecycleMixin` (config load/compare/commit/rollback) is mixed in by a
|
||||
driver itself.
|
||||
|
||||
### Fingerprints
|
||||
|
||||
`DeviceTypeDriver` carries what discovery matches a device against: `PORT_SPECS`,
|
||||
`SNMP_OBJECT_ID_PREFIX`, `SNMP_FINGERPRINT`, `SSH_FINGERPRINT`, `HTTP_FINGERPRINT` and
|
||||
`OUI_PREFIXES`. A `FingerprintRule` is a lowercase substring with a weight; `mandatory`
|
||||
disqualifies the driver when it is missing, `negative` subtracts.
|
||||
|
||||
Discovery compares drivers by score divided by the most a driver could score. Rules that
|
||||
can never match together, such as one vendor's landing page or another's, share a
|
||||
`group`: only the strongest rule of a group counts towards that maximum
|
||||
(`rules_max_weight`). Without it, a driver that recognises three vendors' pages would
|
||||
score a third as well as one that recognises a single page:
|
||||
|
||||
```python
|
||||
HTTP_FINGERPRINT = [
|
||||
FingerprintRule("aten international", 6.0, group="landing_page"), # Supermicro
|
||||
FingerprintRule("idrac", 6.0, group="landing_page"),
|
||||
FingerprintRule("integrated lights-out", 6.0, group="landing_page"), # HPE iLO
|
||||
]
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Access Point
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "napalm-device-types"
|
||||
version = "4.7.1"
|
||||
version = "4.8.0"
|
||||
description = "Abstract device-type base classes for NAPALM drivers"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -8,6 +8,7 @@ from napalm_device_types import (
|
||||
DeviceTypeDriver,
|
||||
FingerprintRule,
|
||||
FirewallDriver,
|
||||
rules_max_weight,
|
||||
HypervisorDriver,
|
||||
OSDriver,
|
||||
PortSpec,
|
||||
@@ -90,6 +91,53 @@ def test_fingerprint_rule_negative():
|
||||
assert r.negative is True
|
||||
|
||||
|
||||
def test_fingerprint_rule_has_no_group_by_default():
|
||||
assert FingerprintRule("fritz!box").group is None
|
||||
|
||||
|
||||
def test_fingerprint_rule_group():
|
||||
r = FingerprintRule("idrac", weight=6.0, group="landing_page")
|
||||
assert r.group == "landing_page"
|
||||
|
||||
|
||||
# ── rules_max_weight: the most a set of rules can add ─────────────────────────
|
||||
|
||||
def test_max_weight_sums_rules_that_can_match_together():
|
||||
rules = [FingerprintRule("proxmox virtual environment", 9.0), FingerprintRule("pve", 2.0)]
|
||||
assert rules_max_weight(rules) == 11.0
|
||||
|
||||
|
||||
def test_max_weight_counts_one_rule_per_group():
|
||||
# Alternatives: a page is ATEN's or iDRAC's or iLO's, never all three. Summing
|
||||
# them made a driver that knows three BMC families lose to one that knows none.
|
||||
rules = [
|
||||
FingerprintRule("aten international", 6.0, group="landing_page"),
|
||||
FingerprintRule("idrac", 6.0, group="landing_page"),
|
||||
FingerprintRule("integrated lights-out", 4.0, group="landing_page"),
|
||||
]
|
||||
assert rules_max_weight(rules) == 6.0
|
||||
|
||||
|
||||
def test_max_weight_mixes_groups_and_single_rules():
|
||||
rules = [
|
||||
FingerprintRule("redfishversion", 10.0, group="redfish_answer"),
|
||||
FingerprintRule("@message.extendedinfo", 8.0, group="redfish_answer"),
|
||||
FingerprintRule("#serviceroot.", 5.0),
|
||||
FingerprintRule("aten international", 6.0, group="landing_page"),
|
||||
FingerprintRule("idrac", 6.0, group="landing_page"),
|
||||
]
|
||||
assert rules_max_weight(rules) == 21.0
|
||||
|
||||
|
||||
def test_max_weight_ignores_negative_rules():
|
||||
rules = [FingerprintRule("vcenter", 9.0), FingerprintRule("vmware esxi", 9.0, negative=True)]
|
||||
assert rules_max_weight(rules) == 9.0
|
||||
|
||||
|
||||
def test_max_weight_of_no_rules_is_zero():
|
||||
assert rules_max_weight([]) == 0.0
|
||||
|
||||
|
||||
def test_fingerprint_rule_is_immutable():
|
||||
r = FingerprintRule("test", weight=3.0)
|
||||
with pytest.raises(AttributeError):
|
||||
|
||||
Reference in new issue
Block a user