Skip to content

Add machinetracker collapse-duplicates option#4099

Open
adobloug wants to merge 2 commits into
Uninett:masterfrom
adobloug:feature/machinetracker-collapse-duplicates
Open

Add machinetracker collapse-duplicates option#4099
adobloug wants to merge 2 commits into
Uninett:masterfrom
adobloug:feature/machinetracker-collapse-duplicates

Conversation

@adobloug

@adobloug adobloug commented Jun 29, 2026

Copy link
Copy Markdown

Scope and purpose

Fixes #4098.

NAV records one Arp row per collecting router by design, so an address served by several routers (e.g. an HSRP pair) shows up as multiple overlapping rows in a machinetracker IP search for active addresses.

This adds an opt-in "Collapse duplicates" checkbox to the IP search form:

  • Default off → existing behavior unchanged (raw rows preserved).
  • When on → rows for the same (ip, mac) whose active time windows overlap are merged into one (newest kept).

Disjoint windows (genuine connect/disconnect history) are preserved — overlap test is s1 < e2 and s2 < e1. Still-active rows (end_time = INFINITY) always overlap, so they collapse to one.

This pull request

  • changes the UI (adds one checkbox to the machinetracker IP search form)

Contributor Checklist

  • Added a changelog fragment for towncrier
  • Added/amended tests for new/changed code
  • Added/changed documentation — not applicable; behavior is self-explanatory from the checkbox help text
  • Linted/formatted the code with ruff, easiest by using pre-commit
  • Wrote the commit message so that the first line continues the sentence "If applied, this commit will ...", starts with a capital letter, does not end with punctuation and is 50 characters or less long. See https://cbea.ms/git-commit/
  • Based this pull request on the correct upstream branch: new feature, based on master
  • If applicable: Created new issues if this PR does not fix the issue completely/there is further work to be done — not applicable
  • If it's not obvious from a linked issue, described how to interact with NAV in order for a reviewer to observe the effects of this change first-hand (see below)
  • If this results in changes in the UI: Added screenshots of the before and after
  • If this adds a new Python source code file: Added the boilerplate header to that file — not applicable; no new source files

How to observe

  1. Have (or simulate) arp rows where the same (ip, mac) is collected by two routers — two rows, same ip/mac, both end_time = infinity, different sysname/netbox.
  2. Go to Machinetracker → IP search, enter the prefix/address, filter = Active.
  3. Without "Collapse duplicates": two rows per address. With it checked: one row.
  4. Verify a genuine reconnect (same ip/mac, disjoint time windows) still shows both rows when collapse is on.

Trade-off

With collapse on, the kept row is the newest, so the dropped router's name in the Source column is not shown. Acceptable since the option is opt-in. A future enhancement could annotate "seen by N routers".

Screenshots

70 days - collapsed 70 days - not collapsed only active - collapsed only active - not collapsed

🤖 Generated with Claude Code

@lunkwill42
lunkwill42 self-requested a review July 9, 2026 11:36

@lunkwill42 lunkwill42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — the design is sound and correctly opt-in, the disjoint-history preservation is right, and the collapsed row count is plumbed through to the counters correctly. I ran the machinetracker integration suite locally and it's green.

Having this as a single commit is borderline, but I'll allow it.

A few inline notes:

  • The collapse_overlapping / create_active_row suggestions are optional polish (a type annotation on the new helper, and the setdefault idiom) — the original codebase carries no type annotations, as those did not exist in Python at the time, but it's always good to see it on new code.
  • One I'd like before merge: the tests assert how many rows survive but never which one, so the headline "keep the newest" guarantee is currently untested (see the test-file comment).

Only other thing: the contributor checklist still has the before/after screenshots unchecked — since this adds a checkbox to the IP search form, could you attach those to the PR description?

Comment thread python/nav/web/machinetracker/utils.py Outdated
return result


def collapse_overlapping(rows):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: annotate the new helper. The rows are Arp instances (they come straight from get_arp_records), and your docstring already calls them "Arp result rows", so the type just formalises that:

Suggested change
def collapse_overlapping(rows):
def collapse_overlapping(rows: list[Arp]) -> list[Arp]:

If you take this, add Arp to the import on line 28 so the name resolves:
from nav.models.manage import Arp, Prefix, Netbox, Interface

Comment thread python/nav/web/machinetracker/views.py Outdated
"""Creates a tracker tuple where the result is active"""
ip = str(ip_key)
rows = ip_result[ip_key]
touched_keys = []

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: a set lets the dedup happen for free (pairs with the setdefault change below).

Suggested change
touched_keys = []
touched_keys = set()

Comment thread python/nav/web/machinetracker/views.py Outdated
Comment on lines +224 to +228
key = (row.ip, row.mac)
if key not in tracker:
tracker[key] = []
touched_keys.append(key)
tracker[key].append(row)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: setdefault is the idiomatic append-to-dict-of-lists, and with touched_keys as a set you no longer need the "first time only" guard:

Suggested change
key = (row.ip, row.mac)
if key not in tracker:
tracker[key] = []
touched_keys.append(key)
tracker[key].append(row)
key = (row.ip, row.mac)
tracker.setdefault(key, []).append(row)
touched_keys.add(key)

This is behaviour-preserving.

_row(now - timedelta(hours=1), INFINITY, "router-a"),
_row(now - timedelta(hours=2), INFINITY, "router-b"),
]
assert len(collapse_overlapping(rows)) == 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole point of the feature is that the newest row is the survivor, but nothing here asserts which row is kept — a collapse that kept the oldest would pass this test unchanged. Worth pinning down (router-a has the newer start_time):

Suggested change
assert len(collapse_overlapping(rows)) == 1
kept = collapse_overlapping(rows)
assert len(kept) == 1
assert kept[0].sysname == "router-a" # newer start_time wins

@adobloug
adobloug force-pushed the feature/machinetracker-collapse-duplicates branch from 98bbfa5 to ccf4c87 Compare July 9, 2026 13:15
adobloug and others added 2 commits July 9, 2026 15:33
NAV records one Arp row per collecting router by design, so an address
served by several routers (e.g. an HSRP pair) appears as multiple
overlapping rows in a machinetracker IP search for active addresses.

Add an opt-in "Collapse duplicates" checkbox to the IP search form.
When enabled, rows for the same (ip, mac) whose active time windows
overlap are collapsed to one (newest kept). Disjoint windows (genuine
connect/disconnect history) are preserved. Default off, so existing
behavior is unchanged.

Note: the newest row is kept whole -- its own time window is shown,
not a union of the duplicates' windows. If a dropped duplicate had a
later end_time, that later end is not reflected.

Fixes Uninett#4098

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review feedback on the collapse-duplicates option:

- Pin the "keep newest" guarantee: the collapse test asserted only
  how many rows survived, not which one, so a collapse that kept the
  oldest would have passed. Assert the newer-start_time row wins.
- Annotate collapse_overlapping() with list[Arp] -> list[Arp] and
  import Arp, formalising what the docstring already states.
- Use the setdefault dict-of-lists idiom in create_active_row and
  track touched keys in a set, dropping the first-time-only guard.
  Behaviour-preserving.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adobloug
adobloug force-pushed the feature/machinetracker-collapse-duplicates branch from ccf4c87 to d5bac8d Compare July 9, 2026 13:34
@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

@adobloug

adobloug commented Jul 9, 2026

Copy link
Copy Markdown
Author

Thanks for the review! Applied all of it:

  • Type annotationcollapse_overlapping(rows: list[Arp]) -> list[Arp] added, plus Arp in the import on line 28.
  • set + setdefaulttouched_keys is now a set and create_active_row uses tracker.setdefault(key, []).append(row); dropped the "first time only" guard.
  • "keep the newest" now tested — the collapse test asserts kept[0].sysname == "router-a" (newer start_time), so a keep-oldest regression would fail.

While in there I also flagged a design caveat in the commit message: the newest row is kept whole — its own window is shown, not a union of the duplicates' windows, so a dropped duplicate's later end_time isn't reflected. In practice the newest row's window dominates for overlapping HSRP-style duplicates.

Rebased on latest master. Before/after screenshots attached to the PR description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Machinetracker: optionally collapse duplicate active addresses from HSRP/redundant routers

2 participants