Compare commits

...

194 Commits

Author SHA1 Message Date
bctiemann
de1c5120dd Merge pull request #21346 from netbox-community/release-v4.5.2
CI / build (20.x, 3.13) (push) Failing after 14s
CI / build (20.x, 3.12) (push) Failing after 16s
CI / build (20.x, 3.14) (push) Failing after 18s
CodeQL / Analyze (actions) (push) Failing after 32s
CodeQL / Analyze (javascript-typescript) (push) Failing after 25s
CodeQL / Analyze (python) (push) Failing after 25s
Release v4.5.2
2026-02-03 08:42:21 -05:00
Jeremy Stretch
87d2e02c85 Release v4.5.2 2026-02-03 08:09:14 -05:00
github-actions
cbbc4f74b8 Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 30s
CodeQL / Analyze (javascript-typescript) (push) Failing after 28s
CodeQL / Analyze (python) (push) Failing after 27s
2026-02-03 05:22:13 +00:00
Martin Hauser
be5bd74d4e feat(ipam): Add parent object fields for Services
CI / build (20.x, 3.12) (push) Failing after 13s
CI / build (20.x, 3.13) (push) Failing after 9s
CI / build (20.x, 3.14) (push) Failing after 9s
CodeQL / Analyze (actions) (push) Failing after 31s
CodeQL / Analyze (python) (push) Failing after 36s
CodeQL / Analyze (javascript-typescript) (push) Failing after 40s
Include `parent_object_type` and `parent_object_id` in `clone_fields`
for services. This improves cloning behavior for models using parent
object references, ensuring more accurate data duplication.

Fixes #21168
2026-02-02 16:05:09 -05:00
Jason Novinger
cf12bb5bf5 Fixes #20902: Avoid conflict when Git URL contains embedded username (#21252)
CI / build (20.x, 3.12) (push) Failing after 14s
CI / build (20.x, 3.13) (push) Failing after 17s
CI / build (20.x, 3.14) (push) Failing after 15s
CodeQL / Analyze (actions) (push) Failing after 37s
CodeQL / Analyze (javascript-typescript) (push) Failing after 38s
CodeQL / Analyze (python) (push) Failing after 36s
2026-02-02 11:16:32 -08:00
Jeremy Stretch
c060eef1d8 Closes #21300: Cache model-specific custom field lookups for the duration of a request (#21334) 2026-02-02 10:58:12 -08:00
bctiemann
96f0debe6e Merge pull request #21328 from netbox-community/21327-ContentTypeField-caching
Closes #21327: Leverage `get_by_natural_key()` to resolve ContentTypes
2026-02-02 13:46:04 -05:00
Martin Hauser
b26c7f34cd feat(models): Handle GFK attributes in CloningMixin
Extend the CloningMixin to inject GenericForeignKey (GFK) attributes
when both content type and ID fields are present. Improves support for
models using GFK fields during cloning operations.

Fixes #21201
2026-02-02 13:02:32 -05:00
bctiemann
d6428c6aa4 Merge pull request #21314 from marsteel/21233-UI-Add-horizontal-padding-to-Release-info-section
Fixes #21233: UI Add horizontal padding to Release info section in Navigation menu
2026-02-02 11:17:30 -05:00
github-actions
e3eca98897 Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 31s
CodeQL / Analyze (javascript-typescript) (push) Failing after 46s
CodeQL / Analyze (python) (push) Failing after 49s
2026-01-31 05:14:50 +00:00
Jeremy Stretch
cdc735fe41 Closes #21302: Avoid redundant uniqueness checks in REST API serializers
CI / build (20.x, 3.12) (push) Failing after 13s
CI / build (20.x, 3.13) (push) Failing after 13s
CI / build (20.x, 3.14) (push) Failing after 24s
CodeQL / Analyze (actions) (push) Failing after 33s
CodeQL / Analyze (javascript-typescript) (push) Failing after 45s
CodeQL / Analyze (python) (push) Failing after 40s
2026-01-30 19:36:42 -05:00
Jeremy Stretch
aa4a9da955 Closes #21303: Cache serialized post-change data on object (#21325)
* Closes #21303: Cache serialized post-change data on object

* Set to_objectchange.alters_data

* Restructure logic for determining post-change snapshot
2026-01-30 14:49:12 -05:00
Jeremy Stretch
5c6fc2fb6f Closes #21110: Support for cursor-based pagination in GraphQL API (#21322) 2026-01-30 11:45:35 -08:00
Jeremy Stretch
ad29cb2d66 Closes #21263: Prefetch related objects after creating/updating objects via REST API (#21329)
* Closes #21263: Prefetch related objects after creating/updating objects via REST API

* Add comment re: ordering by PK
2026-01-30 14:13:05 -05:00
Aditya Sharma
bec5ecf6a9 Closes #21209: Accept case-insensitive model names in configuration (#21275)
CI / build (20.x, 3.12) (push) Failing after 9s
CI / build (20.x, 3.13) (push) Failing after 8s
CI / build (20.x, 3.14) (push) Failing after 8s
CodeQL / Analyze (actions) (push) Failing after 25s
CodeQL / Analyze (javascript-typescript) (push) Failing after 35s
CodeQL / Analyze (python) (push) Failing after 37s
NetBox now accepts case-insensitive model identifiers in configuration, allowing
both lowercase (e.g. "dcim.site") and PascalCase (e.g. "dcim.Site") for
DEFAULT_DASHBOARD, CUSTOM_VALIDATORS, and PROTECTION_RULES.
This makes model name handling consistent with FIELD_CHOICES.

- Add a shared case-insensitive config lookup helper (get_config_value_ci())
- Use the helper in extras/signals.py and core/signals.py
- Update FIELD_CHOICES ChoiceSetMeta to support case-insensitive replace/extend
  (only compute extend choices if no replacement is defined)
- Add unit tests for get_config_value_ci()
- Add integration tests for case-insensitive FIELD_CHOICES replacement/extension
- Update documentation examples to use PascalCase consistently
2026-01-30 13:48:38 +01:00
github-actions
c98f55dbd2 Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 32s
CodeQL / Analyze (javascript-typescript) (push) Failing after 50s
CodeQL / Analyze (python) (push) Failing after 52s
2026-01-30 05:18:59 +00:00
Jeremy Stretch
dfe20532a1 Closes #21327: Leverage get_by_natural_key() to resolve ContentTypes 2026-01-29 19:46:22 -05:00
Martin Hauser
359179fd4a fix(dcim): Add port mapping creation for module install (#21308)
CI / build (20.x, 3.12) (push) Failing after 17s
CI / build (20.x, 3.13) (push) Failing after 19s
CI / build (20.x, 3.14) (push) Failing after 16s
CodeQL / Analyze (actions) (push) Failing after 6m50s
CodeQL / Analyze (javascript-typescript) (push) Failing after 7m4s
CodeQL / Analyze (python) (push) Failing after 7m2s
2026-01-29 14:37:57 -08:00
Arthur Hanson
c44e8606f7 21129 Store queue_name in Job so correctly deleted in RQ (#21309)
* Add queue name to Job

* Add queue name to serializer, filterset, detail view

* fix job queue delete

* fix job queue delete

* review feedback
2026-01-29 15:29:33 -05:00
github-actions
8e620ef325 Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 18s
CodeQL / Analyze (javascript-typescript) (push) Failing after 17s
CodeQL / Analyze (python) (push) Failing after 18s
2026-01-29 05:17:01 +00:00
Jeremy Stretch
1526e437f1 Closes #21244: Introduce ability to omit specific fields from REST API responses (#21312)
CI / build (20.x, 3.12) (push) Failing after 14s
CI / build (20.x, 3.13) (push) Failing after 10s
CI / build (20.x, 3.14) (push) Failing after 11s
CodeQL / Analyze (actions) (push) Failing after 21s
CodeQL / Analyze (javascript-typescript) (push) Failing after 17s
CodeQL / Analyze (python) (push) Failing after 17s
Introduce support for omitting specific serializer fields via an
`omit` parameter, acting as the inverse of `fields`.
Wire it through the API viewset and queryset optimization helpers
so omitted fields don’t trigger unnecessary annotations/prefetches,
and document the new behavior.
2026-01-28 22:06:46 +01:00
Martin Hauser
0b507eb207 fix(ipam): Include scope params in Prefix creation links
CI / build (20.x, 3.12) (push) Failing after 11s
CI / build (20.x, 3.13) (push) Failing after 11s
CI / build (20.x, 3.14) (push) Failing after 12s
CodeQL / Analyze (actions) (push) Failing after 7m12s
CodeQL / Analyze (javascript-typescript) (push) Failing after 18s
CodeQL / Analyze (python) (push) Failing after 28s
Update prefix creation URLs to pass `scope_type` and `scope` (replacing
the legacy `site` query parameter) for both the Child Prefixes
"Add Prefix" button and in-table available-prefix links.
Scope parameters are only rendered when a scope is defined, so
unscoped prefixes remain unchanged.

Fixes #21262
2026-01-28 15:19:44 -05:00
Elliott Balsley
5a36e79215 Fixes #20977: Apply defaults for missing script variables (#21295)
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Ensure script variables fall back to their defined defaults when a value is not
submitted (e.g. via "Run again" or other minimal POSTs).

- Populate omitted script variables with their initial/default values before
  validation and job enqueueing
- Treat falsy defaults (e.g. False/0) as valid defaults
- Add a test asserting defaults are included in enqueued job data
- Remove the redundant default from ScriptValidationErrorTest
2026-01-28 15:35:33 +01:00
Martin Hauser
2a0f26623b Fixes #21254: Fix release check failure when stale latest_release cache can't be unpickled (#21282)
* fix(misc): Handle cache unpickling failure in release check

Guard `cache.get('latest_release')` during release checks to prevent a
500 when stale cached data can't be unpickled after dependency upgrades.
On failure, log at debug level and delete the affected cache key.

Fixes #21254

* Correct comment

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2026-01-28 09:28:20 -05:00
MA Gang
43ae52089f Add padding to release info div
Add padding to release info div in layout.html
2026-01-28 14:29:38 +01:00
github-actions
1a603981b2 Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-28 05:07:33 +00:00
Aditya Sharma
245495b2fe Closes #21228: Add image attachments support to RackType model (#21276)
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-27 09:36:11 -08:00
bctiemann
8d3eb69055 Merge pull request #21264 from netbox-community/19869-provide-information-about-lag-targets-in-lag-members-section
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Fixes #19869: Display peer connections for LAG member interfaces
2026-01-27 10:23:14 -05:00
bctiemann
7e3b60f194 Merge pull request #21299 from netbox-community/20172-ability-to-query-for-cabled-interfaces-via-graphql
Closes #20172: Add `cabled` filter for DCIM interfaces in GraphQL
2026-01-27 10:13:27 -05:00
bctiemann
5338c842b8 Merge pull request #21289 from llamafilm/20052-loglevel
Fixes #20052: improve logging for faulty scripts
2026-01-27 10:10:17 -05:00
bctiemann
9186b0edaa Merge pull request #21281 from netbox-community/21176-remove-iprange-checkboxes
Fixes #21176: Remove checkboxes from IP ranges in mixed-type tables
2026-01-27 10:08:37 -05:00
bctiemann
d883be9e56 Merge pull request #21246 from adionit7/21150-docs-config-menu-path
Fixes #21150: Correct Dynamic Configuration menu path in documentation
2026-01-27 08:43:52 -05:00
bctiemann
6fc7fa6c64 Merge pull request #21220 from netbox-community/15801-vlan-overview-device-interfaces-list-with-connection-link
Closes #15801: Add link peer and connection columns to `VLANDeviceTable`
2026-01-27 08:35:33 -05:00
Martin Hauser
3a33df0e43 feat(forms): Add Owner Group support to Filter Forms
Introduces support for `owner_group` in various filter forms, improving
ownership granularity.
Updates DynamicModel fields to handle relationships
between `owner_group` and `owner` effectively.

Fixes #21081
2026-01-27 08:34:42 -05:00
github-actions
433f46746e Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-27 05:07:09 +00:00
Jeremy Stretch
8f5f91fcfe Closes #21259: Cache ObjectType results for the duration of a request (#21287)
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-26 15:07:13 -08:00
Martin Hauser
1a2175127e Fixes #21202: Avoid clearing scope on clone (#21265) 2026-01-26 16:14:36 -06:00
Martin Hauser
e859807d1d docs(guides): Update Ubuntu reference to 24.04
Update the installation and administration guides to reference
Ubuntu 24.04 instead of 22.04 where applicable, and refresh examples
to match NetBox v4.5.

This includes updates to Python version requirements, NetBox shell
commands, Redis configuration, and sample outputs to align with current
compatibility and best practices.

Fixes #21297
2026-01-26 15:43:59 -05:00
Jeremy Stretch
a8c997ff29 Closes #21260: Defer object serialization for events pipeline (#21286) 2026-01-26 14:35:00 -06:00
adionit7
4a28ab98f4 Fixes #21115: Include attribute_data in ModuleType YAML export
- Added airflow and attribute_data fields to ModuleType.to_yaml() method
- Ensures custom JSON properties from module type profiles are properly exported
- Maintains consistency with import functionality in ModuleTypeImportForm
2026-01-26 15:01:21 -05:00
Martin Hauser
3636d55017 fix(nav): Show Authentication admin menu items based on object perms (#21283)
Replace hardcoded menu entries for Users, Groups, API Tokens, and
Permissions with `get_model_item()`. This drops the `staff_only` gate
and relies on the standard model permission checks, restoring visibility
of these Admin menu items for non-superusers with the relevant object
permissions.

Fixes #21242
2026-01-26 11:34:46 -08:00
Aditya Sharma
aa69e96818 Fixes #21173: Fix plugin menu registration order timing issue (#21248)
* Fixes #21173: Fix plugin menu registration order timing issue

- Converted static MENUS list to dynamic get_menus() function
- Ensures plugin menus are built at request time after all plugins complete ready()
- Fixes issue where only first few plugin menus appeared in navigation sidebar
- Updated navigation template tag to call get_menus() dynamically

* Fix ruff linting errors

- Add missing blank line before get_menus() function definition
- Remove trailing whitespace

* Add @cache decorator to get_menus() for performance optimization

Per reviewer feedback, the menu list is now cached since it doesn't change
without a Django restart. This eliminates redundant list building on each request.

---------

Co-authored-by: adionit7 <adionit7@users.noreply.github.com>
2026-01-26 10:34:57 -08:00
Martin Hauser
1745d2ae93 feat(dcim): Add filter for cabled objects in GraphQL
Introduces a `cabled` filter to the GraphQL API for DCIM. Allows
filtering objects based on whether they are connected to a cable,
improving query customization.

Fixes #20172
2026-01-26 15:39:56 +01:00
Elliott Balsley
e097a848dc display error in UI 2026-01-24 19:04:14 -08:00
Elliott Balsley
595be6dcd4 log the error with error level instead of debug 2026-01-24 19:04:06 -08:00
github-actions
a9e50238eb Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2026-01-24 05:03:22 +00:00
Arthur Hanson
a9a300197a Clear Rack Face when clear Rack (#21182)
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
* #20383 clear rack face if no rack on edit

* #20383 clear rack face if no rack on edit

* review changes

* review changes
2026-01-23 12:26:27 -05:00
Jeremy Stretch
3dcca73ecc Fixes #21249: Avoid unneeded user query when no event rules are present (#21250) 2026-01-23 09:44:54 -06:00
Jason Novinger
cedbeb7b19 Fixes #21176: Remove checkboxes from IP ranges in mixed-type tables
When IP addresses and IP ranges are displayed together in a prefix's
  IP Addresses tab, only IP addresses should be selectable for bulk
  operations since the bulk delete form doesn't support mixed object types.

  - Override render_pk() in AnnotatedIPAddressTable to conditionally render
    checkboxes only for the table's primary model type (IPAddress)
  - Add warning comment to add_requested_prefixes() about fake Prefix objects
  - Add regression test to verify IPAddress has checkboxes but IPRange does not
2026-01-23 09:36:15 -06:00
Martin Hauser
a45b6b170d feat(dcim): Show peer connections for LAG members
Add `InterfaceLAGMemberTable` for the LAG Members panel on
LAG interface detail views. The table includes the parent device,
member interface/type, and a peer column which renders
connected endpoints (including the peer LAG when present).

Fixes #19869
2026-01-22 20:41:40 +01:00
bctiemann
4b4c542dce Add truncate_middle filter for middle-ellipsis on long filenames (#21253)
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2026-01-22 09:40:48 -08:00
github-actions
077d9b1129 Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-22 05:07:49 +00:00
Aditya Sharma
e81ccb9be6 Fixes #21214: Clean up AutoSyncRecord when detaching from DataSource (#21219)
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
Co-authored-by: adionit7 <adionit7@users.noreply.github.com>
2026-01-21 16:38:27 -06:00
Jeremy Stretch
bc83d04c8f Introduce performance issue template (#21247) 2026-01-21 16:34:01 -06:00
adionit7
42ecf3cac0 Fixes #21150: Correct Dynamic Configuration menu path in documentation
- Updated menu path from 'Admin > Extras > Configuration Revisions'
  to 'Admin > System > Configuration History'
- Reflects actual location in NetBox admin interface
2026-01-21 22:53:29 +05:30
Matthew Papaleo
339ad455e4 Support for max_length and max_depth standardised for prefix_list, aggreate/prefixes and prefix/prefixes
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-21 10:02:06 -05:00
Martin Hauser
af8e53d8fb feat(ipam): Add connection/link peer to VLANDeviceTable
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
The VLAN Device Interfaces table now includes `connection` and
`link_peer` columns, using the existing interface templates to render
peer/connection context consistently.

Fixes #15801
2026-01-21 13:04:39 +01:00
github-actions
f24376cfab Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-21 05:07:22 +00:00
Jeremy Stretch
47d4ae29c1 Release v4.5.1
CodeQL / Analyze (python) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2026-01-20 14:44:04 -05:00
bctiemann
8fce672682 Merge pull request #21238 from netbox-community/21160-follow-up-null-option
Fixes #21160: Handle "null" choice selection in widgets
2026-01-20 13:39:54 -05:00
Antoine Keranflec'h
f776b97415 fixes #21139 support api filter for core (#21192) 2026-01-20 09:10:27 -08:00
Aditya Sharma
3cc1f30287 Fixes #21213: Make Tag weight field required in forms (#21218)
The weight field was explicitly declared with required=False in TagForm
and TagImportForm, allowing empty submissions that would crash with a
database IntegrityError since the column is NOT NULL.

By removing the explicit field override, Django now auto-generates the
form field from the model, which has default=1000 and is required.

Co-authored-by: adionit7 <adionit7@users.noreply.github.com>
2026-01-20 08:50:31 -08:00
Martin Hauser
6d166aa10d feat(utilities): Handle "null" choice selection in widgets
Enhances widget handling by preserving "null" choice values in both
individual and mixed-object selections. Updates tests to validate UI
rendering and ensure compatibility with null sentinel values.
2026-01-20 17:29:48 +01:00
Aditya Sharma
040a2ae9a9 Enable specifying mask length when creating IP addresses via available-ips endpoint (#21193)
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
* Enable specifying mask length when creating IP addresses via available-ips endpoint

Fixes #21144

Allow clients to specify an arbitrary mask length when creating IP addresses
from a parent prefix or range using the 'next available' REST API endpoint.

Changes:
- Updated AvailableIPAddressesView to use PrefixLengthSerializer as write_serializer_class
- Enhanced PrefixLengthSerializer to support both 'prefix' and 'parent' context keys
- Added validation to ensure requested prefix_length >= parent mask_length
- Updated prep_object_data to use requested prefix_length if provided, otherwise fall back to parent mask_length for backwards compatibility
- Updated API schema documentation to reflect PrefixLengthSerializer usage

This enables use cases like creating loopback IP addresses with /32 mask length
from a parent prefix with a shorter mask length.

* Refine available-ips prefix length handling

Keep PrefixLengthSerializer strict for available-prefixes and introduce
AvailableIPRequestSerializer for the available-ips endpoint, where
prefix_length is optional and validated against the parent prefix/range.

* Revert PrefixLengthSerializer to original strict state

PrefixLengthSerializer should remain required and strict for the
available-prefixes endpoint. The optional prefix_length functionality
for available-ips is handled by AvailableIPRequestSerializer.

* Add API test; misc cleanup

---------

Co-authored-by: adionit7 <adionit7@users.noreply.github.com>
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2026-01-20 11:20:02 -05:00
Martin Hauser
39f11f28fb fix(core): Cache table existence for ObjectType checks
Introduces a cached `_table_exists` flag to avoid repeated database
introspection queries for `core_objecttype`.
Improves performance during ObjectType lookups and reduces
redundant query overhead.

Fixes #21231
2026-01-20 11:15:14 -05:00
Jeremy Stretch
62b9025a9e Fixes #21181: Handle AuthenticationFailed exception on /media endpoint (#21224) 2026-01-20 08:07:18 -08:00
Jeremy Stretch
21091f22e6 Closes #21234: Add #20966 to the changelog for v4.4.9 (#21236) 2026-01-20 09:22:03 -06:00
github-actions
3efa23cf8f Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-20 05:07:49 +00:00
bctiemann
0f62137957 Merge pull request #21199 from netbox-community/21178-change-rack-dimensions-display-to-be-more-consistent
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
Fixes #21178: Use localized “millimeters” for rack mounting depth (follow-up)
2026-01-19 14:14:24 -05:00
Martin Hauser
7858ccb712 feat(extras): Add AVIF support for image attachments
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Extends allowed image file formats to include AVIF for better modern
format support. Introduces a constants mapping for image formats to
centralize file type definitions. Updates form widgets and utilities
to leverage the new constants, enabling more flexible and consistent
image handling.

Fixes #21039
2026-01-19 09:56:06 -05:00
Martin Hauser
6b7b38ee0a fix(users): Refactor object permission query logic
Simplifies the `OBJECTPERMISSION_OBJECT_TYPES` definition by adjusting
query filters and introducing new conditions for specific app labels
and models.

Fixes #21051
2026-01-19 09:30:36 -05:00
matthew-242
c8f17e06a2 Add support to filter on cached relations _location, _region, _site and _site_group to ScopedFilterMixin (#21162) 2026-01-19 09:09:03 -05:00
Jeremy Stretch
edace6aff4 Fixes #21166: Fix support for filtering on unsigned 32-bit integer values in GraphQL API (#21186)
* Fixes #21166: Fix support for filtering on unsigned 32-bit integer values in GraphQL API

* tunnel_id should also use BigIntegerLookup
2026-01-19 08:54:39 -05:00
github-actions
586bc132b6 Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2026-01-17 05:02:55 +00:00
Arthur Hanson
52a2b934a0 Fixes #21160: Fix performance issue rendering FilterSet forms w/ large choicesets (#21200)
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2026-01-16 16:34:12 -06:00
Martin Hauser
3d1f18d6dd fix(dcim): Localize mounting depth format string
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
Replaces the fixed format string for `mounting_depth` with a localized
version using `gettext_lazy`. This ensures proper translation of the
unit label for internationalization purposes.

Fixes #21178
2026-01-16 19:53:49 +01:00
Micky
3e2a26984f Fixes #21165: Changes filterset to show VLAN group instead of site (#21190) 2026-01-16 09:24:29 -06:00
adionit7
f5f0c19860 Remove obsolete pre-commit hook script
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
The legacy pre-commit hook script was scheduled for removal in NetBox v4.3, as noted in the TODO comment within the file. Users should now use the pre-commit tool instead.
2026-01-16 09:03:08 -05:00
bctiemann
8da9b11ab8 Merge pull request #21154 from netbox-community/21124-moduletype-front-ports
Fixes #21124: Fix rear port selection when creating front ports on a module type
2026-01-16 08:28:39 -05:00
Arthur Hanson
ca67fa9999 Fix #21134: fix bulk rename ModuleType (#21180) 2026-01-16 03:23:28 -06:00
Jeremy Stretch
eff768192e Fixes #21140: Ensure default panel attribute labels are translated (#21153) 2026-01-16 01:35:35 -06:00
github-actions
1e297d55ee Update source translation strings
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-16 05:04:49 +00:00
Daniel Sheppard
4c35630434 Reorder migrations
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
2026-01-15 16:57:28 -06:00
bctiemann
fdb987ef91 Merge pull request #21183 from netbox-community/21178-change-rack-dimensions-display-to-be-more-consistent
Fixes #21178: Add spacing in mounting depth format string
2026-01-15 17:48:39 -05:00
bctiemann
b5a23db43c Merge pull request #21164 from netbox-community/21118-site
fix performance regression for Site save, use bulk_update for cached fields
2026-01-15 17:48:01 -05:00
bctiemann
366b69aff7 Merge pull request #21143 from netbox-community/21050-device-oob-ip-may-become-orphaned
Fixes #21050: Prevent reassignment of OOB IPs
2026-01-15 17:47:00 -05:00
bctiemann
c3e8c5e69c Merge pull request #21100 from netbox-community/21097-graphql-id-lookups
Fixes #21097: Fix comparison lookups for ID filters in GraphQL API
2026-01-15 17:44:22 -05:00
adionit7
b55f36469d Update CodeQL Action from v3 to v4
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
- Update github/codeql-action/init from @v3 to @v4
- Update github/codeql-action/analyze from @v3 to @v4

Fixes #21156
2026-01-15 16:46:25 -05:00
Martin Hauser
1c46215cd5 feat(extras): Allow updates to data_source and data_file via API
Adds support for PATCHing ConfigContext and ConfigContextProfile with
integer IDs for `data_source` and `data_file`.
Adds regression tests to validate assignment and API functionality.

Fixes #20933
2026-01-15 14:37:16 -05:00
Martin Hauser
7fded2fd87 fix(dcim): Add spacing in mounting depth format string
Corrects the format string for mounting depth to include a space
between the value and the unit (`mm`) for consistency with other
measurements.

Fixes #21178
2026-01-15 18:52:25 +01:00
Martin Hauser
0ddc5805c4 fix(core): Use gettext_lazy in data.py
Replace `gettext()` with `gettext_lazy()` to avoid locale-dependent
model serialization (and false-positive pending migration warnings).
Also make a missing `ValidationError` message translatable and
format-safe.

Fixes #21175
2026-01-15 12:47:05 -05:00
github-actions
c1bbc026e2 Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-15 05:05:36 +00:00
Arthur
8cbfe94fba fix performance regression for Site save, use bulk_update for cached fields 2026-01-14 16:30:40 -08:00
Jason Novinger
434334d927 Fixes #20239: Prevent shared mutable state in PluginMenuItem and PluginMenuButton (#21099)
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
PluginMenuItem and PluginMenuButton classes used mutable class-level
defaults for `permissions` and `buttons` attributes, causing permission
leakage between instances when these attributes were modified without
explicit parameters.

Changed to initialize these attributes as fresh lists per instance in
__init__ when not explicitly provided, following standard Python pattern
for avoiding mutable default arguments.
2026-01-14 12:50:35 -08:00
Jeremy Stretch
fff99fd3ff Fixes #21124: Fix rear port selection when creating front ports on a module type 2026-01-14 09:46:04 -05:00
Jeremy Stretch
6bd083b7ed Closes #21142: Enable filtering device components by site/location/rack directly via GraphQL API (#21145) 2026-01-14 08:06:55 -06:00
bctiemann
f38faf2e01 Merge pull request #21135 from netbox-community/21102-fix-graphiql-explorer
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
Fixes #21102: Fix GraphiQL explorer UI
2026-01-13 12:33:58 -05:00
Martin Hauser
f4892caa51 fix(ipam): Prevent reassignment of OOB IPs
Disable reassignment of IP addresses designated as primary or OOB for
parent objects. Adds validation to block changes when an IP is marked as
the OOB IP.

Fixes #21050
2026-01-13 18:13:31 +01:00
Mark Robert Coleman
e60807adc5 Fixes #21121: Expand changelog message doc/add cross-references (#21138) 2026-01-13 09:58:06 -06:00
github-actions
e14934e5a5 Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-13 05:05:43 +00:00
Adam
ae03723e43 Fixes #21105: Update help text for token field on API page. (#21106)
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
Co-authored-by: Jason Novinger <jnovinger@gmail.com>
2026-01-12 19:17:35 -06:00
Jeremy Stretch
c0f79df91f Introduce a new issue type for feature removals (#21092)
Co-authored-by: Jason Novinger <jnovinger@gmail.com>
2026-01-12 15:41:25 -06:00
Jeremy Stretch
edbfd0bae6 Fixes #21117: Avoid exception when attempting to create v2 token without API_TOKEN_PEPPERS defined (#21132) 2026-01-12 15:40:42 -06:00
Jeremy Stretch
c3e111c769 Fixes #21102: Fix GraphiQL explorer UI 2026-01-12 14:34:17 -05:00
Mario
c11f4b3716 21075-rename-l2vpn-terminations-menu-entry
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2026-01-12 10:40:45 -05:00
Jeremy Stretch
a54ad24b47 Fixes #21097: Fix comparison lookups for ID filters in GraphQL API 2026-01-08 16:34:13 -05:00
Martin Hauser
3624b88c3f Closes #21035: Add .gitkeep to track the media directory (#21074)
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
2026-01-08 14:33:06 -06:00
github-actions
f54ed8bb7f Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
2026-01-08 05:04:46 +00:00
Jeremy Stretch
5d0609e729 Bump Python version for update-translation-strings action (#21083)
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2026-01-07 15:26:21 -08:00
Brian Tiemann
865b88e724 Make module_bay recursion check on Module.clean tolerant of unset module.module_bay
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-07 10:19:02 -05:00
Jeremy Stretch
e73db97d46 Merge pull request #21079 from netbox-community/feature
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
Release v4.5.0
2026-01-06 16:12:06 -05:00
Jeremy Stretch
6f2ba5c75c Merge branch 'main' into feature
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
2026-01-06 13:05:07 -05:00
Jeremy Stretch
fa8a9ef9de Release v4.4.10
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-06 12:30:03 -05:00
Jeremy Stretch
6beb079b97 Revert "Fixed #20950: Add missing module and device properties in module-bay (#21005)"
This reverts commit 860db9590b.
2026-01-06 10:38:41 -05:00
bctiemann
bad688b8aa Merge pull request #21069 from netbox-community/21067-cable-profile-error
Fixes #21067: Force update of cable terminations when changing cable profile
2026-01-06 09:48:54 -05:00
github-actions
c8aad24a1b Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-06 05:04:58 +00:00
bctiemann
42bd876604 Merge pull request #21072 from netbox-community/21071-exception-request-url
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
Closes #21071: Include the request method & URL when displaying a server error
2026-01-05 20:20:46 -05:00
bctiemann
f903442cb9 Merge pull request #21065 from netbox-community/21049-clean-stale-cf-data
Fixes #21049: Remove stale custom field data during object validation
2026-01-05 20:19:46 -05:00
Jason Novinger
5a64cb712d Fixes #21064: Ensures that extra choices preserve nested colons 2026-01-05 16:38:16 -05:00
Jason Novinger
4d90d559be Fix permission constraint example error 2026-01-05 16:33:21 -05:00
Jeremy Stretch
19de058f94 Closes #21071: Include the request method & URL when displaying a server error 2026-01-05 16:09:39 -05:00
Jeremy Stretch
d3e4c02807 Fixes #21067: Force update of cable terminations when changing cable profile 2026-01-05 15:14:04 -05:00
Jeremy Stretch
dc00e19c3c Fixes #21063: Check for duplicate choice values when validating a custom field choice set (#21066) 2026-01-05 13:10:04 -06:00
Jeremy Stretch
6ed6da49d9 Update test 2026-01-05 11:00:54 -05:00
Prince Kumar
7154d4ae2e Closes #20953: Show interfaces bridged to an interface in the UI (#21010)
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-01-05 09:40:38 -06:00
Jeremy Stretch
bc26529be8 Fixes #21049: Remove stale custom field data during object validation 2026-01-05 09:49:32 -05:00
github-actions
da64c564ae Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2026-01-01 05:07:03 +00:00
Jeremy Stretch
6199b3e039 FIxes #19506: Add filter forms for component templates (#21057)
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
Co-authored-by: Callum <callum@reja.au>
Co-authored-by: Callum <96725140+callumau@users.noreply.github.com>
2025-12-31 09:50:39 -06:00
Jeremy Stretch
ebada4bf72 Closes #21001: Annotate plugin filterset registration in v4.5 release notes (#21058)
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-12-31 09:42:47 -06:00
github-actions
2a391253a5 Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-12-31 05:05:09 +00:00
Jason Novinger
914653d63e Fixes #21045: Allow saving Site with associated Prefix
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
This was a result of the fix for #20944 optimizing a query to only
include the `id` field with `.only(id)`. Since `Prefix.__init__()`
caches original values from other fields (`_prefix` and `_vrf_id`),
these cached values are `None` at init-time.

This might not normally be a problem, but the sequence of events in
the bug report also end up causing the `handle_prefix_saved` handler
to run, which uses an ORM lookup, (either `net_contained_or_equal`
original`net_contained`) that does not support a query argument of
`None`.
2025-12-30 12:26:48 -05:00
Martin Hauser
3813aad8b1 Fixes #20320: Ensure related interface options availibility in bulk edit (#21006) 2025-12-30 10:17:14 -06:00
Jeremy Stretch
ea5371040e Fixes #20817: Re-enable sync button when disabling scheduled syncing for a data source (#21055) 2025-12-30 10:05:08 -06:00
Unknown
6c824cc48f Fixes #20044: Elevations stuck in light mode (#21037)
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
Co-authored-by: UnknownTy <meaphunter+git@hotmail.com>
Co-authored-by: Jason Novinger <jnovinger@gmail.com>
2025-12-29 16:27:03 -06:00
Jeremy Stretch
c78b8401dc Fixes #21020: Fix object filtering for image attachments panel (#21030)
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-12-29 15:19:24 -06:00
Jeremy Stretch
f510e40428 Closes #21047: Add compatibility matrix to plugin setup instructions (#21048) 2025-12-29 11:39:51 -06:00
Daniel Sheppard
3cc15ecaf0 Merge branch 'feature' into feature-ip-prefix-link
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
2025-12-28 11:41:39 -06:00
Prince Kumar
860db9590b Fixed #20950: Add missing module and device properties in module-bay (#21005)
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-12-23 13:34:06 -06:00
Jeremy Stretch
7c63d001b1 Release v4.4.9 2025-12-23 12:02:30 -05:00
Jeremy Stretch
93119f52c3 Fixes #21032: Avoid subquery in RestrictedQuerySet where unnecessary 2025-12-23 10:15:06 -05:00
github-actions
ee2aa35cba Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-12-23 05:04:20 +00:00
bctiemann
7896a48075 Merge pull request #21029 from netbox-community/21011-configrevision-save
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
Fixes #21011: Avoid updating database when loading active ConfigRevision
2025-12-22 14:19:19 -05:00
bctiemann
eb87c3f304 Merge pull request #21000 from netbox-community/20011-misleading-error-message
Fixes #20011: Provide accurate error for bulk import duplicate IDs
2025-12-22 14:12:36 -05:00
Vincent Simonin
3acbb0a08c Fix on delete cascade entity order (#20949)
* Fix on delete cascade entity order

Since [#20708](https://github.com/netbox-community/netbox/pull/20708)
relation with a on delete RESTRICT are not deleted in the proper order.
Then the error `violate not-null constraint` occurs and breaks the
delete cascade feature.

* Revert unrelated and simplify changes
2025-12-22 13:19:02 -05:00
Jeremy Stretch
f67cc47def Fixes #21011: Avoid updating database when loading active ConfigRevision 2025-12-22 11:00:04 -05:00
Martin Hauser
f7219e0672 Closes #20309: Add ASDOT notation support for ASN ranges (#21004)
* feat(ipam): Add ASDOT notation support for ASN ranges

Introduces ASDOT notation for ASN Ranges to improve readability of large
AS numbers. Adds `start_asdot` and `end_asdot` properties, columns, and
display logic for ASN ranges in the UI.

Fixes #20309

* Wrap "ASDOT" with parentheses in column header

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-12-22 10:06:08 -05:00
Prince Kumar
e5a975176d Fixed #20944: Ensure cached scope fields stay consistent when Region, Site, or Location changes (#20986) 2025-12-22 09:48:43 -05:00
github-actions
83ee4fb593 Update source translation strings
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-12-20 05:02:02 +00:00
bctiemann
db8271c904 Fixes #20114: Preserve parent bay during device bulk import when tags are present (#21019)
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-12-19 17:05:32 -06:00
github-actions
5a24f99c9d Update source translation strings
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-12-18 05:03:18 +00:00
Jeremy Stretch
9318c91405 Closes #20720: Add support for Latvian translations (#21003) 2025-12-17 15:20:04 -06:00
Martin Hauser
5c6aaf2388 Closes #20900: Allow multiple choices in CustomField select filter fields (#20992) 2025-12-17 14:32:46 -06:00
Jason Novinger
265f375595 Fixes #20876: Allow editing IPAddress in IPRange marked populated 2025-12-17 13:03:45 -05:00
Jason Novinger
d95fa8dbb2 Fixes #20011: UI Error msg for duplicate IDs in bulk import 2025-12-17 09:21:17 -06:00
bctiemann
2699149016 Merge pull request #20963 from pheus/20491-normalize-arrayfield-values-to-inclusive-pairs-for-api-tests
Fixes #20491: Normalize numeric range array fields for API test comparisons
2025-12-16 15:40:44 -05:00
vo42
f371004809 Fixes #20969: Fix FrontPortTemplateFilterSet rear_port_id queryset. (#20987)
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-12-16 11:23:18 -08:00
github-actions
ad29402b87 Update source translation strings
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-12-13 05:02:00 +00:00
Jason Novinger
598f8d034d Fixes #20912: Clear ModuleBay parent when module assignment removed (#20974)
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-12-12 13:31:59 -08:00
Arthur Hanson
ec13a79907 Fixes #20875: Fix updating of denormalized fields for component models (#20956) 2025-12-12 13:29:34 -06:00
github-actions
21f4036782 Update source translation strings
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-12-12 05:03:16 +00:00
bctiemann
ce3738572c Merge pull request #20967 from netbox-community/20966-remove-stick-scroll
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
Fixes #20966: Fix broken optgroup stickiness in ObjectType multiselect
2025-12-11 19:44:16 -05:00
bctiemann
cbb979934e Merge pull request #20958 from netbox-community/17976-manufacturer-devicetype_count
Fixes #17976: Remove devicetype_count from nested manufacturer to correct OpenAPI schema
2025-12-11 19:42:26 -05:00
bctiemann
642d83a4c6 Merge pull request #20937 from netbox-community/20560-bulk-import-prefix
Fixes #20560: Fix VLAN disambiguation in prefix bulk import
2025-12-11 19:40:59 -05:00
Jason Novinger
a06c12c6b8 Fixes #20966: Fix broken optgroup stickiness in ObjectType multiselect 2025-12-11 08:59:16 -06:00
Martin Hauser
60fce84c96 feat(ipam): Normalize numeric ranges in API output
Adds logic to handle numeric range fields in API responses by
converting them into inclusive `[low, high]` pairs for consistent
behavior. Updates test cases with `vid_ranges` fields to reflect the
changes.

Closes #20491
2025-12-10 21:11:23 +01:00
Jeremy Stretch
59afa0b41d Fix test 2025-12-10 09:01:11 -05:00
Jeremy Stretch
14b246cb8a Fixes #17976: Remove devicetype_count from nested manufacturer to correct OpenAPI schema 2025-12-10 08:23:48 -05:00
github-actions
f0507d00bf Update source translation strings
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-12-10 05:02:48 +00:00
Arthur Hanson
77b389f105 Fixes #20873: fix webhooks with image fields (#20955) 2025-12-09 22:06:11 -06:00
Jason Novinger
9ae53fc232 Fixes #20560: Fix VLAN disambiguation in prefix bulk import 2025-12-05 16:39:28 -06:00
Daniel Sheppard
5ada585129 Remove unrelated development path
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
2025-11-25 08:16:46 -06:00
Daniel Sheppard
b03158f1de Add pgtrigger as dependancy
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
2025-11-25 00:06:35 -06:00
Daniel Sheppard
bdde4b7e94 Switch to using triggers
Still outstanding:

* IPAddress and IPRange triggers
* Triggers for VRF changes on Prefix
* Triggers for changing to "container" on Prefix
* Rework logic for saving on all models
2025-11-25 00:01:23 -06:00
Daniel Sheppard
905656f13e Add migration 2025-11-07 09:10:51 -06:00
Daniel Sheppard
42c2dc57f8 Develop triggers for setting parents 2025-11-07 09:02:30 -06:00
Daniel Sheppard
56673f4d88 Signal optimizations 2025-11-05 18:10:38 -06:00
Daniel Sheppard
955c64b68c Re-order migrations 2025-09-14 10:49:44 -05:00
Daniel Sheppard
912e6e4fb1 Merge branch 'feature' of https://github.com/netbox-community/netbox into feature-ip-prefix-link
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
2025-09-11 20:46:47 -05:00
Daniel Sheppard
90d277610c Merge branch 'feature' of https://github.com/netbox-community/netbox into feature-ip-prefix-link 2025-09-03 22:15:38 -05:00
Daniel Sheppard
b1bc933e98 Clean up Prefix TODOs
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-08-08 09:29:12 -05:00
Daniel Sheppard
b54196f595 Merge branch 'feature' into feature-ip-prefix-link
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
2025-08-07 21:33:58 -05:00
Daniel Sheppard
0d31449df8 Optimize prefix assignment. Fix tests
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-07-10 12:59:59 -05:00
Daniel Sheppard
76e85683ac Re-apply de-duplication to IPRangeSerializer 2025-07-09 13:18:28 -05:00
Daniel Sheppard
f844ec5703 Merge branch 'feature' into feature-ip-prefix-link 2025-07-09 13:00:51 -05:00
Daniel Sheppard
7eb3a8d379 Fix some tests 2025-07-09 12:56:05 -05:00
Daniel Sheppard
ade4354ca4 Fix some test errors 2025-07-09 11:12:52 -05:00
Daniel Sheppard
697d5bd876 Slightly DRY the migration 2025-07-09 10:52:24 -05:00
Daniel Sheppard
b19f81cede More work on IP Address/Range and Prefix relationship 2025-07-09 10:36:41 -05:00
Daniel Sheppard
c5e7b21147 Add additional FKs 2025-06-05 09:43:18 -05:00
Daniel Sheppard
c211b624d0 Merge branch 'feature' of https://github.com/netbox-community/netbox into feature-ip-prefix-link
# Conflicts:
#	netbox/ipam/forms/bulk_import.py
2025-05-15 08:40:58 -05:00
Daniel Sheppard
4c8301b3a5 Update migration 2025-05-15 08:38:03 -05:00
Daniel Sheppard
68d0b58293 Update migration 2025-04-10 08:22:23 -05:00
Daniel Sheppard
738ef63527 Update from feature 2025-04-09 10:24:56 -05:00
Daniel Sheppard
747fef0bc2 Work on IP to Prefix ForeignKey relationship 2025-02-24 14:03:18 -06:00
31 changed files with 1217 additions and 96 deletions
+5
View File
@@ -35,6 +35,11 @@ django-mptt==0.17.0
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
django-pglocks
# Manager for managing PostgreSQL triggers
# https://github.com/AmbitionEng/django-pgtrigger/blob/main/CHANGELOG.md
django-pgtrigger
# Prometheus metrics library for Django
# https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md
django-prometheus
+20 -11
View File
@@ -59,18 +59,24 @@ class PrefixSerializer(PrimaryModelSerializer):
vlan = VLANSerializer(nested=True, required=False, allow_null=True)
status = ChoiceField(choices=PrefixStatusChoices, required=False)
role = RoleSerializer(nested=True, required=False, allow_null=True)
children = serializers.IntegerField(read_only=True)
_children = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(read_only=True)
prefix = IPNetworkField()
class Meta:
model = Prefix
fields = [
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'vrf', 'scope_type', 'scope_id', 'scope',
'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'owner', 'comments', 'tags',
'custom_fields', 'created', 'last_updated', 'children', '_depth',
'id', 'url', 'display_url', 'display', 'family', 'aggregate', 'parent', 'prefix', 'vrf', 'scope_type',
'scope_id', 'scope', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_children', '_depth',
]
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth')
brief_fields = ('id', 'url', 'display', 'family', 'aggregate', 'parent', 'prefix', 'description', '_depth')
def get_fields(self):
fields = super(PrefixSerializer, self).get_fields()
fields['parent'] = PrefixSerializer(nested=True, read_only=True)
return fields
class PrefixLengthSerializer(serializers.Serializer):
@@ -124,7 +130,9 @@ class AvailablePrefixSerializer(serializers.Serializer):
# IP ranges
#
class IPRangeSerializer(PrimaryModelSerializer):
prefix = PrefixSerializer(nested=True, required=False, allow_null=True)
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
start_address = IPAddressField()
end_address = IPAddressField()
@@ -136,11 +144,11 @@ class IPRangeSerializer(PrimaryModelSerializer):
class Meta:
model = IPRange
fields = [
'id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant',
'status', 'role', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'mark_populated', 'mark_utilized',
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'start_address', 'end_address', 'size', 'vrf',
'tenant', 'status', 'role', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created',
'last_updated', 'mark_populated', 'mark_utilized',
]
brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description')
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'start_address', 'end_address', 'description')
#
@@ -148,6 +156,7 @@ class IPRangeSerializer(PrimaryModelSerializer):
#
class IPAddressSerializer(PrimaryModelSerializer):
prefix = PrefixSerializer(nested=True, required=False, allow_null=True)
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
address = IPAddressField()
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
@@ -166,11 +175,11 @@ class IPAddressSerializer(PrimaryModelSerializer):
class Meta:
model = IPAddress
fields = [
'id', 'url', 'display_url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role',
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'address', 'vrf', 'tenant', 'status', 'role',
'assigned_object_type', 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside',
'dns_name', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'family', 'address', 'description')
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'address', 'description')
class AvailableIPSerializer(serializers.Serializer):
+40
View File
@@ -340,6 +340,26 @@ class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet,
field_name='prefix',
lookup_expr='net_mask_length__lte'
)
aggregate_id = django_filters.ModelMultipleChoiceFilter(
queryset=Aggregate.objects.all(),
label=_('Aggregate'),
)
aggregate = django_filters.ModelMultipleChoiceFilter(
field_name='aggregate__prefix',
queryset=Aggregate.objects.all(),
to_field_name='prefix',
label=_('Aggregate (Prefix)'),
)
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Prefix.objects.all(),
label=_('Parent Prefix'),
)
parent = django_filters.ModelMultipleChoiceFilter(
field_name='parent__prefix',
queryset=Prefix.objects.all(),
to_field_name='prefix',
label=_('Parent Prefix (Prefix)'),
)
vrf_id = django_filters.ModelMultipleChoiceFilter(
queryset=VRF.objects.all(),
label=_('VRF'),
@@ -484,6 +504,16 @@ class IPRangeFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
method='search_contains',
label=_('Ranges which contain this prefix or IP'),
)
prefix_id = django_filters.ModelMultipleChoiceFilter(
queryset=Prefix.objects.all(),
label=_('Prefix (ID)'),
)
prefix = django_filters.ModelMultipleChoiceFilter(
field_name='prefix__prefix',
queryset=Prefix.objects.all(),
to_field_name='prefix',
label=_('Prefix'),
)
vrf_id = django_filters.ModelMultipleChoiceFilter(
queryset=VRF.objects.all(),
label=_('VRF'),
@@ -569,6 +599,16 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
method='search_by_parent',
label=_('Parent prefix'),
)
prefix_id = django_filters.ModelMultipleChoiceFilter(
queryset=Prefix.objects.all(),
label=_('Prefix (ID)'),
)
prefix = django_filters.ModelMultipleChoiceFilter(
field_name='prefix__prefix',
queryset=Prefix.objects.all(),
to_field_name='prefix',
label=_('Prefix (prefix)'),
)
address = MultiValueCharFilter(
method='filter_address',
label=_('Address'),
+23 -3
View File
@@ -168,6 +168,11 @@ class RoleBulkEditForm(OrganizationalModelBulkEditForm):
class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
parent = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
label=_('Parent Prefix')
)
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
@@ -221,7 +226,7 @@ class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
model = Prefix
fieldsets = (
FieldSet('tenant', 'status', 'role', 'description'),
FieldSet('vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')),
FieldSet('parent', 'vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')),
FieldSet('scope_type', 'scope', name=_('Scope')),
FieldSet('vlan_group', 'vlan', name=_('VLAN Assignment')),
)
@@ -231,6 +236,11 @@ class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
class IPRangeBulkEditForm(PrimaryModelBulkEditForm):
prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
label=_('Prefix')
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
@@ -272,6 +282,16 @@ class IPRangeBulkEditForm(PrimaryModelBulkEditForm):
class IPAddressBulkEditForm(PrimaryModelBulkEditForm):
prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
label=_('Prefix')
)
prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
label=_('Prefix')
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
@@ -307,10 +327,10 @@ class IPAddressBulkEditForm(PrimaryModelBulkEditForm):
model = IPAddress
fieldsets = (
FieldSet('status', 'role', 'tenant', 'description'),
FieldSet('vrf', 'mask_length', 'dns_name', name=_('Addressing')),
FieldSet('prefix', 'vrf', 'mask_length', 'dns_name', name=_('Addressing')),
)
nullable_fields = (
'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
'prefix', 'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
)
+62 -2
View File
@@ -155,6 +155,18 @@ class RoleImportForm(OrganizationalModelImportForm):
class PrefixImportForm(ScopedImportForm, PrimaryModelImportForm):
aggregate = CSVModelChoiceField(
label=_('Aggregate'),
queryset=Aggregate.objects.all(),
to_field_name='prefix',
required=False
)
parent = CSVModelChoiceField(
label=_('Prefix'),
queryset=Prefix.objects.all(),
to_field_name='prefix',
required=False
)
vrf = CSVModelChoiceField(
label=_('VRF'),
queryset=VRF.objects.all(),
@@ -242,8 +254,26 @@ class PrefixImportForm(ScopedImportForm, PrimaryModelImportForm):
queryset = self.fields['vlan'].queryset.filter(query)
self.fields['vlan'].queryset = queryset
# Limit Prefix queryset by assigned vrf
vrf = data.get('vrf')
query = Q()
if vrf:
query &= Q(**{
f"vrf__{self.fields['vrf'].to_field_name}": vrf
})
queryset = self.fields['parent'].queryset.filter(query)
self.fields['parent'].queryset = queryset
class IPRangeImportForm(PrimaryModelImportForm):
prefix = CSVModelChoiceField(
label=_('Prefix'),
queryset=Prefix.objects.all(),
to_field_name='prefix',
required=True,
help_text=_('Assigned prefix')
)
vrf = CSVModelChoiceField(
label=_('VRF'),
queryset=VRF.objects.all(),
@@ -278,8 +308,29 @@ class IPRangeImportForm(PrimaryModelImportForm):
'description', 'owner', 'comments', 'tags',
)
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
# Limit Prefix queryset by assigned vrf
vrf = data.get('vrf')
query = Q()
if vrf:
query &= Q(**{
f"vrf__{self.fields['vrf'].to_field_name}": vrf
})
queryset = self.fields['prefix'].queryset.filter(query)
self.fields['prefix'].queryset = queryset
class IPAddressImportForm(PrimaryModelImportForm):
prefix = CSVModelChoiceField(
label=_('Prefix'),
queryset=Prefix.objects.all(),
required=False,
to_field_name='prefix',
help_text=_('Assigned prefix')
)
vrf = CSVModelChoiceField(
label=_('VRF'),
queryset=VRF.objects.all(),
@@ -347,8 +398,8 @@ class IPAddressImportForm(PrimaryModelImportForm):
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'fhrp_group',
'is_primary', 'is_oob', 'dns_name', 'description', 'owner', 'comments', 'tags',
'prefix', 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface',
'fhrp_group', 'is_primary', 'is_oob', 'dns_name', 'owner', 'description', 'comments', 'tags',
]
def __init__(self, data=None, *args, **kwargs):
@@ -356,6 +407,15 @@ class IPAddressImportForm(PrimaryModelImportForm):
if data:
# Limit Prefix queryset by assigned vrf
vrf = data.get('vrf')
query = Q()
if vrf:
query &= Q(**{f"vrf__{self.fields['vrf'].to_field_name}": vrf})
queryset = self.fields['prefix'].queryset.filter(query)
self.fields['prefix'].queryset = queryset
# Limit interface queryset by assigned device
if data.get('device'):
self.fields['interface'].queryset = Interface.objects.filter(
+22 -3
View File
@@ -211,6 +211,12 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFi
choices=PREFIX_MASK_LENGTH_CHOICES,
label=_('Mask length')
)
aggregate_id = DynamicModelMultipleChoiceField(
queryset=Aggregate.objects.all(),
required=False,
label=_('Aggregate'),
null_option='Global'
)
vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
@@ -285,10 +291,18 @@ class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelF
model = IPRange
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')),
FieldSet(
'prefix', 'family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')
),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
)
prefix = DynamicModelMultipleChoiceField(
queryset=Prefix.objects.all(),
required=False,
label=_('Prefix'),
null_option='None'
)
family = forms.ChoiceField(
required=False,
choices=add_blank_choice(IPAddressFamilyChoices),
@@ -333,7 +347,7 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
fieldsets = (
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
FieldSet(
'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
'prefix', 'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
name=_('Attributes')
),
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
@@ -341,7 +355,7 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
)
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'prefix_id', 'parent', 'status', 'role')
parent = forms.CharField(
required=False,
widget=forms.TextInput(
@@ -361,6 +375,11 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
choices=IPADDRESS_MASK_LENGTH_CHOICES,
label=_('Mask length')
)
prefix_id = DynamicModelMultipleChoiceField(
queryset=Prefix.objects.all(),
required=False,
label=_('Prefix'),
)
vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
+31 -8
View File
@@ -241,6 +241,11 @@ class PrefixForm(TenancyForm, ScopedForm, PrimaryModelForm):
class IPRangeForm(TenancyForm, PrimaryModelForm):
prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
label=_('Prefix')
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
@@ -255,8 +260,8 @@ class IPRangeForm(TenancyForm, PrimaryModelForm):
fieldsets = (
FieldSet(
'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized', 'description',
'tags', name=_('IP Range')
'prefix', 'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized',
'description', 'tags', name=_('IP Range')
),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
@@ -264,12 +269,21 @@ class IPRangeForm(TenancyForm, PrimaryModelForm):
class Meta:
model = IPRange
fields = [
'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_populated',
'mark_utilized', 'description', 'owner', 'comments', 'tags',
'prefix', 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant',
'mark_populated', 'mark_utilized', 'description', 'owner', 'comments', 'tags',
]
class IPAddressForm(TenancyForm, PrimaryModelForm):
prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
context={
'vrf': 'vrf',
},
selector=True,
label=_('Prefix'),
)
interface = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
@@ -315,7 +329,7 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
)
fieldsets = (
FieldSet('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')),
FieldSet('prefix', 'address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
FieldSet(
TabbedGroups(
@@ -331,8 +345,8 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent', 'nat_inside',
'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
'prefix', 'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent',
'nat_inside', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
]
def __init__(self, *args, **kwargs):
@@ -457,6 +471,15 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
context={
'vrf': 'vrf',
},
selector=True,
label=_('Prefix'),
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
@@ -466,7 +489,7 @@ class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags',
'address', 'prefix', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags',
]
+6
View File
@@ -170,6 +170,7 @@ class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
@strawberry_django.filter_type(models.IPAddress, lookups=True)
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
prefix: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
address: FilterLookup[str] | None = strawberry_django.filter_field()
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
vrf_id: ID | None = strawberry_django.filter_field()
@@ -221,6 +222,7 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
@strawberry_django.filter_type(models.IPRange, lookups=True)
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
prefix: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
start_address: FilterLookup[str] | None = strawberry_django.filter_field()
end_address: FilterLookup[str] | None = strawberry_django.filter_field()
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
@@ -275,6 +277,10 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
@strawberry_django.filter_type(models.Prefix, lookups=True)
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
aggregate: Annotated['AggregateFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
parent: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
prefix: FilterLookup[str] | None = strawberry_django.filter_field()
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
vrf_id: ID | None = strawberry_django.filter_field()
+4
View File
@@ -143,6 +143,7 @@ class FHRPGroupAssignmentType(BaseObjectType):
)
class IPAddressType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
address: str
prefix: Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')] | None
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
nat_inside: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None
@@ -167,6 +168,7 @@ class IPAddressType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
pagination=True
)
class IPRangeType(ContactsMixin, PrimaryObjectType):
prefix: Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')] | None
start_address: str
end_address: str
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
@@ -181,6 +183,8 @@ class IPRangeType(ContactsMixin, PrimaryObjectType):
pagination=True
)
class PrefixType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
aggregate: Annotated["AggregateType", strawberry.lazy('ipam.graphql.types')] | None
parent: Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')] | None
prefix: str
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@@ -0,0 +1,58 @@
# Generated by Django 5.0.9 on 2025-02-20 16:49
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0086_gfk_indexes'),
]
operations = [
migrations.AddField(
model_name='prefix',
name='parent',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='children',
to='ipam.prefix',
),
),
migrations.AddField(
model_name='ipaddress',
name='prefix',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='ip_addresses',
to='ipam.prefix',
),
),
migrations.AddField(
model_name='iprange',
name='prefix',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='ip_ranges',
to='ipam.prefix',
),
),
migrations.AddField(
model_name='prefix',
name='aggregate',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='prefixes',
to='ipam.aggregate',
),
),
]
@@ -0,0 +1,132 @@
# Generated by Django 5.0.9 on 2025-02-20 16:49
import sys
import time
from django.db import migrations, models
from ipam.choices import PrefixStatusChoices
def draw_progress(count, total, length=20):
if total == 0:
return
progress = count / total
percent = int(progress * 100)
bar = int(progress * length)
sys.stdout.write('\r')
sys.stdout.write(f"[{'=' * bar:{length}s}] {percent}%")
sys.stdout.flush()
def set_prefix(apps, schema_editor, model, attr='address', parent_attr='prefix', parent_model='Prefix'):
start = time.time()
ChildModel = apps.get_model('ipam', model)
ParentModel = apps.get_model('ipam', parent_model)
addresses = ChildModel.objects.all()
total = addresses.count()
if total == 0:
return
print('\r\n')
print(f'Migrating {parent_model}')
print('\r\n')
i = 0
draw_progress(i, total, 50)
for address in addresses:
i += 1
address_attr = getattr(address, attr)
prefixes = ParentModel.objects.filter(
prefix__net_contains_or_equals=str(address_attr.ip),
prefix__net_mask_length__lte=address_attr.prefixlen,
)
setattr(address, parent_attr, prefixes.last())
try:
address.save()
except Exception as e:
print(f'Error at {address}')
raise e
draw_progress(i, total, 50)
end = time.time()
print(f"\r\nElapsed Time: {end - start:.2f}s")
def set_ipaddress_prefix(apps, schema_editor):
set_prefix(apps, schema_editor, 'IPAddress')
def unset_ipaddress_prefix(apps, schema_editor):
IPAddress = apps.get_model('ipam', 'IPAddress')
IPAddress.objects.update(prefix=None)
def set_iprange_prefix(apps, schema_editor):
set_prefix(apps, schema_editor, 'IPRange', 'start_address')
def unset_iprange_prefix(apps, schema_editor):
IPRange = apps.get_model('ipam', 'IPRange')
IPRange.objects.update(prefix=None)
def set_prefix_aggregate(apps, schema_editor):
set_prefix(apps, schema_editor, 'Prefix', 'prefix', 'aggregate', 'Aggregate')
def unset_prefix_aggregate(apps, schema_editor):
Prefix = apps.get_model('ipam', 'Prefix')
Prefix.objects.update(aggregate=None)
def set_prefix_parent(apps, schema_editor):
Prefix = apps.get_model('ipam', 'Prefix')
start = time.time()
addresses = Prefix.objects.all()
i = 0
total = addresses.count()
if total == 0:
return
print('\r\n')
draw_progress(i, total, 50)
for address in addresses:
i += 1
prefixes = Prefix.objects.exclude(pk=address.pk).filter(
models.Q(vrf=address.vrf, prefix__net_contains=str(address.prefix.ip))
| models.Q(
vrf=None,
status=PrefixStatusChoices.STATUS_CONTAINER,
prefix__net_contains=str(address.prefix.ip),
)
)
if not prefixes.exists():
draw_progress(i, total, 50)
continue
address.parent = prefixes.last()
address.save()
draw_progress(i, total, 50)
end = time.time()
print(f"\r\nElapsed Time: {end - start:.2f}s")
def unset_prefix_parent(apps, schema_editor):
Prefix = apps.get_model('ipam', 'Prefix')
Prefix.objects.update(parent=None)
class Migration(migrations.Migration):
dependencies = [
('ipam', '0087_ipaddress_iprange_prefix_parent'),
]
operations = [
migrations.RunPython(set_ipaddress_prefix, unset_ipaddress_prefix),
migrations.RunPython(set_iprange_prefix, unset_iprange_prefix),
migrations.RunPython(set_prefix_aggregate, unset_prefix_aggregate),
migrations.RunPython(set_prefix_parent, unset_prefix_parent),
]
@@ -0,0 +1,25 @@
# Generated by Django 5.2.5 on 2025-11-25 03:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0089_prefix_ipam_prefix_delete_prefix_ipam_prefix_insert'),
]
operations = [
migrations.AlterField(
model_name='prefix',
name='parent',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name='children',
to='ipam.prefix',
),
),
]
@@ -0,0 +1,43 @@
# Generated by Django 5.2.5 on 2025-11-06 03:24
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ipam', '0088_ipaddress_iprange_prefix_parent_data'),
]
operations = [
pgtrigger.migrations.AddTrigger(
model_name='prefix',
trigger=pgtrigger.compiler.Trigger(
name='ipam_prefix_delete',
sql=pgtrigger.compiler.UpsertTriggerSql(
func="\n-- Update Child Prefix's with Prefix's PARENT\nUPDATE ipam_prefix SET parent_id=OLD.parent_id WHERE parent_id=OLD.id;\nRETURN OLD;\n", # noqa: E501
hash='899e1943cb201118be7ef02f36f49747224774f2',
operation='DELETE',
pgid='pgtrigger_ipam_prefix_delete_e7810',
table='ipam_prefix',
when='BEFORE',
),
),
),
pgtrigger.migrations.AddTrigger(
model_name='prefix',
trigger=pgtrigger.compiler.Trigger(
name='ipam_prefix_insert',
sql=pgtrigger.compiler.UpsertTriggerSql(
func="\nUPDATE ipam_prefix\nSET parent_id=NEW.id \nWHERE \n prefix << NEW.prefix\n AND\n (\n (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n OR\n (\n NEW.vrf_id IS NULL\n AND\n NEW.status = 'container'\n AND\n NOT EXISTS(\n SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id\n )\n )\n )\n AND id != NEW.id\n AND NOT EXISTS (\n SELECT 1 FROM ipam_prefix p\n WHERE\n p.prefix >> ipam_prefix.prefix\n AND p.prefix << NEW.prefix\n AND (\n (p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))\n OR\n (p.vrf_id IS NULL AND p.status = 'container')\n )\n AND p.id != NEW.id\n )\n;\nRETURN NEW;\n", # noqa: E501
hash='0e05bbe61861227a9eb710b6c94bae9e0cc7119e',
operation='INSERT',
pgid='pgtrigger_ipam_prefix_insert_46c72',
table='ipam_prefix',
when='AFTER',
),
),
),
]
@@ -0,0 +1,65 @@
# Generated by Django 5.2.5 on 2025-11-25 06:00
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ipam', '0089_alter_prefix_parent'),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name='prefix',
name='ipam_prefix_delete',
),
pgtrigger.migrations.RemoveTrigger(
model_name='prefix',
name='ipam_prefix_insert',
),
pgtrigger.migrations.AddTrigger(
model_name='prefix',
trigger=pgtrigger.compiler.Trigger(
name='ipam_prefix_delete',
sql=pgtrigger.compiler.UpsertTriggerSql(
func="\n-- Update Child Prefix's with Prefix's PARENT This is a safe assumption based on the fact that the parent would be the\n-- next direct parent for anything else that could contain this prefix\nUPDATE ipam_prefix SET parent_id=OLD.parent_id WHERE parent_id=OLD.id;\nRETURN OLD;\n", # noqa: E501
hash='ee3f890009c05a3617428158e7b6f3d77317885d',
operation='DELETE',
pgid='pgtrigger_ipam_prefix_delete_e7810',
table='ipam_prefix',
when='BEFORE',
),
),
),
pgtrigger.migrations.AddTrigger(
model_name='prefix',
trigger=pgtrigger.compiler.Trigger(
name='ipam_prefix_insert',
sql=pgtrigger.compiler.UpsertTriggerSql(
func="\n-- Update the prefix with the new parent if the parent is the most appropriate prefix\nUPDATE ipam_prefix\nSET parent_id=NEW.id\nWHERE\n prefix << NEW.prefix\n AND\n (\n (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n OR\n (\n NEW.vrf_id IS NULL\n AND\n NEW.status = 'container'\n AND\n NOT EXISTS(\n SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id\n )\n )\n )\n AND id != NEW.id\n AND NOT EXISTS (\n SELECT 1 FROM ipam_prefix p\n WHERE\n p.prefix >> ipam_prefix.prefix\n AND p.prefix << NEW.prefix\n AND (\n (p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))\n OR\n (p.vrf_id IS NULL AND p.status = 'container')\n )\n AND p.id != NEW.id\n )\n;\nRETURN NEW;\n", # noqa: E501
hash='1d71498f09e767183d3b0d29c06c9ac9e2cc000a',
operation='INSERT',
pgid='pgtrigger_ipam_prefix_insert_46c72',
table='ipam_prefix',
when='AFTER',
),
),
),
pgtrigger.migrations.AddTrigger(
model_name='prefix',
trigger=pgtrigger.compiler.Trigger(
name='ipam_prefix_update',
sql=pgtrigger.compiler.UpsertTriggerSql(
func="\n-- When a prefix changes, reassign any IPAddresses that no longer\n-- fall within the new prefix range to the parent prefix (or set null if no parent exists)\nUPDATE ipam_prefix\nSET parent_id = OLD.parent_id\nWHERE\n parent_id = NEW.id\n -- IP address no longer contained within the updated prefix\n AND NOT (prefix << NEW.prefix);\n\n-- Update the prefix with the new parent if the parent is the most appropriate prefix\nUPDATE ipam_prefix\nSET parent_id=NEW.id\nWHERE\n prefix << NEW.prefix\n AND\n (\n (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n OR\n (\n NEW.vrf_id IS NULL\n AND\n NEW.status = 'container'\n AND\n NOT EXISTS(\n SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id\n )\n )\n )\n AND id != NEW.id\n AND NOT EXISTS (\n SELECT 1 FROM ipam_prefix p\n WHERE\n p.prefix >> ipam_prefix.prefix\n AND p.prefix << NEW.prefix\n AND (\n (p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))\n OR\n (p.vrf_id IS NULL AND p.status = 'container')\n )\n AND p.id != NEW.id\n )\n;\nRETURN NEW;\n", # noqa: E501
hash='747230a84703df5a4aa3d32e7f45b5a32525b799',
operation='UPDATE',
pgid='pgtrigger_ipam_prefix_update_e5fca',
table='ipam_prefix',
when='AFTER',
),
),
),
]
+162 -22
View File
@@ -1,4 +1,5 @@
import netaddr
import pgtrigger
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.indexes import GistIndex
@@ -8,6 +9,7 @@ from django.db.models import F
from django.db.models.functions import Cast
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from netaddr.ip import IPNetwork
from dcim.models.mixins import CachedScopeMixin
from ipam.choices import *
@@ -16,6 +18,8 @@ from ipam.fields import IPNetworkField, IPAddressField
from ipam.lookups import Host
from ipam.managers import IPAddressManager
from ipam.querysets import PrefixQuerySet
from ipam.triggers import ipam_prefix_delete_adjust_prefix_parent, ipam_prefix_insert_adjust_prefix_parent, \
ipam_prefix_update_adjust_prefix_parent
from ipam.validators import DNSValidator
from netbox.config import get_config
from netbox.models import OrganizationalModel, PrimaryModel
@@ -185,31 +189,28 @@ class Aggregate(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
return min(utilization, 100)
class Role(OrganizationalModel):
"""
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
"Management."
"""
weight = models.PositiveSmallIntegerField(
verbose_name=_('weight'),
default=1000
)
class Meta:
ordering = ('weight', 'name')
verbose_name = _('role')
verbose_name_plural = _('roles')
def __str__(self):
return self.name
class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, PrimaryModel):
"""
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be scoped to certain
areas and/or assigned to VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role.
A Prefix can also be assigned to a VLAN where appropriate.
"""
aggregate = models.ForeignKey(
to='ipam.Aggregate',
on_delete=models.SET_NULL, # This is handled by triggers
related_name='prefixes',
blank=True,
null=True,
verbose_name=_('aggregate')
)
parent = models.ForeignKey(
to='ipam.Prefix',
on_delete=models.DO_NOTHING,
related_name='children',
blank=True,
null=True,
verbose_name=_('Prefix')
)
prefix = IPNetworkField(
verbose_name=_('prefix'),
help_text=_('IPv4 or IPv6 network with mask')
@@ -284,7 +285,31 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
verbose_name_plural = _('prefixes')
indexes = (
models.Index(fields=('scope_type', 'scope_id')),
GistIndex(fields=['prefix'], name='ipam_prefix_gist_idx', opclasses=['inet_ops']),
GistIndex(
fields=['prefix'],
name='ipam_prefix_gist_idx',
opclasses=['inet_ops'],
),
)
triggers = (
pgtrigger.Trigger(
name='ipam_prefix_delete',
operation=pgtrigger.Delete,
when=pgtrigger.Before,
func=ipam_prefix_delete_adjust_prefix_parent,
),
pgtrigger.Trigger(
name='ipam_prefix_insert',
operation=pgtrigger.Insert,
when=pgtrigger.After,
func=ipam_prefix_insert_adjust_prefix_parent,
),
pgtrigger.Trigger(
name='ipam_prefix_update',
operation=pgtrigger.Update,
when=pgtrigger.After,
func=ipam_prefix_update_adjust_prefix_parent,
),
)
def __init__(self, *args, **kwargs):
@@ -301,6 +326,8 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
super().clean()
if self.prefix:
if not isinstance(self.prefix, IPNetwork):
self.prefix = IPNetwork(self.prefix)
# /0 masks are not acceptable
if self.prefix.prefixlen == 0:
@@ -308,6 +335,17 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
'prefix': _("Cannot create prefix with /0 mask.")
})
if self.parent:
if self.prefix not in self.parent.prefix:
raise ValidationError({
'parent': _("Prefix must be part of parent prefix.")
})
if self.parent.status != PrefixStatusChoices.STATUS_CONTAINER and self.vrf != self.parent.vrf:
raise ValidationError({
'vrf': _("VRF must match the parent VRF.")
})
# Enforce unique IP space (if applicable)
if (self.vrf is None and get_config().ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
duplicate_prefixes = self.get_duplicates()
@@ -321,6 +359,14 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
})
def save(self, *args, **kwargs):
vrf_id = self.vrf.pk if self.vrf else None
if not self.pk and not self.parent:
parent = self.find_parent_prefix(self)
self.parent = parent
elif self.parent and (self.prefix != self._prefix or vrf_id != self._vrf_id):
parent = self.find_parent_prefix(self)
self.parent = parent
if isinstance(self.prefix, netaddr.IPNetwork):
@@ -346,11 +392,11 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
return netaddr.IPAddress(self.prefix).format(netaddr.ipv6_full)
@property
def depth(self):
def depth_count(self):
return self._depth
@property
def children(self):
def children_count(self):
return self._children
def _set_prefix_length(self, value):
@@ -490,11 +536,52 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
return min(utilization, 100)
@classmethod
def find_parent_prefix(cls, network):
prefixes = Prefix.objects.filter(
models.Q(
vrf=network.vrf,
prefix__net_contains=str(network.prefix)
) | models.Q(
vrf=None,
status=PrefixStatusChoices.STATUS_CONTAINER,
prefix__net_contains=str(network.prefix),
)
)
return prefixes.last()
class Role(OrganizationalModel):
"""
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
"Management."
"""
weight = models.PositiveSmallIntegerField(
verbose_name=_('weight'),
default=1000
)
class Meta:
ordering = ('weight', 'name')
verbose_name = _('role')
verbose_name_plural = _('roles')
def __str__(self):
return self.name
class IPRange(ContactsMixin, PrimaryModel):
"""
A range of IP addresses, defined by start and end addresses.
"""
prefix = models.ForeignKey(
to='ipam.Prefix',
on_delete=models.SET_NULL,
related_name='ip_ranges',
null=True,
blank=True,
verbose_name=_('prefix'),
)
start_address = IPAddressField(
verbose_name=_('start address'),
help_text=_('IPv4 or IPv6 address (with mask)')
@@ -564,6 +651,27 @@ class IPRange(ContactsMixin, PrimaryModel):
super().clean()
if self.start_address and self.end_address:
# If prefix is set, validate suitability
if self.prefix:
# Check that start address and end address are within the prefix range
if self.start_address not in self.prefix.prefix and self.end_address not in self.prefix.prefix:
raise ValidationError({
'start_address': _("Start address must be part of the selected prefix"),
'end_address': _("End address must be part of the selected prefix.")
})
elif self.start_address not in self.prefix.prefix:
raise ValidationError({
'start_address': _("Start address must be part of the selected prefix")
})
elif self.end_address not in self.prefix.prefix:
raise ValidationError({
'end_address': _("End address must be part of the selected prefix.")
})
# Check that VRF matches prefix VRF
if self.vrf != self.prefix.vrf:
raise ValidationError({
'vrf': _("VRF must match the prefix VRF.")
})
# Check that start & end IP versions match
if self.start_address.version != self.end_address.version:
@@ -720,6 +828,14 @@ class IPRange(ContactsMixin, PrimaryModel):
return min(float(child_count) / self.size * 100, 100)
@classmethod
def find_prefix(self, address):
prefixes = Prefix.objects.filter(
models.Q(prefix__net_contains=address.start_address) & Q(prefix__net_contains=address.end_address),
vrf=address.vrf,
)
return prefixes.last()
class IPAddress(ContactsMixin, PrimaryModel):
"""
@@ -732,6 +848,14 @@ class IPAddress(ContactsMixin, PrimaryModel):
for example, when mapping public addresses to private addresses. When an Interface has been assigned an IPAddress
which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP.
"""
prefix = models.ForeignKey(
to='ipam.Prefix',
on_delete=models.SET_NULL,
related_name='ip_addresses',
blank=True,
null=True,
verbose_name=_('Prefix')
)
address = IPAddressField(
verbose_name=_('address'),
help_text=_('IPv4 or IPv6 address (with mask)')
@@ -819,6 +943,7 @@ class IPAddress(ContactsMixin, PrimaryModel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._address = self.address
# Denote the original assigned object (if any) for validation in clean()
self._original_assigned_object_id = self.__dict__.get('assigned_object_id')
self._original_assigned_object_type_id = self.__dict__.get('assigned_object_type_id')
@@ -865,6 +990,16 @@ class IPAddress(ContactsMixin, PrimaryModel):
super().clean()
if self.address:
# If prefix is set, validate suitability
if self.prefix:
if self.address not in self.prefix.prefix:
raise ValidationError({
'prefix': _("IP address must be part of the selected prefix.")
})
if self.vrf != self.prefix.vrf:
raise ValidationError({
'vrf': _("IP address VRF must match the prefix VRF.")
})
# /0 masks are not acceptable
if self.address.prefixlen == 0:
@@ -1005,3 +1140,8 @@ class IPAddress(ContactsMixin, PrimaryModel):
def get_role_color(self):
return IPAddressRoleChoices.colors.get(self.role)
@classmethod
def find_prefix(self, address):
prefixes = Prefix.objects.filter(prefix__net_contains=address.address, vrf=address.vrf)
return prefixes.last()
+7 -3
View File
@@ -53,11 +53,12 @@ class IPAddressIndex(SearchIndex):
model = models.IPAddress
fields = (
('address', 100),
('prefix', 200),
('dns_name', 300),
('description', 500),
('comments', 5000),
)
display_attrs = ('vrf', 'tenant', 'status', 'role', 'description')
display_attrs = ('prefix', 'vrf', 'tenant', 'status', 'role', 'description')
@register_search
@@ -66,10 +67,11 @@ class IPRangeIndex(SearchIndex):
fields = (
('start_address', 100),
('end_address', 300),
('prefix', 400),
('description', 500),
('comments', 5000),
)
display_attrs = ('vrf', 'tenant', 'status', 'role', 'description')
display_attrs = ('prefix', 'vrf', 'tenant', 'status', 'role', 'description')
@register_search
@@ -77,10 +79,12 @@ class PrefixIndex(SearchIndex):
model = models.Prefix
fields = (
('prefix', 110),
('parent', 200),
('aggregate', 300),
('description', 500),
('comments', 5000),
)
display_attrs = ('scope', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description')
display_attrs = ('scope', 'aggregate', 'parent', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description')
@register_search
+25 -9
View File
@@ -152,6 +152,10 @@ class PrefixUtilizationColumn(columns.UtilizationColumn):
class PrefixTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True
)
prefix = columns.TemplateColumn(
verbose_name=_('Prefix'),
template_code=PREFIX_LINK_WITH_DEPTH,
@@ -230,9 +234,9 @@ class PrefixTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
class Meta(PrimaryModelTable.Meta):
model = Prefix
fields = (
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group',
'scope', 'scope_type', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'contacts',
'comments', 'tags', 'created', 'last_updated',
'pk', 'id', 'prefix', 'status', 'parent', 'prefix', 'prefix_flat', 'children', 'vrf', 'utilization',
'tenant', 'tenant_group', 'scope', 'scope_type', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized',
'contacts', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'scope', 'vlan', 'role',
@@ -246,8 +250,11 @@ class PrefixTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
#
# IP ranges
#
class IPRangeTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
prefix = tables.Column(
verbose_name=_('Prefix'),
linkify=True
)
start_address = tables.Column(
verbose_name=_('Start address'),
linkify=True
@@ -284,9 +291,9 @@ class IPRangeTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
class Meta(PrimaryModelTable.Meta):
model = IPRange
fields = (
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group',
'mark_populated', 'mark_utilized', 'utilization', 'description', 'contacts', 'comments', 'tags',
'created', 'last_updated',
'pk', 'id', 'start_address', 'end_address', 'prefix', 'size', 'vrf', 'status', 'role', 'tenant',
'tenant_group', 'mark_populated', 'mark_utilized', 'utilization', 'description', 'contacts',
'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
@@ -301,10 +308,18 @@ class IPRangeTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
#
class IPAddressTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
prefix = tables.Column(
verbose_name=_('Prefix'),
linkify=True
)
address = tables.TemplateColumn(
template_code=IPADDRESS_LINK,
verbose_name=_('IP Address')
)
prefix = tables.Column(
linkify=True,
verbose_name=_('Prefix')
)
vrf = tables.TemplateColumn(
template_code=VRF_LINK,
verbose_name=_('VRF')
@@ -353,8 +368,9 @@ class IPAddressTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable
class Meta(PrimaryModelTable.Meta):
model = IPAddress
fields = (
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside',
'assigned', 'dns_name', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
'pk', 'id', 'address', 'vrf', 'prefix', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside',
'nat_outside', 'assigned', 'dns_name', 'description', 'comments', 'contacts', 'tags', 'created',
'last_updated',
)
default_columns = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
+10 -2
View File
@@ -16,12 +16,20 @@ PREFIX_COPY_BUTTON = """
PREFIX_LINK_WITH_DEPTH = """
{% load helpers %}
{% if record.depth %}
{% if record.depth_count %}
{% if object %}
<div class="record-depth">
{% for i in record.depth|as_range %}
{% for i in record.depth_count|parent_depth:object|as_range %}
<span>•</span>
{% endfor %}
</div>
{% else %}
<div class="record-depth">
{% for i in record.depth_count|as_range %}
<span>•</span>
{% endfor %}
</div>
{% endif %}
{% endif %}
""" + PREFIX_LINK
+6 -3
View File
@@ -407,7 +407,8 @@ class RoleTest(APIViewTestCases.APIViewTestCase):
class PrefixTest(APIViewTestCases.APIViewTestCase):
model = Prefix
brief_fields = ['_depth', 'description', 'display', 'family', 'id', 'prefix', 'url']
# TODO: Alter for parent prefix
brief_fields = ['_depth', 'aggregate', 'description', 'display', 'family', 'id', 'parent', 'prefix', 'url']
create_data = [
{
'prefix': '192.168.4.0/24',
@@ -622,7 +623,8 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
class IPRangeTest(APIViewTestCases.APIViewTestCase):
model = IPRange
brief_fields = ['description', 'display', 'end_address', 'family', 'id', 'start_address', 'url']
# TODO: Alter for parent prefix
brief_fields = ['description', 'display', 'end_address', 'family', 'id', 'prefix', 'start_address', 'url']
create_data = [
{
'start_address': '192.168.4.10/24',
@@ -780,7 +782,8 @@ class IPRangeTest(APIViewTestCases.APIViewTestCase):
class IPAddressTest(APIViewTestCases.APIViewTestCase):
model = IPAddress
brief_fields = ['address', 'description', 'display', 'family', 'id', 'url']
# TODO: Alter for parent prefix
brief_fields = ['address', 'description', 'display', 'family', 'id', 'prefix', 'url']
create_data = [
{
'address': '192.168.0.4/24',
+6
View File
@@ -901,6 +901,10 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
# TODO: Test for parent prefix
# TODO: Test for children?
# TODO: Test for aggregate
class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IPRange.objects.all()
@@ -1079,6 +1083,7 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self):
# TODO: Alter for prefix
params = {'parent': ['10.0.1.0/24', '10.0.2.0/24']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'parent': ['10.0.1.0/25']} # Range 10.0.1.100-199 is not fully contained by 10.0.1.0/25
@@ -1318,6 +1323,7 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self):
# TODO: Alter for prefix
params = {'parent': ['10.0.0.0/30', '2001:db8::/126']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
+316 -15
View File
@@ -39,6 +39,26 @@ class TestAggregate(TestCase):
class TestIPRange(TestCase):
@classmethod
def setUpTestData(cls):
cls.vrf = VRF.objects.create(name='VRF A', rd='1:1')
cls.prefixes = (
# IPv4
Prefix(prefix='192.0.0.0/16'),
Prefix(prefix='192.0.2.0/24'),
Prefix(prefix='192.0.0.0/16', vrf=cls.vrf),
# IPv6
Prefix(prefix='2001:db8::/32'),
Prefix(prefix='2001:db8::/64'),
)
for prefix in cls.prefixes:
prefix.clean()
prefix.save()
def test_overlapping_range(self):
iprange_192_168 = IPRange.objects.create(
@@ -87,6 +107,69 @@ class TestIPRange(TestCase):
)
iprange_4_198_201.clean()
def test_parent_prefix(self):
ranges = (
IPRange(
start_address=IPNetwork('192.0.0.1/24'),
end_address=IPNetwork('192.0.0.254/24'),
prefix=self.prefixes[0]
),
IPRange(
start_address=IPNetwork('192.0.2.1/24'),
end_address=IPNetwork('192.0.2.254/24'),
prefix=self.prefixes[1]
),
IPRange(
start_address=IPNetwork('192.0.2.1/24'),
end_address=IPNetwork('192.0.2.254/24'),
vrf=self.vrf,
prefix=self.prefixes[2]
),
IPRange(
start_address=IPNetwork('2001:db8::/64'),
end_address=IPNetwork('2001:db8::ffff/64'),
prefix=self.prefixes[4]
),
IPRange(
start_address=IPNetwork('2001:db8:2::/64'),
end_address=IPNetwork('2001:db8:2::ffff/64'),
prefix=self.prefixes[3]
),
)
for range in ranges:
range.clean()
range.save()
self.assertEqual(ranges[0].prefix, self.prefixes[0])
self.assertEqual(ranges[1].prefix, self.prefixes[1])
self.assertEqual(ranges[2].prefix, self.prefixes[2])
self.assertEqual(ranges[3].prefix, self.prefixes[4])
def test_parent_prefix_change(self):
range = IPRange(
start_address=IPNetwork('192.0.1.1/24'),
end_address=IPNetwork('192.0.1.254/24'),
prefix=self.prefixes[0]
)
range.clean()
range.save()
prefix = Prefix(prefix='192.0.0.0/17')
prefix.clean()
prefix.save()
range.refresh_from_db()
self.assertEqual(range.prefix, prefix)
# TODO: Prefix Altered
# TODO: Prefix Deleted
# TODO: Prefix falls outside range
# TODO: Prefix VRF does not match range VRF
class TestPrefix(TestCase):
@@ -169,19 +252,21 @@ class TestPrefix(TestCase):
prefix=IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER
)
ips = IPAddress.objects.bulk_create((
IPAddress(address=IPNetwork('10.0.0.1/24'), vrf=None),
IPAddress(address=IPNetwork('10.0.1.1/24'), vrf=vrfs[0]),
IPAddress(address=IPNetwork('10.0.2.1/24'), vrf=vrfs[1]),
IPAddress(address=IPNetwork('10.0.3.1/24'), vrf=vrfs[2]),
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.0.1/24'), vrf=None),
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.1.1/24'), vrf=vrfs[0]),
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.2.1/24'), vrf=vrfs[1]),
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.3.1/24'), vrf=vrfs[2]),
))
child_ip_pks = {p.pk for p in parent_prefix.get_child_ips()}
child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
# Global container should return all children
self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk, ips[2].pk, ips[3].pk})
parent_prefix.vrf = vrfs[0]
parent_prefix.save()
child_ip_pks = {p.pk for p in parent_prefix.get_child_ips()}
parent_prefix.refresh_from_db()
child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
# VRF container is limited to its own VRF
self.assertSetEqual(child_ip_pks, {ips[1].pk})
@@ -344,17 +429,21 @@ class TestPrefixHierarchy(TestCase):
prefixes = (
# IPv4
Prefix(prefix='10.0.0.0/8', _depth=0, _children=2),
Prefix(prefix='10.0.0.0/16', _depth=1, _children=1),
Prefix(prefix='10.0.0.0/24', _depth=2, _children=0),
Prefix(prefix='10.0.0.0/8'),
Prefix(prefix='10.0.0.0/16'),
Prefix(prefix='10.0.0.0/24'),
Prefix(prefix='192.168.0.0/16'),
# IPv6
Prefix(prefix='2001:db8::/32', _depth=0, _children=2),
Prefix(prefix='2001:db8::/40', _depth=1, _children=1),
Prefix(prefix='2001:db8::/48', _depth=2, _children=0),
Prefix(prefix='2001:db8::/32'),
Prefix(prefix='2001:db8::/40'),
Prefix(prefix='2001:db8::/48'),
)
Prefix.objects.bulk_create(prefixes)
for prefix in prefixes:
prefix.clean()
prefix.save()
def test_create_prefix4(self):
# Create 10.0.0.0/12
@@ -362,15 +451,19 @@ class TestPrefixHierarchy(TestCase):
prefixes = Prefix.objects.filter(prefix__family=4)
self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8'))
self.assertEqual(prefixes[0].parent, None)
self.assertEqual(prefixes[0]._depth, 0)
self.assertEqual(prefixes[0]._children, 3)
self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/12'))
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8'))
self.assertEqual(prefixes[1]._depth, 1)
self.assertEqual(prefixes[1]._children, 2)
self.assertEqual(prefixes[2].prefix, IPNetwork('10.0.0.0/16'))
self.assertEqual(prefixes[2].parent.prefix, IPNetwork('10.0.0.0/12'))
self.assertEqual(prefixes[2]._depth, 2)
self.assertEqual(prefixes[2]._children, 1)
self.assertEqual(prefixes[3].prefix, IPNetwork('10.0.0.0/24'))
self.assertEqual(prefixes[3].parent.prefix, IPNetwork('10.0.0.0/16'))
self.assertEqual(prefixes[3]._depth, 3)
self.assertEqual(prefixes[3]._children, 0)
@@ -380,15 +473,19 @@ class TestPrefixHierarchy(TestCase):
prefixes = Prefix.objects.filter(prefix__family=6)
self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32'))
self.assertEqual(prefixes[0].parent, None)
self.assertEqual(prefixes[0]._depth, 0)
self.assertEqual(prefixes[0]._children, 3)
self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/36'))
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32'))
self.assertEqual(prefixes[1]._depth, 1)
self.assertEqual(prefixes[1]._children, 2)
self.assertEqual(prefixes[2].prefix, IPNetwork('2001:db8::/40'))
self.assertEqual(prefixes[2].parent.prefix, IPNetwork('2001:db8::/36'))
self.assertEqual(prefixes[2]._depth, 2)
self.assertEqual(prefixes[2]._children, 1)
self.assertEqual(prefixes[3].prefix, IPNetwork('2001:db8::/48'))
self.assertEqual(prefixes[3].parent.prefix, IPNetwork('2001:db8::/40'))
self.assertEqual(prefixes[3]._depth, 3)
self.assertEqual(prefixes[3]._children, 0)
@@ -400,12 +497,15 @@ class TestPrefixHierarchy(TestCase):
prefixes = Prefix.objects.filter(prefix__family=4)
self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8'))
self.assertEqual(prefixes[0].parent, None)
self.assertEqual(prefixes[0]._depth, 0)
self.assertEqual(prefixes[0]._children, 2)
self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/12'))
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8'))
self.assertEqual(prefixes[1]._depth, 1)
self.assertEqual(prefixes[1]._children, 1)
self.assertEqual(prefixes[2].prefix, IPNetwork('10.0.0.0/16'))
self.assertEqual(prefixes[2].parent.prefix, IPNetwork('10.0.0.0/12'))
self.assertEqual(prefixes[2]._depth, 2)
self.assertEqual(prefixes[2]._children, 0)
@@ -417,12 +517,15 @@ class TestPrefixHierarchy(TestCase):
prefixes = Prefix.objects.filter(prefix__family=6)
self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32'))
self.assertEqual(prefixes[0].parent, None)
self.assertEqual(prefixes[0]._depth, 0)
self.assertEqual(prefixes[0]._children, 2)
self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/36'))
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32'))
self.assertEqual(prefixes[1]._depth, 1)
self.assertEqual(prefixes[1]._children, 1)
self.assertEqual(prefixes[2].prefix, IPNetwork('2001:db8::/40'))
self.assertEqual(prefixes[2].parent.prefix, IPNetwork('2001:db8::/36'))
self.assertEqual(prefixes[2]._depth, 2)
self.assertEqual(prefixes[2]._children, 0)
@@ -437,14 +540,17 @@ class TestPrefixHierarchy(TestCase):
prefixes = Prefix.objects.filter(vrf__isnull=True, prefix__family=4)
self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8'))
self.assertEqual(prefixes[0].parent, None)
self.assertEqual(prefixes[0]._depth, 0)
self.assertEqual(prefixes[0]._children, 1)
self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/24'))
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8'))
self.assertEqual(prefixes[1]._depth, 1)
self.assertEqual(prefixes[1]._children, 0)
prefixes = Prefix.objects.filter(vrf=vrf)
self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/16'))
self.assertEqual(prefixes[0].parent, None)
self.assertEqual(prefixes[0]._depth, 0)
self.assertEqual(prefixes[0]._children, 0)
@@ -459,14 +565,17 @@ class TestPrefixHierarchy(TestCase):
prefixes = Prefix.objects.filter(vrf__isnull=True, prefix__family=6)
self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32'))
self.assertEqual(prefixes[0].parent, None)
self.assertEqual(prefixes[0]._depth, 0)
self.assertEqual(prefixes[0]._children, 1)
self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/48'))
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32'))
self.assertEqual(prefixes[1]._depth, 1)
self.assertEqual(prefixes[1]._children, 0)
prefixes = Prefix.objects.filter(vrf=vrf)
self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/40'))
self.assertEqual(prefixes[0].parent, None)
self.assertEqual(prefixes[0]._depth, 0)
self.assertEqual(prefixes[0]._children, 0)
@@ -476,9 +585,11 @@ class TestPrefixHierarchy(TestCase):
prefixes = Prefix.objects.filter(prefix__family=4)
self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8'))
self.assertEqual(prefixes[0].parent, None)
self.assertEqual(prefixes[0]._depth, 0)
self.assertEqual(prefixes[0]._children, 1)
self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/24'))
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8'))
self.assertEqual(prefixes[1]._depth, 1)
self.assertEqual(prefixes[1]._children, 0)
@@ -488,9 +599,11 @@ class TestPrefixHierarchy(TestCase):
prefixes = Prefix.objects.filter(prefix__family=6)
self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32'))
self.assertEqual(prefixes[0].parent, None)
self.assertEqual(prefixes[0]._depth, 0)
self.assertEqual(prefixes[0]._children, 1)
self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/48'))
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32'))
self.assertEqual(prefixes[1]._depth, 1)
self.assertEqual(prefixes[1]._children, 0)
@@ -500,15 +613,20 @@ class TestPrefixHierarchy(TestCase):
prefixes = Prefix.objects.filter(prefix__family=4)
self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8'))
self.assertEqual(prefixes[0].parent, None)
self.assertEqual(prefixes[0]._depth, 0)
self.assertEqual(prefixes[0]._children, 3)
self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/16'))
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8'))
self.assertEqual(prefixes[1]._depth, 1)
self.assertEqual(prefixes[1]._children, 1)
self.assertEqual(prefixes[2].prefix, IPNetwork('10.0.0.0/16'))
self.assertEqual(prefixes[2].parent.prefix, IPNetwork('10.0.0.0/8'))
self.assertEqual(prefixes[2]._depth, 1)
self.assertEqual(prefixes[2]._children, 1)
self.assertEqual(prefixes[3].prefix, IPNetwork('10.0.0.0/24'))
# TODO: How to we resolve the parent for duplicate prefixes
self.assertEqual(prefixes[3].parent.prefix, IPNetwork('10.0.0.0/16'))
self.assertEqual(prefixes[3]._depth, 2)
self.assertEqual(prefixes[3]._children, 0)
@@ -518,20 +636,158 @@ class TestPrefixHierarchy(TestCase):
prefixes = Prefix.objects.filter(prefix__family=6)
self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32'))
self.assertEqual(prefixes[0].parent, None)
self.assertEqual(prefixes[0]._depth, 0)
self.assertEqual(prefixes[0]._children, 3)
self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/40'))
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32'))
self.assertEqual(prefixes[1]._depth, 1)
self.assertEqual(prefixes[1]._children, 1)
self.assertEqual(prefixes[2].prefix, IPNetwork('2001:db8::/40'))
self.assertEqual(prefixes[2].parent.prefix, IPNetwork('2001:db8::/32'))
self.assertEqual(prefixes[2]._depth, 1)
self.assertEqual(prefixes[2]._children, 1)
self.assertEqual(prefixes[3].prefix, IPNetwork('2001:db8::/48'))
self.assertEqual(prefixes[3].parent.prefix, IPNetwork('2001:db8::/40'))
self.assertEqual(prefixes[3]._depth, 2)
self.assertEqual(prefixes[3]._children, 0)
class TestTriggers(TestCase):
"""
Test the automatic updating of depth and child count in response to changes made within
the prefix hierarchy.
"""
@classmethod
def setUpTestData(cls):
vrfs = (
VRF(name='VRF A'),
VRF(name='VRF B'),
)
for vrf in vrfs:
vrf.clean()
vrf.save()
cls.prefixes = (
# IPv4
Prefix(prefix='10.0.0.0/8'),
Prefix(prefix='10.0.0.0/16'),
Prefix(prefix='10.0.0.0/22'),
Prefix(prefix='10.0.0.0/23'),
Prefix(prefix='10.0.2.0/23'),
Prefix(prefix='10.0.0.0/24'),
Prefix(prefix='10.0.1.0/24'),
Prefix(prefix='10.0.2.0/24'),
Prefix(prefix='10.0.3.0/24'),
Prefix(prefix='10.1.0.0/16', status='container'),
Prefix(prefix='10.1.0.0/22', vrf=vrfs[0]),
Prefix(prefix='10.1.0.0/23', vrf=vrfs[0]),
Prefix(prefix='10.1.2.0/23', vrf=vrfs[0]),
Prefix(prefix='10.1.0.0/24', vrf=vrfs[0]),
Prefix(prefix='10.1.1.0/24', vrf=vrfs[0]),
Prefix(prefix='10.1.2.0/24', vrf=vrfs[0]),
Prefix(prefix='10.1.3.0/24', vrf=vrfs[0]),
)
for prefix in cls.prefixes:
prefix.clean()
prefix.save()
def test_current_hierarchy(self):
self.assertIsNone(Prefix.objects.get(prefix='10.0.0.0/8').parent)
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/16').parent, Prefix.objects.get(prefix='10.0.0.0/8'))
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/22').parent, Prefix.objects.get(prefix='10.0.0.0/16'))
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/23').parent, Prefix.objects.get(prefix='10.0.0.0/22'))
self.assertEqual(Prefix.objects.get(prefix='10.0.2.0/23').parent, Prefix.objects.get(prefix='10.0.0.0/22'))
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/24').parent, Prefix.objects.get(prefix='10.0.0.0/23'))
self.assertEqual(Prefix.objects.get(prefix='10.0.1.0/24').parent, Prefix.objects.get(prefix='10.0.0.0/23'))
self.assertEqual(Prefix.objects.get(prefix='10.0.2.0/24').parent, Prefix.objects.get(prefix='10.0.2.0/23'))
self.assertEqual(Prefix.objects.get(prefix='10.0.3.0/24').parent, Prefix.objects.get(prefix='10.0.2.0/23'))
def test_basic_insert(self):
pfx = Prefix.objects.create(prefix='10.0.0.0/21')
self.assertIsNotNone(Prefix.objects.get(prefix='10.0.0.0/22').parent)
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/22').parent, pfx)
def test_vrf_insert(self):
vrf = VRF.objects.get(name='VRF A')
pfx = Prefix.objects.create(prefix='10.1.0.0/21', vrf=vrf)
parent = Prefix.objects.get(prefix='10.1.0.0/16')
self.assertIsNotNone(Prefix.objects.get(prefix='10.1.0.0/21').parent)
self.assertEqual(Prefix.objects.get(prefix='10.1.0.0/21').parent, parent)
self.assertIsNotNone(Prefix.objects.get(prefix='10.1.0.0/22').parent)
self.assertEqual(Prefix.objects.get(prefix='10.1.0.0/22').parent, pfx)
def test_basic_delete(self):
Prefix.objects.get(prefix='10.0.0.0/23').delete()
parent = Prefix.objects.get(prefix='10.0.0.0/22')
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/24').parent, parent)
self.assertEqual(Prefix.objects.get(prefix='10.0.1.0/24').parent, parent)
self.assertEqual(Prefix.objects.get(prefix='10.0.2.0/24').parent, Prefix.objects.get(prefix='10.0.2.0/23'))
def test_vrf_delete(self):
Prefix.objects.get(prefix='10.1.0.0/23').delete()
parent = Prefix.objects.get(prefix='10.1.0.0/22')
self.assertEqual(Prefix.objects.get(prefix='10.1.0.0/24').parent, parent)
self.assertEqual(Prefix.objects.get(prefix='10.1.1.0/24').parent, parent)
self.assertEqual(Prefix.objects.get(prefix='10.1.2.0/24').parent, Prefix.objects.get(prefix='10.1.2.0/23'))
def test_basic_update(self):
pfx = Prefix.objects.get(prefix='10.0.0.0/23')
parent = Prefix.objects.get(prefix='10.0.0.0/22')
pfx.prefix = '10.3.0.0/23'
pfx.parent = Prefix.objects.get(prefix='10.0.0.0/8')
pfx.clean()
pfx.save()
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/24').parent, parent)
self.assertEqual(Prefix.objects.get(prefix='10.0.1.0/24').parent, parent)
self.assertEqual(Prefix.objects.get(prefix='10.0.2.0/24').parent, Prefix.objects.get(prefix='10.0.2.0/23'))
def test_vrf_update(self):
pfx = Prefix.objects.get(prefix='10.1.0.0/23')
parent = Prefix.objects.get(prefix='10.1.0.0/22')
pfx.prefix = '10.3.0.0/23'
pfx.parent = None
pfx.clean()
pfx.save()
self.assertEqual(Prefix.objects.get(prefix='10.1.0.0/24').parent, parent)
self.assertEqual(Prefix.objects.get(prefix='10.1.1.0/24').parent, parent)
self.assertEqual(Prefix.objects.get(prefix='10.1.2.0/24').parent, Prefix.objects.get(prefix='10.1.2.0/23'))
# TODO: Test VRF Changes
class TestIPAddress(TestCase):
"""
Test the automatic updating of depth and child count in response to changes made within
the prefix hierarchy.
"""
@classmethod
def setUpTestData(cls):
cls.vrf = VRF.objects.create(name='VRF A', rd='1:1')
cls.prefixes = (
# IPv4
Prefix(prefix='192.0.0.0/16'),
Prefix(prefix='192.0.2.0/24'),
Prefix(prefix='192.0.0.0/16', vrf=cls.vrf),
# IPv6
Prefix(prefix='2001:db8::/32'),
Prefix(prefix='2001:db8::/64'),
)
for prefix in cls.prefixes:
prefix.clean()
prefix.save()
def test_get_duplicates(self):
ips = IPAddress.objects.bulk_create((
@@ -543,6 +799,44 @@ class TestIPAddress(TestCase):
self.assertSetEqual(set(duplicate_ip_pks), {ips[1].pk, ips[2].pk})
def test_parent_prefix(self):
ips = (
IPAddress(address=IPNetwork('192.0.0.1/24'), prefix=self.prefixes[0]),
IPAddress(address=IPNetwork('192.0.2.1/24'), prefix=self.prefixes[1]),
IPAddress(address=IPNetwork('192.0.2.1/24'), vrf=self.vrf, prefix=self.prefixes[2]),
IPAddress(address=IPNetwork('2001:db8::/64'), prefix=self.prefixes[4]),
IPAddress(address=IPNetwork('2001:db8:2::/64'), prefix=self.prefixes[3]),
)
for ip in ips:
ip.clean()
ip.save()
self.assertEqual(ips[0].prefix, self.prefixes[0])
self.assertEqual(ips[1].prefix, self.prefixes[1])
self.assertEqual(ips[2].prefix, self.prefixes[2])
self.assertEqual(ips[3].prefix, self.prefixes[4])
self.assertEqual(ips[4].prefix, self.prefixes[3])
def test_parent_prefix_change(self):
ip = IPAddress(address=IPNetwork('192.0.1.1/24'), prefix=self.prefixes[0])
ip.clean()
ip.save()
prefix = Prefix(prefix='192.0.1.0/17')
prefix.clean()
prefix.save()
ip.refresh_from_db()
self.assertEqual(ip.prefix, prefix)
# TODO: Prefix Altered
# TODO: Prefix Deleted
# TODO: Prefix does not contain IP Address
# TODO: Prefix VRF does not match IP Address VRF
#
# Uniqueness enforcement tests
#
@@ -559,13 +853,20 @@ class TestIPAddress(TestCase):
self.assertRaises(ValidationError, duplicate_ip.clean)
def test_duplicate_vrf(self):
vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=False)
vrf = VRF.objects.get(rd='1:1')
vrf.enforce_unique = False
vrf.clean()
vrf.save()
IPAddress.objects.create(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
duplicate_ip = IPAddress(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
self.assertIsNone(duplicate_ip.clean())
def test_duplicate_vrf_unique(self):
vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=True)
vrf = VRF.objects.get(rd='1:1')
vrf.enforce_unique = True
vrf.clean()
vrf.save()
IPAddress.objects.create(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
duplicate_ip = IPAddress(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
self.assertRaises(ValidationError, duplicate_ip.clean)
+19 -7
View File
@@ -421,6 +421,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
tags = create_tags('Alpha', 'Bravo', 'Charlie')
# TODO: Alter for prefix
cls.form_data = {
'prefix': IPNetwork('192.0.2.0/24'),
'scope_type': ContentType.objects.get_for_model(Site).pk,
@@ -436,6 +437,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
site = sites[0].pk
# TODO: Alter for prefix
cls.csv_data = (
"vrf,prefix,status,scope_type,scope_id",
f"VRF 1,10.4.0.0/16,active,dcim.site,{site}",
@@ -443,6 +445,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
f"VRF 1,10.6.0.0/16,active,dcim.site,{site}",
)
# TODO: Alter for prefix
cls.csv_update_data = (
"id,description,status",
f"{prefixes[0].pk},New description 7,{PrefixStatusChoices.STATUS_RESERVED}",
@@ -450,6 +453,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
f"{prefixes[2].pk},New description 9,{PrefixStatusChoices.STATUS_RESERVED}",
)
# TODO: Alter for prefix
cls.bulk_edit_data = {
'vrf': vrfs[1].pk,
'tenant': None,
@@ -477,9 +481,9 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
def test_prefix_ipranges(self):
prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16'))
ip_ranges = (
IPRange(start_address='192.168.0.1/24', end_address='192.168.0.100/24', size=99),
IPRange(start_address='192.168.1.1/24', end_address='192.168.1.100/24', size=99),
IPRange(start_address='192.168.2.1/24', end_address='192.168.2.100/24', size=99),
IPRange(prefix=prefix, start_address='192.168.0.1/24', end_address='192.168.0.100/24', size=99),
IPRange(prefix=prefix, start_address='192.168.1.1/24', end_address='192.168.1.100/24', size=99),
IPRange(prefix=prefix, start_address='192.168.2.1/24', end_address='192.168.2.100/24', size=99),
)
IPRange.objects.bulk_create(ip_ranges)
self.assertEqual(prefix.get_child_ranges().count(), 3)
@@ -491,12 +495,12 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
def test_prefix_ipaddresses(self):
prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16'))
ip_addresses = (
IPAddress(address=IPNetwork('192.168.0.1/16')),
IPAddress(address=IPNetwork('192.168.0.2/16')),
IPAddress(address=IPNetwork('192.168.0.3/16')),
IPAddress(prefix=prefix, address=IPNetwork('192.168.0.1/16')),
IPAddress(prefix=prefix, address=IPNetwork('192.168.0.2/16')),
IPAddress(prefix=prefix, address=IPNetwork('192.168.0.3/16')),
)
IPAddress.objects.bulk_create(ip_addresses)
self.assertEqual(prefix.get_child_ips().count(), 3)
self.assertEqual(prefix.ip_addresses.all().count(), 3)
url = reverse('ipam:prefix_ipaddresses', kwargs={'pk': prefix.pk})
self.assertHttpStatus(self.client.get(url), 200)
@@ -594,6 +598,7 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
tags = create_tags('Alpha', 'Bravo', 'Charlie')
# TODO: Alter for prefix
cls.form_data = {
'start_address': IPNetwork('192.0.5.10/24'),
'end_address': IPNetwork('192.0.5.100/24'),
@@ -607,6 +612,7 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'tags': [t.pk for t in tags],
}
# TODO: Alter for prefix
cls.csv_data = (
"vrf,start_address,end_address,status",
"VRF 1,10.1.0.1/16,10.1.9.254/16,active",
@@ -614,6 +620,7 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
"VRF 1,10.3.0.1/16,10.3.9.254/16,active",
)
# TODO: Alter for prefix
cls.csv_update_data = (
"id,description,status",
f"{ip_ranges[0].pk},New description 7,{IPRangeStatusChoices.STATUS_RESERVED}",
@@ -621,6 +628,7 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
f"{ip_ranges[2].pk},New description 9,{IPRangeStatusChoices.STATUS_RESERVED}",
)
# TODO: Alter for prefix
cls.bulk_edit_data = {
'vrf': vrfs[1].pk,
'tenant': None,
@@ -687,6 +695,7 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
),
)
FHRPGroup.objects.bulk_create(fhrp_groups)
# TODO: Alter for prefix
cls.form_data = {
'vrf': vrfs[1].pk,
'address': IPNetwork('192.0.2.99/24'),
@@ -699,6 +708,7 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'tags': [t.pk for t in tags],
}
# TODO: Alter for prefix
cls.csv_data = (
"vrf,address,status,fhrp_group",
"VRF 1,192.0.2.4/24,active,FHRP Group 1",
@@ -706,6 +716,7 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
"VRF 1,192.0.2.6/24,active,FHRP Group 3",
)
# TODO: Alter for prefix
cls.csv_update_data = (
"id,description,status",
f"{ipaddresses[0].pk},New description 7,{IPAddressStatusChoices.STATUS_RESERVED}",
@@ -713,6 +724,7 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
f"{ipaddresses[2].pk},New description 9,{IPAddressStatusChoices.STATUS_RESERVED}",
)
# TODO: Alter for prefix
cls.bulk_edit_data = {
'vrf': vrfs[1].pk,
'tenant': None,
+91
View File
@@ -0,0 +1,91 @@
ipam_prefix_delete_adjust_prefix_parent = """
-- Update Child Prefix's with Prefix's PARENT This is a safe assumption based on the fact that the parent would be the
-- next direct parent for anything else that could contain this prefix
UPDATE ipam_prefix SET parent_id=OLD.parent_id WHERE parent_id=OLD.id;
RETURN OLD;
"""
ipam_prefix_insert_adjust_prefix_parent = """
-- Update the prefix with the new parent if the parent is the most appropriate prefix
UPDATE ipam_prefix
SET parent_id=NEW.id
WHERE
prefix << NEW.prefix
AND
(
(vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
OR
(
NEW.vrf_id IS NULL
AND
NEW.status = 'container'
AND
NOT EXISTS(
SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id
)
)
)
AND id != NEW.id
AND NOT EXISTS (
SELECT 1 FROM ipam_prefix p
WHERE
p.prefix >> ipam_prefix.prefix
AND p.prefix << NEW.prefix
AND (
(p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))
OR
(p.vrf_id IS NULL AND p.status = 'container')
)
AND p.id != NEW.id
)
;
RETURN NEW;
"""
ipam_prefix_update_adjust_prefix_parent = """
-- When a prefix changes, reassign any IPAddresses that no longer
-- fall within the new prefix range to the parent prefix (or set null if no parent exists)
UPDATE ipam_prefix
SET parent_id = OLD.parent_id
WHERE
parent_id = NEW.id
-- IP address no longer contained within the updated prefix
AND NOT (prefix << NEW.prefix);
-- Update the prefix with the new parent if the parent is the most appropriate prefix
UPDATE ipam_prefix
SET parent_id=NEW.id
WHERE
prefix << NEW.prefix
AND
(
(vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
OR
(
NEW.vrf_id IS NULL
AND
NEW.status = 'container'
AND
NOT EXISTS(
SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id
)
)
)
AND id != NEW.id
AND NOT EXISTS (
SELECT 1 FROM ipam_prefix p
WHERE
p.prefix >> ipam_prefix.prefix
AND p.prefix << NEW.prefix
AND (
(p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))
OR
(p.vrf_id IS NULL AND p.status = 'container')
)
AND p.id != NEW.id
)
;
RETURN NEW;
"""
+2 -2
View File
@@ -687,13 +687,13 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
template_name = 'ipam/prefix/ip_addresses.html'
tab = ViewTab(
label=_('IP Addresses'),
badge=lambda x: x.get_child_ips().count(),
badge=lambda x: x.ip_addresses.count(),
permission='ipam.view_ipaddress',
weight=700
)
def get_children(self, request, parent):
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
return parent.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
def prep_table_data(self, request, queryset, parent):
if not request.GET.get('q') and not get_table_ordering(request, self.table):
+1
View File
@@ -454,6 +454,7 @@ INSTALLED_APPS = [
'sorl.thumbnail',
'taggit',
'timezone_field',
'pgtrigger',
'core',
'account',
'circuits',
+4
View File
@@ -14,6 +14,10 @@
<th scope="row">{% trans "Family" %}</th>
<td>IPv{{ object.family }}</td>
</tr>
<tr>
<th scope="row">{% trans "Prefix" %}</th>
<td>{{ object.prefix|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "VRF" %}</th>
<td>
@@ -14,6 +14,7 @@
<div class="row">
<h2 class="col-9 offset-3">{% trans "IP Addresses" %}</h2>
</div>
{% render_field model_form.prefix %}
{% render_field form.pattern %}
{% render_field model_form.status %}
{% render_field model_form.role %}
+4
View File
@@ -13,6 +13,10 @@
<th scope="row">{% trans "Family" %}</th>
<td>IPv{{ object.family }}</td>
</tr>
<tr>
<th scope="row">{% trans "Prefix" %}</th>
<td>{{ object.prefix|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Starting Address" %}</th>
<td>{{ object.start_address }}</td>
+1 -1
View File
@@ -109,7 +109,7 @@
{% endif %}
</td>
</tr>
{% with child_ip_count=object.get_child_ips.count %}
{% with child_ip_count=object.ip_addresses.count %}
<tr>
<th scope="row">{% trans "Child IPs" %}</th>
<td>
+20
View File
@@ -280,6 +280,26 @@ def as_range(n):
return range(n)
@register.filter()
def parent_depth(n, parent=None):
"""
Return the depth of a node based on the parent's depth
"""
parent_depth = 0
if parent and hasattr(parent, 'depth_count'):
parent_depth = parent.depth_count + 1
elif parent and hasattr(parent, 'depth'):
try:
parent_depth = int(parent.depth) + 1
except TypeError:
pass
try:
depth = int(n) - int(parent_depth)
except TypeError:
return n
return depth
@register.filter()
def meters_to_feet(n):
"""
+1
View File
@@ -7,6 +7,7 @@ django-graphiql-debug-toolbar==0.2.0
django-htmx==1.27.0
django-mptt==0.17.0
django-pglocks==1.0.4
django-pgtrigger==4.15.4
django-prometheus==2.4.1
django-redis==6.0.0
django-rich==2.2.0