Compare commits

...

181 Commits

Author SHA1 Message Date
Jeremy Stretch
5d9644988b Misc cleanup
CI / build (20.x, 3.14) (push) Failing after 12s
CI / build (20.x, 3.13) (push) Failing after 14s
CI / build (20.x, 3.12) (push) Failing after 16s
2026-03-19 11:19:00 -04:00
Jeremy Stretch
0f9491a9a8 Use separate templates for front and rear port mappings 2026-03-19 10:55:34 -04:00
Jeremy Stretch
d194c804a3 Add 'add' action for VC members panel 2026-03-19 10:30:16 -04:00
Jeremy Stretch
3bdbbc4116 Add 'add' action for inventory items panel 2026-03-19 10:24:14 -04:00
Jeremy Stretch
18779a222e Add layout for InterfaceView 2026-03-19 09:42:30 -04:00
Jeremy Stretch
2bf187c061 Cleanup
CI / build (20.x, 3.14) (push) Failing after 13s
CI / build (20.x, 3.13) (push) Failing after 15s
CI / build (20.x, 3.12) (push) Failing after 17s
2026-03-18 16:24:44 -04:00
Jeremy Stretch
7163840279 #20923: Migrate remaining DCIM views to new UI layouts 2026-03-18 12:31:54 -04:00
github-actions
c3c7cf15b2 Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 1m3s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m9s
CodeQL / Analyze (python) (push) Failing after 1m10s
2026-03-18 05:28:51 +00:00
Jeremy Stretch
2b7049c39c Release v4.5.5 (#21672)
CI / build (20.x, 3.12) (push) Failing after 36s
CI / build (20.x, 3.13) (push) Failing after 9s
CI / build (20.x, 3.14) (push) Failing after 10s
CodeQL / Analyze (actions) (push) Failing after 49s
CodeQL / Analyze (javascript-typescript) (push) Failing after 49s
CodeQL / Analyze (python) (push) Failing after 57s
* Release v4.5.5

* Pin django-rq to <4.0
2026-03-17 14:58:14 -04:00
Martin Hauser
3ededeb0e7 fix(circuits): Clear Circuit Termination cache on change
Move cache update logic from signal to model save method and track
original values to properly clear old cache when circuit_id or term_side
changes. Add comprehensive tests for all cache update scenarios.

Fixes #21686
2026-03-17 13:16:22 -04:00
bctiemann
66f6b2b6f9 Merge pull request #21649 from netbox-community/21556-fix-dropdown-clearing
CI / build (20.x, 3.12) (push) Failing after 39s
CI / build (20.x, 3.13) (push) Failing after 8s
CI / build (20.x, 3.14) (push) Failing after 9s
CodeQL / Analyze (actions) (push) Failing after 48s
CodeQL / Analyze (javascript-typescript) (push) Failing after 53s
CodeQL / Analyze (python) (push) Failing after 54s
Fixes #21556: Restore previous value (if applicable) after clearing related dropdown
2026-03-17 12:06:14 -04:00
Jeremy Stretch
61cef9400d Fixes #21556: Restore previous value (if applicable) after clearing related dropdown 2026-03-17 11:33:53 -04:00
Jonathan Senecal
d57f230f37 Fixes #21653: Fix multi-position tracing in CablePath.from_origin() (#21681)
* Add failing tests for multi-position cable path tracing

* Fix multi-position tracing in CablePath.from_origin()

* Add failing test for multi-connector trunk cable tracing through patch panel

* Fix multi-connector profiled cable tracing in CablePath.from_origin()
2026-03-17 14:16:03 +01:00
Rob Duffy
472dc3882e Fixes #21673: UI Bug with Displaying Primary IP Address with NAT IP on a VM
CI / build (20.x, 3.12) (push) Failing after 9s
CI / build (20.x, 3.13) (push) Failing after 10s
CI / build (20.x, 3.14) (push) Failing after 10s
CodeQL / Analyze (actions) (push) Failing after 1m11s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m17s
CodeQL / Analyze (python) (push) Failing after 1m19s
2026-03-17 08:54:03 +01:00
github-actions
21f78049bc Update source translation strings
CI / build (20.x, 3.12) (push) Failing after 16s
CI / build (20.x, 3.13) (push) Failing after 14s
CI / build (20.x, 3.14) (push) Failing after 30s
CodeQL / Analyze (actions) (push) Failing after 1m4s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m10s
CodeQL / Analyze (python) (push) Failing after 1m9s
2026-03-14 05:18:31 +00:00
Jeremy Stretch
e28ed7446c Fixes #21578: Enable assignment of scope object by name when bulk importing prefixes/VLAN groups (#21671) 2026-03-13 16:27:26 -07:00
Jeremy Stretch
9b57512b12 Fixes #21579: Display 'add script' button only if user has sufficient permission (#21628)
CI / build (20.x, 3.12) (push) Failing after 38s
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 46s
CodeQL / Analyze (javascript-typescript) (push) Failing after 50s
CodeQL / Analyze (python) (push) Failing after 53s
* Fixes #21579: Display 'add script' button only if user has sufficient permission

* Check for core.add_managedfile permission too
2026-03-13 22:08:03 +01:00
github-actions
da79cc775d Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 1m7s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m14s
CodeQL / Analyze (python) (push) Failing after 1m11s
2026-03-13 05:20:12 +00:00
Jeremy Stretch
6f5fd26183 Fixes #20077: Fix form field focus bug on Edge
CI / build (20.x, 3.12) (push) Failing after 16s
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 1m11s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m16s
CodeQL / Analyze (python) (push) Failing after 1m12s
2026-03-12 14:49:43 -04:00
Jason Novinger
10157394ae Fixes #21651: Disable ordering on MACAddress is_primary column
is_primary is a cached_property, not a database field, so attempting
to order by it raises a FieldError.
2026-03-12 14:48:58 -04:00
Jeremy Stretch
ae0907fb37 Fixes #20934: Fix flicker when navigating in dark mode (#21650) 2026-03-12 09:38:04 -07:00
Martin Hauser
fea6ad61fd fix(virtualization): Hide VM Add Components dropdown without change permission (#21634)
Wrap the VirtualMachine "Add Components" dropdown in a
`virtualization.change_virtualmachine` permission check to match Device
behavior and prevent users without change permission from seeing
component add actions.

Fixes #21580
2026-03-12 09:30:40 -07:00
bctiemann
675e68f276 Merge pull request #21623 from netbox-community/20923-migrate-vpn-views
CI / build (20.x, 3.12) (push) Failing after 19s
CI / build (20.x, 3.13) (push) Failing after 25s
CI / build (20.x, 3.14) (push) Failing after 39s
CodeQL / Analyze (actions) (push) Failing after 1m6s
CodeQL / Analyze (javascript-typescript) (push) Failing after 58s
CodeQL / Analyze (python) (push) Failing after 57s
#20923: Convert `vpn` views to new UI layout
2026-03-12 09:14:48 -04:00
bctiemann
20b907a8c9 Merge pull request #21630 from netbox-community/21114-data-source
#21114 Allow specifying exclude directories for Data Sources
2026-03-12 09:11:12 -04:00
Jason Novinger
8ccb0f7b63 Closes #20923: Migrate wireless app views to declarative UI layouts (#21646)
* #20923: Migrate wireless app views to declarative UI layouts

Convert WirelessLANGroup, WirelessLAN, and WirelessLink detail views
from legacy HTML templates to declarative Python layout definitions.

New files:
- wireless/ui/panels.py: Panel classes for all three model detail views
- templates/wireless/attrs/auth_psk.html: Secret toggle for PSK field
- templates/wireless/panels/wirelesslink_interface_{a,b}.html: Interface
  panels for WirelessLink detail view

Removed:
- templates/wireless/inc/authentication_attrs.html
- templates/wireless/inc/wirelesslink_interface.html

* Consolidate wireless link interface templates into ObjectPanel subclass

Replace duplicate wirelesslink_interface_{a,b}.html templates with a
single shared template and WirelessLinkInterfacePanel(ObjectPanel)
subclass that injects the correct interface via get_context().

* Rename WirelessLANAuthenticationPanel to WirelessAuthenticationPanel

Drop the 'LAN' qualifier since the panel is shared by both WirelessLAN
and WirelessLink views.

* Fix accessor shadowing in WirelessLinkInterfacePanel

Rename __init__ parameter from 'accessor' to 'interface_attr' to avoid
shadowing ObjectPanel.accessor, which would cause super().get_context()
to resolve the wrong context key.

* Use SimpleLayout for WirelessLinkView

Replace explicit Layout with SimpleLayout, which auto-includes plugin
content panels. Remove unused Row, Column, and PluginContentPanel
imports.
2026-03-12 08:55:50 -04:00
bctiemann
068fce4d7c Merge pull request #21608 from netbox-community/21440-oob-ip-import
Fixes #21440: Avoid erroneously clearing primary/OOB IP assignments during bulk import/update
2026-03-12 08:31:40 -04:00
bctiemann
2e4bce2dad Merge pull request #21555 from ITJamie/patch-3
Add changelog message documentation in custom scripts
2026-03-12 08:29:19 -04:00
GeertJohan
dad96c525f Fixes #21618: Preserve cable terminations when bulk-editing cable profile
When `update_terminations(force=True)` is called (e.g. after a profile
change), cache the termination objects from the database before deleting
CableTermination records. Without this, the `a_terminations`/`b_terminations`
properties fall back to querying the (now-empty) DB and return empty lists,
resulting in all terminations being lost.

Also removes a leftover debug print statement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 08:23:34 -04:00
Martin Hauser
cac3c1221c Closes #21631: Remove duplicate 'created' field in RackReservation table (#21632)
CI / build (20.x, 3.12) (push) Failing after 58s
CI / build (20.x, 3.13) (push) Failing after 9s
CI / build (20.x, 3.14) (push) Failing after 10s
CodeQL / Analyze (actions) (push) Failing after 45s
CodeQL / Analyze (javascript-typescript) (push) Failing after 48s
CodeQL / Analyze (python) (push) Failing after 50s
2026-03-11 11:49:01 -05:00
Jeremy Stretch
3a9d00a537 Update the lock-threads workflow
CI / build (20.x, 3.13) (push) Failing after 34s
CI / build (20.x, 3.12) (push) Failing after 36s
CI / build (20.x, 3.14) (push) Failing after 52s
CodeQL / Analyze (actions) (push) Failing after 1m25s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m17s
CodeQL / Analyze (python) (push) Failing after 1m16s
2026-03-11 08:56:39 -04:00
github-actions
4040e4f266 Update source translation strings 2026-03-11 05:19:17 +00:00
Jeremy Stretch
f938309ed9 Second attempt to fix @claude for PRs from forks (#21633)
CI / build (20.x, 3.12) (push) Failing after 10s
CI / build (20.x, 3.13) (push) Failing after 10s
CI / build (20.x, 3.14) (push) Failing after 10s
CodeQL / Analyze (actions) (push) Failing after 48s
CodeQL / Analyze (javascript-typescript) (push) Failing after 54s
CodeQL / Analyze (python) (push) Failing after 57s
2026-03-10 10:35:28 -07:00
Arthur
86f6de40d2 add docs and tests 2026-03-10 08:58:07 -07:00
Arthur
83c6149e49 #21114 Allow specifying exclude directories for Data Sources 2026-03-10 08:46:47 -07:00
Jeremy Stretch
98d898aba9 Fix the Claude action for external PRs (#21629) 2026-03-10 08:26:36 -07:00
Arthur Hanson
07bb6aa365 #20923: Migrate Users object to declarative layouts (#21568)
This continues the migration of object views in the user app to NetBox v4.5’s declarative layouts.
Replace legacy object view templates with declarative layouts for:
   - Users
   - Groups
   - API Tokens
   - Permissions
   - Owner Groups
   - Owners
2026-03-10 16:04:24 +01:00
pobradovic08
f3c34b30ec Fixes #21402: Prefetch device_type and manufacturer for brief mode API responses (#21616)
* Fixes #21402: Prefetch device_type and manufacturer for brief mode API responses

Add select_related for device_type__manufacturer on the DeviceViewSet
queryset to prevent N+1 queries when rendering unnamed devices in brief
mode.

* Use prefetch_related instead of select_related for device_type__manufacturer
2026-03-10 10:38:17 -04:00
github-actions
2281889e9d Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 1m8s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m13s
CodeQL / Analyze (python) (push) Failing after 1m17s
2026-03-10 05:18:47 +00:00
Jeremy Stretch
b19d0d61f4 Delete unused template 2026-03-09 15:48:04 -04:00
Jeremy Stretch
d64c4d75f8 #20923: Convert vpn views to new UI layout 2026-03-09 15:25:25 -04:00
Arthur Hanson
b5bd8905ca #21330 optimize the assignment of tags when saving an object (#21595)
CI / build (20.x, 3.12) (push) Failing after 46s
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 44s
CodeQL / Analyze (javascript-typescript) (push) Failing after 45s
CodeQL / Analyze (python) (push) Failing after 49s
* #21330 optimize object tag creation

* ruff fixes

* optimize

* review changes

* fix

* Update netbox/extras/managers.py

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2026-03-09 14:11:14 -04:00
Jeremy Stretch
cb5521f818 Closes #21468: copy_safe_request() should retain non-sensitive HTTP request headers (#21577)
- Define `HTTP_REQUEST_META_SENSITIVE` to serve as a blacklist for
  known-sensitive headers
- Modify `copy_safe_request()` to copy all non-sensitive headers
  (ignoring any not defined as strings)
- Add the `CopySafeRequestTests` test suite
2026-03-09 16:54:00 +01:00
Jeremy Stretch
3cb854b7d5 Closes #21611: Replace calls to .count() with .exists() (#21612)
Replace two boolean evaluations of .count() with .exists()
2026-03-09 16:46:38 +01:00
Jeremy Stretch
d980837da0 Fixes #20385: Ensure GraphQL API respects MAX_PAGE_SIZE (#21617)
- Extend `apply_pagination()` to check for and apply `MAX_PAGE_SIZE`
- Add a test
2026-03-09 14:58:23 +01:00
github-actions
5c19afc07c Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 24s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m9s
CodeQL / Analyze (python) (push) Failing after 1m17s
2026-03-07 05:14:28 +00:00
Jeremy Stretch
67defb3228 Fixes #21531: Fix search functionality for location when combined with other filters (#21599)
CodeQL / Analyze (actions) (push) Failing after 5s
CI / build (20.x, 3.12) (push) Failing after 20s
CodeQL / Analyze (javascript-typescript) (push) Failing after 4s
CI / build (20.x, 3.13) (push) Failing after 15s
CodeQL / Analyze (python) (push) Failing after 3s
CI / build (20.x, 3.14) (push) Failing after 14s
2026-03-06 11:54:10 -06:00
Martin Hauser
cca4cc61b6 Fixes #21512: Fix GraphQL filtering for device, module components, templates (#21602) 2026-03-06 11:23:45 -06:00
Jamie (Bear) Murphy
9b0c6110bb Clarify optional changelog message in custom-scripts
Added comment to clarify optional changelog message.
2026-03-06 17:13:52 +00:00
Martin Hauser
758b230403 docs(webhooks): Update context variables and example payload (#21607)
Clarify webhook context variable names and event types.
Replace `model` with `object_type`, update event values to match actual
output (`created` vs. `create`), and refresh example JSON to reflect the
current API response format, including new fields like `display` and
`display_url`.

Fixes #21489
2026-03-06 09:04:30 -08:00
Jeremy Stretch
8ea33df148 Fixes #20915: Ensure preferred language is applied during SSO login (#21590) 2026-03-06 10:00:33 -06:00
Jeremy Stretch
c86210f024 Fixes #21440: Avoid erroneously clearing primary/OOB IP assignments during bulk import/update 2026-03-06 10:48:06 -05:00
Jeremy Stretch
685c1afdcf Update CONTRIBUTING.md (#21606)
- Enforce a limit of three open PRs per community contributor
- Clarify AI content policy
- Misc rewording
2026-03-06 16:32:19 +01:00
Martin Hauser
d62a0d7d8d fix(extras): Add missing COOKIES and method to NetBoxFakeRequest
Populate COOKIES dict and set method to POST in runscript command's
NetBoxFakeRequest. Ensures the fake request object more closely mimics
a real Django request, preventing potential issues with code expecting
these attributes.

Fixes #21486
2026-03-06 09:52:26 -05:00
bctiemann
1c527366c9 Merge pull request #21597 from netbox-community/21012-interface-vlans-list
Fixes #21012: Ensure all tagged VLANs assigned to an interface are listed under the interface detail UI view
2026-03-06 09:18:33 -05:00
Jeremy Stretch
e1684fb645 Display the interface's untagged VLAN in the attributes table 2026-03-06 07:37:46 -05:00
Jeremy Stretch
969ae81574 Fixes #21380: Fix display of the background workers list on small screens (#21598)
CodeQL / Analyze (actions) (push) Failing after 14s
CodeQL / Analyze (javascript-typescript) (push) Failing after 7s
CI / build (20.x, 3.13) (push) Failing after 31s
CI / build (20.x, 3.12) (push) Failing after 33s
CodeQL / Analyze (python) (push) Failing after 6s
CI / build (20.x, 3.14) (push) Failing after 31s
Wrap the table in a `.table-responsive` to enable horizontal scrolling
within the table body.
2026-03-06 07:45:01 +01:00
github-actions
baec71fcaf Update source translation strings 2026-03-06 05:17:32 +00:00
Jeremy Stretch
44abeeff5a Fixes #21012: Ensure all tagged VLANs assigned to an interface are listed under the interface detail UI view 2026-03-05 16:35:31 -05:00
Martin Hauser
93e01d5b07 fix(dcim): Correct object type for child Site Group actions
CI / build (20.x, 3.12) (push) Failing after 12s
CI / build (20.x, 3.14) (push) Failing after 10s
CI / build (20.x, 3.13) (push) Failing after 12s
CodeQL / Analyze (actions) (push) Failing after 9s
CodeQL / Analyze (javascript-typescript) (push) Failing after 10s
CodeQL / Analyze (python) (push) Failing after 12s
Replace `dcim.Region` with `dcim.SiteGroup` in child Site Group actions
for the DCIM view. Ensures the correct model is referenced when adding
child Site Groups, improving functionality and aligning with the
expected behavior.

Fixes #21586
2026-03-05 13:59:18 -05:00
Jeremy Stretch
fa5f9430fc Fixes #20468: Fix range lookups for numeric GraphQL filters (#21589)
CodeQL / Analyze (actions) (push) Failing after 6s
CI / build (20.x, 3.12) (push) Failing after 21s
CodeQL / Analyze (javascript-typescript) (push) Failing after 4s
CI / build (20.x, 3.13) (push) Failing after 19s
CI / build (20.x, 3.14) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 4s
* Fixes #20468: Fix range lookups for numeric GraphQL filters

* Update netbox/netbox/tests/test_graphql.py

---------

Co-authored-by: Martin Hauser <mhauser@netboxlabs.com>
2026-03-05 17:10:49 +01:00
Jeremy Stretch
351066c73f Limit auto-review workflow to GitHub org members (#21570) 2026-03-05 08:06:43 -08:00
bctiemann
e6db3f75ea Merge pull request #21588 from netbox-community/19867-preserve-per_page-param
Fixes #19867: Retain the `per_page` URL parameter after editing an object
2026-03-05 09:56:32 -05:00
Jeremy Stretch
04244e188f #20923: Migrate DCIM view templates (#21372)
* Permit passing template_name to Panel instance

* Define UI layout for ModuleType view

* Define UI layout for DeviceRole view

* Define UI layout for Platform view

* Define UI layout for Module view

* Misc cleanup

* Linkify module bay
2026-03-05 08:43:46 -05:00
Jeremy Stretch
eaad5cc26f Fixes #19867: Retain the per_page URL parameter after editing an object 2026-03-05 08:26:47 -05:00
Jason Novinger
a1d82e45a0 Closes #21571: Bump minimatch and markdown-it to resolve security alerts (#21573)
CI / build (20.x, 3.12) (push) Failing after 12s
CI / build (20.x, 3.13) (push) Failing after 11s
CI / build (20.x, 3.14) (push) Failing after 11s
CodeQL / Analyze (actions) (push) Failing after 1m4s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m4s
CodeQL / Analyze (python) (push) Failing after 1m5s
Add yarn resolutions to force patched versions of two transitive
dependencies flagged by dependabot:

- minimatch 3.1.2 → 3.1.5 (GHSA-7r86-cg39-jmmj, high severity ReDoS)
- markdown-it 14.1.0 → 14.1.1 (CVE-2026-2327, medium severity ReDoS)
2026-03-04 16:08:02 +01:00
github-actions
e4f7f080b3 Update source translation strings
CodeQL / Analyze (javascript-typescript) (push) Failing after 57s
CodeQL / Analyze (actions) (push) Failing after 1m0s
CodeQL / Analyze (python) (push) Failing after 58s
2026-03-04 05:17:48 +00:00
bctiemann
983ba4fda8 Merge pull request #21562 from netbox-community/release-v4.5.4
CI / build (20.x, 3.12) (push) Failing after 41s
CI / build (20.x, 3.13) (push) Failing after 10s
CI / build (20.x, 3.14) (push) Failing after 10s
CodeQL / Analyze (actions) (push) Failing after 42s
CodeQL / Analyze (javascript-typescript) (push) Failing after 45s
CodeQL / Analyze (python) (push) Failing after 48s
Release v4.5.4
2026-03-03 15:07:18 -05:00
Jeremy Stretch
54462595a6 Release v4.5.4 2026-03-03 12:46:15 -05:00
Jeremy Stretch
8ab752b9ad Closes #21451: Upgrade tom-select to v2.5.2 (#21563) 2026-03-03 18:35:36 +01:00
Jeremy Stretch
b11cc31f9d Closes #21559: Add CLAUDE.md
CI / build (20.x, 3.12) (push) Failing after 11s
CI / build (20.x, 3.13) (push) Failing after 10s
CI / build (20.x, 3.14) (push) Failing after 10s
CodeQL / Analyze (actions) (push) Failing after 58s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m0s
CodeQL / Analyze (python) (push) Failing after 59s
2026-03-03 12:01:33 -05:00
Martin Hauser
3f02309538 fix(ipam): Avoid allocating IPv6 subnet-router anycast address (#21547)
Ensure available IP selection for IPv6 non-pool prefixes excludes the
subnet-router anycast address (RFC 4291), so allocation starts at ::1
for typical prefixes (e.g. /64).
Add tests for IPv4/IPv6 pools and special cases (/31-/32, /127-/128).

Fixes #21347
2026-03-03 08:26:44 -08:00
Martin Hauser
53345f194a refactor(graphql): Replace FilterLookup[str] with StrFilterLookup
Replace usages of FilterLookup[str] with StrFilterLookup in GraphQL
filter definitions to align with strawberry-graphql-django v0.75.1.
This silences upstream warnings and helps avoid DuplicatedTypeName
errors.

Fixes #21450
2026-03-03 11:17:13 -05:00
Jeremy Stretch
139557b8dd Fixes #21524: Fix IndexError when serializing stale cable paths (#21525) 2026-03-03 16:37:45 +01:00
bctiemann
fcf02bd8bb Merge pull request #21453 from netbox-community/21429-cable-create-add-another-does-not-carry-over-termination
Fixes #21429: Add Cable cloning and fix "Create & Add Another" to preserve Termination Types
2026-03-03 09:44:35 -05:00
Martin Hauser
7d6989ff34 Closes #21477: Add cached relation filters to GraphQL for Cable (#21506) 2026-03-03 08:01:45 -06:00
Jamie (Bear) Murphy
1be917fb90 Add changelog message documentation in custom scripts
Add changelog message documentation in custom scripts
2026-03-03 13:10:04 +00:00
Arthur Hanson
3b0b95c265 Closes #21550: Call snapshot() before saving related objects (#21551)
Add missing pre-change `snapshot()` calls in views/forms before updating
and saving related objects (device bays, virtual chassis members, and
bulk-import primary MAC/IP assignments), so changelog entries include
pre-change data.
2026-03-03 14:01:04 +01:00
github-actions
cdc2fb2f06 Update source translation strings
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m12s
CodeQL / Analyze (actions) (push) Failing after 1m14s
CodeQL / Analyze (python) (push) Failing after 1m11s
2026-03-03 05:20:47 +00:00
Jeremy Stretch
7ec656bc7c Introduce GitHub actions for Claude Code review (#21545)
CI / build (20.x, 3.12) (push) Failing after 1m45s
CI / build (20.x, 3.13) (push) Failing after 20s
CI / build (20.x, 3.14) (push) Failing after 15m6s
CodeQL / Analyze (actions) (push) Failing after 15m21s
CodeQL / Analyze (javascript-typescript) (push) Failing after 15m25s
CodeQL / Analyze (python) (push) Failing after 15m20s
2026-03-02 10:39:23 -06:00
Rob Duffy
06bbae0f84 Fixes #21527: UI Bug with Displaying Primary IP Address with NAT IP on a Device 2026-03-02 08:57:52 -05:00
Arthur Hanson
8ff9fd26d1 Closes #20787: Address warnings from generation of OpenAPI schema (#21521) 2026-03-02 14:38:39 +01:00
github-actions
a0e23ac3c9 Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 57s
CodeQL / Analyze (javascript-typescript) (push) Failing after 56s
CodeQL / Analyze (python) (push) Failing after 55s
2026-02-28 05:11:26 +00:00
Jeremy Stretch
071d4a63aa Fixes #21518: Ensure proper display of decimal custom fields with a zero value (#21523)
CodeQL / Analyze (actions) (push) Failing after 15m27s
CI / build (20.x, 3.12) (push) Failing after 15m41s
CI / build (20.x, 3.13) (push) Failing after 15m40s
CI / build (20.x, 3.14) (push) Failing after 15m39s
CodeQL / Analyze (javascript-typescript) (push) Failing after 51s
CodeQL / Analyze (python) (push) Failing after 59s
2026-02-27 09:13:53 -08:00
github-actions
7db2739465 Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 23s
CodeQL / Analyze (javascript-typescript) (push) Failing after 31s
CodeQL / Analyze (python) (push) Failing after 29s
2026-02-26 05:25:45 +00:00
Dave Bevan
74326edc20 Add new Ethernet types for 10GE and 40GE
CI / build (20.x, 3.12) (push) Failing after 1m8s
CI / build (20.x, 3.13) (push) Failing after 15s
CI / build (20.x, 3.14) (push) Failing after 11s
CodeQL / Analyze (actions) (push) Failing after 19s
CodeQL / Analyze (javascript-typescript) (push) Failing after 29s
CodeQL / Analyze (python) (push) Failing after 30s
Closes #21394
2026-02-25 16:34:00 -05:00
Grische
2ef21f7097 Fixes: #21456 - Improve config_context rendering with GraphQL (#21495) 2026-02-25 16:17:04 -05:00
Kartik
3adcdc34c3 clarify E501 enforcement 2026-02-25 15:33:25 -05:00
Martin Hauser
f33109e485 fix(dcim): Rename facility to facility_id in panel attrs (#21482)
Corrects field mismatch by aligning the attribute name with the
data model. This change ensures consistency in attribute mappings
and improves clarity in the codebase.

Fixes #21481
2026-02-25 12:20:51 -08:00
github-actions
d10453883f Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 34s
CodeQL / Analyze (javascript-typescript) (push) Failing after 39s
CodeQL / Analyze (python) (push) Failing after 41s
2026-02-21 05:16:36 +00:00
bctiemann
6dbd8f6170 Merge pull request #21507 from netbox-community/21497-pin-ruff-in-ci-to-avoid-surprise-breakages
CI / build (20.x, 3.12) (push) Failing after 18s
CI / build (20.x, 3.13) (push) Failing after 23s
CI / build (20.x, 3.14) (push) Failing after 33s
CodeQL / Analyze (actions) (push) Failing after 33s
CodeQL / Analyze (javascript-typescript) (push) Failing after 32s
CodeQL / Analyze (python) (push) Failing after 29s
Fixes #21497: Pin Ruff 0.15.2 and run CI via ruff-action
2026-02-20 16:59:46 -05:00
Jason Novinger
715f9d150c Closes #21385: Add contact assignment support to virtual circuits
Adds ContactsMixin to VirtualCircuit model and GraphQL type, and includes
'contacts' in table fields. Verified: UI Contacts tab, REST API POST (201),
GraphQL contacts query.
2026-02-20 16:59:37 -05:00
Martin Hauser
f4567ba099 chore(ci): Pin Ruff 0.15.2 and run via ruff-action
Pin Ruff to v0.15.2 in CI and pre-commit to avoid breakages from
upstream releases. Run Ruff via astral-sh/ruff-action (pinned by SHA)
instead of installing Ruff via pip.
Document where Ruff is pinned and keep the release checklist/style guide
in sync.

Fixes #21472
Fixes #21497
2026-02-20 20:38:11 +01:00
Jeremy Stretch
d5e8f7dafa Closes #21459: Avoid prefetching data for hidden table columns (#21460)
CI / build (20.x, 3.12) (push) Failing after 49s
CI / build (20.x, 3.13) (push) Failing after 16s
CI / build (20.x, 3.14) (push) Failing after 11s
CodeQL / Analyze (actions) (push) Failing after 19s
CodeQL / Analyze (javascript-typescript) (push) Failing after 29s
CodeQL / Analyze (python) (push) Failing after 32s
2026-02-20 10:36:46 -08:00
Jeremy Stretch
32e2a17c88 Merge pull request #21503 from netbox-community/21500-enable-linting-for-all-ordering-ruf022
Closes #21500: Enable RUF022 to enforce sorted `__all__` lists
2026-02-20 11:52:00 -05:00
Martin Hauser
3beef34355 chore(ruff): Sort __all__ definitions across modules
Apply consistent alphabetical ordering to `__all__` lists in the
circuits module. Enhances readability and alignment with established
linting guidelines.
2026-02-20 15:36:01 +01:00
Martin Hauser
85d6242962 chore(ruff): Add RUF022 and tweak formatting in ruff.toml
Enable `RUF022` to enforce sorted `__all__` lists. Adjust comment
alignment and whitespace for improved readability and consistency
in ruff.toml configuration.
2026-02-20 15:34:58 +01:00
bctiemann
bb1a44d35b Merge pull request #21479 from netbox-community/21478-graphql-union-type-resolution-for-connected-endpoints
Fixes #21478: Fix GraphQL connected endpoint type resolution for Console Ports
2026-02-20 09:27:36 -05:00
bctiemann
ae6f1f9ae3 Merge pull request #21496 from netbox-community/20923-convert-virtualization-views-to-new-ui-layout
Closes #20923: Migrate Virtualization object views to declarative layouts
2026-02-20 09:26:56 -05:00
Arthur Hanson
915ac90119 20911 Fix sorting in dropdown (#21101)
* Fix TomSelect dropdown ordering

* cleanup

* cleanup

* cleanup

* use correct node version

* change ordering field, remove front-end changes

* rebuild tree after rename

* add migration

* fix migration

* fix migration

* fix migration

* fix migration

* fix migration

* cleanup

* use bulk_update and rebuild

* use bulk_update and rebuild

* cleanup

* fix csv import

* Review feedback

* Review feedback

* fix dropdown sorting

* fix ordering

* review feedback

* review feedback
2026-02-20 09:03:47 -05:00
Martin Hauser
cc47afc401 refactor(virtualization): Port to declarative layout
Add declarative layout panels for Cluster, Cluster Group, Cluster Type,
Virtual Disk, and VM Interface, including addressing, VLAN assignment,
and FHRP group handling.

Expand the declarative layout primitives:
- add GFK attribute rendering support
- add panel for rendering context-provided tables
- update templates to support new panels/attrs

Closes #20923
2026-02-20 14:58:20 +01:00
Martin Hauser
20fee95a9a Closes #21499: Restore deterministic Ruff linting (match Ruff 0.15.1 preview defaults)
Explicitly set `select` rules to maintain compatibility with
Ruff 0.15.1. Ensures deterministic linting behavior despite changes in
Ruff 0.15.2 defaults.
See https://github.com/astral-sh/ruff/releases/tag/0.15.2 for more
details.
2026-02-20 14:40:45 +01:00
github-actions
d2002c64b4 Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 41s
CodeQL / Analyze (javascript-typescript) (push) Failing after 48s
CodeQL / Analyze (python) (push) Failing after 51s
2026-02-20 05:23:09 +00:00
Martin Hauser
1b295f1d69 Closes #21473: Enable UP rules and modernize string formatting (#21488)
CI / build (20.x, 3.12) (push) Failing after 55s
CI / build (20.x, 3.13) (push) Failing after 17s
CI / build (20.x, 3.14) (push) Failing after 11s
CodeQL / Analyze (actions) (push) Failing after 25s
CodeQL / Analyze (javascript-typescript) (push) Failing after 50s
CodeQL / Analyze (python) (push) Failing after 48s
2026-02-19 10:25:08 -06:00
Martin Hauser
2c200a4fd3 Closes #21369: Add lazy loading and decoding options for ImageAttr (#21444)
Introduces `load_lazy` and `decoding` parameters to `ImageAttr` for
enhanced image handling. Lazy loading improves page performance, while
configurable decoding options provide greater flexibility. Updates the
template to conditionally include these attributes in rendered HTML.

Fixes #21369
2026-02-19 09:22:16 -06:00
bctiemann
fb71cafb51 Closes: #21284: Mark request_id and username fields in EventContext as deprecated (#21485)
Add searchable deprecation comments on request_id and username fields
in EventContext.
2026-02-19 14:03:47 +01:00
github-actions
f373adb636 Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 45s
CodeQL / Analyze (javascript-typescript) (push) Failing after 52s
CodeQL / Analyze (python) (push) Failing after 54s
2026-02-19 05:26:47 +00:00
Martin Hauser
e84b062393 fix(dcim): Correct type check for ConsolePort in GraphQL mixin
Fixes a typo in the `resolve_type` method where `ConsolePortType` was
mistakenly referenced instead of `ConsolePort`. Ensures the correct
GraphQL type is returned for ConsolePort instances.

Fixes #21478
2026-02-18 23:19:36 +01:00
Martin Hauser
ef52ac4203 chore(ruff): Enable RET rules and add explicit fallbacks
CI / build (20.x, 3.12) (push) Failing after 10s
CI / build (20.x, 3.13) (push) Failing after 10s
CI / build (20.x, 3.14) (push) Failing after 10s
CodeQL / Analyze (actions) (push) Failing after 30s
CodeQL / Analyze (javascript-typescript) (push) Failing after 46s
CodeQL / Analyze (python) (push) Failing after 46s
Adopt Ruff `RET` to improve return-flow consistency across the codebase.
Simplify control flow by removing redundant `else` blocks after
`return`, and add explicit `return None` (or equivalent) fallbacks
where appropriate to preserve existing behavior.

Fixes #21411
2026-02-18 16:49:36 -05:00
bctiemann
b22e490847 Fixes: #20490 - Add filtering of Script objects based on object permissions with custom constraints (#21212) 2026-02-18 15:37:40 -05:00
Martin Hauser
945e7ade0a Fixes #21407: Enable I (isort) and stabilize import ordering (#21458)
CI / build (20.x, 3.12) (push) Failing after 10s
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 37s
CodeQL / Analyze (javascript-typescript) (push) Failing after 48s
CodeQL / Analyze (python) (push) Failing after 49s
- Adopt Ruff `I` (isort) rules for consistent import sorting
- Add two `# isort: split` boundaries to keep required imports pinned
  in `__init__.py` modules
2026-02-18 10:41:51 -06:00
github-actions
7300104cea Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 36s
CodeQL / Analyze (javascript-typescript) (push) Failing after 41s
CodeQL / Analyze (python) (push) Failing after 42s
2026-02-18 05:28:02 +00:00
bctiemann
2900429769 Merge pull request #21441 from netbox-community/21410-tighten-up-ruff-configuration-defaults
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 10s
CodeQL / Analyze (actions) (push) Failing after 1m3s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m14s
CodeQL / Analyze (python) (push) Failing after 1m15s
Fixes #21410: Expand Ruff exclusions and standardize formatting settings
2026-02-17 13:14:11 -05:00
Martin Hauser
278c82dd88 chore(ruff): Expand configuration for linting and formatting
Update `ruff.toml` with additional exclusions, linting rules, and
formatting preferences. Includes support for respecting `.gitignore`
and a consistent coding style.

Fixes #21410
2026-02-17 18:31:15 +01:00
Martin Hauser
951d856c3c feat(dcim): Add Cable cloning with Termination mapping
Introduce `clone()` method for the Cable model to enable cloning
its attributes, including termination type and parent selectors.
Updates mappings to align with CableForm workflows, supporting
"Clone" and "Create & Add Another" actions.

Fixes #21429
2026-02-17 18:30:36 +01:00
Jeremy Stretch
c029782cf5 Release v4.5.3 2026-02-17 10:37:44 -05:00
Martin Hauser
bdd23f3d17 fix(extras): Handle username fallback for job events
Fallback to the associated user when username is missing from job
lifecycle event contexts. Add a regression test to ensure JOB_COMPLETED
webhooks are enqueued without a request context.

Fixes #21371
2026-02-17 08:15:58 -05:00
github-actions
af6e18b7d4 Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 54s
CodeQL / Analyze (javascript-typescript) (push) Failing after 59s
CodeQL / Analyze (python) (push) Failing after 1m1s
2026-02-17 05:26:34 +00:00
Jeremy Stretch
816c5d4bea Fixes #21412: Defer monkey-patching until after settings have been loaded (#21415)
CI / build (20.x, 3.12) (push) Failing after 13s
CI / build (20.x, 3.13) (push) Failing after 10s
CI / build (20.x, 3.14) (push) Failing after 9s
CodeQL / Analyze (actions) (push) Failing after 1m0s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m10s
CodeQL / Analyze (python) (push) Failing after 1m9s
2026-02-16 18:17:50 +01:00
Martin Hauser
f4c3c90bab perf(filters): Avoid ContentType join in ContentTypeFilter
Resolve the ContentType via get_by_natural_key() and filter by the
FK value to prevent an unnecessary join to django_content_type.

Fixes #21420
2026-02-16 12:06:31 -05:00
Martin Hauser
862593f2dd fix(circuits): Persist CircuitType owner field
CircuitTypeForm rendered `owner` twice and did not persist ownership
because the displayed fields didn't match the fields processed by the
form. Remove `owner` from the fieldset and include it in `Meta.fields`
to keep rendering and form processing in sync.

Fixes #21397
2026-02-16 08:54:34 -05:00
Martin Hauser
f4c27fd494 fix(ipam): Use bulk_update in VLANGroup VID range migration
Replace per-row `save()` calls with `bulk_update` when populating
VLANGroup VLAN ID ranges during migration.

This avoids triggering post_save handlers (e.g. search cache/indexing)
on existing VLANGroup records and updates only the relevant fields,
improving both reliability and performance on larger databases.

Fixes #21375
2026-02-16 08:53:16 -05:00
Martin Hauser
ae736ef407 fix(dcim): Render device height as rack units via floatformat
Use `TemplatedAttr` for device height and render using Django's
`floatformat` filter so 0.0 is displayed as `0U` (and whole-U values
omit the decimal).

Fixes #21267
2026-02-16 08:37:50 -05:00
github-actions
d95b1186fb Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 31s
CodeQL / Analyze (javascript-typescript) (push) Failing after 36s
CodeQL / Analyze (python) (push) Failing after 38s
2026-02-14 05:18:04 +00:00
Jason Novinger
d6b9d30086 Fixes #20442: Mark template-accessible methods with alters_data=True (#21431)
CI / build (20.x, 3.12) (push) Failing after 13s
CI / build (20.x, 3.13) (push) Failing after 11s
CI / build (20.x, 3.14) (push) Failing after 11s
CodeQL / Analyze (actions) (push) Failing after 40s
CodeQL / Analyze (javascript-typescript) (push) Failing after 46s
CodeQL / Analyze (python) (push) Failing after 44s
Add alters_data=True to methods that modify database or filesystem state
and are accessible from Jinja2 sandbox template contexts:

- UserConfig.set(), clear(): Persist preference changes when commit=True
- ManagedFile.sync_data(): Writes files to scripts/reports storage
- ScriptModule.sync_classes(), sync_data(): Creates/deletes Script objects
- Job.start(), terminate(): Updates job status, creates notifications

Methods intentionally not protected:
- DataFile.refresh_from_disk(): Only modifies instance attributes in memory
- Overridden save()/delete(): Django's AltersData mixin auto-propagates
- Properties like Script.python_class: Not callable in template context

Ref: #20356 for exploit details demonstrating the vulnerability
2026-02-13 10:44:18 -08:00
Martin Hauser
9be5aa188c chore(ruff): Update target Python version to 3.12 (#21405)
Set the `target-version` in `ruff.toml` to Python 3.12. Ensures the
linter aligns with the version used in the project's environment.

Fixes #21404
2026-02-13 10:39:09 -08:00
Jason Novinger
f113557e81 Fixes #21127: Clear _path on interfaces when removed from cable
When editing a cable to remove an interface from the B side, the _path
field on the removed interface was not being cleared. This caused the
interface table to display stale connection info via _path.destinations.

Two changes:
- Signal handler now clears _path when termination removed from origins
- CablePath.delete() clears _path on origins (mirrors save() behavior)
2026-02-13 13:36:09 -05:00
Arthur
de812a5a85 21390 skip m2m processing for internal models to avoid extraneous ObjectChange records 2026-02-13 13:27:25 -05:00
Jason Novinger
0b7375136d Closes #21016: Add missing MPTT tree indexes (#21432)
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 10s
CodeQL / Analyze (actions) (push) Failing after 41s
CodeQL / Analyze (javascript-typescript) (push) Failing after 47s
CodeQL / Analyze (python) (push) Failing after 49s
Upgrade django-mptt to 0.18.0 and add empty indexes tuple to MPTT model
Meta classes. The empty tuple triggers Django's migration detection for
indexes that django-mptt adds dynamically (see
django-mptt/django-mptt#682). We cannot define the indexes explicitly
because the MPTT fields don't exist when the Meta class is evaluated.

Affected models: Region, SiteGroup, Location, DeviceRole, Platform,
ModuleBay, InventoryItem, InventoryItemTemplate, TenantGroup,
ContactGroup, WirelessLANGroup
2026-02-13 17:00:04 +01:00
Jeremy Stretch
1190adde2b Closes #21419: Improve query efficiency for MultipleChoiceFilter (#21421)
* Pass distinct=False to all ModelMultipleChoiceFilters associated with a ForeignKey field

* Pass distinct=False to all MultipleChoiceFilters associated with a concrete model
2026-02-13 12:31:36 +01:00
Arthur Hanson
2330874a8c Fixes #21277: Record pre-change snapshot when adding devices to cluster in UI (#21424) 2026-02-13 04:41:41 -06:00
Jeremy Stretch
dc738c7102 Closes #21257: Introduce & adopt MultiValueContentTypeFilter (#21417) 2026-02-13 04:24:36 -06:00
Jeremy Stretch
76fd3e3c61 Fixes #21196: q filter should match on primary IP only for IP address values (#21401) 2026-02-13 04:08:01 -06:00
github-actions
4ee64a7731 Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 44s
CodeQL / Analyze (javascript-typescript) (push) Failing after 48s
CodeQL / Analyze (python) (push) Failing after 50s
2026-02-13 05:27:16 +00:00
Arthur Hanson
0bb22dee0c Allow REDIS KWARGS to be set in configuration.py (#21377)
CI / build (20.x, 3.12) (push) Failing after 12s
CI / build (20.x, 3.13) (push) Failing after 12s
CI / build (20.x, 3.14) (push) Failing after 12s
CodeQL / Analyze (actions) (push) Failing after 44s
CodeQL / Analyze (javascript-typescript) (push) Failing after 57s
CodeQL / Analyze (python) (push) Failing after 1m5s
* Allow REDIS KWARGS to be set in configuration.py

* cleanup

* cleanup

* cleanup

* Update netbox/netbox/settings.py

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Update netbox/netbox/settings.py

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* document in REDIS config section

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2026-02-12 08:35:20 -05:00
Jason Novinger
6c383f293c Fixes #20435: Fix navigation margin issue when scrollbar appears (#21403)
Override Tabler's problematic margin-left: calc(100vw - 100%) rule that
causes a gap between the sidebar and main content when vertical scrollbar
is present on Windows/Linux browsers.

Uses scrollbar-gutter: stable to match the upstream fix in Tabler PR #2548.
2026-02-12 11:30:33 +01:00
github-actions
5bf516c63d Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 35s
CodeQL / Analyze (javascript-typescript) (push) Failing after 28s
CodeQL / Analyze (python) (push) Failing after 33s
2026-02-12 05:28:54 +00:00
Aditya Sharma
7df062d590 Fixes #21358: Prevent exception when sorting by Token column (#21391)
CI / build (20.x, 3.12) (push) Failing after 12s
CI / build (20.x, 3.13) (push) Failing after 12s
CI / build (20.x, 3.14) (push) Failing after 11s
CodeQL / Analyze (actions) (push) Failing after 1m8s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m17s
CodeQL / Analyze (python) (push) Failing after 1m19s
Mark the `token` TemplateColumn as non-orderable since it maps to a
Python property rather than a database field, causing a FieldError
when django-tables2 attempts to sort by it.

Add a regression test for TokenTable following the existing pattern
in circuits and vpn test suites.
2026-02-12 00:21:49 +01:00
Aditya Sharma
4b22be03a0 Fixes #21354: Fix Swagger-UI generating wrong URLs when BASE_PATH is set (#21392) 2026-02-11 11:35:13 -08:00
Dylan Lucci
24769ce127 Closes #21266: Add installed device table columns to DeviceBay table (#21348)
CI / build (20.x, 3.12) (push) Failing after 16s
CI / build (20.x, 3.13) (push) Failing after 21s
CI / build (20.x, 3.14) (push) Failing after 30s
CodeQL / Analyze (actions) (push) Failing after 33s
CodeQL / Analyze (javascript-typescript) (push) Failing after 33s
CodeQL / Analyze (python) (push) Failing after 29s
Expose additional properties of the device installed in each bay as
configurable table columns.

- Rename `role` → `installed_role`
- Rename `device_type` → `installed_device_type`
- Add `installed_description`, `installed_serial`, and
  `installed_asset_tag` columns to `DeviceBayTable`

---------

Co-authored-by: Martin Hauser <mhauser@netboxlabs.com>
2026-02-11 13:55:37 +01:00
github-actions
164e9db98d Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 33s
CodeQL / Analyze (javascript-typescript) (push) Failing after 37s
CodeQL / Analyze (python) (push) Failing after 39s
2026-02-11 05:29:43 +00:00
Martin Hauser
23f1c86e9c Closes #20211: Use thumbnails for ImageAttachment hover previews to improve page load performance (#21386)
CI / build (20.x, 3.12) (push) Failing after 13s
CI / build (20.x, 3.13) (push) Failing after 10s
CI / build (20.x, 3.14) (push) Failing after 10s
CodeQL / Analyze (actions) (push) Failing after 42s
CodeQL / Analyze (javascript-typescript) (push) Failing after 53s
CodeQL / Analyze (python) (push) Failing after 49s
2026-02-10 11:01:33 -06:00
Martin Hauser
02ffdd9d5d Closes #21268: Add Device Type details panel to Device view (#21368) 2026-02-10 10:37:35 -06:00
Martin Hauser
5013297326 feat(virtualization): Refactor VirtualMachine view to UI layout
Migrate the VirtualMachine detail view to SimpleLayout with standardized
panels for attributes, clusters, and resources. Modularize templates
to improve maintainability and reuse.

Fixes #21337
2026-02-10 10:22:18 -05:00
Daniel Sheppard
289cb4e1bc Simplify find_parent_prefix method
CI / build (20.x, 3.14) (push) Failing after 12s
CI / build (20.x, 3.13) (push) Failing after 14s
CI / build (20.x, 3.12) (push) Failing after 16s
2026-02-10 08:42:21 -06:00
Daniel Sheppard
6e24ce6a1a Fix test failure by casting to IPAddress 2026-02-10 08:29:03 -06:00
github-actions
584e0a9b8c Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 29s
CodeQL / Analyze (javascript-typescript) (push) Failing after 39s
CodeQL / Analyze (python) (push) Failing after 45s
2026-02-10 05:29:34 +00:00
Daniel Sheppard
c6bcfea429 Fine-tune forms, tests, and some model save() functions
CI / build (20.x, 3.12) (push) Failing after 10s
CI / build (20.x, 3.13) (push) Failing after 17s
CI / build (20.x, 3.14) (push) Failing after 17s
2026-02-09 14:57:24 -06:00
Martin Hauser
3ac9d0b8bf Closes #20981: Enhance JSON rendering for Custom Validators and Protection Rules in Config Revision View (#21376)
CI / build (20.x, 3.12) (push) Failing after 18s
CI / build (20.x, 3.13) (push) Failing after 17s
CI / build (20.x, 3.14) (push) Failing after 13s
CodeQL / Analyze (actions) (push) Failing after 29s
CodeQL / Analyze (javascript-typescript) (push) Failing after 37s
CodeQL / Analyze (python) (push) Failing after 38s
* feat(config): Add extra context to ConfigRevisionView

Introduces `get_extra_context` method for `ConfigRevisionView` to
format JSON-based attributes like `CUSTOM_VALIDATORS`,
`DEFAULT_USER_PREFERENCES`, and `PROTECTION_RULES`.
This ensures clearer rendering of configuration data in the UI.

Fixes #20981

* Reduce padding on JSON blocks

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2026-02-09 09:48:39 -05:00
Daniel Sheppard
d18d7369e6 Add missing migration
CI / build (20.x, 3.13) (push) Failing after 13s
CI / build (20.x, 3.12) (push) Failing after 16s
CI / build (20.x, 3.14) (push) Failing after 12s
2026-02-08 17:18:49 -06:00
Daniel Sheppard
42e2fd7fb3 Update Triggers and add new functions to triggers to handle certain cases
CI / build (20.x, 3.13) (push) Failing after 14s
CI / build (20.x, 3.14) (push) Failing after 12s
CI / build (20.x, 3.12) (push) Failing after 17s
2026-02-06 15:31:42 -06:00
Daniel Sheppard
cd636168ee Add ignore for migration files to ignore line length requirements as per our developer docs 2026-02-06 15:31:22 -06:00
Daniel Sheppard
b45da7b1e4 Merge branch 'feature' into feature-ip-prefix-link
CI / build (20.x, 3.12) (push) Failing after 12s
CI / build (20.x, 3.13) (push) Failing after 11s
CI / build (20.x, 3.14) (push) Failing after 10s
2026-02-06 10:14:22 -06:00
github-actions
b387ea5f58 Update source translation strings
CodeQL / Analyze (actions) (push) Failing after 54s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1m1s
CodeQL / Analyze (python) (push) Failing after 59s
2026-02-06 05:22:42 +00:00
bctiemann
ba9f6bf359 Fixes: #19129 - Richer display of MAC addresses in InterfaceTable when multiple MACs are present (#21270)
CI / build (20.x, 3.13) (push) Failing after 15s
CI / build (20.x, 3.12) (push) Failing after 17s
CI / build (20.x, 3.14) (push) Failing after 27s
CodeQL / Analyze (actions) (push) Failing after 30s
CodeQL / Analyze (javascript-typescript) (push) Failing after 25s
CodeQL / Analyze (python) (push) Failing after 25s
* Richer display of MAC addresses in InterfaceTable when multiple MACs are present

* Fix docstring

* Fix docstring

* Use mac_address_display in interface detail page

* Ensure "-" null placeholder still shows up on detail page

* Also include vminterface.html

* Simplify Multiple MAC addresses with additional selectable column for tables in list view and detail view

* Use ManyToManyColumn
2026-02-05 11:16:31 -05:00
Martin Hauser
ee6cbdcefe Fixes #21320: Prevent Rack validation errors when site or optional fields are missing during import (#21321)
CI / build (20.x, 3.12) (push) Failing after 18s
CI / build (20.x, 3.13) (push) Failing after 14s
CI / build (20.x, 3.14) (push) Failing after 12s
CodeQL / Analyze (actions) (push) Failing after 24s
CodeQL / Analyze (javascript-typescript) (push) Failing after 30s
CodeQL / Analyze (python) (push) Failing after 27s
2026-02-03 09:32:07 -06: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
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
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
724 changed files with 73293 additions and 72449 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.5.2
placeholder: v4.5.5
validations:
required: true
- type: dropdown
+1 -1
View File
@@ -27,7 +27,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.5.2
placeholder: v4.5.5
validations:
required: true
- type: dropdown
+1 -1
View File
@@ -8,7 +8,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.5.2
placeholder: v4.5.5
validations:
required: true
- type: dropdown
+8 -4
View File
@@ -55,6 +55,13 @@ jobs:
- name: Check out repo
uses: actions/checkout@v4
- name: Check Python linting & PEP8 compliance
uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 # v3.6.1
with:
version: "0.15.2"
args: "check --output-format=github"
src: "netbox/"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
@@ -82,7 +89,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install ruff coverage tblib
pip install coverage tblib
- name: Build documentation
run: mkdocs build
@@ -93,9 +100,6 @@ jobs:
- name: Check for missing migrations
run: python netbox/manage.py makemigrations --check
- name: Check PEP8 compliance
run: ruff check netbox/
- name: Check UI ESLint, TypeScript, and Prettier Compliance
run: yarn --cwd netbox/project-static validate
+37
View File
@@ -0,0 +1,37 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
jobs:
claude-review:
# Only run for PRs submitted by organization members or owners
if: |
github.repository == 'netbox-community/netbox' &&
(github.event.pull_request.author_association == 'MEMBER' ||
github.event.pull_request.author_association == 'OWNER')
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@e763fe78de2db7389e04818a00b5ff8ba13d1360 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
+80
View File
@@ -0,0 +1,80 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
# Workaround for claude-code-action bug with fork PRs: The action fetches by branch name
# (git fetch origin --depth=N <branch>), but fork PR branches don't exist on origin.
# Fix: redirect origin to the fork's URL so the action can fetch the branch directly.
- name: Configure git remote for fork PRs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Determine PR number based on event type
if [ "${{ github.event_name }}" = "issue_comment" ]; then
PR_NUMBER="${{ github.event.issue.number }}"
elif [ "${{ github.event_name }}" = "pull_request_review_comment" ] || [ "${{ github.event_name }}" = "pull_request_review" ]; then
PR_NUMBER="${{ github.event.pull_request.number }}"
else
exit 0 # issues event — no PR branch to worry about
fi
# Fetch fork info in one API call; silently skip if this is not a PR
PR_INFO=$(gh pr view "${PR_NUMBER}" --json isCrossRepository,headRepositoryOwner,headRepository 2>/dev/null || echo "")
if [ -z "$PR_INFO" ]; then
exit 0
fi
IS_FORK=$(echo "$PR_INFO" | jq -r '.isCrossRepository')
if [ "$IS_FORK" = "true" ]; then
FORK_OWNER=$(echo "$PR_INFO" | jq -r '.headRepositoryOwner.login')
FORK_REPO=$(echo "$PR_INFO" | jq -r '.headRepository.name')
echo "Fork PR detected from ${FORK_OWNER}/${FORK_REPO}: updating origin to fork URL"
git remote set-url origin "https://github.com/${FORK_OWNER}/${FORK_REPO}.git"
fi
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@e763fe78de2db7389e04818a00b5ff8ba13d1360 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'
+4 -4
View File
@@ -11,14 +11,14 @@ permissions:
pull-requests: write
discussions: write
concurrency:
group: lock-threads
jobs:
lock:
if: github.repository == 'netbox-community/netbox'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
- uses: dessant/lock-threads@v6.0.0
with:
issue-inactive-days: 90
pr-inactive-days: 30
discussion-inactive-days: 180
issue-lock-reason: 'resolved'
+1 -1
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.1
rev: v0.15.2
hooks:
- id: ruff
name: "Ruff linter"
+84
View File
@@ -0,0 +1,84 @@
# NetBox
Network source-of-truth and infrastructure resource modeling (IRM) tool combining DCIM and IPAM. Built on Django + PostgreSQL + Redis.
## Tech Stack
- Python 3.12+ / Django / Django REST Framework
- PostgreSQL (required), Redis (required for caching/queuing)
- GraphQL via Strawberry, background jobs via RQ
- Docs: MkDocs (in `docs/`)
## Repository Layout
- `netbox/` — Django project root; run all `manage.py` commands from here
- `netbox/netbox/` — Core settings, URLs, WSGI entrypoint
- `netbox/<app>/` — Django apps: `circuits`, `core`, `dcim`, `ipam`, `extras`, `tenancy`, `virtualization`, `wireless`, `users`, `vpn`
- `docs/` — MkDocs documentation source
- `contrib/` — Example configs (systemd, nginx, etc.) and other resources
## Development Setup
```bash
python -m venv ~/.venv/netbox
source ~/.venv/netbox/bin/activate
pip install -r requirements.txt
# Copy and configure
cp netbox/netbox/configuration.example.py netbox/netbox/configuration.py
# Edit configuration.py: set DATABASE, REDIS, SECRET_KEY, ALLOWED_HOSTS
cd netbox/
python manage.py migrate
python manage.py runserver
```
## Key Commands
All commands run from the `netbox/` subdirectory with venv active.
```bash
# Development server
python manage.py runserver
# Run full test suite
export NETBOX_CONFIGURATION=netbox.configuration_testing
python manage.py test
# Faster test runs (no DB rebuild, parallel)
python manage.py test --keepdb --parallel 4
# Migrations
python manage.py makemigrations
python manage.py migrate
# Shell
python manage.py nbshell # NetBox-enhanced shell
```
## Architecture Conventions
- **Apps**: Each Django app owns its models, views, API serializers, filtersets, forms, and tests.
- **REST API**: DRF serializers live in `<app>/api/serializers.py`; viewsets in `<app>/api/views.py`; URLs auto-registered in `<app>/api/urls.py`.
- **GraphQL**: Strawberry types in `<app>/graphql/types.py`.
- **Filtersets**: `<app>/filtersets.py` — used for both UI filtering and API `?filter=` params.
- **Tables**: `django-tables2` used for all object list views (`<app>/tables.py`).
- **Templates**: Django templates in `netbox/templates/<app>/`.
- **Tests**: Mirror the app structure in `<app>/tests/`. Use `netbox.configuration_testing` for test config.
## Coding Standards
- Follow existing Django conventions; don't reinvent patterns already present in the codebase.
- New models must include `created`, `last_updated` fields (inherit from `NetBoxModel` where appropriate).
- Every model exposed in the UI needs: model, serializer, filterset, form, table, views, URL route, and tests.
- API serializers must include a `url` field (absolute URL of the object).
- Use `FeatureQuery` for generic relations (config contexts, custom fields, tags, etc.).
- Avoid adding new dependencies without strong justification.
## Branch & PR Conventions
- Branch naming: `<issue-number>-short-description` (e.g., `1234-device-typerror`)
- Use the `main` branch for patch releases; `feature` tracks work for the upcoming minor/major release.
- Every PR must reference an approved GitHub issue.
- PRs must include tests for new functionality.
## Gotchas
- `configuration.py` is gitignored — never commit it.
- `manage.py` lives in `netbox/`, NOT the repo root. Running from the wrong directory is a common mistake.
- `NETBOX_CONFIGURATION` env var controls which settings module loads; set to `netbox.configuration_testing` for tests.
- The `extras` app is a catch-all for cross-cutting features (custom fields, tags, webhooks, scripts).
- Plugins API: only documented public APIs are stable. Internal NetBox code is subject to change without notice.
- See `docs/development/` for the full contributing guide and code style details.
+4 -2
View File
@@ -84,6 +84,8 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
* It's very important that you not submit a pull request until a relevant issue has been opened **and** assigned to you. Otherwise, you risk wasting time on work that may ultimately not be needed.
* Community members are limited to a maximum of **three open PRs** at any time. This is to avoid the accumulation of too much parallel work and maintain focus on already PRs under review. If you already have three NetBox PRs open, please wait for at least one of them to be merged (or closed) before opening another.
* New pull requests should generally be based off of the `main` branch. This branch, in keeping with the [trunk-based development](https://trunkbaseddevelopment.com/) approach, is used for ongoing development and bug fixes and always represents the newest stable code, from which releases are periodically branched. (If you're developing for an upcoming minor release, use `feature` instead.)
* In most cases, it is not necessary to add a changelog entry: A maintainer will take care of this when the PR is merged. (This helps avoid merge conflicts resulting from multiple PRs being submitted simultaneously.)
@@ -96,10 +98,10 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
greater than 80 characters in length
> [!CAUTION]
> Any contributions which include AI-generated or reproduced content will be rejected.
> Any contributions which include solely AI-generated or reproduced content will be rejected. All PRs must be submitted by a human.
* Some other tips to keep in mind:
* If you'd like to volunteer for someone else's issue, please post a comment on that issue letting us know. (This will allow the maintainers to assign it to you.)
* If you'd like to volunteer for someone else's issue, please post a comment on that issue letting us know. (GitHub allows only people who have commented on an issue to be assigned as its owner.)
* Check out our [developer docs](https://docs.netbox.dev/en/stable/development/getting-started/) for tips on setting up your development environment.
* All new functionality must include relevant tests where applicable.
+7 -4
View File
@@ -27,9 +27,7 @@ django-graphiql-debug-toolbar
django-htmx
# Modified Preorder Tree Traversal (recursive nesting of objects)
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
# v0.18.0 introduces errant migrations which need to be resolved
django-mptt==0.17.0
django-mptt
# Context managers for PostgreSQL advisory locks
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
@@ -49,7 +47,8 @@ django-rich
# Django integration for RQ (Reqis queuing)
# https://github.com/rq/django-rq/blob/master/CHANGELOG.md
django-rq
# See https://github.com/netbox-community/netbox/issues/21696
django-rq<4.0
# Provides a variety of storage backends
# https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst
@@ -100,6 +99,10 @@ jsonschema
# https://python-markdown.github.io/changelog/
Markdown
# MkDocs
# https://github.com/mkdocs/mkdocs/releases
mkdocs<2.0
# MkDocs Material theme (for documentation build)
# https://squidfunk.github.io/mkdocs-material/changelog/
mkdocs-material
+2
View File
@@ -349,6 +349,7 @@
"5gbase-t",
"10gbase-br-d",
"10gbase-br-u",
"10gbase-cu",
"10gbase-cx4",
"10gbase-er",
"10gbase-lr",
@@ -367,6 +368,7 @@
"40gbase-fr4",
"40gbase-lr4",
"40gbase-sr4",
"40gbase-sr4-bd",
"50gbase-cr",
"50gbase-er",
"50gbase-fr",
+1341 -559
View File
File diff suppressed because one or more lines are too long
+42
View File
@@ -200,6 +200,48 @@ REDIS = {
!!! note
It is permissible to use Sentinel for only one database and not the other.
### SSL Configuration
If you need to configure SSL/TLS for Redis beyond the basic `SSL`, `CA_CERT_PATH`, and `INSECURE_SKIP_TLS_VERIFY` options (for example, client certificates, a specific TLS version, or custom ciphers), you can pass additional parameters via the `KWARGS` key in either the `tasks` or `caching` subsection.
NetBox already maps `CA_CERT_PATH` to `ssl_ca_certs` and (for caching) `INSECURE_SKIP_TLS_VERIFY` to `ssl_cert_reqs`; only add `KWARGS` when you need to override or extend those settings (for example, to supply client certificates or restrict TLS version or ciphers).
* `KWARGS` - Optional dictionary of additional SSL/TLS (or other) parameters passed to the Redis client. These are passed directly to the underlying Redis client: for `tasks` to [redis-py](https://redis-py.readthedocs.io/en/stable/connections.html), and for `caching` to the [django-redis](https://github.com/jazzband/django-redis#configure-as-cache-backend) connection pool.
Example:
```python
REDIS = {
'tasks': {
'HOST': 'redis.example.com',
'PORT': 1234,
'SSL': True,
'CA_CERT_PATH': '/etc/ssl/certs/ca.crt',
'KWARGS': {
'ssl_certfile': '/path/to/client-cert.pem',
'ssl_keyfile': '/path/to/client-key.pem',
'ssl_min_version': ssl.TLSVersion.TLSv1_2,
'ssl_ciphers': 'HIGH:!aNULL',
},
},
'caching': {
'HOST': 'redis.example.com',
'PORT': 1234,
'SSL': True,
'CA_CERT_PATH': '/etc/ssl/certs/ca.crt',
'KWARGS': {
'ssl_certfile': '/path/to/client-cert.pem',
'ssl_keyfile': '/path/to/client-key.pem',
'ssl_min_version': ssl.TLSVersion.TLSv1_2,
'ssl_ciphers': 'HIGH:!aNULL',
},
}
}
```
!!! note
If you use `ssl.TLSVersion` in your configuration (e.g. `ssl_min_version`), add `import ssl` at the top of your configuration file.
---
## SECRET_KEY
+3 -1
View File
@@ -18,7 +18,8 @@ They can also be used as a mechanism for validating the integrity of data within
Custom scripts are Python code which exists outside the NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish.
!!! danger "Only install trusted scripts"
Custom scripts have unrestricted access to change anything in the databse and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
Custom scripts have unrestricted access to change anything in the database and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
## Writing Custom Scripts
@@ -214,6 +215,7 @@ if obj.pk and hasattr(obj, 'snapshot'):
obj.snapshot()
obj.property = "New Value"
obj._changelog_message = 'Example Message Text' # Optional
obj.full_clean()
obj.save()
```
+8
View File
@@ -168,6 +168,14 @@ Update the static OpenAPI schema definition at `contrib/openapi.json` with the m
./manage.py spectacular --format openapi-json > ../contrib/openapi.json
```
### Update Development Dependencies
Keep development tooling versions consistent across the project. If you upgrade a dev-only dependency, update all places where its pinned so local tooling and CI run the same versions.
* Ruff:
* `.pre-commit-config.yaml`
* `.github/workflows/ci.yml`
### Submit a Pull Request
Commit the above changes and submit a pull request titled **"Release vX.Y.Z"** to merge the current release branch (e.g. `release-vX.Y.Z`) into `main`. Copy the documented release notes into the pull request's body.
+10 -1
View File
@@ -34,7 +34,8 @@ The following rules are ignored when linting.
##### [E501](https://docs.astral.sh/ruff/rules/line-too-long/): Line too long
NetBox does not enforce a hard restriction on line length, although a maximum length of 120 characters is strongly encouraged for Python code where possible. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
NetBox enforces a maximum line length of 120 characters for Python code using Ruff (E501).
The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
##### [F403](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star/): Undefined local with import star
@@ -47,6 +48,14 @@ Wildcard imports (for example, `from .constants import *`) are acceptable under
The justification for ignoring this rule is the same as F403 above.
##### [RET504](https://docs.astral.sh/ruff/rules/unnecessary-assign/): Unnecessary assign
There are multiple instances where it is more readable and clearer to first assign to a variable and then return it.
##### [UP032](https://docs.astral.sh/ruff/rules/f-string/): f-string
For localizable strings, it is necessary to not use the `f-string` syntax, as Django's translation functions (e.g. `gettext_lazy`) require plain string literals.
### Introducing New Dependencies
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and supply chain attacks.
+15 -11
View File
@@ -23,9 +23,9 @@ For example, you might create a NetBox webhook to [trigger a Slack message](http
The following data is available as context for Jinja2 templates:
* `event` - The type of event which triggered the webhook: created, updated, or deleted.
* `model` - The NetBox model which triggered the change.
* `event` - The type of event which triggered the webhook: `created`, `updated`, or `deleted`.
* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format).
* `object_type` - The NetBox model which triggered the change in the form `app_label.model_name`.
* `username` - The name of the user account associated with the change.
* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
@@ -38,18 +38,20 @@ If no body template is specified, the request body will be populated with a JSON
```json
{
"event": "created",
"timestamp": "2021-03-09 17:55:33.968016+00:00",
"model": "site",
"timestamp": "2026-03-06T15:11:23.503186+00:00",
"object_type": "dcim.site",
"username": "jstretch",
"request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a",
"request_id": "17af32f0-852a-46ca-a7d4-33ecd0c13de6",
"data": {
"id": 19,
"id": 4,
"url": "/api/dcim/sites/4/",
"display_url": "/dcim/sites/4/",
"display": "Site 1",
"name": "Site 1",
"slug": "site-1",
"status":
"status": {
"value": "active",
"label": "Active",
"id": 1
"label": "Active"
},
"region": null,
...
@@ -57,8 +59,10 @@ If no body template is specified, the request body will be populated with a JSON
"snapshots": {
"prechange": null,
"postchange": {
"created": "2021-03-09",
"last_updated": "2021-03-09T17:55:33.851Z",
"created": "2026-03-06T15:11:23.484Z",
"owner": null,
"description": "",
"comments": "",
"name": "Site 1",
"slug": "site-1",
"status": "active",
+9 -6
View File
@@ -36,13 +36,16 @@ If false, synchronization will be disabled.
### Ignore Rules
A set of rules (one per line) identifying filenames to ignore during synchronization. Some examples are provided below. See Python's [`fnmatch()` documentation](https://docs.python.org/3/library/fnmatch.html) for a complete reference.
A set of rules (one per line) identifying files or paths to ignore during synchronization. Rules are matched against both the full relative path (e.g. `subdir/file.txt`) and the bare filename, so path-based patterns can be used to exclude entire directories. Some examples are provided below. See Python's [`fnmatch()` documentation](https://docs.python.org/3/library/fnmatch.html) for a complete reference.
| Rule | Description |
|----------------|------------------------------------------|
| `README` | Ignore any files named `README` |
| `*.txt` | Ignore any files with a `.txt` extension |
| `data???.json` | Ignore e.g. `data123.json` |
| Rule | Description |
|-----------------------|------------------------------------------------------|
| `README` | Ignore any files named `README` |
| `*.txt` | Ignore any files with a `.txt` extension |
| `data???.json` | Ignore e.g. `data123.json` |
| `subdir/*` | Ignore all files within `subdir/` |
| `subdir/*/*` | Ignore all files one level deep within `subdir/` |
| `*/dev/*` | Ignore files inside any directory named `dev/` |
### Sync Interval
+10 -10
View File
@@ -77,14 +77,14 @@ The file path to a particular certificate authority (CA) file to use when valida
## Context Data
The following context variables are available in to the text and link templates.
The following context variables are available to the text and link templates.
| Variable | Description |
|--------------|----------------------------------------------------|
| `event` | The event type (`create`, `update`, or `delete`) |
| `timestamp` | The time at which the event occured |
| `model` | The type of object impacted |
| `username` | The name of the user associated with the change |
| `request_id` | The unique request ID |
| `data` | A complete serialized representation of the object |
| `snapshots` | Pre- and post-change snapshots of the object |
| Variable | Description |
|---------------|------------------------------------------------------|
| `event` | The event type (`created`, `updated`, or `deleted`) |
| `timestamp` | The time at which the event occurred |
| `object_type` | The type of object impacted (`app_label.model_name`) |
| `username` | The name of the user associated with the change |
| `request_id` | The unique request ID |
| `data` | A complete serialized representation of the object |
| `snapshots` | Pre- and post-change snapshots of the object |
+106
View File
@@ -1,5 +1,111 @@
# NetBox v4.5
## v4.5.5 (2026-03-17)
### Enhancements
* [#21114](https://github.com/netbox-community/netbox/issues/21114) - Support path exclusions for data source synchronization
* [#21578](https://github.com/netbox-community/netbox/issues/21578) - Support identifying scope object by name or slug when bulk importing scoped objects
### Performance Improvements
* [#21330](https://github.com/netbox-community/netbox/issues/21330) - Optimize the assignment of tags when saving objects
* [#21402](https://github.com/netbox-community/netbox/issues/21402) - Avoid excessive database queries when rendering unnamed devices via the REST API
* [#21611](https://github.com/netbox-community/netbox/issues/21611) - Replace inefficient calls to `.count()` with `.exists()`
### Bug Fixes
* [#19867](https://github.com/netbox-community/netbox/issues/19867) - Preserve the "per page" pagination setting when returning from object edit forms
* [#20077](https://github.com/netbox-community/netbox/issues/20077) - Fix form field focus bug in Microsoft Edge
* [#20385](https://github.com/netbox-community/netbox/issues/20385) - Enforce `MAX_PAGE_SIZE` limit for GraphQL API requests
* [#20468](https://github.com/netbox-community/netbox/issues/20468) - Fix range-based filter lookups for integer fields in GraphQL API
* [#20915](https://github.com/netbox-community/netbox/issues/20915) - Restore user language preference after login via social authentication
* [#20934](https://github.com/netbox-community/netbox/issues/20934) - Fix dark mode flicker on page load
* [#21012](https://github.com/netbox-community/netbox/issues/21012) - Add pagination for VLAN table on interface view to prevent silent truncation at 100 entries
* [#21380](https://github.com/netbox-community/netbox/issues/21380) - Fix display of the background tasks table on mobile
* [#21440](https://github.com/netbox-community/netbox/issues/21440) - Avoid erroneously clearing primary/OOB IP assignments during bulk import/update
* [#21468](https://github.com/netbox-community/netbox/issues/21468) - Preserve safe custom HTTP headers when copying requests for background job processing
* [#21486](https://github.com/netbox-community/netbox/issues/21486) - Fix `AttributeError` exception caused by missing `COOKIES` attribute on `NetBoxFakeRequest`
* [#21512](https://github.com/netbox-community/netbox/issues/21512) - Fix GraphQL filter field name mismatch for device component types (e.g. `console_ports`)
* [#21531](https://github.com/netbox-community/netbox/issues/21531) - Fix search functionality for location when combined with other filters
* [#21556](https://github.com/netbox-community/netbox/issues/21556) - Avoid clearing the platform field when changing device type in the device edit form
* [#21579](https://github.com/netbox-community/netbox/issues/21579) - Hide the script "Add" button for users lacking the required permission
* [#21580](https://github.com/netbox-community/netbox/issues/21580) - Hide the virtual machine "Add components" dropdown for users lacking change permission
* [#21586](https://github.com/netbox-community/netbox/issues/21586) - Fix broken "Add child group" link in site group view (was pointing to the region endpoint)
* [#21618](https://github.com/netbox-community/netbox/issues/21618) - Fix cable termination points being lost when bulk-editing the cable profile
* [#21651](https://github.com/netbox-community/netbox/issues/21651) - Disable sorting by the `is_primary` column in the MAC address list view
* [#21653](https://github.com/netbox-community/netbox/issues/21653) - Fix profile-based cable tracing when a single origin carries multiple positions
* [#21673](https://github.com/netbox-community/netbox/issues/21673) - Fix display of primary IP address with associated NAT IP on virtual machine view
* [#21686](https://github.com/netbox-community/netbox/issues/21686) - Clean up cached circuit attributes when reassigning a circuit termination
---
## v4.5.4 (2026-03-03)
### Enhancements
* [#21369](https://github.com/netbox-community/netbox/issues/21369) - Support lazy-loading of image attachments
* [#21385](https://github.com/netbox-community/netbox/issues/21385) - Add contact assignment support for virtual circuits
* [#21394](https://github.com/netbox-community/netbox/issues/21394) - Add 10GBASE-CU and 40GBASE-SR4 BiDi interface types
* [#21477](https://github.com/netbox-community/netbox/issues/21477) - Extend GraphQL API filters for cables
### Performance Improvements
* [#21456](https://github.com/netbox-community/netbox/issues/21456) - Improve performance of config context resolution via GraphQL API
* [#21459](https://github.com/netbox-community/netbox/issues/21459) - Avoid prefetching data for hidden table columns
### Bug Fixes
* [#20490](https://github.com/netbox-community/netbox/issues/20490) - Restrict visibility of scripts in list view to users with view permission
* [#20911](https://github.com/netbox-community/netbox/issues/20911) - Sort module bay options alphabetically when installing a module
* [#21347](https://github.com/netbox-community/netbox/issues/21347) - The allocation of IPv6 addresses from a non-pool prefix should start at one, not zero
* [#21429](https://github.com/netbox-community/netbox/issues/21429) - Termination type should persist when employing "create & add another" workflow for cables
* [#21478](https://github.com/netbox-community/netbox/issues/21478) - Fix GraphQL union type resolution for connected console ports
* [#21481](https://github.com/netbox-community/netbox/issues/21481) - Fix display of facility ID on rack view
* [#21518](https://github.com/netbox-community/netbox/issues/21518) - Fix decimal custom field displaying as unset when value is zero
* [#21524](https://github.com/netbox-community/netbox/issues/21524) - Avoid `IndexError` exception when encountering stale cable paths
* [#21527](https://github.com/netbox-community/netbox/issues/21527) - Fix display of primary IP address with associated NAT IP on device view
* [#21550](https://github.com/netbox-community/netbox/issues/21550) - Ensure pre-change snapshots are recorded for related objects
---
## v4.5.3 (2026-02-17)
### Enhancements
* [#19129](https://github.com/netbox-community/netbox/issues/19129) - Improve display of multiple MAC addresses within interfaces table
* [#20981](https://github.com/netbox-community/netbox/issues/20981) - Enhance JSON rendering for custom validators and protection rules in config revision view
* [#21240](https://github.com/netbox-community/netbox/issues/21240) - Add support for configuring Redis `KWARGS` parameters
* [#21257](https://github.com/netbox-community/netbox/issues/21257) - `ContentTypeFilter` now accepts multiple values
* [#21266](https://github.com/netbox-community/netbox/issues/21266) - Add table columns representing installed devices to the device bays table
* [#21267](https://github.com/netbox-community/netbox/issues/21267) - Normalize device height formatting in rack units (display "0U")
* [#21268](https://github.com/netbox-community/netbox/issues/21268) - Add device type details panel to device view
* [#21337](https://github.com/netbox-community/netbox/issues/21337) - Show the assigned platform's parent on the virtual machine UI view
### Performance Improvements
* [#20211](https://github.com/netbox-community/netbox/issues/20211) - Use thumbnails for image attachment hover previews to improve page load performance
* [#21016](https://github.com/netbox-community/netbox/issues/21016) - Restore missing SQL indexes for MPTT fields
* [#21196](https://github.com/netbox-community/netbox/issues/21196) - `q` filter should match on primary IP only for IP address values when filtering devices/VMs
* [#21420](https://github.com/netbox-community/netbox/issues/21420) - Improve query performance of `ContentTypeFilter`
* [#21421](https://github.com/netbox-community/netbox/issues/21421) - Eliminate extraneous application of `DISTINCT` to queries for `MultipleChoiceFilter`
### Bug Fixes
* [#20435](https://github.com/netbox-community/netbox/issues/20435) - Fix navigation menu margin issue when scrollbar appears
* [#21127](https://github.com/netbox-community/netbox/issues/21127) - Ensure assigned cable paths are cleared when removing terminations from a cable
* [#21277](https://github.com/netbox-community/netbox/issues/21277) - Record pre-change snapshot when adding cluster members via UI
* [#21320](https://github.com/netbox-community/netbox/issues/21320) - Avoid validation failures when site or optional fields are missing during rack import
* [#21354](https://github.com/netbox-community/netbox/issues/21354) - Fix base URL in Swagger when `BASE_PATH` is set
* [#21358](https://github.com/netbox-community/netbox/issues/21358) - Token list in UI cannot be ordered by token value
* [#21371](https://github.com/netbox-community/netbox/issues/21371) - Fix `KeyError` exception when triggering a webhook from an event rule
* [#21375](https://github.com/netbox-community/netbox/issues/21375) - Address failure condition in `ipam.0070_vlangroup_vlan_id_ranges` migration
* [#21390](https://github.com/netbox-community/netbox/issues/21390) - Avoid creating "empty" changelog records for related objects when processing manyo-to-many relations
* [#21397](https://github.com/netbox-community/netbox/issues/21397) - Correct rendering of owner field in CircuitType edit form
* [#21412](https://github.com/netbox-community/netbox/issues/21412) - Avoid `AttributeError` exception on initialization when a plugin has local imports in `__init__.py`
---
## v4.5.2 (2026-02-03)
### Enhancements
+1
View File
@@ -1,6 +1,7 @@
from django.urls import include, path
from utilities.urls import get_model_urls
from . import views
app_name = 'account'
+7 -7
View File
@@ -2,14 +2,15 @@ import logging
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import update_last_login
from django.contrib.auth.signals import user_logged_in
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import render, resolve_url
from django.shortcuts import get_object_or_404, redirect, render, resolve_url
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import urlencode
@@ -35,11 +36,11 @@ from utilities.request import safe_for_redirect
from utilities.string import remove_linebreaks
from utilities.views import register_model_view
#
# Login/logout
#
class LoginView(View):
"""
Perform user authentication via the web UI.
@@ -139,9 +140,8 @@ class LoginView(View):
return response
else:
username = form['username'].value()
logger.debug(f"Login form validation failed for username: {remove_linebreaks(username)}")
username = form['username'].value()
logger.debug(f"Login form validation failed for username: {remove_linebreaks(username)}")
return render(request, self.template_name, {
'form': form,
+1 -1
View File
@@ -1,2 +1,2 @@
from .serializers_.providers import *
from .serializers_.circuits import *
from .serializers_.providers import *
+15 -5
View File
@@ -4,24 +4,34 @@ from rest_framework import serializers
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES
from circuits.models import (
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
VirtualCircuitTermination, VirtualCircuitType,
Circuit,
CircuitGroup,
CircuitGroupAssignment,
CircuitTermination,
CircuitType,
VirtualCircuit,
VirtualCircuitTermination,
VirtualCircuitType,
)
from dcim.api.serializers_.device_components import InterfaceSerializer
from dcim.api.serializers_.cables import CabledObjectSerializer
from dcim.api.serializers_.device_components import InterfaceSerializer
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.gfk_fields import GFKSerializerField
from netbox.api.serializers import (
NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer,
NetBoxModelSerializer,
OrganizationalModelSerializer,
PrimaryModelSerializer,
WritableNestedSerializer,
)
from netbox.choices import DistanceUnitChoices
from tenancy.api.serializers_.tenants import TenantSerializer
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
__all__ = (
'CircuitSerializer',
'CircuitGroupAssignmentSerializer',
'CircuitGroupSerializer',
'CircuitSerializer',
'CircuitTerminationSerializer',
'CircuitTypeSerializer',
'VirtualCircuitSerializer',
@@ -5,6 +5,7 @@ from ipam.api.serializers_.asns import ASNSerializer
from ipam.models import ASN
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import PrimaryModelSerializer
from .nested import NestedProviderAccountSerializer
__all__ = (
+1 -1
View File
@@ -1,6 +1,6 @@
from netbox.api.routers import NetBoxRouter
from . import views
from . import views
router = NetBoxRouter()
router.APIRootView = views.CircuitsRootView
+1
View File
@@ -4,6 +4,7 @@ from circuits import filtersets
from circuits.models import *
from dcim.api.views import PassThroughPortMixin
from netbox.api.viewsets import NetBoxModelViewSet
from . import serializers
+2 -1
View File
@@ -9,7 +9,8 @@ class CircuitsConfig(AppConfig):
def ready(self):
from netbox.models.features import register_models
from . import signals, search # noqa: F401
from . import search, signals # noqa: F401
from .models import CircuitTermination
# Register models
+1 -1
View File
@@ -2,11 +2,11 @@ from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
#
# Circuits
#
class CircuitStatusChoices(ChoiceSet):
key = 'Circuit.status'
-1
View File
@@ -1,6 +1,5 @@
from django.db.models import Q
# models values for ContentTypes which may be CircuitTermination termination types
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
'region', 'sitegroup', 'site', 'location', 'providernetwork',
+45 -4
View File
@@ -9,9 +9,13 @@ from ipam.models import ASN
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
MultiValueCharFilter,
MultiValueContentTypeFilter,
MultiValueNumberFilter,
TreeNodeMultipleChoiceFilter,
)
from utilities.filtersets import register_filterset
from .choices import *
from .models import *
@@ -21,9 +25,9 @@ __all__ = (
'CircuitGroupFilterSet',
'CircuitTerminationFilterSet',
'CircuitTypeFilterSet',
'ProviderNetworkFilterSet',
'ProviderAccountFilterSet',
'ProviderFilterSet',
'ProviderNetworkFilterSet',
'VirtualCircuitFilterSet',
'VirtualCircuitTerminationFilterSet',
'VirtualCircuitTypeFilterSet',
@@ -99,11 +103,13 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
distinct=False,
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider__slug',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Provider (slug)'),
)
@@ -127,11 +133,13 @@ class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
distinct=False,
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider__slug',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Provider (slug)'),
)
@@ -163,22 +171,26 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
distinct=False,
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider__slug',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Provider (slug)'),
)
provider_account_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account',
queryset=ProviderAccount.objects.all(),
distinct=False,
label=_('Provider account (ID)'),
)
provider_account = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account__account',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='account',
label=_('Provider account (account)'),
)
@@ -189,16 +201,19 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
)
type_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitType.objects.all(),
distinct=False,
label=_('Circuit type (ID)'),
)
type = django_filters.ModelMultipleChoiceFilter(
field_name='type__slug',
queryset=CircuitType.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Circuit type (slug)'),
)
status = django_filters.MultipleChoiceFilter(
choices=CircuitStatusChoices,
distinct=False,
null_value=None
)
region_id = TreeNodeMultipleChoiceFilter(
@@ -245,10 +260,12 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
)
termination_a_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitTermination.objects.all(),
distinct=False,
label=_('Termination A (ID)'),
)
termination_z_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitTermination.objects.all(),
distinct=False,
label=_('Termination A (ID)'),
)
@@ -279,9 +296,10 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
)
circuit_id = django_filters.ModelMultipleChoiceFilter(
queryset=Circuit.objects.all(),
distinct=False,
label=_('Circuit'),
)
termination_type = ContentTypeFilter()
termination_type = MultiValueContentTypeFilter()
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
@@ -310,12 +328,14 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
distinct=False,
field_name='_site',
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='_site__slug',
queryset=Site.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Site (slug)'),
)
@@ -334,17 +354,20 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
distinct=False,
field_name='_provider_network',
label=_('ProviderNetwork (ID)'),
)
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='circuit__provider_id',
queryset=Provider.objects.all(),
distinct=False,
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='circuit__provider__slug',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Provider (slug)'),
)
@@ -381,7 +404,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
method='search',
label=_('Search'),
)
member_type = ContentTypeFilter()
member_type = MultiValueContentTypeFilter()
circuit = MultiValueCharFilter(
method='filter_circuit',
field_name='cid',
@@ -414,11 +437,13 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
)
group_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitGroup.objects.all(),
distinct=False,
label=_('Circuit group (ID)'),
)
group = django_filters.ModelMultipleChoiceFilter(
field_name='group__slug',
queryset=CircuitGroup.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Circuit group (slug)'),
)
@@ -488,41 +513,49 @@ class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_network__provider',
queryset=Provider.objects.all(),
distinct=False,
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider_network__provider__slug',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Provider (slug)'),
)
provider_account_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account',
queryset=ProviderAccount.objects.all(),
distinct=False,
label=_('Provider account (ID)'),
)
provider_account = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account__account',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='account',
label=_('Provider account (account)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
distinct=False,
label=_('Provider network (ID)'),
)
type_id = django_filters.ModelMultipleChoiceFilter(
queryset=VirtualCircuitType.objects.all(),
distinct=False,
label=_('Virtual circuit type (ID)'),
)
type = django_filters.ModelMultipleChoiceFilter(
field_name='type__slug',
queryset=VirtualCircuitType.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Virtual circuit type (slug)'),
)
status = django_filters.MultipleChoiceFilter(
choices=CircuitStatusChoices,
distinct=False,
null_value=None
)
@@ -548,41 +581,49 @@ class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
)
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
queryset=VirtualCircuit.objects.all(),
distinct=False,
label=_('Virtual circuit'),
)
role = django_filters.MultipleChoiceFilter(
choices=VirtualCircuitTerminationRoleChoices,
distinct=False,
null_value=None
)
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_network__provider',
queryset=Provider.objects.all(),
distinct=False,
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_network__provider__slug',
queryset=Provider.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Provider (slug)'),
)
provider_account_id = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_account',
queryset=ProviderAccount.objects.all(),
distinct=False,
label=_('Provider account (ID)'),
)
provider_account = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_account__account',
queryset=ProviderAccount.objects.all(),
distinct=False,
to_field_name='account',
label=_('Provider account (account)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
distinct=False,
field_name='virtual_circuit__provider_network',
label=_('Provider network (ID)'),
)
interface_id = django_filters.ModelMultipleChoiceFilter(
queryset=Interface.objects.all(),
distinct=False,
field_name='interface',
label=_('Interface (ID)'),
)
+9 -3
View File
@@ -4,7 +4,10 @@ from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from circuits.choices import (
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices,
CircuitCommitRateChoices,
CircuitPriorityChoices,
CircuitStatusChoices,
VirtualCircuitTerminationRoleChoices,
)
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
from circuits.models import *
@@ -15,7 +18,10 @@ from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditFor
from tenancy.models import Tenant
from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import (
ColorField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
ColorField,
ContentTypeChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
@@ -27,8 +33,8 @@ __all__ = (
'CircuitGroupBulkEditForm',
'CircuitTerminationBulkEditForm',
'CircuitTypeBulkEditForm',
'ProviderBulkEditForm',
'ProviderAccountBulkEditForm',
'ProviderBulkEditForm',
'ProviderNetworkBulkEditForm',
'VirtualCircuitBulkEditForm',
'VirtualCircuitTerminationBulkEditForm',
+2 -2
View File
@@ -12,14 +12,14 @@ from tenancy.models import Tenant
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
__all__ = (
'CircuitImportForm',
'CircuitGroupAssignmentImportForm',
'CircuitGroupImportForm',
'CircuitImportForm',
'CircuitTerminationImportForm',
'CircuitTerminationImportRelatedForm',
'CircuitTypeImportForm',
'ProviderImportForm',
'ProviderAccountImportForm',
'ProviderImportForm',
'ProviderNetworkImportForm',
'VirtualCircuitImportForm',
'VirtualCircuitTerminationImportForm',
+6 -3
View File
@@ -2,7 +2,10 @@ from django import forms
from django.utils.translation import gettext as _
from circuits.choices import (
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices,
CircuitCommitRateChoices,
CircuitPriorityChoices,
CircuitStatusChoices,
CircuitTerminationSideChoices,
VirtualCircuitTerminationRoleChoices,
)
from circuits.models import *
@@ -10,7 +13,7 @@ from dcim.models import Location, Region, Site, SiteGroup
from ipam.models import ASN
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet
@@ -22,8 +25,8 @@ __all__ = (
'CircuitGroupFilterForm',
'CircuitTerminationFilterForm',
'CircuitTypeFilterForm',
'ProviderFilterForm',
'ProviderAccountFilterForm',
'ProviderFilterForm',
'ProviderNetworkFilterForm',
'VirtualCircuitFilterForm',
'VirtualCircuitTerminationFilterForm',
+10 -5
View File
@@ -4,7 +4,9 @@ from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from circuits.choices import (
CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices, VirtualCircuitTerminationRoleChoices,
CircuitCommitRateChoices,
CircuitTerminationPortSpeedChoices,
VirtualCircuitTerminationRoleChoices,
)
from circuits.constants import *
from circuits.models import *
@@ -14,7 +16,10 @@ from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelF
from tenancy.forms import TenancyForm
from utilities.forms import get_field_value
from utilities.forms.fields import (
ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
ContentTypeChoiceField,
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
SlugField,
)
from utilities.forms.mixins import DistanceValidationMixin
from utilities.forms.rendering import FieldSet, InlineFields
@@ -27,8 +32,8 @@ __all__ = (
'CircuitGroupForm',
'CircuitTerminationForm',
'CircuitTypeForm',
'ProviderForm',
'ProviderAccountForm',
'ProviderForm',
'ProviderNetworkForm',
'VirtualCircuitForm',
'VirtualCircuitTerminationForm',
@@ -91,13 +96,13 @@ class ProviderNetworkForm(PrimaryModelForm):
class CircuitTypeForm(OrganizationalModelForm):
fieldsets = (
FieldSet('name', 'slug', 'color', 'description', 'owner', 'tags'),
FieldSet('name', 'slug', 'color', 'description', 'tags'),
)
class Meta:
model = CircuitType
fields = [
'name', 'slug', 'color', 'description', 'comments', 'tags',
'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
]
+1 -1
View File
@@ -3,9 +3,9 @@ import strawberry
from circuits.choices import *
__all__ = (
'CircuitPriorityEnum',
'CircuitStatusEnum',
'CircuitTerminationSideEnum',
'CircuitPriorityEnum',
'VirtualCircuitTerminationRoleEnum',
)
+1 -1
View File
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Annotated, TYPE_CHECKING
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
+16 -15
View File
@@ -1,10 +1,10 @@
from datetime import date
from typing import Annotated, TYPE_CHECKING
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, FilterLookup, DateFilterLookup
from strawberry_django import BaseFilterLookup, DateFilterLookup, StrFilterLookup
from circuits import models
from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
from dcim.graphql.filters import InterfaceFilter, LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
from ipam.graphql.filters import ASNFilter
from netbox.graphql.filter_lookups import IntegerLookup
from .enums import *
__all__ = (
@@ -27,8 +28,8 @@ __all__ = (
'CircuitGroupFilter',
'CircuitTerminationFilter',
'CircuitTypeFilter',
'ProviderFilter',
'ProviderAccountFilter',
'ProviderFilter',
'ProviderNetworkFilter',
'VirtualCircuitFilter',
'VirtualCircuitTerminationFilter',
@@ -61,9 +62,9 @@ class CircuitTerminationFilter(
upstream_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
xconnect_id: FilterLookup[str] | None = strawberry_django.filter_field()
pp_info: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field()
xconnect_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
pp_info: StrFilterLookup[str] | None = strawberry_django.filter_field()
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
# Cached relations
_provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@@ -91,7 +92,7 @@ class CircuitFilter(
TenancyFilterMixin,
PrimaryModelFilter
):
cid: FilterLookup[str] | None = strawberry_django.filter_field()
cid: StrFilterLookup[str] | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -144,8 +145,8 @@ class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, Cha
@strawberry_django.filter_type(models.Provider, lookups=True)
class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field()
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
circuits: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -158,18 +159,18 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
strawberry_django.filter_field()
)
provider_id: ID | None = strawberry_django.filter_field()
account: FilterLookup[str] | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field()
account: StrFilterLookup[str] | None = strawberry_django.filter_field()
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
class ProviderNetworkFilter(PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field()
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
provider_id: ID | None = strawberry_django.filter_field()
service_id: FilterLookup[str] | None = strawberry_django.filter_field()
service_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
@@ -179,7 +180,7 @@ class VirtualCircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
cid: FilterLookup[str] | None = strawberry_django.filter_field()
cid: StrFilterLookup[str] | None = strawberry_django.filter_field()
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@@ -217,4 +218,4 @@ class VirtualCircuitTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin,
strawberry_django.filter_field()
)
interface_id: ID | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field()
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
+11 -13
View File
@@ -1,5 +1,3 @@
from typing import List
import strawberry
import strawberry_django
@@ -9,34 +7,34 @@ from .types import *
@strawberry.type(name="Query")
class CircuitsQuery:
circuit: CircuitType = strawberry_django.field()
circuit_list: List[CircuitType] = strawberry_django.field()
circuit_list: list[CircuitType] = strawberry_django.field()
circuit_termination: CircuitTerminationType = strawberry_django.field()
circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field()
circuit_termination_list: list[CircuitTerminationType] = strawberry_django.field()
circuit_type: CircuitTypeType = strawberry_django.field()
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
circuit_type_list: list[CircuitTypeType] = strawberry_django.field()
circuit_group: CircuitGroupType = strawberry_django.field()
circuit_group_list: List[CircuitGroupType] = strawberry_django.field()
circuit_group_list: list[CircuitGroupType] = strawberry_django.field()
circuit_group_assignment: CircuitGroupAssignmentType = strawberry_django.field()
circuit_group_assignment_list: List[CircuitGroupAssignmentType] = strawberry_django.field()
circuit_group_assignment_list: list[CircuitGroupAssignmentType] = strawberry_django.field()
provider: ProviderType = strawberry_django.field()
provider_list: List[ProviderType] = strawberry_django.field()
provider_list: list[ProviderType] = strawberry_django.field()
provider_account: ProviderAccountType = strawberry_django.field()
provider_account_list: List[ProviderAccountType] = strawberry_django.field()
provider_account_list: list[ProviderAccountType] = strawberry_django.field()
provider_network: ProviderNetworkType = strawberry_django.field()
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
provider_network_list: list[ProviderNetworkType] = strawberry_django.field()
virtual_circuit: VirtualCircuitType = strawberry_django.field()
virtual_circuit_list: List[VirtualCircuitType] = strawberry_django.field()
virtual_circuit_list: list[VirtualCircuitType] = strawberry_django.field()
virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field()
virtual_circuit_termination_list: List[VirtualCircuitTerminationType] = strawberry_django.field()
virtual_circuit_termination_list: list[VirtualCircuitTerminationType] = strawberry_django.field()
virtual_circuit_type: VirtualCircuitTypeType = strawberry_django.field()
virtual_circuit_type_list: List[VirtualCircuitTypeType] = strawberry_django.field()
virtual_circuit_type_list: list[VirtualCircuitTypeType] = strawberry_django.field()
+29 -26
View File
@@ -1,4 +1,4 @@
from typing import Annotated, List, TYPE_CHECKING, Union
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
@@ -8,6 +8,7 @@ from dcim.graphql.mixins import CabledObjectMixin
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType, PrimaryObjectType
from tenancy.graphql.types import TenantType
from .filters import *
if TYPE_CHECKING:
@@ -20,9 +21,9 @@ __all__ = (
'CircuitTerminationType',
'CircuitType',
'CircuitTypeType',
'ProviderType',
'ProviderAccountType',
'ProviderNetworkType',
'ProviderType',
'VirtualCircuitTerminationType',
'VirtualCircuitType',
'VirtualCircuitTypeType',
@@ -36,10 +37,10 @@ __all__ = (
pagination=True
)
class ProviderType(ContactsMixin, PrimaryObjectType):
networks: List[Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')]]
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
accounts: List[Annotated["ProviderAccountType", strawberry.lazy('circuits.graphql.types')]]
networks: list[Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')]]
circuits: list[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
asns: list[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
accounts: list[Annotated["ProviderAccountType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@@ -50,7 +51,7 @@ class ProviderType(ContactsMixin, PrimaryObjectType):
)
class ProviderAccountType(ContactsMixin, PrimaryObjectType):
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
circuits: list[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@@ -61,7 +62,7 @@ class ProviderAccountType(ContactsMixin, PrimaryObjectType):
)
class ProviderNetworkType(PrimaryObjectType):
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
circuit_terminations: List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
circuit_terminations: list[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@@ -71,16 +72,17 @@ class ProviderNetworkType(PrimaryObjectType):
pagination=True
)
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
circuit: Annotated['CircuitType', strawberry.lazy('circuits.graphql.types')]
@strawberry_django.field
def termination(self) -> Annotated[Union[
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
], strawberry.union("CircuitTerminationTerminationType")] | None:
def termination(self) -> Annotated[
Annotated['LocationType', strawberry.lazy('dcim.graphql.types')]
| Annotated['RegionType', strawberry.lazy('dcim.graphql.types')]
| Annotated['SiteGroupType', strawberry.lazy('dcim.graphql.types')]
| Annotated['SiteType', strawberry.lazy('dcim.graphql.types')]
| Annotated['ProviderNetworkType', strawberry.lazy('circuits.graphql.types')],
strawberry.union('CircuitTerminationTerminationType'),
] | None:
return self.termination
@@ -93,7 +95,7 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob
class CircuitTypeType(OrganizationalObjectType):
color: str
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
circuits: list[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@@ -109,7 +111,7 @@ class CircuitType(PrimaryObjectType, ContactsMixin):
termination_z: CircuitTerminationType | None
type: CircuitTypeType
tenant: TenantType | None
terminations: List[CircuitTerminationType]
terminations: list[CircuitTerminationType]
@strawberry_django.type(
@@ -129,13 +131,14 @@ class CircuitGroupType(OrganizationalObjectType):
pagination=True
)
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
group: Annotated['CircuitGroupType', strawberry.lazy('circuits.graphql.types')]
@strawberry_django.field
def member(self) -> Annotated[Union[
Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')],
Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')],
], strawberry.union("CircuitGroupAssignmentMemberType")] | None:
def member(self) -> Annotated[
Annotated['CircuitType', strawberry.lazy('circuits.graphql.types')]
| Annotated['VirtualCircuitType', strawberry.lazy('circuits.graphql.types')],
strawberry.union('CircuitGroupAssignmentMemberType'),
] | None:
return self.member
@@ -148,7 +151,7 @@ class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
class VirtualCircuitTypeType(OrganizationalObjectType):
color: str
virtual_circuits: List[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
virtual_circuits: list[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
@@ -174,11 +177,11 @@ class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
filters=VirtualCircuitFilter,
pagination=True
)
class VirtualCircuitType(PrimaryObjectType):
class VirtualCircuitType(ContactsMixin, PrimaryObjectType):
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
provider_account: ProviderAccountType | None
type: Annotated["VirtualCircuitTypeType", strawberry.lazy('circuits.graphql.types')] = strawberry_django.field(
select_related=["type"]
)
tenant: TenantType | None
terminations: List[VirtualCircuitTerminationType]
terminations: list[VirtualCircuitTerminationType]
+3 -2
View File
@@ -1,7 +1,8 @@
import django.db.models.deletion
from django.db import migrations, models
import ipam.fields
from utilities.json import CustomFieldJSONEncoder
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
@@ -1,6 +1,6 @@
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
from django.db import migrations, models
class Migration(migrations.Migration):
@@ -1,6 +1,7 @@
# Generated by Django 4.2.5 on 2023-10-20 21:25
from django.db import migrations
import utilities.fields
@@ -1,8 +1,9 @@
import django.db.models.deletion
import taggit.managers
import utilities.json
from django.db import migrations, models
import utilities.json
class Migration(migrations.Migration):
dependencies = [
+43 -2
View File
@@ -8,10 +8,16 @@ from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from dcim.models import CabledObjectModel
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
from netbox.models.mixins import DistanceMixin
from netbox.models.features import (
ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin,
ContactsMixin,
CustomFieldsMixin,
CustomLinksMixin,
ExportTemplatesMixin,
ImageAttachmentsMixin,
TagsMixin,
)
from netbox.models.mixins import DistanceMixin
from .base import BaseCircuitType
__all__ = (
@@ -341,6 +347,13 @@ class CircuitTermination(
verbose_name = _('circuit termination')
verbose_name_plural = _('circuit terminations')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Cache original values to detect changes
self._orig_circuit_id = self.__dict__.get('circuit_id')
self._orig_term_side = self.__dict__.get('term_side')
def __str__(self):
return f'{self.circuit}: Termination {self.term_side}'
@@ -354,11 +367,39 @@ class CircuitTermination(
raise ValidationError(_("A circuit termination must attach to a terminating object."))
def save(self, *args, **kwargs):
is_new = self._state.adding
update_fields = kwargs.get('update_fields')
# Only consider circuit/term_side changes if those fields
# are actually being persisted
if update_fields is not None:
tracking_relevant = 'circuit' in update_fields or 'term_side' in update_fields
else:
tracking_relevant = True
circuit_changed = tracking_relevant and self._orig_circuit_id and self._orig_circuit_id != self.circuit_id
term_side_changed = tracking_relevant and self._orig_term_side and self._orig_term_side != self.term_side
# Cache objects associated with the terminating object (for filtering)
self.cache_related_objects()
super().save(*args, **kwargs)
# Clear the old termination reference if circuit or term_side changed
if circuit_changed or term_side_changed:
old_termination_name = f'termination_{self._orig_term_side.lower()}'
Circuit.objects.filter(pk=self._orig_circuit_id).update(**{old_termination_name: None})
# Update the cache if this is a new termination or circuit/term_side changed
if is_new or circuit_changed or term_side_changed:
# Update the new circuit's termination reference
termination_name = f'termination_{self.term_side.lower()}'
Circuit.objects.filter(pk=self.circuit_id).update(**{termination_name: self.pk})
# Update cached values for subsequent saves
self._orig_circuit_id = self.circuit_id
self._orig_term_side = self.term_side
def cache_related_objects(self):
self._provider_network = self._region = self._site_group = self._site = self._location = None
if self.termination_type:
+1 -1
View File
@@ -6,9 +6,9 @@ from netbox.models import PrimaryModel
from netbox.models.features import ContactsMixin
__all__ = (
'ProviderNetwork',
'Provider',
'ProviderAccount',
'ProviderNetwork',
)
+5 -2
View File
@@ -8,7 +8,8 @@ from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from netbox.models import ChangeLoggedModel, PrimaryModel
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin
from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin
from .base import BaseCircuitType
__all__ = (
@@ -29,7 +30,7 @@ class VirtualCircuitType(BaseCircuitType):
verbose_name_plural = _('virtual circuit types')
class VirtualCircuit(PrimaryModel):
class VirtualCircuit(ContactsMixin, PrimaryModel):
"""
A virtual connection between two or more endpoints, delivered across one or more physical circuits.
"""
@@ -184,6 +185,8 @@ class VirtualCircuitTermination(
return self.virtual_circuit.terminations.filter(
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB
)
# Fallback for unexpected roles
return self.virtual_circuit.terminations.none()
def clean(self):
super().clean()
+1
View File
@@ -1,4 +1,5 @@
from netbox.search import SearchIndex, register_search
from . import models
+1 -11
View File
@@ -2,20 +2,10 @@ from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from dcim.signals import rebuild_paths
from .models import CircuitTermination
@receiver(post_save, sender=CircuitTermination)
def update_circuit(instance, **kwargs):
"""
When a CircuitTermination has been modified, update its parent Circuit.
"""
termination_name = f'termination_{instance.term_side.lower()}'
instance.circuit.refresh_from_db()
setattr(instance.circuit, termination_name, instance)
instance.circuit.save()
@receiver((post_save, post_delete), sender=CircuitTermination)
def rebuild_cablepaths(instance, raw=False, **kwargs):
"""
+1
View File
@@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
from circuits.models import *
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from .columns import CommitRateColumn
__all__ = (
+1 -1
View File
@@ -7,9 +7,9 @@ from netbox.tables import PrimaryModelTable, columns
from tenancy.tables import ContactsColumnMixin
__all__ = (
'ProviderTable',
'ProviderAccountTable',
'ProviderNetworkTable',
'ProviderTable',
)
+1 -1
View File
@@ -71,7 +71,7 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModel
model = VirtualCircuit
fields = (
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
'tenant_group', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
+10 -1
View File
@@ -5,7 +5,16 @@ from circuits.filtersets import *
from circuits.models import *
from dcim.choices import InterfaceTypeChoices, LocationStatusChoices
from dcim.models import (
Cable, Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Region, Site, SiteGroup
Cable,
Device,
DeviceRole,
DeviceType,
Interface,
Location,
Manufacturer,
Region,
Site,
SiteGroup,
)
from ipam.models import ASN, RIR
from netbox.choices import DistanceUnitChoices
+148
View File
@@ -0,0 +1,148 @@
from django.test import TestCase
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, ProviderNetwork
from dcim.models import Site
class CircuitTerminationTestCase(TestCase):
@classmethod
def setUpTestData(cls):
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
cls.sites = (
Site.objects.create(name='Site 1', slug='site-1'),
Site.objects.create(name='Site 2', slug='site-2'),
)
cls.circuits = (
Circuit.objects.create(cid='Circuit 1', provider=provider, type=circuit_type),
Circuit.objects.create(cid='Circuit 2', provider=provider, type=circuit_type),
)
cls.provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider)
def test_circuit_termination_creation_populates_circuit_cache(self):
"""
When a CircuitTermination is created, the parent Circuit's termination_a or termination_z
cache field should be populated.
"""
# Create A termination
termination_a = CircuitTermination.objects.create(
circuit=self.circuits[0],
term_side='A',
termination=self.sites[0],
)
self.circuits[0].refresh_from_db()
self.assertEqual(self.circuits[0].termination_a, termination_a)
self.assertIsNone(self.circuits[0].termination_z)
# Create Z termination
termination_z = CircuitTermination.objects.create(
circuit=self.circuits[0],
term_side='Z',
termination=self.sites[1],
)
self.circuits[0].refresh_from_db()
self.assertEqual(self.circuits[0].termination_a, termination_a)
self.assertEqual(self.circuits[0].termination_z, termination_z)
def test_circuit_termination_circuit_change_clears_old_cache(self):
"""
When a CircuitTermination's circuit is changed, the old Circuit's cache should be cleared
and the new Circuit's cache should be populated.
"""
# Create termination on self.circuits[0]
termination = CircuitTermination.objects.create(
circuit=self.circuits[0],
term_side='A',
termination=self.sites[0],
)
self.circuits[0].refresh_from_db()
self.assertEqual(self.circuits[0].termination_a, termination)
# Move termination to self.circuits[1]
termination.circuit = self.circuits[1]
termination.save()
self.circuits[0].refresh_from_db()
self.circuits[1].refresh_from_db()
# Old circuit's cache should be cleared
self.assertIsNone(self.circuits[0].termination_a)
# New circuit's cache should be populated
self.assertEqual(self.circuits[1].termination_a, termination)
def test_circuit_termination_term_side_change_clears_old_cache(self):
"""
When a CircuitTermination's term_side is changed, the old side's cache should be cleared
and the new side's cache should be populated.
"""
# Create A termination
termination = CircuitTermination.objects.create(
circuit=self.circuits[0],
term_side='A',
termination=self.sites[0],
)
self.circuits[0].refresh_from_db()
self.assertEqual(self.circuits[0].termination_a, termination)
self.assertIsNone(self.circuits[0].termination_z)
# Change from A to Z
termination.term_side = 'Z'
termination.save()
self.circuits[0].refresh_from_db()
# A side should be cleared, Z side should be populated
self.assertIsNone(self.circuits[0].termination_a)
self.assertEqual(self.circuits[0].termination_z, termination)
def test_circuit_termination_circuit_and_term_side_change(self):
"""
When both circuit and term_side are changed, the old Circuit's old side cache should be
cleared and the new Circuit's new side cache should be populated.
"""
# Create A termination on self.circuits[0]
termination = CircuitTermination.objects.create(
circuit=self.circuits[0],
term_side='A',
termination=self.sites[0],
)
self.circuits[0].refresh_from_db()
self.assertEqual(self.circuits[0].termination_a, termination)
# Change to self.circuits[1] Z side
termination.circuit = self.circuits[1]
termination.term_side = 'Z'
termination.save()
self.circuits[0].refresh_from_db()
self.circuits[1].refresh_from_db()
# Old circuit's A side should be cleared
self.assertIsNone(self.circuits[0].termination_a)
self.assertIsNone(self.circuits[0].termination_z)
# New circuit's Z side should be populated
self.assertIsNone(self.circuits[1].termination_a)
self.assertEqual(self.circuits[1].termination_z, termination)
def test_circuit_termination_deletion_clears_cache(self):
"""
When a CircuitTermination is deleted, the parent Circuit's cache should be cleared.
"""
termination = CircuitTermination.objects.create(
circuit=self.circuits[0],
term_side='A',
termination=self.sites[0],
)
self.circuits[0].refresh_from_db()
self.assertEqual(self.circuits[0].termination_a, termination)
# Delete the termination
termination.delete()
self.circuits[0].refresh_from_db()
# Cache should be cleared (SET_NULL behavior)
self.assertIsNone(self.circuits[0].termination_a)
+1 -1
View File
@@ -1,4 +1,4 @@
from django.test import RequestFactory, tag, TestCase
from django.test import RequestFactory, TestCase, tag
from circuits.models import CircuitTermination
from circuits.tables import CircuitTerminationTable
+1
View File
@@ -1,6 +1,7 @@
from django.urls import include, path
from utilities.urls import get_model_urls
from . import views
app_name = 'circuits'
+2 -1
View File
@@ -5,14 +5,15 @@ from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, B
from netbox.views import generic
from utilities.query import count_related
from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables
from .models import *
#
# Providers
#
@register_model_view(Provider, 'list', path='', detail=False)
class ProviderListView(generic.ObjectListView):
queryset = Provider.objects.annotate(
+38 -7
View File
@@ -2,10 +2,16 @@ import re
import typing
from collections import OrderedDict
from drf_spectacular.extensions import OpenApiSerializerFieldExtension, OpenApiSerializerExtension, _SchemaType
from drf_spectacular.contrib.django_filters import DjangoFilterExtension
from drf_spectacular.extensions import OpenApiSerializerExtension, OpenApiSerializerFieldExtension, _SchemaType
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.plumbing import (
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
build_basic_type,
build_choice_field,
build_media_type_object,
build_object_type,
follow_field_source,
get_doc,
)
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import Direction
@@ -19,6 +25,29 @@ BULK_ACTIONS = ("bulk_destroy", "bulk_partial_update", "bulk_update")
WRITABLE_ACTIONS = ("PATCH", "POST", "PUT")
class NetBoxDjangoFilterExtension(DjangoFilterExtension):
"""
Overrides drf-spectacular's DjangoFilterExtension to fix a regression in v0.29.0 where
_get_model_field() incorrectly double-appends to_field_name when field_name already ends
with that value (e.g. field_name='tags__slug', to_field_name='slug' produces the invalid
path ['tags', 'slug', 'slug']). This caused hundreds of spurious warnings during schema
generation for filters such as TagFilter, TenancyFilterSet.tenant, and OwnerFilterMixin.owner.
See: https://github.com/netbox-community/netbox/issues/20787
https://github.com/tfranzel/drf-spectacular/issues/1475
"""
priority = 1
def _get_model_field(self, filter_field, model):
if not filter_field.field_name:
return None
path = filter_field.field_name.split('__')
to_field_name = filter_field.extra.get('to_field_name')
if to_field_name is not None and path[-1] != to_field_name:
path.append(to_field_name)
return follow_field_source(model, path, emit_warnings=False)
class FixTimeZoneSerializerField(OpenApiSerializerFieldExtension):
target_class = 'timezone_field.rest_framework.TimeZoneSerializerField'
@@ -35,7 +64,7 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
if direction == 'request':
return build_cf
elif direction == "response":
if direction == "response":
value = build_cf
label = {
**build_basic_type(OpenApiTypes.STR),
@@ -49,6 +78,10 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
}
)
# TODO: This function should never implicitly/explicitly return `None`
# The fallback should be well-defined (drf-spectacular expects request/response naming).
return None
def viewset_handles_bulk_create(view):
"""Check if view automatically provides list-based bulk create"""
@@ -71,8 +104,7 @@ class NetBoxAutoSchema(AutoSchema):
def is_bulk_action(self):
if hasattr(self.view, "action") and self.view.action in BULK_ACTIONS:
return True
else:
return False
return False
def get_operation_id(self):
"""
@@ -312,8 +344,7 @@ class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension):
if direction == "response":
component = auto_schema.resolve_serializer(self.target.serializer, direction)
return component.ref if component else None
else:
return build_basic_type(OpenApiTypes.INT)
return build_basic_type(OpenApiTypes.INT)
class FixIntegerRangeSerializerSchema(OpenApiSerializerExtension):
+3 -3
View File
@@ -34,14 +34,14 @@ class ObjectTypeSerializer(BaseModelSerializer):
@extend_schema_field(OpenApiTypes.STR)
def get_rest_api_endpoint(self, obj):
if not (model := obj.model_class()):
return
return None
try:
return get_action_url(model, action='list', rest_api=True)
except NoReverseMatch:
return
return None
@extend_schema_field(OpenApiTypes.STR)
def get_description(self, obj):
if not (model := obj.model_class()):
return
return None
return inspect.getdoc(model)
+1 -1
View File
@@ -2,8 +2,8 @@ from rest_framework import serializers
from rest_framework.reverse import reverse
__all__ = (
'BackgroundTaskSerializer',
'BackgroundQueueSerializer',
'BackgroundTaskSerializer',
'BackgroundWorkerSerializer',
)
+1
View File
@@ -1,4 +1,5 @@
from netbox.api.routers import NetBoxRouter
from . import views
app_name = 'core-api'
+2 -2
View File
@@ -23,6 +23,7 @@ from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import LimitOffsetListPagination
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
from utilities.api import IsSuperuser
from . import serializers
@@ -284,5 +285,4 @@ class BackgroundTaskViewSet(BaseRQViewSet):
stopped_jobs = stop_rq_job(id)
if len(stopped_jobs) == 1:
return HttpResponse(status=200)
else:
return HttpResponse(status=204)
return HttpResponse(status=204)
+4 -3
View File
@@ -6,7 +6,7 @@ from django.db.migrations.operations import AlterModelOptions
from django.utils.translation import gettext as _
from core.events import *
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
from netbox.events import EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING, EventType
from utilities.migration import custom_deconstruct
# Ignore verbose_name & verbose_name_plural Meta options when calculating model migrations
@@ -23,9 +23,10 @@ class CoreConfig(AppConfig):
def ready(self):
from core.api import schema # noqa: F401
from core.checks import check_duplicate_indexes # noqa: F401
from netbox.models.features import register_models
from . import data_backends, events, search # noqa: F401
from netbox import context_managers # noqa: F401
from netbox.models.features import register_models
from . import data_backends, events, search # noqa: F401
# Register models
register_models(*self.get_models())
+2 -2
View File
@@ -1,6 +1,6 @@
from django.core.checks import Error, register, Tags
from django.db.models import Index, UniqueConstraint
from django.apps import apps
from django.core.checks import Error, Tags, register
from django.db.models import Index, UniqueConstraint
__all__ = (
'check_duplicate_indexes',
+1 -1
View File
@@ -2,11 +2,11 @@ from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
#
# Data sources
#
class DataSourceStatusChoices(ChoiceSet):
NEW = 'new'
QUEUED = 'queued'
+1
View File
@@ -15,6 +15,7 @@ from netbox.utils import register_data_backend
from utilities.constants import HTTP_PROXY_SUPPORTED_SCHEMAS, HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS
from utilities.proxy import resolve_proxies
from utilities.socks import ProxyPoolManager
from .exceptions import SyncError
__all__ = (
-1
View File
@@ -1,5 +1,4 @@
import logging
from dataclasses import dataclass, field
from datetime import datetime
+16 -5
View File
@@ -6,8 +6,9 @@ from django.utils.translation import gettext as _
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, PrimaryModelFilterSet
from netbox.utils import get_data_backend_choices
from users.models import User
from utilities.filters import ContentTypeFilter
from utilities.filters import MultiValueContentTypeFilter
from utilities.filtersets import register_filterset
from .choices import *
from .models import *
@@ -25,14 +26,17 @@ __all__ = (
class DataSourceFilterSet(PrimaryModelFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=get_data_backend_choices,
distinct=False,
null_value=None
)
status = django_filters.MultipleChoiceFilter(
choices=DataSourceStatusChoices,
distinct=False,
null_value=None
)
sync_interval = django_filters.MultipleChoiceFilter(
choices=JobIntervalChoices,
distinct=False,
null_value=None
)
@@ -57,11 +61,13 @@ class DataFileFilterSet(ChangeLoggedModelFilterSet):
)
source_id = django_filters.ModelMultipleChoiceFilter(
queryset=DataSource.objects.all(),
distinct=False,
label=_('Data source (ID)'),
)
source = django_filters.ModelMultipleChoiceFilter(
field_name='source__name',
queryset=DataSource.objects.all(),
distinct=False,
to_field_name='name',
label=_('Data source (name)'),
)
@@ -86,9 +92,10 @@ class JobFilterSet(BaseFilterSet):
)
object_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ObjectType.objects.with_feature('jobs'),
distinct=False,
field_name='object_type_id',
)
object_type = ContentTypeFilter()
object_type = MultiValueContentTypeFilter()
created = django_filters.DateTimeFilter()
created__before = django_filters.DateTimeFilter(
field_name='created',
@@ -127,6 +134,7 @@ class JobFilterSet(BaseFilterSet):
)
status = django_filters.MultipleChoiceFilter(
choices=JobStatusChoices,
distinct=False,
null_value=None
)
queue_name = django_filters.CharFilter()
@@ -180,18 +188,21 @@ class ObjectChangeFilterSet(BaseFilterSet):
label=_('Search'),
)
time = django_filters.DateTimeFromToRangeFilter()
changed_object_type = ContentTypeFilter()
changed_object_type = MultiValueContentTypeFilter()
changed_object_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ContentType.objects.all()
queryset=ContentType.objects.all(),
distinct=False,
)
related_object_type = ContentTypeFilter()
related_object_type = MultiValueContentTypeFilter()
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(),
distinct=False,
label=_('User (ID)'),
)
user = django_filters.ModelMultipleChoiceFilter(
field_name='user__username',
queryset=User.objects.all(),
distinct=False,
to_field_name='username',
label=_('User name'),
)
+4 -1
View File
@@ -9,7 +9,10 @@ from netbox.utils import get_data_backend_choices
from users.models import User
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import (
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
ContentTypeChoiceField,
ContentTypeMultipleChoiceField,
DynamicModelMultipleChoiceField,
TagFilterField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DateTimePicker
+2 -2
View File
@@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin
from core.models import *
from netbox.config import get_config, PARAMS
from netbox.config import PARAMS, get_config
from netbox.forms import NetBoxModelForm, PrimaryModelForm
from netbox.registry import registry
from netbox.utils import get_data_backend_choices
@@ -43,7 +43,7 @@ class DataSourceForm(PrimaryModelForm):
attrs={
'rows': 5,
'class': 'font-monospace',
'placeholder': '.cache\n*.txt'
'placeholder': '.cache\n*.txt\nsubdir/*'
}
),
}
+1 -1
View File
@@ -1,6 +1,6 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Annotated, TYPE_CHECKING
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
+15 -14
View File
@@ -1,14 +1,15 @@
from datetime import datetime
from typing import Annotated, TYPE_CHECKING
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
from django.contrib.contenttypes.models import ContentType as DjangoContentType
from strawberry.scalars import ID
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup, StrFilterLookup
from core import models
from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter
from .enums import *
if TYPE_CHECKING:
@@ -16,10 +17,10 @@ if TYPE_CHECKING:
from users.graphql.filters import UserFilter
__all__ = (
'ContentTypeFilter',
'DataFileFilter',
'DataSourceFilter',
'ObjectChangeFilter',
'ContentTypeFilter',
)
@@ -31,23 +32,23 @@ class DataFileFilter(BaseModelFilter):
strawberry_django.filter_field()
)
source_id: ID | None = strawberry_django.filter_field()
path: FilterLookup[str] | None = strawberry_django.filter_field()
path: StrFilterLookup[str] | None = strawberry_django.filter_field()
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
hash: FilterLookup[str] | None = strawberry_django.filter_field()
hash: StrFilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_type(models.DataSource, lookups=True)
class DataSourceFilter(PrimaryModelFilter):
name: FilterLookup[str] | None = strawberry_django.filter_field()
type: FilterLookup[str] | None = strawberry_django.filter_field()
source_url: FilterLookup[str] | None = strawberry_django.filter_field()
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
type: StrFilterLookup[str] | None = strawberry_django.filter_field()
source_url: StrFilterLookup[str] | None = strawberry_django.filter_field()
status: (
BaseFilterLookup[Annotated['DataSourceStatusEnum', strawberry.lazy('core.graphql.enums')]] | None
) = strawberry_django.filter_field()
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
ignore_rules: FilterLookup[str] | None = strawberry_django.filter_field()
ignore_rules: StrFilterLookup[str] | None = strawberry_django.filter_field()
parameters: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@@ -61,8 +62,8 @@ class DataSourceFilter(PrimaryModelFilter):
class ObjectChangeFilter(BaseModelFilter):
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
user_name: FilterLookup[str] | None = strawberry_django.filter_field()
request_id: FilterLookup[str] | None = strawberry_django.filter_field()
user_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
request_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
action: (
BaseFilterLookup[Annotated['ObjectChangeActionEnum', strawberry.lazy('core.graphql.enums')]] | None
) = strawberry_django.filter_field()
@@ -75,7 +76,7 @@ class ObjectChangeFilter(BaseModelFilter):
strawberry_django.filter_field()
)
related_object_id: ID | None = strawberry_django.filter_field()
object_repr: FilterLookup[str] | None = strawberry_django.filter_field()
object_repr: StrFilterLookup[str] | None = strawberry_django.filter_field()
prechange_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@@ -86,5 +87,5 @@ class ObjectChangeFilter(BaseModelFilter):
@strawberry_django.filter_type(DjangoContentType, lookups=True)
class ContentTypeFilter(BaseModelFilter):
app_label: FilterLookup[str] | None = strawberry_django.filter_field()
model: FilterLookup[str] | None = strawberry_django.filter_field()
app_label: StrFilterLookup[str] | None = strawberry_django.filter_field()
model: StrFilterLookup[str] | None = strawberry_django.filter_field()
+2 -2
View File
@@ -1,4 +1,4 @@
from typing import Annotated, List, TYPE_CHECKING
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django
@@ -20,7 +20,7 @@ __all__ = (
class ChangelogMixin:
@strawberry_django.field
def changelog(self, info: Info) -> List[Annotated['ObjectChangeType', strawberry.lazy('.types')]]: # noqa: F821
def changelog(self, info: Info) -> list[Annotated['ObjectChangeType', strawberry.lazy('.types')]]: # noqa: F821
content_type = ContentType.objects.get_for_model(self)
object_changes = ObjectChange.objects.filter(
changed_object_type=content_type,
+2 -4
View File
@@ -1,5 +1,3 @@
from typing import List
import strawberry
import strawberry_django
@@ -9,7 +7,7 @@ from .types import *
@strawberry.type(name="Query")
class CoreQuery:
data_file: DataFileType = strawberry_django.field()
data_file_list: List[DataFileType] = strawberry_django.field()
data_file_list: list[DataFileType] = strawberry_django.field()
data_source: DataSourceType = strawberry_django.field()
data_source_list: List[DataSourceType] = strawberry_django.field()
data_source_list: list[DataSourceType] = strawberry_django.field()
+3 -2
View File
@@ -1,4 +1,4 @@
from typing import Annotated, List
from typing import Annotated
import strawberry
import strawberry_django
@@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType as DjangoContentType
from core import models
from netbox.graphql.types import BaseObjectType, PrimaryObjectType
from .filters import *
__all__ = (
@@ -33,7 +34,7 @@ class DataFileType(BaseObjectType):
pagination=True
)
class DataSourceType(PrimaryObjectType):
datafiles: List[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]
datafiles: list[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]
@strawberry_django.type(
+1
View File
@@ -13,6 +13,7 @@ from netbox.config import Config
from netbox.jobs import JobRunner, system_job
from netbox.search.backends import search_backend
from utilities.proxy import resolve_proxies
from .choices import DataSourceStatusChoices, JobIntervalChoices
from .models import DataSource
+1 -1
View File
@@ -144,7 +144,7 @@ class Command(BaseCommand):
# If Python code has been passed, execute it and exit.
if options['command']:
exec(options['command'], namespace)
return
return None
# Try to enable tab-complete
try:
@@ -4,7 +4,6 @@ from django_rq.management.commands.rqworker import Command as _Command
from netbox.registry import registry
DEFAULT_QUEUES = ('high', 'default', 'low')
logger = logging.getLogger('netbox.rqworker')
@@ -1,6 +1,7 @@
import core.models.object_types
from django.db import migrations
import core.models.object_types
class Migration(migrations.Migration):
dependencies = [
+2 -1
View File
@@ -1,4 +1,5 @@
from .object_types import *
from .object_types import * # isort: split
from .change_logging import *
from .config import *
from .data import *
+1 -2
View File
@@ -10,8 +10,7 @@ from mptt.models import MPTTModel
from core.choices import ObjectChangeActionChoices
from core.querysets import ObjectChangeQuerySet
from netbox.models.features import ChangeLoggingMixin
from netbox.models.features import has_feature
from netbox.models.features import ChangeLoggingMixin, has_feature
from utilities.data import shallow_compare_dict
__all__ = (
+2 -1
View File
@@ -1,7 +1,8 @@
from django.core.cache import cache
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from utilities.querysets import RestrictedQuerySet
+10 -7
View File
@@ -19,6 +19,7 @@ from netbox.models import PrimaryModel
from netbox.models.features import JobsMixin
from netbox.registry import registry
from utilities.querysets import RestrictedQuerySet
from ..choices import *
from ..exceptions import SyncError
@@ -68,7 +69,7 @@ class DataSource(JobsMixin, PrimaryModel):
ignore_rules = models.TextField(
verbose_name=_('ignore rules'),
blank=True,
help_text=_("Patterns (one per line) matching files to ignore when syncing")
help_text=_("Patterns (one per line) matching files or paths to ignore when syncing")
)
parameters = models.JSONField(
verbose_name=_('parameters'),
@@ -97,6 +98,7 @@ class DataSource(JobsMixin, PrimaryModel):
def get_type_display(self):
if backend := registry['data_backends'].get(self.type):
return backend.label
return None
def get_status_color(self):
return DataSourceStatusChoices.colors.get(self.status)
@@ -256,21 +258,22 @@ class DataSource(JobsMixin, PrimaryModel):
if path.startswith('.'):
continue
for file_name in file_names:
if not self._ignore(file_name):
paths.add(os.path.join(path, file_name))
file_path = os.path.join(path, file_name)
if not self._ignore(file_path):
paths.add(file_path)
logger.debug(f"Found {len(paths)} files")
return paths
def _ignore(self, filename):
def _ignore(self, file_path):
"""
Returns a boolean indicating whether the file should be ignored per the DataSource's configured
ignore rules.
ignore rules. file_path is the full relative path (e.g. "subdir/file.txt").
"""
if filename.startswith('.'):
if os.path.basename(file_path).startswith('.'):
return True
for rule in self.ignore_rules.splitlines():
if fnmatchcase(filename, rule):
if fnmatchcase(file_path, rule) or fnmatchcase(os.path.basename(file_path), rule):
return True
return False
+5 -4
View File
@@ -4,15 +4,16 @@ from functools import cached_property
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.core.files.storage import storages
from django.db import models
from django.utils.translation import gettext as _
from ..choices import ManagedFileRootPathChoices
from extras.storage import ScriptFileSystemStorage
from netbox.models.features import SyncedDataMixin
from utilities.querysets import RestrictedQuerySet
from ..choices import ManagedFileRootPathChoices
__all__ = (
'ManagedFile',
)
@@ -78,8 +79,7 @@ class ManagedFile(SyncedDataMixin, models.Model):
'scripts': settings.SCRIPTS_ROOT,
'reports': settings.REPORTS_ROOT,
}[self.file_root]
else:
return ""
return ""
def sync_data(self):
if self.data_file:
@@ -89,6 +89,7 @@ class ManagedFile(SyncedDataMixin, models.Model):
with storage.open(self.full_path, 'wb+') as new_file:
new_file.write(self.data_file.data)
sync_data.alters_data = True
@cached_property
def storage(self):
+3 -1
View File
@@ -146,7 +146,7 @@ class Job(models.Model):
if self.object_type:
if self.object_type.model == 'reportmodule':
return reverse('extras:report_result', kwargs={'job_pk': self.pk})
elif self.object_type.model == 'scriptmodule':
if self.object_type.model == 'scriptmodule':
return reverse('extras:script_result', kwargs={'job_pk': self.pk})
return reverse('core:job', args=[self.pk])
@@ -216,6 +216,7 @@ class Job(models.Model):
# Send signal
job_start.send(self)
start.alters_data = True
def terminate(self, status=JobStatusChoices.STATUS_COMPLETED, error=None):
"""
@@ -245,6 +246,7 @@ class Job(models.Model):
# Send signal
job_end.send(self)
terminate.alters_data = True
def log(self, record: logging.LogRecord):
"""
+4 -1
View File
@@ -218,19 +218,22 @@ class ObjectType(ContentType):
def app_verbose_name(self):
if model := self.model_class():
return model._meta.app_config.verbose_name
return None
@property
def model_verbose_name(self):
if model := self.model_class():
return model._meta.verbose_name
return None
@property
def model_verbose_name_plural(self):
if model := self.model_class():
return model._meta.verbose_name_plural
return None
@property
def is_plugin_model(self):
if not (model := self.model_class()):
return # Return null if model class is invalid
return None # Return null if model class is invalid
return isinstance(model._meta.app_config, PluginConfig)
+1 -2
View File
@@ -1,7 +1,6 @@
import datetime
import importlib
from dataclasses import dataclass, field
from typing import Optional
import requests
from django.conf import settings
@@ -55,7 +54,7 @@ class Plugin:
tag_line: str = ''
description_short: str = ''
slug: str = ''
author: Optional[PluginAuthor] = None
author: PluginAuthor | None = None
created_at: datetime.datetime = None
updated_at: datetime.datetime = None
license_type: str = ''
+1
View File
@@ -1,4 +1,5 @@
from netbox.search import SearchIndex, register_search
from . import models
+26 -19
View File
@@ -3,11 +3,11 @@ from threading import local
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.signals import request_finished
from django.db.models import CASCADE, RESTRICT
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
from django.db.models.signals import m2m_changed, post_migrate, post_save, pre_delete
from django.dispatch import receiver, Signal
from django.core.signals import request_finished
from django.dispatch import Signal, receiver
from django.utils.translation import gettext_lazy as _
from django_prometheus.models import model_deletes, model_inserts, model_updates
@@ -18,10 +18,11 @@ from extras.events import enqueue_event
from extras.models import Tag
from extras.utils import run_validators
from netbox.config import get_config
from utilities.data import get_config_value_ci
from netbox.context import current_request, events_queue
from netbox.models.features import ChangeLoggingMixin, get_model_features, model_is_public
from utilities.data import get_config_value_ci
from utilities.exceptions import AbortRequest
from .models import ConfigRevision, DataSource, ObjectChange
__all__ = (
@@ -209,22 +210,28 @@ def handle_deleted_object(sender, instance, **kwargs):
# for the forward direction of the relationship, ensuring that the change is recorded.
# Similarly, for many-to-one relationships, we set the value on the related object to None
# and save it to trigger a change record on that object.
for relation in instance._meta.related_objects:
if type(relation) not in [ManyToManyRel, ManyToOneRel]:
continue
related_model = relation.related_model
related_field_name = relation.remote_field.name
if not issubclass(related_model, ChangeLoggingMixin):
# We only care about triggering the m2m_changed signal for models which support
# change logging
continue
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
obj.snapshot() # Ensure the change record includes the "before" state
if type(relation) is ManyToManyRel:
getattr(obj, related_field_name).remove(instance)
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT):
setattr(obj, related_field_name, None)
obj.save()
#
# Skip this for private models (e.g. CablePath) whose lifecycle is an internal
# implementation detail. Django's on_delete handlers (e.g. SET_NULL) already take
# care of the database integrity; recording changelog entries for the related
# objects would be spurious. (Ref: #21390)
if not getattr(instance, '_netbox_private', False):
for relation in instance._meta.related_objects:
if type(relation) not in [ManyToManyRel, ManyToOneRel]:
continue
related_model = relation.related_model
related_field_name = relation.remote_field.name
if not issubclass(related_model, ChangeLoggingMixin):
# We only care about triggering the m2m_changed signal for models which support
# change logging
continue
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
obj.snapshot() # Ensure the change record includes the "before" state
if type(relation) is ManyToManyRel:
getattr(obj, related_field_name).remove(instance)
elif type(relation) is ManyToOneRel and relation.null and relation.on_delete not in (CASCADE, RESTRICT):
setattr(obj, related_field_name, None)
obj.save()
# Enqueue the object for event processing
queue = events_queue.get()
+1 -1
View File
@@ -2,5 +2,5 @@ from .change_logging import *
from .config import *
from .data import *
from .jobs import *
from .tasks import *
from .plugins import *
from .tasks import *
+1
View File
@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
from core.models import ObjectChange
from netbox.tables import NetBoxTable, columns
from .template_code import *
__all__ = (
+2 -1
View File
@@ -1,8 +1,9 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from core.models import *
from netbox.tables import NetBoxTable, PrimaryModelTable, columns
from .columns import BackendTypeColumn
from .template_code import DATA_SOURCE_SYNC_BUTTON
+1 -1
View File
@@ -1,10 +1,10 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from netbox.tables import BaseTable, NetBoxTable, columns
from core.constants import JOB_LOG_ENTRY_LEVELS
from core.models import Job
from core.tables.columns import BadgeColumn
from netbox.tables import BaseTable, NetBoxTable, columns
class JobTable(NetBoxTable):
+1
View File
@@ -2,6 +2,7 @@ import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from netbox.tables import BaseTable, columns
from .template_code import PLUGIN_IS_INSTALLED, PLUGIN_NAME_TEMPLATE
__all__ = (
+6 -4
View File
@@ -1,17 +1,19 @@
import uuid
from django_rq import get_queue
from django_rq.workers import get_worker
from django.urls import reverse
from django.utils import timezone
from rq.job import Job as RQ_Job, JobStatus
from django_rq import get_queue
from django_rq.workers import get_worker
from rest_framework import status
from rq.job import Job as RQ_Job
from rq.job import JobStatus
from rq.registry import FailedJobRegistry, StartedJobRegistry
from rest_framework import status
from users.constants import TOKEN_PREFIX
from users.models import Token, User
from utilities.testing import APITestCase, APIViewTestCases, TestCase
from utilities.testing.utils import disable_logging
from ..models import *
+10 -1
View File
@@ -7,7 +7,16 @@ from core.choices import ObjectChangeActionChoices
from core.models import ObjectChange, ObjectType
from dcim.choices import InterfaceTypeChoices, ModuleStatusChoices, SiteStatusChoices
from dcim.models import (
Cable, CableTermination, Device, DeviceRole, DeviceType, Manufacturer, Module, ModuleBay, ModuleType, Interface,
Cable,
CableTermination,
Device,
DeviceRole,
DeviceType,
Interface,
Manufacturer,
Module,
ModuleBay,
ModuleType,
Site,
)
from extras.choices import *
+7 -6
View File
@@ -1,5 +1,5 @@
import uuid
from datetime import datetime, timezone
from datetime import UTC, datetime
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
@@ -8,6 +8,7 @@ from dcim.models import Site
from ipam.models import IPAddress
from users.models import User
from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests
from ..choices import *
from ..filtersets import *
from ..models import *
@@ -99,21 +100,21 @@ class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
DataFile(
source=data_sources[0],
path='dir1/file1.txt',
last_updated=datetime(2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
last_updated=datetime(2023, 1, 1, 0, 0, 0, tzinfo=UTC),
size=1000,
hash='442da078f0111cbdf42f21903724f6597c692535f55bdfbbea758a1ae99ad9e1'
),
DataFile(
source=data_sources[1],
path='dir1/file2.txt',
last_updated=datetime(2023, 1, 2, 0, 0, 0, tzinfo=timezone.utc),
last_updated=datetime(2023, 1, 2, 0, 0, 0, tzinfo=UTC),
size=2000,
hash='a78168c7c97115bafd96450ed03ea43acec495094c5caa28f0d02e20e3a76cc2'
),
DataFile(
source=data_sources[2],
path='dir1/file3.txt',
last_updated=datetime(2023, 1, 3, 0, 0, 0, tzinfo=timezone.utc),
last_updated=datetime(2023, 1, 3, 0, 0, 0, tzinfo=UTC),
size=3000,
hash='12b8827a14c4d5a2f30b6c6e2b7983063988612391c6cbe8ee7493b59054827a'
),
@@ -237,9 +238,9 @@ class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_changed_object_type(self):
params = {'changed_object_type': 'dcim.site'}
params = {'changed_object_type': ['dcim.site']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'changed_object_type_id': [ContentType.objects.get(app_label='dcim', model='site').pk]}
params = {'changed_object_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+23 -3
View File
@@ -1,15 +1,35 @@
from unittest.mock import patch, MagicMock
from unittest.mock import MagicMock, patch
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.test import TestCase
from core.models import DataSource, Job, ObjectType
from core.choices import ObjectChangeActionChoices
from dcim.models import Site, Location, Device
from core.models import DataSource, Job, ObjectType
from dcim.models import Device, Location, Site
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
class DataSourceIgnoreRulesTestCase(TestCase):
def test_no_ignore_rules(self):
ds = DataSource(ignore_rules='')
self.assertFalse(ds._ignore('README.md'))
self.assertFalse(ds._ignore('subdir/file.py'))
def test_ignore_by_filename(self):
ds = DataSource(ignore_rules='*.txt')
self.assertTrue(ds._ignore('notes.txt'))
self.assertTrue(ds._ignore('subdir/notes.txt'))
self.assertFalse(ds._ignore('notes.py'))
def test_ignore_by_subdirectory(self):
ds = DataSource(ignore_rules='dev/*')
self.assertTrue(ds._ignore('dev/README.md'))
self.assertTrue(ds._ignore('dev/script.py'))
self.assertFalse(ds._ignore('prod/script.py'))
class DataSourceChangeLoggingTestCase(TestCase):
def test_password_added_on_create(self):
+1
View File
@@ -4,6 +4,7 @@ Unit tests for OpenAPI schema generation.
Refs: #20638
"""
import json
from django.test import TestCase

Some files were not shown because too many files have changed in this diff Show More