Saving a VLANGroup without changes creates a new changelog entry #11673

Closed
opened 2025-12-29 21:48:21 +01:00 by adam · 2 comments
Owner

Originally created by @pheus on GitHub (Oct 2, 2025).

Originally assigned to: @pheus on GitHub.

NetBox Edition

NetBox Community

NetBox Version

v4.4.2

Python Version

3.12

Steps to Reproduce

  1. Navigate to IPAM → VLAN Groups; create a VLAN group (or open an existing one).
  2. In VLAN ID Ranges, enter a value such as 100-200, 300-300 and click Save.
  3. Reopen the same VLAN group, do not change any fields, and click Save again.
  4. Open the object’s Changelog tab (or Other → Change Log) and filter by this object.

Expected Behavior

No new changelog entry should be created if no fields were changed.

Observed Behavior

A new changelog entry is recorded on each save, even when the form or API payload is identical in meaning. The difference appears isolated to vid_ranges and stems from representation drift:

  • UI/forms (and CSV import) return inclusive ranges (e.g., NumericRange(lo, hi, bounds='[]')).
  • PostgreSQL int4range canonicalizes to half‑open [lo, hi) on round‑trip.
  • Change logging compares serialized pre/post snapshots textually, so these representations don’t match even though the set of VLAN IDs is unchanged.
Originally created by @pheus on GitHub (Oct 2, 2025). Originally assigned to: @pheus on GitHub. ### NetBox Edition NetBox Community ### NetBox Version v4.4.2 ### Python Version 3.12 ### Steps to Reproduce 1. Navigate to **IPAM → VLAN Groups**; create a VLAN group (or open an existing one). 2. In **VLAN ID Ranges**, enter a value such as `100-200, 300-300` and click **Save**. 3. Reopen the same VLAN group, **do not change any fields**, and click **Save** again. 4. Open the object’s **Changelog** tab (or **Other → Change Log**) and filter by this object. ### Expected Behavior No new changelog entry should be created if no fields were changed. ### Observed Behavior A new changelog entry is recorded on each save, even when the form or API payload is identical in meaning. The difference appears isolated to `vid_ranges` and stems from representation drift: - UI/forms (and CSV import) return **inclusive** ranges (e.g., `NumericRange(lo, hi, bounds='[]')`). - PostgreSQL `int4range` canonicalizes to **half‑open** `[lo, hi)` on round‑trip. - Change logging compares serialized pre/post snapshots textually, so these representations don’t match even though the set of VLAN IDs is unchanged.
adam added the type: bugstatus: acceptedseverity: low labels 2025-12-29 21:48:21 +01:00
adam closed this issue 2025-12-29 21:48:21 +01:00
Author
Owner

@pheus commented on GitHub (Oct 2, 2025):

Context: inclusive vs half‑open ranges

For discrete ranges (VLAN IDs), users naturally think in inclusive terms: 10–20 means VLANs 10 through 20. NetBox’s UI modeling follows this mental model and yields inclusive NumericRange(lo, hi, bounds='[]').

PostgreSQL, however, uses a canonical representation for discrete range types (e.g., int4range): lower‑inclusive, upper‑exclusive [lo, hi). On read‑back, values may be returned in this canonical form regardless of how they were written. The two representations are set‑equivalent ([10,20] equals [10,21) for integers), but their textual form differs.

NetBox’s change logging records pre‑ and post‑change snapshots and diffs them textually. If the pre‑snapshot (DB‑derived) uses canonical [) while the post‑snapshot (form/API‑derived) uses inclusive [], a change can be recorded even when the set of VLAN IDs didn’t actually change.

Suggestion:
Normalize on input: Convert incoming vid_ranges to the same canonical form used for storage (half‑open [lo, hi)) before the object is saved and snapshot is taken, so in‑memory state matches the DB snapshot.

Minimal code changes (two places):

  • utilities.data.string_to_ranges
    values.append(NumericRange(int(lower), int(upper) + 1, bounds='[)'))
    
  • netbox.api.fields.IntegerRangeSerializer
    return NumericRange(data[0], data[1] + 1, bounds='[)')
    
@pheus commented on GitHub (Oct 2, 2025): **Context: inclusive vs half‑open ranges** For discrete ranges (VLAN IDs), users naturally think in **inclusive** terms: `10–20` means VLANs 10 through 20. NetBox’s UI modeling follows this mental model and yields inclusive `NumericRange(lo, hi, bounds='[]')`. PostgreSQL, however, uses a **canonical** representation for discrete range types (e.g., `int4range`): **lower‑inclusive, upper‑exclusive** `[lo, hi)`. On read‑back, values may be returned in this canonical form regardless of how they were written. The two representations are **set‑equivalent** (`[10,20]` equals `[10,21)` for integers), but their **textual** form differs. NetBox’s change logging records pre‑ and post‑change snapshots and diffs them textually. If the pre‑snapshot (DB‑derived) uses canonical `[)` while the post‑snapshot (form/API‑derived) uses inclusive `[]`, a change can be recorded even when the set of VLAN IDs didn’t actually change. **Suggestion:** **Normalize on input:** Convert incoming `vid_ranges` to the same canonical form used for storage (half‑open `[lo, hi)`) before the object is saved and snapshot is taken, so in‑memory state matches the DB snapshot. **Minimal code changes (two places):** - `utilities.data.string_to_ranges` ```python values.append(NumericRange(int(lower), int(upper) + 1, bounds='[)')) ``` - `netbox.api.fields.IntegerRangeSerializer` ```python return NumericRange(data[0], data[1] + 1, bounds='[)') ```
Author
Owner

@pheus commented on GitHub (Oct 2, 2025):

I’d like to contribute this fix. Could you please assign the issue to me? Thanks!

@pheus commented on GitHub (Oct 2, 2025): I’d like to contribute this fix. Could you please assign the issue to me? Thanks!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/netbox#11673