Compare commits

...

223 Commits

Author SHA1 Message Date
Daniel Sheppard
49bcf57834 Update Style 2025-02-19 14:58:34 -06:00
Daniel Sheppard
01cb7beb68 Update bundle 2025-02-13 10:51:48 -06:00
Daniel Sheppard
762ebe23f9 Merge branch '9583-add_column_specific_search_field_to_tables' of https://github.com/netbox-community/netbox into 9583-add_column_specific_search_field_to_tables 2025-02-13 10:50:13 -06:00
Daniel Sheppard
714495486b Merge branch 'feature' of https://github.com/netbox-community/netbox into 9583-add_column_specific_search_field_to_tables 2025-02-13 10:48:14 -06:00
Jeremy Stretch
697610db94 Closes #18541: Document support for auth_required attribute on PluginMenuItem 2025-02-06 14:45:25 -05:00
Arthur
d202b3529d Merge branch 'main' into feature 2025-02-04 13:05:40 -08:00
bctiemann
260adfc9e7 Merge pull request #18569 from netbox-community/release-v4.2.3
Release 4.2.3
2025-02-04 15:35:45 -05:00
Brian Tiemann
9391f48d62 Update static bundle 2025-02-04 15:20:08 -05:00
Brian Tiemann
6e165435e2 Release 4.2.3 2025-02-04 15:09:37 -05:00
Jeremy Stretch
75417c9cd5 Closes #17587: Add release_track attribute to PluginConfig 2025-02-03 13:54:52 -05:00
Jeremy Stretch
43841939a0 Closes #18540: Track installed plugins in registry 2025-02-03 13:42:32 -05:00
mr1716
29f405d27e #18496 Fixing Broken Link For Custom Links Documentation 2025-02-03 10:12:29 -05:00
Jeremy Stretch
f829f34b43 Closes #18559: Add a build parameter to ReleaseInfo (#18560)
* Closes #18559: Add a build parameter to ReleaseInfo

* Adjust dataclass typing
2025-02-03 09:44:00 -05:00
github-actions
0b794de40e Update source translation strings 2025-02-01 05:02:11 +00:00
Brian Tiemann
b2bc842f1c Remove 'provider' from VirtualCircuitIndex.display_attrs 2025-01-31 14:42:39 -05:00
Brian Tiemann
c8decf4c21 Add auth_required attrib on PluginMenuItem 2025-01-31 14:40:49 -05:00
Renato Almeida de Oliveira Zaroubin
e12a5d2edc Add get_extra_addanother_params method in IPAddressEditView 2025-01-31 14:33:26 -05:00
bctiemann
4c5fbb7326 Merge pull request #18544 from renatoalmeidaoliveira/18154-make-BaseTable-aware-default_user_preferences-annon-users
FIX #18154 - Cannot change default table preferences for anonymous users
2025-01-31 14:25:20 -05:00
mr1716
8aecf53d0e #18513 Updating Documentation Relating To Strawberry-Django Links 2025-01-31 14:14:48 -05:00
Renato Almeida de Oliveira Zaroubin
7d6089775e remove extra line 2025-01-31 18:48:50 +00:00
Renato Almeida de Oliveira Zaroubin
f5bdf7b593 Simplify Anon user logic 2025-01-31 18:03:55 +00:00
Renato Almeida de Oliveira Zaroubin
62148bb83c Check if DEFAULT_USER_PREFERENCES are configured 2025-01-30 21:51:37 +00:00
Renato Almeida de Oliveira Zaroubin
22af6dd05f Add default user preferences tables testing in BaseTable 2025-01-30 21:09:36 +00:00
github-actions
5514df9dee Update source translation strings 2025-01-30 05:02:02 +00:00
Brian Tiemann
5cd7c6d167 Add <meta> tag reflecting settings.HOSTNAME 2025-01-29 12:50:08 -05:00
bctiemann
9ac79ebbdf Merge pull request #18516 from tobiasge/dont-fail-on-debug
Fix #18515: Don't fail in DEBUG mode
2025-01-28 15:20:52 -05:00
bctiemann
e6c0519ada Merge pull request #18517 from mr1716/main
#18512 Update required-parameters spelling
2025-01-28 15:17:59 -05:00
Tobias Genannt
07403f690a Fix #18515: Don't fail in DEBUG mode
When no Redis server is reachable management commands failed without
this try...except block.
2025-01-28 19:40:22 +01:00
mr1716
80e466dab7 #18512 Update required-parameters spelling 2025-01-28 09:06:37 -05:00
Brian Tiemann
34fa3835be NB-717 Update dashboard news feed URL to eliminate multiple 301 redirects 2025-01-28 08:26:35 -05:00
Brian Tiemann
7a6bb34d21 Reword references to develop and master branches 2025-01-28 08:26:05 -05:00
github-actions
968214b64a Update source translation strings 2025-01-28 05:02:09 +00:00
bctiemann
cf64f3cc43 Merge pull request #18444 from netbox-community/18433-fix-macaddress-primary-for-interface
Fixes: #18433 - Fix missing is_primary property on MACAddress model
2025-01-27 13:50:28 -05:00
Daniel Sheppard
ee5d7cfe31 Merge branch 'main' into 18433-fix-macaddress-primary-for-interface 2025-01-27 12:11:37 -06:00
Jeremy Stretch
57fa1dd18d Add remaining issue types 2025-01-24 10:20:43 -05:00
Jeremy Stretch
b2b47ac740 Closes #18484: Exempt changes to GitHub templates from CI 2025-01-24 09:47:40 -05:00
Jeremy Stretch
313f44646b Assign GitHub issue type on creation 2025-01-24 09:33:04 -05:00
Brian Tiemann
c2daa70099 Fix typo in Site Groups docs 2025-01-24 08:10:39 -05:00
Brian Tiemann
5fce4eef8e Add note about \l command 2025-01-24 08:09:10 -05:00
Brian Tiemann
b1e7530295 Add warning about UTF8 encoding in PostgreSQL 2025-01-24 08:09:10 -05:00
github-actions
da9b452327 Update source translation strings 2025-01-24 05:02:20 +00:00
bctiemann
31efd1fe57 Merge pull request #18452 from netbox-community/18449-fix-formatting-errors
Fixes: #18449 - Clean up some formatting errors
2025-01-23 19:51:42 -05:00
bctiemann
a8168899b8 Merge pull request #18450 from netbox-community/18447-fix-legacy-macaddress-sorting
Fixes: #18447 - Fix sorting by `mac_address` field
2025-01-23 19:50:12 -05:00
bctiemann
e02ae72f0c Merge pull request #18445 from netbox-community/18436-fix-unassign-mac-with-primary
Fixes: #18436 - Prevent unassigning mac address when primary on an interface
2025-01-23 19:45:53 -05:00
Daniel Sheppard
3219609253 Change to @cached_property 2025-01-23 18:30:54 -06:00
Daniel Sheppard
adcb6bebd2 Remove mac_address from tables. 2025-01-22 14:14:56 -06:00
Daniel Sheppard
ad4e4e89a7 Update VirtualMachineVMInterfaceTable 2025-01-21 11:15:33 -06:00
Daniel Sheppard
c56a39a168 Fixes: #18449 - Clean up some formatting errors 2025-01-21 10:44:46 -06:00
Daniel Sheppard
d1914595f6 Fixes: #18447 - Fix sorting by mac_address field
* Disable sorting by `mac_address` for legacy `mac_address` field for Device and VM Interfaces
* Ensure `primary_mac_address` field is included in field list for Device and VM Interfaces
2025-01-21 10:15:33 -06:00
Jeremy Stretch
b913661297 Fixes #18438: Specify batch_size for migrations which run bulk_update() 2025-01-21 09:48:35 -05:00
Daniel Sheppard
bec97df242 Fix Tests 2025-01-20 23:44:36 -06:00
Daniel Sheppard
22e320084a Update UI to disable interface assignment when assigned as primary 2025-01-20 23:06:29 -06:00
Daniel Sheppard
277acd3a31 Fixes: #18436 - Prevent unassigning mac address when primary on an interface 2025-01-20 22:49:55 -06:00
Daniel Sheppard
51a79505fe Fixes: #18433 - Fix missing is_primary property on MACAddress model 2025-01-20 22:21:36 -06:00
Daniel Sheppard
e7649f0cc2 Fix test error 2025-01-20 18:45:28 -06:00
Daniel Sheppard
434cf37988 Fix test error and OOB-swap 2025-01-20 18:27:26 -06:00
Daniel Sheppard
451a1f498d Remove debug 2025-01-20 17:35:20 -06:00
Daniel Sheppard
694fcd3a79 Fix assets 2025-01-20 17:31:16 -06:00
Daniel Sheppard
7e5d8eaf05 Debugging 2025-01-20 17:21:29 -06:00
Daniel Sheppard
1958965c36 More bundle debugging 2025-01-20 17:16:33 -06:00
Daniel Sheppard
e12ae3acde Figure out what is failing 2025-01-20 17:11:11 -06:00
Daniel Sheppard
a28871296c Fix assets 2025-01-20 17:07:25 -06:00
Daniel Sheppard
ee83895e2a Fix bundles 2025-01-20 16:42:59 -06:00
Daniel Sheppard
764c676ffc Fix bundles 2025-01-20 15:54:07 -06:00
Daniel Sheppard
92f6f426b3 Removed unused import 2025-01-20 14:51:15 -06:00
Daniel Sheppard
4381ea9265 Merge Head 2025-01-20 14:24:47 -06:00
Daniel Sheppard
92c3bbc9db Correct issue with filter_form not passing through render_table from django_tables2 2025-01-20 14:21:41 -06:00
Daniel Sheppard
8d65973c82 Simplify table header filter rendering 2025-01-20 13:13:19 -06:00
Daniel Sheppard
882aa30043 Simplify filter generation 2025-01-20 12:06:26 -06:00
Daniel Sheppard
f2c74db761 Update display of filtering chits 2025-01-20 12:03:41 -06:00
Daniel Sheppard
852535e3d4 Merge in updates from feature 2025-01-20 11:41:26 -06:00
github-actions
d11deb6678 Update source translation strings 2025-01-18 05:02:12 +00:00
Jeremy Stretch
f845b2cf07 Release v4.2.2 2025-01-17 15:05:09 -05:00
atownson
2ed4a2b005 Fixes: #18369 - Remove the json filter for protection rules (#18388)
* Remove the json filter for protection rules

* Configure PROTECTION_RULE config attribute to use ConfigJSONEncoder as serializer

* Tweak getattr()

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-01-17 14:02:12 -05:00
Jeremy Stretch
5b9210dfa5 Fixes #18392: Exclude config contexts assigned to locations for VMs 2025-01-17 13:14:05 -05:00
Jeremy Stretch
4a13664e0f Closes #18425: Remove the triage priority field from GitHub issue templates 2025-01-17 11:06:17 -05:00
Jeremy Stretch
a9f3c74b0c Fixes #18379: Ensure RSS feed content within dashboard widget is sanitized 2025-01-17 10:25:22 -05:00
Brian Tiemann
50b7f46fc0 Migrate DEFAULT_FILE_STORAGE to STORAGES 2025-01-17 09:04:51 -05:00
Brian Tiemann
07ad4c1321 Make GFK scope field sortable=False on tables where it appears 2025-01-17 08:52:12 -05:00
bctiemann
4a1fea3504 Fixes: #18336 - Perform Rack object validation of u_height and starting_unit on rack_type if present (#18395)
* Perform Rack object validation of u_height and starting_unit on rack_type if present

* Calculate effective values before doing validation
2025-01-17 08:45:17 -05:00
bctiemann
993d8f1480 Fixes: #18373 - Fix validation of site in Assign Device to Cluster flow (#18375)
* Fix validation of site in Assign Device to Cluster flow

* Validate Location as well as Site scope
2025-01-17 08:35:17 -05:00
bctiemann
c3efa2149c Fixes: #18350 - Remove 'site' and 'provider_network' from CircuitTerminationIndex.display_attrs (#18351)
* Remove 'site' and 'provider_network' from CircuitTerminationIndex.display_attrs

* Use '_site' and '_provider_network' in display_attrs

* Replace private fields with 'termination'
2025-01-17 08:28:43 -05:00
Jeremy Stretch
a75fa53d4d Closes #18348: Disable legacy pre-commit hook script 2025-01-13 08:50:34 -05:00
Jeremy Stretch
e75d327f38 Fixes #18376: Include tagged VLANs in interfaces list for Q-in-Q interfaces 2025-01-10 09:10:34 -05:00
github-actions
a79d869bd8 Update source translation strings 2025-01-10 05:02:08 +00:00
Brian Tiemann
32422d1683 Don't cache CACHE_KEY_CATALOG_ERROR if ISOLATED_DEPLOYMENT is True 2025-01-09 15:21:27 -05:00
Jeremy Stretch
571f604ce8 Fixes #18368: Restore missing fields on REST API serializer for MAC addresses 2025-01-09 14:53:03 -05:00
Jeremy Stretch
b12c8c880f Fixes #18363: Fix assignment of MAC addresses to interfaces via REST API (#18367)
* Fixes #18363: Fix assignment of MAC addresses to interfaces via REST API

* Add missing API & view tests
2025-01-09 13:55:19 -05:00
Jeremy Stretch
b11f179527 Closes #18362: Create a system job for census reporting 2025-01-09 11:56:09 -05:00
Brian Tiemann
80e1fd02bb Update docs to indicate PostgreSQL 13+ requirement 2025-01-09 10:58:51 -05:00
github-actions
4090afbf24 Update source translation strings 2025-01-09 05:02:09 +00:00
Jeremy Stretch
d04fc11c61 Release v4.2.1 (#18346)
* Release v4.2.1

* Add changelog for #18282
2025-01-08 10:19:28 -05:00
Brian Tiemann
f6b8c1966d Use order_by to change ordering behavior of VLAN column rather than changing accessor 2025-01-08 09:54:00 -05:00
Brian Tiemann
4456c488f1 Change PrefixTable.vlan to represent the VLAN ID rather than the VLAN object, to enable more useful sorting by VLAN ID rather than site-grouped VLAN objects 2025-01-08 09:54:00 -05:00
Jeremy Stretch
53aa2c8624 Fixes #18329: Pin strawberry-graphql-django to v0.52.0 to resolve upstream bug 2025-01-08 08:59:54 -05:00
github-actions
ffac0974dd Update source translation strings 2025-01-08 05:02:12 +00:00
bctiemann
e518f08604 Fixes: #18316 - Fix PrefixIndex reference to 'site' (#18322)
* Fix PrefixIndex reference to 'site'

* Fix ClusterIndex reference to 'site' and add 'scope' to WirelessLANIndex
2025-01-07 10:47:05 -05:00
Tobias Genannt
4ae5529362 Fix #18314: Use get to avoid KeyError 2025-01-07 10:39:55 -05:00
Jeremy Stretch
ef6c89ee5d Fixes #18324: Correct filter names for certain related object listings 2025-01-07 10:34:35 -05:00
Jeremy Stretch
9c960c2387 Fixes #18318: Correct navigation breadcrumbs for module type UI view 2025-01-07 10:28:22 -05:00
github-actions
ed541220e8 Update source translation strings 2025-01-07 05:02:25 +00:00
Jeremy Stretch
14cec518f5 Closes #18311: Update minimum required version of PostgreSQL 2025-01-06 17:04:13 -05:00
Jeremy Stretch
9d82a668a4 Release v4.2.0 2025-01-06 16:13:24 -05:00
Jeremy Stretch
b7610971c0 Closes #13366: Update documentation for main branch (#18309)
* Closes #13366: Update documentation for main branch

* Clarify wording
2025-01-06 15:29:03 -05:00
Jeremy Stretch
ab0a1f0bbc Merge pull request #18308 from netbox-community/feature
Prep for v4.2.0 release
2025-01-06 14:02:29 -05:00
Jeremy Stretch
5d1070796d Merge branch 'develop' into feature 2025-01-06 13:42:57 -05:00
Jeremy Stretch
83d62315cc Closes #18153: Introduce virtual circuit types (#18300)
* Closes #18153: Introduce virtual circuit types

* Fix TagTestCase

* Fix GraphQL API test
2025-01-06 13:37:43 -05:00
Jeremy Stretch
ab8fc3de5e Merge branch 'master' into develop 2025-01-06 11:25:43 -05:00
Jeremy Stretch
67657efe1c Release v4.1.11 2025-01-06 11:24:29 -05:00
bctiemann
c9ee699633 Fixes: #18263 - Iterate through a freshly queried set of CableTerminations to find endpoints in update_connected_endpoints (#18264)
* Iterate through a freshly queried set of CableTerminations to find endpoints in update_connected_endpoints

* Add defensive break if q_filter has not been populated
2025-01-06 09:54:13 -05:00
Brian Tiemann
89d7487197 Update some detail views with prefetch_related from 'site' to 'scope' 2025-01-06 09:48:14 -05:00
github-actions
40f22533d1 Update source translation strings 2025-01-04 05:02:13 +00:00
Jeremy Stretch
c3b0de3ebd Closes #18281: Support group assignment for virtual circuits (#18291)
* Rename circuit to member on CircuitGroupAssignment

* Support group assignment for virtual circuits

* Update release notes

* Introduce separate nav menu heading for circuit groups

* Add generic relations for group assignments

* Remove obsolete code

* Clean up bulk import & extend tests
2025-01-03 13:42:47 -05:00
bctiemann
e8e3981da5 Fixes: #18289 - Add 'created' and 'last_updated' fields to ModuleTypeTable (#18292)
* Add 'created' and 'last_updated' fields to ModuleTypeTable for consistency

* Add 'created' and 'last_updated' fields to ModuleTable for consistency
2025-01-03 12:35:04 -05:00
Jeremy Stretch
b9abb3200c Fixes #18271: Require only encryption OR authentication algorithm when creating an IPSec proposal via REST API 2025-01-03 12:33:58 -05:00
Jeremy Stretch
10748edc3a Fixes #18222: Include action data from event rule in webhook and custom script data 2025-01-03 09:39:05 -05:00
Jeremy Stretch
6f4bec7644 Fixes #18278: Restore missing columns on MACAddressTable 2024-12-30 14:00:29 -05:00
bctiemann
0cda10a204 Fixes: #18203 - Validate that scope is selected if scope type is specified (#18254)
* Validate that a scope has been selected if a scope_type is specified, on CachedScopeMixin models

* Cleaner logic

* Call super().clean() after validating scope_type/scope
2024-12-30 12:36:46 -05:00
Jeremy Stretch
685264c757 Merge branch 'develop' into feature 2024-12-30 12:30:34 -05:00
Thor Selmer Dreier-Hansen
f03489f58e Add distinct() to filtering VLANs by assigned interface (#18274) 2024-12-27 15:11:51 -05:00
bctiemann
13c26ccb0c Fixes: #18184 - Gracefully handle unavailable internet connection on RSS feed dashboard widget if ISOLATED_DEPLOYMENT is set (#18186)
* Suppress adding the RSS feed widget to the dashboard if ISOLATED_DEPLOYMENT is set

* Add config option on RSSFeedWidget to specify requires_internet and to display a more appropriate error if ISOLATED_DEPLOYMENT is set

* Remove skipping behavior from utils.py

* Add required=False
2024-12-16 11:46:28 -05:00
Jeremy Stretch
aa56b99566 Closes #18045: Enable adding a new MAC to an interface via quick add (#18200)
* Closes #18045: Enable adding a new MAC to an interface via quick add

* Misc cleanup
2024-12-16 10:57:09 -05:00
Jeremy Stretch
39ca3ce571 Merge branch 'develop' into feature 2024-12-12 12:13:45 -05:00
bctiemann
0219dd7a70 Fixes: #18192 - Use assigned_object instead of interface in display_attrs (#18199)
* Use assigned_object instead of interface in display_attrs

* Remove mac_address
2024-12-11 08:26:48 -05:00
Jeremy Stretch
edc9852229 Fixes #18194: Always pass POST data to bulk edit form 2024-12-10 16:23:30 -05:00
Jeremy Stretch
012e815f3b Merge branch 'feature' into 9583-add_column_specific_search_field_to_tables 2024-12-06 15:59:06 -05:00
Jeremy Stretch
ce365dab0a Release v4.2-beta1 2024-12-02 10:00:56 -05:00
Jeremy Stretch
a998893f7e #16547: Update buildschema management command 2024-12-02 09:46:47 -05:00
Jeremy Stretch
27d970df41 Update UI dependencies 2024-12-02 09:32:38 -05:00
Jeremy Stretch
678d89d406 Update documentation for v4.2 2024-11-26 12:38:29 -05:00
Jeremy Stretch
99339501cd Merge branch 'develop' into feature 2024-11-26 12:23:13 -05:00
Jeremy Stretch
d511ba487d #13086: Add virtual circuit to InterfaceTable 2024-11-26 12:20:59 -05:00
Jeremy Stretch
3ee951b0d0 Fix missing/incorrect documentation links 2024-11-26 10:45:30 -05:00
Jeremy Stretch
64e56cd7c8 #16971: Improve example in documentation 2024-11-26 10:35:30 -05:00
Arthur Hanson
a24576f126 7848 Add RQ API (#17938)
* 7848 Add Background Tasks (RQ) to API

* 7848 Tasks

* 7848 cleanup

* 7848 add worker support

* 7848 switch to APIView

* 7848 Task detail view

* 7848 Task enqueue, requeue, stop

* 7848 Task enqueue, requeue, stop

* 7848 Task enqueue, requeue, stop

* 7848 tests

* 7848 tests

* 7848 OpenAPI doc generation

* 7848 OpenAPI doc generation

* 7848 review changes

* 7848 viewset

* 7848 viewset

* 7848 fix tests

* 7848 more viewsets

* 7848 fix docstring

* 7848 review comments

* 7848 review comments - get all tasks

* 7848 queue detail view

* 7848 cleanup

* 7848 cleanup

* 7848 cleanup

* 7848 cleanup

* Rename viewsets for consistency w/serializers

* Misc cleanup

* 7848 review changes

* 7848 review changes

* 7848 add test

* 7848 queue detail view

* 7848 fix tests

* 7848 fix the spectacular test failure

* 7848 fix the spectacular test failure

* Misc cleanup

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-11-26 10:01:06 -05:00
Jeremy Stretch
9b4b56febc #13428: Misc cleanup 2024-11-26 09:56:33 -05:00
Jeremy Stretch
d093b21bc0 #17761: Set null=True on Site.time_zone 2024-11-25 16:50:53 -05:00
Jeremy Stretch
b841875f63 #16783: Misc cleanup 2024-11-25 15:30:15 -05:00
Jeremy Stretch
f17545788f #16547: Reorder API serializer fields for Circuit 2024-11-25 15:26:20 -05:00
Jeremy Stretch
17189456c9 #13086: Include button to terminate virtual circuit on interfaces table 2024-11-25 14:51:59 -05:00
Jeremy Stretch
b45b8f3a4d #7336: Correct API test 2024-11-25 14:02:45 -05:00
Jeremy Stretch
dd29c0ede5 #16136: Remove obsolete accommodation for Django admin UI 2024-11-25 13:35:15 -05:00
Jeremy Stretch
02cbdc10f2 #9604: Remove provider_network from CircuitTerminationSerializer & CircuitCircuitTerminationSerializer 2024-11-25 13:28:17 -05:00
Jeremy Stretch
6d65d92c38 #7336: Misc cleanup 2024-11-25 13:26:17 -05:00
Jeremy Stretch
5afa6d7964 Closed #18093: Remove redirects for pre-v4.1 virtual disk views 2024-11-25 10:32:31 -05:00
Jeremy Stretch
0946a536f3 #4867: Misc cleanup 2024-11-25 09:56:02 -05:00
Jeremy Stretch
14d769a501 Draft v4.2 release notes 2024-11-22 13:39:52 -05:00
Jeremy Stretch
ff7a59db2e Closes #17752: Rename URL paths for bulk import to *_bulk_import 2024-11-22 12:51:06 -05:00
Jeremy Stretch
343a4af591 Closes #18022: Extend linter (ruff) to enforce line length limit (120 chars) (#18067)
* Enable E501 rule
* Configure ruff formatter
* Reformat migration files to fix line length violations
* Fix various E501 errors
* Move table template code to template_code.py & ignore E501 errors
* Reformat raw SQL
2024-11-21 15:58:11 -05:00
Jeremy Stretch
f08e36e538 Merge branch 'develop' into feature 2024-11-21 14:00:57 -05:00
Jeremy Stretch
a0b4b0afe0 Closes #18023: Employ register_model_view() for list views (#18029)
* Extend register_model_view() to enable registering list views

* Register circuits list views with register_model_view()

* Register core list views with register_model_view()

* Fix bulk_edit & bulk_delete URL paths

* Register dcim list views with register_model_view() (WIP)

* Register dcim list views with register_model_view()

* Register extras list views with register_model_view()

* Register ipam list views with register_model_view()

* Register tenancy list views with register_model_view()

* Register users list views with register_model_view()

* Register virtualization list views with register_model_view()

* Register vpn list views with register_model_view()

* Register wireless list views with register_model_view()

* Add change note for register_model_view()
2024-11-20 15:54:37 -05:00
Jeremy Stretch
d2168b107f Closes #13086: Virtual circuits (#17933)
* WIP

* Add API tests

* Add remaining tests

* Add model docs

* Show virtual circuit connections on interfaces

* Misc cleanup per PR feedback

* Renumber migration

* Support nested terminations for virtual circuit bulk import
2024-11-19 10:58:39 -05:00
Brian Tiemann
7376314821 Access _site of cluster instead of site 2024-11-18 17:05:42 -05:00
bctiemann
353214098b 4867 multiple mac addresses (#17902)
* Create MACAddress model and migrations to convert existing .mac_address fields to standalone objects

* Add migrations

* All views/filtering working and documentation done; no unit tests yet

* Redo migrations following VLAN Translation

* Remove mac_address filter fields and add table columns for device/vm

* Remove unnecessary "bulk rename"

* Fix filterset tests for Device

* Fix filterset tests for Interface

* Fix tests on single-object forms

* Fix serializer tests

* Fix filterset tests for VMInterface

* Fix filterset tests for Device and VirtualMachine

* Move new field check into lookup_map iteration

* Fix general MACAddress filter tests

* Add GraphQL types/filters/schema

* Fix bulk edit/create tests (bulk editing Interfaces will be unsupported because of inheritance from ComponentBulkEditForm)

* Make mac_address read_only on InterfaceSerializer/VMInterfaceSerializer

* Undo unrelated work

* Cleanup unused IPAddress derived stuff

* API endpoints

* Add serializer objects to interface serializers

* Clean up unnecessary bulk create forms/views/routes

* Add SearchIndex and adjust indexable fields for Interface and VMInterface

* Reorganize MACAddress classes out of association with DeviceComponents

* Move MACAddressSerializer

* Enforce saving only a single is_primary MACAddress per interface/vminterface

* Perform is_primary validation on MACAddress model and just check if one already exists for the interface

* Remove form-level validation

* Fix check for current is_primary setting when reassigning

* Model cleanup

* Documentation notes and cleanup

* Simplify serializer and add ip_addresses

* Add to VMInterfaceSerializer too

* Style cleanup

* Standardize "MAC Address" instead of "MAC"

* Remove unused views

* Add is_primary field for bulk edit

* HTML cleanup and add copy-to-clipboard button

* Remove mac_address from Interface and VMInterface bulk-edit forms

* Add device and VM filtering

* Use combined assigned_object_parent in table to match structure of IPAddressTable

* Add GFK fields to MACAddressSerializer

* Reorganize "Addressing" sections to remove from proximity to "Device Components" and related groupings

* Clean up migrations

* Misc cleanup

* Add filterset test

* Remove mac_address field from interface forms

* Designate primary MAC address via a ForeignKey on the interface models

* Add serializer fields for primary_mac_address

* Update docs

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-11-18 15:11:24 -05:00
Jeremy Stretch
b4f15092db Closes #5858: Implement a quick-add UI widget for related objects (#18016)
* WIP

* Misc cleanup

* Add warning re: nested quick-adds
2024-11-18 14:44:57 -05:00
Arthur Hanson
9fe6685562 17929 Add Scope Mixins to Prefix (#17930)
* 17929 Add Scope Mixins to Prefix

* 17929 Add Scope Mixins to Prefix

* 17929 fixes for tests

* 17929 merge latest scope changes

* 12596 review changes

* 12596 review changes

* 12596 review changes

* 12596 review changes

* 12596 review changes

* 12596 review changes

* 17929 fix migrations
2024-11-15 14:55:46 -05:00
Arthur Hanson
6ab0792f02 Closes #11279: Replace _name natural key sorting with collation (#18009)
* 11279 add collation

* 11279 add collation

* 11279 add collation

* 11279 add collation

* 11279 fix tables /tests

* 11279 fix tests

* 11279 refactor VirtualDisk

* Clean up migrations

* Misc cleanup

* Correct errant file inclusion

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-11-15 09:32:09 -05:00
Arthur Hanson
75aeaab8ee 12596 Add Allocated Resources to Cluster API (#17956)
* 12596 Add Allocated Resources to Cluster API

* 12596 Add Allocated Resources to Cluster API

* 12596 Add Allocated Resources to Cluster API

* 12596 Add Allocated Resources to Cluster API

* 12596 review changes

* 12596 review changes
2024-11-15 07:55:32 -05:00
Jeremy Stretch
03d413565f Fix linter error 2024-11-07 14:10:15 -05:00
Jeremy Stretch
a183048891 Closes #17951: Extend Ruff ruleset 2024-11-07 11:52:02 -05:00
Arthur Hanson
812ce8471a 10711 Add Scope to WirelessLAN (#17877)
* 7699 Add Scope to Cluster

* 7699 Serializer

* 7699 filterset

* 7699 bulk_edit

* 7699 bulk_import

* 7699 model_form

* 7699 graphql, tables

* 7699 fixes

* 7699 fixes

* 7699 fixes

* 7699 fixes

* 7699 fix tests

* 7699 fix graphql tests for clusters reference

* 7699 fix dcim tests

* 7699 fix ipam tests

* 7699 fix tests

* 7699 use mixin for model

* 7699 change mixin name

* 7699 scope form

* 7699 scope form

* 7699 scoped form, fitlerset

* 7699 review changes

* 7699 move ScopedFilterset

* 7699 move CachedScopeMixin

* 7699 review changes

* 10711 Add Scope to WirelessLAN

* 10711 Add Scope to WirelessLAN

* 10711 Add Scope to WirelessLAN

* 10711 Add Scope to WirelessLAN

* 10711 Add Scope to WirelessLAN

* 7699 review changes

* 7699 refactor mixins

* 7699 _sitegroup -> _site_group

* 7699 update docstring

* fix model

* remove old constants, update filtersets

* 10711 fix GraphQL

* 10711 fix API

* 10711 add tests

* 10711 review changes

* 10711 add tests

* 10711 add scope to detail template

* 10711 add api test

* Extend CSV test data

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-11-07 10:28:02 -05:00
Alexander Haase
4bba92617d Closes #16971: Add system jobs (#17716)
* Fix check for existing jobs

If a job is to be enqueued once and no specific scheduled time is
specified, any scheduled time of existing jobs will be valid. Only if a
specific scheduled time is specified for 'enqueue_once()' can it be
evaluated.

* Allow system jobs to be registered

A new registry key allows background system jobs to be registered and
automatically scheduled when rqworker starts.

* Test scheduling of system jobs

* Fix plugins scheduled job documentation

The documentation reflected a non-production state of the JobRunner
framework left over from development. Now a more practical example
demonstrates the usage.

* Allow plugins to register system jobs

* Rename system job metadata

To clarify which meta-attributes belong to system jobs, each of them is
now prefixed with 'system_'.

* Add predefined job interval choices

* Remove 'system_enabled' JobRunner attribute

Previously, the 'system_enabled' attribute was used to control whether a
job should run or not. However, this can also be accomplished by
evaluating the job's interval.

* Fix test

* Use a decorator to register system jobs

* Specify interval when registering system job

* Update documentation

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-11-01 14:56:08 -04:00
Arthur Hanson
6dc75d8db1 7699 Add Scope to Cluster (#17848)
* 7699 Add Scope to Cluster

* 7699 Serializer

* 7699 filterset

* 7699 bulk_edit

* 7699 bulk_import

* 7699 model_form

* 7699 graphql, tables

* 7699 fixes

* 7699 fixes

* 7699 fixes

* 7699 fixes

* 7699 fix tests

* 7699 fix graphql tests for clusters reference

* 7699 fix dcim tests

* 7699 fix ipam tests

* 7699 fix tests

* 7699 use mixin for model

* 7699 change mixin name

* 7699 scope form

* 7699 scope form

* 7699 scoped form, fitlerset

* 7699 review changes

* 7699 move ScopedFilterset

* 7699 move CachedScopeMixin

* 7699 review changes

* 7699 review changes

* 7699 refactor mixins

* 7699 _sitegroup -> _site_group

* 7699 update docstring

* Misc cleanup

* Update migrations

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-11-01 14:18:23 -04:00
Jeremy Stretch
8767fd8186 Closes #13428: Q-in-Q VLANs (#17822)
* Initial work on #13428 (QinQ)

* Misc cleanup; add tests for Q-in-Q fields

* Address PR feedback
2024-10-31 14:17:06 -04:00
Arthur Hanson
a8eb455f3e 9604 Add Termination to CircuitTermination (#17821)
* 9604 add scope type to CircuitTermination

* 9604 add scope type to CircuitTermination

* 9604 add scope type to CircuitTermination

* 9604 model_forms

* 9604 form filtersets

* 9604 form bulk_import

* 9604 form bulk_edit

* 9604 serializers

* 9604 graphql

* 9604 tests and detail template

* 9604 fix migration merge

* 9604 fix tests

* 9604 fix tests

* 9604 fix table

* 9604 updates

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* 9604 remove provider_network

* 9604 fix tests

* 9604 fix tests

* 9604 fix forms

* 9604 review changes

* 9604 scope->termination

* 9604 fix _circuit_terminations

* 9604 fix _circuit_terminations

* 9604 sitegroup -> site_group

* 9604 update docs

* 9604 fix form termination side reference

* Misc cleanup

* Fix terminations in circuits table

* Fix missing imports

* Clean up termination attrs display

* Add termination & type to CircuitTerminationTable

* Update cable tracing logic

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-10-31 09:55:08 -04:00
bctiemann
f74a9a1c76 Fixes: #7336 - VLAN Translation (#17745)
* VLANTranslationPolicy and VLANTranslationRule models and all associated UI classes

* Change VLANTranslationPolicy to a PrimaryModel and make name unique

* Add serializer classes to InterfaceSerializer

* Remake migrations

* Add GraphQL typing

* Skip tagged models in test

* Missing migration

* Remove get_absolute_url methods

* Remove package-lock.json

* Rebuild migration and add constraints and field options

* Rebuild migrations

* Use DynamicModelChoiceField for policy field

* Make vlan_translation_policy fields on filtersets more consistent with existing __name convention

* Add vlan_translation_table to VMInterface detail page

* Add vlan_translation_policy to VMInterfaceSerializer

* Move vlan_translation_policy fields to model and filterset mixins

* Protect in-use policies against deletion

* Add vlan_translation_policy to fields in VMInterfaceSerializer

* Cleanup indentation

* Remove unnecessary ordering column

* Rebuild migrations

* Search methods and registration

* Ensure 'id' column is present by default

* Add graphql types/filters/schema for VLANTranslationRule

* Filterset tests

* View tests

* API and viewset tests (incomplete)

* Add tags to VLANTranslationRuleForm

* Complete viewset tests for VLANTranslationRule

* Make VLANTranslationRule.policy nullable (but still required)

* Revert "Make VLANTranslationRule.policy nullable (but still required)"

This reverts commit 4c1bb437ef.

* Revert nullability

* Explicitly prefetch policy in graphql

* Documentation of new and affected models

* Add note about select_related in graphql

* Rework policy/rule documentation

* Move vlan_translation_policy into 802.1Q Switching fieldset

* Remove redundant InterfaceVLANTranslationTable

* Conditionally include vlan_translation_table in interface.html and vminterface.html

* Add description field to VLANTranslationRule

* Define vlan_translation_table conditionally

* Add policy (name) filter to VLANTranslationRuleFilterSet

* Revert changes to adding-models.md (moved to another PR)

* Dynamic table for linked rules in vlantranslationpolicy.html

* Misc cleanup

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-10-30 17:09:46 -04:00
Jeremy Stretch
19c5c32965 Merge branch 'develop' into feature 2024-10-30 13:10:47 -04:00
Brian Tiemann
572aad0e20 Update adding-models.md to reflect changes to get_absolute_url 2024-10-29 09:15:24 -04:00
Arthur Hanson
99904c1518 Merge branch 'develop' into feature 2024-10-28 14:29:48 -07:00
Jeremy Stretch
ef1fdf0a01 Closes #17761: Store empty CharField choices as null 2024-10-21 13:12:05 -04:00
Jeremy Stretch
75270c1aef Closes #6414: Enable assigning prefixes to various object types (#17692)
* Replace site FK on Prefix with scope GFK

* Add denormalized relations

* Update prefix filters

* Add generic relations for Prefix

* Update GraphQL type for Prefix model

* Fix tests; misc cleanup

* Remove prefix_count from SiteSerializer

* Remove site field from PrefixBulkEditForm

* Restore scope filters for prefixes

* Fix scope population on PrefixForm init

* Show scope type

* Assign scope during bulk import of prefixes

* Correct handling of GenericForeignKey in PrefixForm

* Add prefix counts to all scoped objects

* Fix migration; linter fix

* Add limit_choices_to on scope_type

* Clean up cache_related_objects()

* Enable bulk editing prefix scope
2024-10-18 15:45:22 -04:00
Jeremy Stretch
c78da79ce6 Merge branch 'develop' into feature 2024-10-18 13:18:11 -04:00
Jeremy Stretch
538f3255a7 Closes #17789: Use a single scope field for VLANGroup bulk edit 2024-10-18 13:16:24 -04:00
Arthur Hanson
b9c6def8ab 15093 Allow plugins to register events pipeline (#17717)
* 15093 add events_pipeline registration to plugins

* 15093 use list

* 15093 add documentation

* Update docs/configuration/index.md

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

* Update docs/configuration/miscellaneous.md

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

* 15093 review changes

* 15093 review changes

* Formatting & readability

* 15093 review changes

* 15093 add test

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-10-18 12:47:16 -04:00
Jeremy Stretch
a8ec06687a Reomove obsolete nested serializers 2024-10-18 08:16:30 -04:00
Jeremy Stretch
6aeb553391 Closes #17029: Deprecate PluginTemplateExtension.model 2024-10-18 08:15:31 -04:00
Daniel Sheppard
d6ab7b7378 Update bundle 2024-10-16 22:24:48 -05:00
Daniel Sheppard
77cb8ac817 Merge branch 'feature' into 9583-add_column_specific_search_field_to_tables 2024-10-16 22:13:01 -05:00
Arthur Hanson
5b8ef2eb44 17778 update node.js 2024-10-16 16:37:55 -04:00
Jeremy Stretch
e370cadcef Merge branch 'develop' into feature 2024-10-16 08:13:43 -04:00
Jeremy Stretch
727de0fb59 Remove unused imports 2024-10-11 12:27:39 -04:00
Jeremy Stretch
b99c356d1d Fix migrations conflict 2024-10-11 11:36:56 -04:00
Jeremy Stretch
256b5dc676 Merge branch 'develop' into feature 2024-10-11 11:29:36 -04:00
Arthur Hanson
65687851fe 16547 Add distance to Circuit (#17629)
* 16547 Add distance to Circuit

* 16547 fix test cases

* 16547 fix test cases

* 16547 add distance to API, forms, tables

* 16547 fixes

* 16547 fixes

* 16547 review changes

* 16547 review changes

* Clean up DistanceColumn

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-10-10 14:37:33 -04:00
Jeremy Stretch
bc597c3c5d Closes #17472: Deprecate the staged changes API 2024-10-10 14:32:39 -04:00
Arthur Hanson
ed1327eeb7 16546 define get_absolute_url method in NetBoxModel (#17693)
* 16546 define get_absolute_url method in NetBoxModel

* 16546 define get_absolute_url method in NetBoxModel

* 16546 define get_absolute_url method in NetBoxModel

* 16546 move get_absolute_url to NetBoxFeatureSet

* 16546 fix CircuitTermination

* Fix linebreaks

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-10-10 09:59:19 -04:00
Arthur Hanson
3f469b0b0a 17476 Upgrade to Django 5.1 (#17620)
* 17476 Upgrade to Django 5.1

* 16136 fix migrations

* 17476 update remove GFK from fields

* 17476 fix

* 17476 update django to bugfix release

* fix comma
2024-10-10 09:08:58 -04:00
Arthur Hanson
1969f0dc71 fix migrations 2024-10-02 12:52:27 -07:00
Arthur Hanson
2c68aca344 17195 Add color to PowerOutlet (#17623)
* 17195 Add color to PowerOutlet

* 17195 Add color to PowerOutlet

* Reorder serializer fields

* Misc cleanup

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-10-02 08:51:41 -04:00
Arthur Hanson
8cd0a3215c 16783 Add status field to InventoryItem (#17627)
* 16783 Add status field to InventoryItem

* 16783 fix tests

* 16783 fix tests

* 16783 review changes
2024-09-30 18:26:14 -04:00
Arthur Hanson
c60a0f4f56 16136 remove Django Admin (#17619)
* 16136 remove Django Admin

* 16136 fix plugin test

* 16136 fix migrations

* Revert "16136 fix migrations"

This reverts commit 80296fa1ec.

* Remove obsolete admin module from dummy plugin

* Remove obsolete admin site configuration

* Remove unused import statement

* Remove obsolete admin module

* Misc cleanup

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-09-27 08:33:02 -04:00
Daniel Sheppard
b12ce97474 Update to latest feature 2024-07-01 22:27:15 -05:00
Daniel Sheppard
0ad1db9912 Regnerate CSS 2024-06-12 23:22:13 -05:00
Daniel Sheppard
f1306783dc Merge branch 'feature' of https://github.com/netbox-community/netbox into 9583-add_column_specific_search_field_to_tables
# Conflicts:
#	netbox/project-static/dist/netbox.css
#	netbox/templates/htmx/table.html
#	netbox/templates/inc/table_htmx.html
2024-06-12 23:10:25 -05:00
Daniel Sheppard
30e653197e Add a table form override and add a table column remap option 2024-06-12 23:01:33 -05:00
Daniel Sheppard
fd38255d31 Rename swap variable back and remove join 2024-04-25 13:04:37 -05:00
Daniel Sheppard
5830ae9e9a Rename variable for doing OOB swaps on the table 2024-04-25 12:53:54 -05:00
Daniel Sheppard
479c69bd2d Fix up duplication of template chits when rendering HTMX 2024-04-22 22:42:42 -05:00
Daniel Sheppard
1c995fa5f9 Perform OOB swap for filter badges 2024-04-15 08:43:51 -05:00
Daniel Sheppard
35cff12974 Apply suggestions from Arthur 2024-04-15 08:10:40 -05:00
Daniel Sheppard
a9aa0cbaa0 Merge in latest feature 2024-04-15 07:30:28 -05:00
Daniel Sheppard
8ad79a64ae Update CSS 2024-03-21 23:18:31 -05:00
Daniel Sheppard
a422a3cd98 Modify logic for table column filtering to further isolate the column filtering and the filterset tab 2024-03-21 23:09:30 -05:00
Daniel Sheppard
8a7df0b98d Update netbox/utilities/templatetags/form_helpers.py
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-03-21 22:37:06 -05:00
Daniel Sheppard
f257f4aad4 Apply suggestions from code review
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-03-21 22:18:34 -05:00
Jeremy Stretch
25a4e9448c Merge branch 'feature' into 9583-add_column_specific_search_field_to_tables 2024-03-20 15:02:12 -04:00
Jeremy Stretch
77bfd620c3 Merge branch 'feature' into 9583-add_column_specific_search_field_to_tables 2024-03-07 08:50:17 -05:00
Daniel Sheppard
84151cbc1a Fix tom-select errors related to field id. Break out render_field function for column filters 2024-02-13 15:23:14 -06:00
Daniel Sheppard
0309796bbb Fix extraneous __all__ entry 2024-02-12 16:09:02 -06:00
Daniel Sheppard
f81f76f862 Optimizations 2024-02-12 16:00:19 -06:00
Daniel Sheppard
c3f1a9601c Update CSS after merge 2024-02-12 09:49:15 -06:00
Daniel Sheppard
4ae6683abc Merge in recent feture changes 2024-02-12 09:44:30 -06:00
Daniel Sheppard
06c1aff04f Fix test failure 2024-02-12 08:17:53 -06:00
Daniel Sheppard
5f69666b7b Change dropdown position and fix test failure 2024-02-11 21:37:33 -06:00
Daniel Sheppard
b54cfd6ba9 Update CSS 2024-02-11 21:17:06 -06:00
Daniel Sheppard
3243ebd1dd Merge remote-tracking branch 'origin/9583-add_column_specific_search_field_to_tables' into 9583-add_column_specific_search_field_to_tables 2024-02-09 16:37:47 -06:00
Daniel Sheppard
03f67f373f CSS update for dropdown 2024-02-09 16:36:13 -06:00
Daniel Sheppard
50557c0f9d Final work on #9583 for basic functionality 2024-02-09 16:35:09 -06:00
Daniel Sheppard
cc423f5071 Apply some quick fixes and add comments 2024-02-07 13:48:32 -06:00
Daniel Sheppard
664a0eba6d Preliminary work on 9583. 2024-02-07 13:40:20 -06:00
Daniel Sheppard
f7294f7087 Preliminary work on 9583. 2024-02-07 13:37:57 -06:00
Daniel Sheppard
e762755e80 HTMX work on 9583. 2024-02-07 12:05:11 -06:00
Daniel Sheppard
4c39516253 Preliminary work on 9583. 2024-02-02 15:49:32 -06:00
544 changed files with 48816 additions and 30087 deletions

View File

@@ -1,5 +1,6 @@
---
name: ✨ Feature Request
type: Feature
description: Propose a new NetBox feature or enhancement
labels: ["type: feature", "status: needs triage"]
body:
@@ -14,7 +15,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.1.10
placeholder: v4.2.3
validations:
required: true
- type: dropdown
@@ -27,19 +28,6 @@ body:
- Other
validations:
required: true
- type: dropdown
attributes:
label: Triage priority
description: >
Issue triage may be prioritized in some cases. Select whichever of the following
conditions applies, if any.
options:
- I volunteer to perform this work (if approved)
- I'm a NetBox Labs customer
- N/A
default: 2
validations:
required: true
- type: textarea
attributes:
label: Proposed functionality

View File

@@ -1,5 +1,6 @@
---
name: 🐛 Bug Report
type: Bug
description: Report a reproducible bug in the current release of NetBox
labels: ["type: bug", "status: needs triage"]
body:
@@ -22,24 +23,11 @@ body:
- Self-hosted
validations:
required: true
- type: dropdown
attributes:
label: Triage priority
description: >
Issue triage may be prioritized in some cases. Select whichever of the following
conditions applies, if any.
options:
- I volunteer to perform this work (if approved)
- I'm a NetBox Labs customer
- N/A
default: 2
validations:
required: true
- type: input
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.1.10
placeholder: v4.2.3
validations:
required: true
- type: dropdown

View File

@@ -1,5 +1,6 @@
---
name: 📖 Documentation Change
type: Documentation
description: Suggest an addition or modification to the NetBox documentation
labels: ["type: documentation", "status: needs triage"]
body:

View File

@@ -1,5 +1,6 @@
---
name: 🌍 Translation
type: Translation
description: Request support for a new language in the user interface
labels: ["type: translation"]
body:

View File

@@ -1,5 +1,6 @@
---
name: 🏡 Housekeeping
type: Housekeeping
description: A change pertaining to the codebase itself (developers only)
labels: ["type: housekeeping"]
body:

View File

@@ -1,5 +1,6 @@
---
name: 🗑️ Deprecation
type: Deprecation
description: The removal of an existing feature or resource
labels: ["type: deprecation"]
body:

View File

@@ -2,7 +2,7 @@
blank_issues_enabled: false
contact_links:
- name: 📖 Contributing Policy
url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md
url: https://github.com/netbox-community/netbox/blob/main/CONTRIBUTING.md
about: "Please read through our contributing policy before opening an issue or pull request."
- name: ❓ Discussion
url: https://github.com/netbox-community/netbox/discussions

View File

@@ -3,11 +3,15 @@ name: CI
on:
push:
paths-ignore:
- '.github/ISSUE_TEMPLATE/**'
- '.github/PULL_REQUEST_TEMPLATE.md'
- 'contrib/**'
- 'docs/**'
- 'netbox/translations/**'
pull_request:
paths-ignore:
- '.github/ISSUE_TEMPLATE/**'
- '.github/PULL_REQUEST_TEMPLATE.md'
- 'contrib/**'
- 'docs/**'
- 'netbox/translations/**'
@@ -28,7 +32,7 @@ jobs:
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
node-version: ['18.x']
node-version: ['20.x']
services:
redis:
image: redis

View File

@@ -38,7 +38,7 @@ jobs:
issues may receive direct feedback. **Do not** attempt to circumvent this
process by "bumping" the issue; doing so will result in its immediate closure
and you may be barred from participating in any future discussions. Please see
our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
our [contributing guide](https://github.com/netbox-community/netbox/blob/main/CONTRIBUTING.md).
# Pull request parameters
close-pr-message: >

View File

@@ -84,7 +84,7 @@ 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.
* New pull requests should generally be based off of the `develop` branch, rather than `master`. The `develop` branch is used for ongoing development, while `master` is used for tracking stable releases. (If you're developing for an upcoming minor release, use `feature` instead.)
* 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.)

View File

@@ -1,12 +1,12 @@
<div align="center">
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo_light.svg" width="400" alt="NetBox logo" />
<img src="https://raw.githubusercontent.com/netbox-community/netbox/main/docs/netbox_logo_light.svg" width="400" alt="NetBox logo" />
<p><strong>The cornerstone of every automated network</strong></p>
<a href="https://github.com/netbox-community/netbox/releases"><img src="https://img.shields.io/github/v/release/netbox-community/netbox" alt="Latest release" /></a>
<a href="https://github.com/netbox-community/netbox/blob/master/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
<a href="https://github.com/netbox-community/netbox/blob/main/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" /></a>
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=main" alt="CI status" /></a>
<p>
<strong><a href="https://github.com/netbox-community/netbox/">NetBox Community</a></strong> |
<strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> |

View File

@@ -1,6 +1,6 @@
# The Python web framework on which NetBox is built
# https://docs.djangoproject.com/en/stable/releases/
Django<5.1
Django<5.2
# Django middleware which permits cross-domain API requests
# https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst
@@ -8,8 +8,6 @@ django-cors-headers
# Runtime UI tool for debugging Django
# https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst
# Pinned for DNS looukp bug; see https://github.com/netbox-community/netbox/issues/16454
# and https://github.com/jazzband/django-debug-toolbar/issues/1927
django-debug-toolbar
# Library for writing reusable URL query filters
@@ -90,7 +88,8 @@ mkdocs-material
# Introspection for embedded code
# https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md
mkdocstrings[python-legacy]
# See #18568
mkdocstrings[python-legacy]==0.27.0
# Library for manipulating IP prefixes and addresses
# https://github.com/netaddr/netaddr/blob/master/CHANGELOG.rst
@@ -101,7 +100,7 @@ netaddr
nh3
# Fork of PIL (Python Imaging Library) for image processing
# https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
# https://github.com/python-pillow/Pillow/releases
Pillow
# PostgreSQL database adapter for Python
@@ -134,7 +133,8 @@ strawberry-graphql
# Strawberry GraphQL Django extension
# https://github.com/strawberry-graphql/strawberry-django/releases
strawberry-graphql-django
# Pinned to v0.52.0 for suspected upstream bug; see #18329
strawberry-graphql-django==0.52.0
# SVG image rendering (used for rack elevations)
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst

View File

@@ -96,14 +96,6 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
---
## DJANGO_ADMIN_ENABLED
Default: False
Setting this to True installs the `django.contrib.admin` app and enables the [Django admin UI](https://docs.djangoproject.com/en/5.0/ref/contrib/admin/). This may be necessary to support older plugins which do not integrate with the native NetBox interface.
---
## ENFORCE_GLOBAL_UNIQUE
!!! tip "Dynamic Configuration Parameter"
@@ -114,6 +106,16 @@ By default, NetBox will prevent the creation of duplicate prefixes and IP addres
---
## EVENTS_PIPELINE
!!! info "This parameter was introduced in NetBox v4.2."
Default: `['extras.events.process_event_queue',]`
NetBox will call dotted paths to the functions listed here for events (create, update, delete) on models as well as when custom EventRules are fired.
---
## FILE_UPLOAD_MAX_MEMORY_SIZE
Default: `2621440` (2.5 MB)

View File

@@ -2,7 +2,7 @@
## ALLOWED_HOSTS
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attackes](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attacks](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
!!! note
This parameter must always be defined as a list or tuple, even if only a single value is provided.
@@ -25,7 +25,7 @@ ALLOWED_HOSTS = ['*']
## DATABASE
NetBox requires access to a PostgreSQL 12 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary:
NetBox requires access to a PostgreSQL 13 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary:
* `NAME` - Database name
* `USER` - PostgreSQL username

View File

@@ -89,8 +89,6 @@ addresses (and [`DEBUG`](./development.md#debug) is true).
## ISOLATED_DEPLOYMENT
!!! info "This feature was introduced in NetBox v4.1."
Default: False
Set this configuration parameter to True for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.

View File

@@ -2,7 +2,7 @@
Custom links allow users to display arbitrary hyperlinks to external content within NetBox object views. These are helpful for cross-referencing related records in systems outside NetBox. For example, you might create a custom link on the device view which links to the current device in a Network Monitoring System (NMS).
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja2 template code](https://jinja2docs.readthedocs.io/en/stable/) through the variable `object`, and custom fields through `object.cf`.
Custom links are created by navigating to Customization > Custom Links. Each link is associated with a particular NetBox object type (site, device, prefix, etc.) and will be displayed on relevant views. Each link has display text and a URL, and data from the NetBox item being viewed can be included in the link using [Jinja template code](https://jinja.palletsprojects.com/en/stable/) through the variable `object`, and custom fields through `object.cf`.
For example, you might define a link like this:

View File

@@ -8,7 +8,7 @@ Each model should define, at a minimum:
* A `Meta` class specifying a deterministic ordering (if ordered by fields other than the primary ID)
* A `__str__()` method returning a user-friendly string representation of the instance
* A `get_absolute_url()` method returning an instance's direct URL (using `reverse()`)
* A `get_absolute_url()` method if necessary; a standard version of the method is defined in the `NetBoxFeatureSet` base class, but you will need to provide your own (returning an instance's direct URL using `reverse()`) if not subclassing that base class
## 2. Define field choices
@@ -78,6 +78,8 @@ Create the following for each model:
Create a GraphQL object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
**Note:** GraphQL unit tests may fail citing null values on a non-nullable field if related objects are prefetched. You may need to fix this by setting the type annotation to be `= strawberry_django.field(select_related=["policy"])` or similar.
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention.
## 14. Add tests

View File

@@ -37,16 +37,12 @@ CHANGELOG.md CONTRIBUTING.md LICENSE.txt netbox README.md scri
### 2. Create a New Branch
The NetBox project utilizes three persistent git branches to track work:
The NetBox project utilizes two persistent git branches to track work:
* `master` - Serves as a snapshot of the current stable release
* `develop` - All development on the upcoming stable (patch) release occurs here
* `feature` - Tracks work on an upcoming minor release
* `main` - All development on the upcoming stable (patch) release occurs here. Releases are published from this branch.
* `feature` - All work planned for the upcoming minor release is done here.
Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. For example, assume that the current NetBox release is v3.3.5. Work applied to the `develop` branch will appear in v3.3.6, and work done under the `feature` branch will be included in the next minor release (v3.4.0).
!!! warning
**Never** merge pull requests into the `master` branch: This branch only ever merges pull requests from the `develop` branch, to effect a new release.
Typically, you'll base pull requests off of the `main` branch, or off of `feature` if you're working on the upcoming minor or major release. For example, assume that the current NetBox release is v4.2.3. Work applied to the `main` branch will appear in v4.2.4, and work done under the `feature` branch will be included in the next minor release (v4.3.0).
To create a new branch, first ensure that you've checked out the desired base branch, then run:

View File

@@ -128,7 +128,7 @@ Fast-forward
```
!!! warning "Avoid Merging Remote Branches"
You generally want to avoid merging branches that exist on the remote (upstream) repository, such as `develop` and `feature`: Merges into these branches should be done via a pull request on GitHub. Only merge branches when it is necessary to consolidate work you've done locally.
You generally want to avoid merging branches that exist on the remote (upstream) repository, namely `main` and `feature`: Merges into these branches should be done via a pull request on GitHub. Only merge branches when it is necessary to consolidate work you've done locally.
### Show Pending Changes
@@ -196,7 +196,7 @@ index 93e125079..4344fb514 100644
+and here too
+
<div align="center">
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
<img src="https://raw.githubusercontent.com/netbox-community/netbox/main/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
</div>
diff --git a/foo.py b/foo.py
new file mode 100644

View File

@@ -8,11 +8,10 @@ NetBox and many of its related projects are maintained on [GitHub](https://githu
![GitHub](../media/development/github.png)
There are three permanent branches in the repository:
There are two permanent branches in the repository:
* `master` - The current stable release. Individual changes should never be pushed directly to this branch, but rather merged from `develop`.
* `develop` - Active development for the upcoming patch release. Pull requests will typically be based on this branch unless they introduce breaking changes that must be deferred until the next minor release.
* `feature` - New feature work to be introduced in the next minor release (e.g. from v3.3 to v3.4).
* `main` - Active development for the upcoming patch release. Pull requests will typically be based on this branch unless they introduce breaking changes that must be deferred until the next minor release.
* `feature` - New feature work to be introduced in the next minor release (e.g. from v4.2 to v4.3).
NetBox components are arranged into Django apps. Each app holds the models, views, and other resources relevant to a particular function:
@@ -57,4 +56,4 @@ NetBox follows the [benevolent dictator](http://oss-watch.ac.uk/resources/benevo
## Licensing
The entire NetBox project is licensed as open source under the [Apache 2.0 license](https://github.com/netbox-community/netbox/blob/master/LICENSE.txt). This is a very permissive license which allows unlimited redistribution of all code within the project. Note that all submissions to the project are subject to the same license.
The entire NetBox project is licensed as open source under the [Apache 2.0 license](https://github.com/netbox-community/netbox/blob/main/LICENSE.txt). This is a very permissive license which allows unlimited redistribution of all code within the project. Note that all submissions to the project are subject to the same license.

View File

@@ -43,9 +43,9 @@ Follow these instructions to perform a new installation of NetBox in a temporary
Upgrading from a previous version typically involves database migrations, which must work without errors. Supported upgrade paths include from one minor version to another within the same major version (i.e. 4.0 to 4.1), as well as from the latest patch version of the previous minor version (i.e. 3.7 to 4.0 or to 4.1). Prior to release, test all these supported paths by loading demo data from the source version and performing a `./manage.py migrate`.
### Merge the Release Branch
### Merge the `feature` Branch
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
Submit a pull request to merge the `feature` branch into the `main` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
### Rebuild Demo Data (After Release)
@@ -55,6 +55,15 @@ After the release of a new minor version, generate a new demo data snapshot comp
## Patch Releases
### Create a Release Branch
Begin by creating a new branch (based off of `main`) to effect the release. This will comprise the changes listed below.
```
git checkout main
git checkout -B release-vX.Y.Z
```
### Notify netbox-docker Project of Any Relevant Changes
Notify the [`netbox-docker`](https://github.com/netbox-community/netbox-docker) maintainers (in **#netbox-docker**) of any changes that may be relevant to their build process, including:
@@ -111,25 +120,19 @@ Then, compile these portable (`.po`) files for use in the application:
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
* Replace the "FUTURE" placeholder in the release notes with the current date.
Commit these changes to the `develop` branch and push upstream.
### Verify CI Build Status
Ensure that continuous integration testing on the `develop` branch is completing successfully. If it fails, take action to correct the failure before proceeding with the release.
### Submit a Pull Request
Submit a pull request titled **"Release vX.Y.Z"** to merge the `develop` branch into `master`. Copy the documented release notes into the pull request's body.
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.
Once CI has completed on the PR, merge it. This effects a new release in the `master` branch.
Once CI has completed and a colleague has reviewed the PR, merge it. This effects a new release in the `main` branch.
### Create a New Release
Create a [new release](https://github.com/netbox-community/netbox/releases/new) on GitHub with the following parameters.
* **Tag:** Current version (e.g. `v3.3.1`)
* **Target:** `master`
* **Title:** Version and date (e.g. `v3.3.1 - 2022-08-25`)
* **Tag:** Current version (e.g. `v4.2.1`)
* **Target:** `main`
* **Title:** Version and date (e.g. `v4.2.1 - 2025-01-17`)
* **Description:** Copy from the pull request body, then promote the `###` headers to `##` ones
Once created, the release will become available for users to install.

View File

@@ -14,10 +14,10 @@ To update the English `.po` file from which all translations are derived, use th
./manage.py makemessages -l en -i "project-static/*"
```
Then, commit the change and push to the `develop` branch on GitHub. Any new strings will appear for translation on Transifex automatically.
Then, commit the change and push to the `main` branch on GitHub. Any new strings will appear for translation on Transifex automatically.
!!! note
It is typically not necessary to update source strings manually, as this is done nightly by a [GitHub action](https://github.com/netbox-community/netbox/blob/develop/.github/workflows/update-translation-strings.yml).
It is typically not necessary to update source strings manually, as this is done nightly by a [GitHub action](https://github.com/netbox-community/netbox/blob/main/.github/workflows/update-translation-strings.yml).
## Updating Translated Strings

View File

@@ -46,7 +46,7 @@ Regions will always be listed alphabetically by name within each parent, and the
Like regions, site groups can be arranged in a recursive hierarchy for grouping sites. However, whereas regions are intended for geographic organization, site groups may be used for functional grouping. For example, you might classify sites as corporate, branch, or customer sites in addition to where they are physically located.
The use of both regions and site groups affords to independent but complementary dimensions across which sites can be organized.
The use of both regions and site groups affords two independent but complementary dimensions across which sites can be organized.
## Sites

View File

@@ -1,7 +1,5 @@
# Notifications
!!! info "This feature was introduced in NetBox v4.1."
NetBox includes a system for generating user notifications, which can be marked as read or deleted by individual users. There are two built-in mechanisms for generating a notification:
* A user can subscribe to an object. When that object is modified, a notification is created to inform the user of the change.

View File

@@ -2,8 +2,8 @@
This section entails the installation and configuration of a local PostgreSQL database. If you already have a PostgreSQL database service in place, skip to [the next section](2-redis.md).
!!! warning "PostgreSQL 12 or later required"
NetBox requires PostgreSQL 12 or later. Please note that MySQL and other relational databases are **not** supported.
!!! warning "PostgreSQL 13 or later required"
NetBox requires PostgreSQL 13 or later. Please note that MySQL and other relational databases are **not** supported.
## Installation
@@ -34,7 +34,7 @@ This section entails the installation and configuration of a local PostgreSQL da
sudo systemctl enable --now postgresql
```
Before continuing, verify that you have installed PostgreSQL 12 or later:
Before continuing, verify that you have installed PostgreSQL 13 or later:
```no-highlight
psql -V
@@ -62,6 +62,9 @@ GRANT CREATE ON SCHEMA public TO netbox;
!!! danger "Use a strong password"
**Do not use the password from the example.** Choose a strong, random password to ensure secure database authentication for your NetBox installation.
!!! danger "Use UTF8 encoding"
Make sure that your database uses `UTF8` encoding (the default for new installations). Especially do not use `SQL_ASCII` encoding, as it can lead to unpredictable and unrecoverable errors. Enter `\l` to check your encoding.
Once complete, enter `\q` to exit the PostgreSQL shell.
## Verify Service Status

View File

@@ -29,7 +29,7 @@ python3 -V
## Download NetBox
This documentation provides two options for installing NetBox: from a downloadable archive, or from the git repository. Installing from a package (option A below) requires manually fetching and extracting the archive for every future update, whereas installation via git (option B) allows for seamless upgrades by re-pulling the `master` branch.
This documentation provides two options for installing NetBox: from a downloadable archive, or from the git repository. Installing from a package (option A below) requires manually fetching and extracting the archive for every future update, whereas installation via git (option B) allows for seamless upgrades by checking out the latest release tag.
### Option A: Download a Release Archive
@@ -67,16 +67,13 @@ If `git` is not already installed, install it:
sudo yum install -y git
```
Next, clone the **master** branch of the NetBox GitHub repository into the current directory. (This branch always holds the current stable release.)
Next, clone the git repository:
```no-highlight
sudo git clone -b master --depth 1 https://github.com/netbox-community/netbox.git .
sudo git clone https://github.com/netbox-community/netbox.git .
```
!!! note
The `git clone` command above utilizes a "shallow clone" to retrieve only the most recent commit. If you need to download the entire history, omit the `--depth 1` argument.
The `git clone` command should generate output similar to the following:
This command should generate output similar to the following:
```
Cloning into '.'...
@@ -88,8 +85,13 @@ Receiving objects: 100% (996/996), 4.26 MiB | 9.81 MiB/s, done.
Resolving deltas: 100% (148/148), done.
```
!!! note
Installation via git also allows you to easily try out different versions of NetBox. To check out a [specific NetBox release](https://github.com/netbox-community/netbox/releases), use the `git checkout` command with the desired release tag. For example, `git checkout v3.0.8`.
Finally, check out the tag for the desired release. You can find these on our [releases page](https://github.com/netbox-community/netbox/releases). Replace `vX.Y.Z` with your selected release tag below.
```
sudo git checkout vX.Y.Z
```
Using this installation method enables easy upgrades in the future by simply checking out the latest release tag.
## Create the NetBox System User

View File

@@ -21,7 +21,7 @@ The following sections detail how to set up a new instance of NetBox:
| Dependency | Supported Versions |
|------------|--------------------|
| Python | 3.10, 3.11, 3.12 |
| PostgreSQL | 12+ |
| PostgreSQL | 13+ |
| Redis | 4.0+ |
Below is a simplified overview of the NetBox application stack for reference:

View File

@@ -20,15 +20,15 @@ NetBox requires the following dependencies:
| Dependency | Supported Versions |
|------------|--------------------|
| Python | 3.10, 3.11, 3.12 |
| PostgreSQL | 12+ |
| PostgreSQL | 13+ |
| Redis | 4.0+ |
## 3. Install the Latest Release
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository.
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by checking out the latest production release from the git repository.
!!! warning
Use the same method as you used to install NetBox originally
Use the same method as you used to install NetBox originally.
If you are not sure how NetBox was installed originally, check with this command:
@@ -36,10 +36,7 @@ If you are not sure how NetBox was installed originally, check with this command
ls -ld /opt/netbox /opt/netbox/.git
```
If NetBox was installed from a release package, then `/opt/netbox` will be a
symlink pointing to the current version, and `/opt/netbox/.git` will not
exist. If it was installed from git, then `/opt/netbox` and
`/opt/netbox/.git` will both exist as normal directories.
If NetBox was installed from a release package, then `/opt/netbox` will be a symlink pointing to the current version, and `/opt/netbox/.git` will not exist. If it was installed from git, then `/opt/netbox` and `/opt/netbox/.git` will both exist as normal directories.
### Option A: Download a Release
@@ -84,20 +81,20 @@ If you followed the original installation guide to set up gunicorn, be sure to c
sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/
```
### Option B: Clone the Git Repository
### Option B: Check Out a Git Release
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
This guide assumes that NetBox is installed at `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following `git` commands:
```no-highlight
cd /opt/netbox
sudo git checkout master
sudo git pull origin master
```
sudo git fetch --tags
git describe --tags $(git rev-list --tags --max-count=1)
```
!!! info "Checking out an older release"
If you need to upgrade to an older version rather than the current stable release, you can check out any valid [git tag](https://github.com/netbox-community/netbox/tags), each of which represents a release. For example, to checkout the code for NetBox v2.11.11, do:
Check out the desired release by specifying its tag:
sudo git checkout v2.11.11
```
sudo git checkout v4.2.0
```
## 4. Run the Upgrade Script

View File

@@ -1,6 +1,6 @@
# GraphQL API Overview
NetBox provides a read-only [GraphQL](https://graphql.org/) API to complement its REST API. This API is powered by [Strawberry Django](https://strawberry-graphql.github.io/strawberry-django/).
NetBox provides a read-only [GraphQL](https://graphql.org/) API to complement its REST API. This API is powered by [Strawberry Django](https://strawberry.rocks/).
## Queries
@@ -47,7 +47,7 @@ NetBox provides both a singular and plural query field for each object type:
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of filters) to fetch all devices.
For more detail on constructing GraphQL queries, see the [GraphQL queries documentation](https://graphql.org/learn/queries/). For filtering and lookup syntax, please refer to the [Strawberry Django documentation](https://strawberry-graphql.github.io/strawberry-django/guide/filters/).
For more detail on constructing GraphQL queries, see the [GraphQL queries documentation](https://graphql.org/learn/queries/). For filtering and lookup syntax, please refer to the [Strawberry Django documentation](https://strawberry.rocks/docs/django/guide/filters).
## Filtering

View File

@@ -79,5 +79,5 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
| HTTP service | nginx or Apache |
| WSGI service | gunicorn or uWSGI |
| Application | Django/Python |
| Database | PostgreSQL 12+ |
| Database | PostgreSQL 13+ |
| Task queuing | Redis/django-rq |

View File

@@ -36,6 +36,12 @@ The operational status of the circuit. By default, the following statuses are av
!!! tip "Custom circuit statuses"
Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Distance
!!! info "This field was introduced in NetBox v4.2."
The distance between the circuit's two endpoints, including a unit designation (e.g. 100 meters or 25 feet).
### Description
A brief description of the circuit.

View File

@@ -1,7 +1,5 @@
# Circuit Groups
!!! info "This feature was introduced in NetBox v4.1."
[Circuits](./circuit.md) can be arranged into administrative groups for organization. The assignment of a circuit to a group is optional.
## Fields

View File

@@ -8,9 +8,9 @@ Circuits can be assigned to [circuit groups](./circuitgroup.md) for correlation
The [circuit group](./circuitgroup.md) being assigned.
### Circuit
### Member
The [circuit](./circuit.md) that is being assigned to the group.
The [circuit](./circuit.md) or [virtual circuit](./virtualcircuit.md) assigned to the group.
### Priority

View File

@@ -21,13 +21,11 @@ Designates the termination as forming either the A or Z end of the circuit.
If selected, the circuit termination will be considered "connected" even if no cable has been connected to it in NetBox.
### Site
### Termination
The [site](../dcim/site.md) with which this circuit termination is associated. Once created, a cable can be connected between the circuit termination and a device interface (or similar component).
!!! info "This field replaced the `site` and `provider_network` fields in NetBox v4.2."
### Provider Network
Circuits which do not connect to a site modeled by NetBox can instead be terminated to a [provider network](./providernetwork.md) representing an unknown network operated by a [provider](./provider.md).
The [region](../dcim/region.md), [site group](../dcim/sitegroup.md), [site](../dcim/site.md), [location](../dcim/location.md) or [provider network](./providernetwork.md) with which this circuit termination is associated. Once created, a cable can be connected between the circuit termination and a device interface (or similar component).
### Port Speed

View File

@@ -0,0 +1,39 @@
# Virtual Circuits
!!! info "This feature was introduced in NetBox v4.2."
A virtual circuit can connect two or more interfaces atop a set of decoupled physical connections. For example, it's very common to form a virtual connection between two virtual interfaces, each of which is bound to a physical interface on its respective device and physically connected to a [provider network](./providernetwork.md) via an independent [physical circuit](./circuit.md).
## Fields
### Provider Network
The [provider network](./providernetwork.md) across which the virtual circuit is formed.
### Provider Account
The [provider account](./provideraccount.md) with which the virtual circuit is associated (if any).
### Circuit ID
The unique identifier assigned to the virtual circuit by its [provider](./provider.md).
### Type
The assigned [virtual circuit type](./virtualcircuittype.md).
### Status
The operational status of the virtual circuit. By default, the following statuses are available:
| Name |
|----------------|
| Planned |
| Provisioning |
| Active |
| Offline |
| Deprovisioning |
| Decommissioned |
!!! tip "Custom circuit statuses"
Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.

View File

@@ -0,0 +1,23 @@
# Virtual Circuit Terminations
!!! info "This feature was introduced in NetBox v4.2."
This model represents the connection of a virtual [interface](../dcim/interface.md) to a [virtual circuit](./virtualcircuit.md).
## Fields
### Virtual Circuit
The [virtual circuit](./virtualcircuit.md) to which the interface is connected.
### Interface
The [interface](../dcim/interface.md) connected to the virtual circuit.
### Role
The functional role of the termination. This depends on the virtual circuit's topology, which is typically either peer-to-peer or hub-and-spoke (multipoint). Valid choices include:
* Peer
* Hub
* Spoke

View File

@@ -0,0 +1,13 @@
# Virtual Circuit Types
Like physical [circuits](./circuit.md), [virtual circuits](./virtualcircuit.md) are classified by functional type. These types are completely customizable, and can help categorize circuits by function or technology.
## Fields
### Name
A unique human-friendly name.
### Slug
A unique URL-friendly identifier. (This value can be used for filtering.)

View File

@@ -45,9 +45,12 @@ The operation duplex (full, half, or auto).
The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.
### MAC Address
### Primary MAC Address
The 48-bit MAC address (for Ethernet interfaces).
The [MAC address](./macaddress.md) assigned to this interface which is designated as its primary.
!!! note "Changed in NetBox v4.2"
The MAC address of an interface (formerly a concrete database field) is available as a property, `mac_address`, which reflects the value of the primary linked [MAC address](./macaddress.md) object.
### WWN
@@ -109,6 +112,7 @@ For switched Ethernet interfaces, this identifies the 802.1Q encapsulation strat
* **Access:** All traffic is assigned to a single VLAN, with no tagging.
* **Tagged:** One untagged "native" VLAN is allowed, as well as any number of tagged VLANs.
* **Tagged (all):** Implies that all VLANs are carried by the interface. One untagged VLAN may be designated.
* **Q-in-Q:** Q-in-Q (IEEE 802.1ad) encapsulation is performed using the assigned SVLAN.
This field must be left blank for routed interfaces which do employ 802.1Q encapsulation.
@@ -120,6 +124,12 @@ The "native" (untagged) VLAN for the interface. Valid only when one of the above
The tagged VLANs which are configured to be carried by this interface. Valid only for the "tagged" 802.1Q mode above.
### Q-in-Q SVLAN
!!! info "This field was introduced in NetBox v4.2."
The assigned service VLAN (for Q-in-Q/802.1ad interfaces).
### Wireless Role
Indicates the configured role for wireless interfaces (access point or station).
@@ -142,3 +152,9 @@ The configured channel width of a wireless interface, in MHz. This is typically
### Wireless LANs
The [wireless LANs](../wireless/wirelesslan.md) for which this interface carries traffic. (Valid for wireless interfaces only.)
### VLAN Translation Policy
!!! info "This field was introduced in NetBox v4.2."
The [VLAN translation policy](../ipam/vlantranslationpolicy.md) that applies to this interface (optional).

View File

@@ -25,6 +25,12 @@ The inventory item's name. If the inventory item is assigned to a parent item, i
An alternative physical label identifying the inventory item.
### Status
!!! info "This field was introduced in NetBox v4.2."
The inventory item's operational status.
### Role
The functional [role](./inventoryitemrole.md) assigned to this inventory item.

View File

@@ -0,0 +1,13 @@
# MAC Addresses
!!! info "This feature was introduced in NetBox v4.2."
A MAC address object in NetBox comprises a single Ethernet link layer address, and represents a MAC address as reported by or assigned to a network interface. MAC addresses can be assigned to [device](../dcim/device.md) and [virtual machine](../virtualization/virtualmachine.md) interfaces. A MAC address can be specified as the primary MAC address for a given device or VM interface.
Most interfaces have only a single MAC address, hard-coded at the factory. However, on some devices (particularly virtual interfaces) it is possible to assign additional MAC addresses or change existing ones. For this reason NetBox allows multiple MACAddress objects to be assigned to a single interface.
## Fields
### MAC Address
The 48-bit MAC address, in colon-hexadecimal notation (e.g. `aa:bb:cc:11:22:33`).

View File

@@ -16,8 +16,6 @@ The device to which this module bay belongs.
### Module
!!! info "This feature was introduced in NetBox v4.1."
The module to which this bay belongs (optional).
### Name

View File

@@ -42,6 +42,4 @@ The numeric weight of the module, including a unit designation (e.g. 3 kilograms
### Airflow
!!! info "The `airflow` field was introduced in NetBox v4.1."
The direction in which air circulates through the device chassis for cooling.

View File

@@ -29,6 +29,12 @@ An alternative physical label identifying the power outlet.
The type of power outlet.
### Color
!!! info "This field was introduced in NetBox v4.2."
The power outlet's color (optional).
### Power Port
When modeling a device which redistributes power from an upstream supply, such as a power distribution unit (PDU), each power outlet should be mapped to the respective [power port](./powerport.md) on the device which supplies power. For example, a 24-outlet PDU may two power ports, each distributing power to 12 of its outlets.

View File

@@ -1,7 +1,5 @@
# Rack Types
!!! info "This feature was introduced in NetBox v4.1."
A rack type defines the physical characteristics of a particular model of [rack](./rack.md).
## Fields

View File

@@ -1,5 +1,8 @@
# Branches
!!! danger "Deprecated Feature"
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
A branch is a collection of related [staged changes](./stagedchange.md) that have been prepared for merging into the active database. A branch can be merged by executing its `commit()` method. Deleting a branch will delete all its related changes.
## Fields

View File

@@ -44,8 +44,6 @@ For object and multiple-object fields only. Designates the type of NetBox object
### Related Object Filter
!!! info "This field was introduced in NetBox v4.1."
For object and multi-object custom fields, a filter may be defined to limit the available objects when populating a field value. This filter maps object attributes to values. For example, `{"status": "active"}` will include only objects with a status of "active."
!!! warning

View File

@@ -10,7 +10,7 @@ See the [event rules documentation](../../features/event-rules.md) for more inf
A unique human-friendly name.
### Content Types
### Object Types
The type(s) of object in NetBox that will trigger the rule.
@@ -38,3 +38,15 @@ The event types which will trigger the rule. At least one event type must be sel
### Conditions
A set of [prescribed conditions](../../reference/conditions.md) against which the triggering object will be evaluated. If the conditions are defined but not met by the object, no action will be taken. An event rule that does not define any conditions will _always_ trigger.
### Action Type
The type of action to take when the rule triggers. This must be one of the following choices:
* Webhook
* Custom script
* Notification
### Action Data
An optional dictionary of JSON data to pass when executing the rule. This can be useful to include additional context data, e.g. when transmitting a webhook.

View File

@@ -1,5 +1,8 @@
# Staged Changes
!!! danger "Deprecated Feature"
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
A staged change represents the creation of a new object or the modification or deletion of an existing object to be performed at some future point. Each change must be assigned to a [branch](./branch.md).
Changes can be applied individually via the `apply()` method, however it is recommended to apply changes in bulk using the parent branch's `commit()` method.

View File

@@ -34,9 +34,11 @@ Designates whether the prefix should be treated as a pool. If selected, the firs
If selected, this prefix will report 100% utilization regardless of how many child objects have been defined within it.
### Site
### Scope
The [site](../dcim/site.md) to which this prefix is assigned (optional).
!!! info "This field replaced the `site` field in NetBox v4.2."
The [region](../dcim/region.md), [site](../dcim/site.md), [site group](../dcim/sitegroup.md) or [location](../dcim/location.md) to which the prefix is assigned (optional).
### VLAN

View File

@@ -26,3 +26,15 @@ The user-defined functional [role](./role.md) assigned to the VLAN.
### VLAN Group or Site
The [VLAN group](./vlangroup.md) or [site](../dcim/site.md) to which the VLAN is assigned.
### Q-in-Q Role
!!! info "This field was introduced in NetBox v4.2."
For VLANs which comprise a Q-in-Q/IEEE 802.1ad topology, this field indicates whether the VLAN is treated as a service or customer VLAN.
### Q-in-Q Service VLAN
!!! info "This field was introduced in NetBox v4.2."
The designated parent service VLAN for a Q-in-Q customer VLAN. This may be set only for Q-in-Q custom VLANs.

View File

@@ -16,8 +16,6 @@ A unique URL-friendly identifier. (This value can be used for filtering.)
### VLAN ID Ranges
!!! info "This field replaced the legacy `min_vid` and `max_vid` fields in NetBox v4.1."
The set of VLAN IDs which are encompassed by the group. By default, this will be the entire range of valid IEEE 802.1Q VLAN IDs (1 to 4094, inclusive). VLANs created within a group must have a VID that falls within one of these ranges. Ranges may not overlap.
### Scope

View File

@@ -0,0 +1,28 @@
# VLAN Translation Policies
!!! info "This feature was introduced in NetBox v4.2."
VLAN translation is a feature that consists of VLAN translation policies and [VLAN translation rules](./vlantranslationrule.md). Many rules can belong to a policy, and each rule defines a mapping of a local to remote VLAN ID (VID). A policy can then be assigned to an [Interface](../dcim/interface.md) or [VMInterface](../virtualization/vminterface.md), and all VLAN translation rules associated with that policy will be visible in the interface details.
There are uniqueness constraints on `(policy, local_vid)` and on `(policy, remote_vid)` in the `VLANTranslationRule` model. Thus, you cannot have multiple rules linked to the same policy that have the same local VID or the same remote VID. A set of policies and rules might look like this:
Policy 1:
- Rule: 100 -> 200
- Rule: 101 -> 201
Policy 2:
- Rule: 100 -> 300
- Rule: 101 -> 301
However this is not allowed:
Policy 3:
- Rule: 100 -> 200
- Rule: 100 -> 300
## Fields
### Name
A unique human-friendly name.

View File

@@ -0,0 +1,21 @@
# VLAN Translation Rules
!!! info "This feature was introduced in NetBox v4.2."
A VLAN translation rule represents a one-to-one mapping of a local VLAN ID (VID) to a remote VID. Many rules can belong to a single policy.
See [VLAN translation policies](./vlantranslationpolicy.md) for an overview of the VLAN Translation feature.
## Fields
### Policy
The [VLAN Translation Policy](./vlantranslationpolicy.md) to which this rule belongs.
### Local VID
VLAN ID (1-4094) in the local network which is to be translated to a remote VID.
### Remote VID
VLAN ID (1-4094) in the remote network to which the local VID will be translated.

View File

@@ -23,6 +23,8 @@ The cluster's operational status.
!!! tip
Additional statuses may be defined by setting `Cluster.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Site
### Scope
The [site](../dcim/site.md) with which the cluster is associated.
!!! info "This field replaced the `site` field in NetBox v4.2."
The [region](../dcim/region.md), [site](../dcim/site.md), [site group](../dcim/sitegroup.md) or [location](../dcim/location.md) with which this cluster is associated.

View File

@@ -57,6 +57,4 @@ The amount of disk storage provisioned, in megabytes.
### Serial Number
!!! info "This field was introduced in NetBox v4.1."
Optional serial number assigned to this virtual machine. Unlike devices, uniqueness is not enforced for virtual machine serial numbers.

View File

@@ -27,9 +27,12 @@ An interface on the same VM with which this interface is bridged.
If not selected, this interface will be treated as disabled/inoperative.
### MAC Address
### Primary MAC Address
The 48-bit MAC address (for Ethernet interfaces).
The [MAC address](../dcim/macaddress.md) assigned to this interface which is designated as its primary.
!!! note "Changed in NetBox v4.2"
The MAC address of an interface (formerly a concrete database field) is available as a property, `mac_address`, which reflects the value of the primary linked [MAC address](../dcim/macaddress.md) object.
### MTU
@@ -42,6 +45,7 @@ For switched Ethernet interfaces, this identifies the 802.1Q encapsulation strat
* **Access:** All traffic is assigned to a single VLAN, with no tagging.
* **Tagged:** One untagged "native" VLAN is allowed, as well as any number of tagged VLANs.
* **Tagged (all):** Implies that all VLANs are carried by the interface. One untagged VLAN may be designated.
* **Q-in-Q:** Q-in-Q (IEEE 802.1ad) encapsulation is performed using the assigned SVLAN.
This field must be left blank for routed interfaces which do employ 802.1Q encapsulation.
@@ -53,6 +57,18 @@ The "native" (untagged) VLAN for the interface. Valid only when one of the above
The tagged VLANs which are configured to be carried by this interface. Valid only for the "tagged" 802.1Q mode above.
### Q-in-Q SVLAN
!!! info "This field was introduced in NetBox v4.2."
The assigned service VLAN (for Q-in-Q/802.1ad interfaces).
### VRF
The [virtual routing and forwarding](../ipam/vrf.md) instance to which this interface is assigned.
### VLAN Translation Policy
!!! info "This field was introduced in NetBox v4.2."
The [VLAN translation policy](../ipam/vlantranslationpolicy.md) that applies to this interface (optional).

View File

@@ -43,3 +43,9 @@ The security cipher used to apply wireless authentication. Options include:
### Pre-Shared Key
The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
### Scope
!!! info "This field was introduced in NetBox v4.2."
The [region](../dcim/region.md), [site](../dcim/site.md), [site group](../dcim/sitegroup.md) or [location](../dcim/location.md) with which this wireless LAN is associated.

View File

@@ -22,8 +22,6 @@ The service set identifier (SSID) for the wireless link (optional).
### Distance
!!! info "This field was introduced in NetBox v4.1."
The distance between the link's two endpoints, including a unit designation (e.g. 100 meters or 25 feet).
### Authentication Type

View File

@@ -1,7 +1,5 @@
# Background Jobs
!!! info "This feature was introduced in NetBox v4.1."
NetBox plugins can defer certain operations by enqueuing [background jobs](../../features/background-jobs.md), which are executed asynchronously by background workers. This is helpful for decoupling long-running processes from the user-facing request-response cycle.
For example, your plugin might need to fetch data from a remote system. Depending on the amount of data and the responsiveness of the remote server, this could take a few minutes. Deferring this task to a queued job ensures that it can be completed in the background, without interrupting the user. The data it fetches can be made available once the job has completed.
@@ -29,6 +27,9 @@ class MyTestJob(JobRunner):
You can schedule the background job from within your code (e.g. from a model's `save()` method or a view) by calling `MyTestJob.enqueue()`. This method passes through all arguments to `Job.enqueue()`. However, no `name` argument must be passed, as the background job name will be used instead.
!!! tip
A set of predefined intervals is available at `core.choices.JobIntervalChoices` for convenience.
### Attributes
`JobRunner` attributes are defined under a class named `Meta` within the job. These are optional, but encouraged.
@@ -46,26 +47,57 @@ As described above, jobs can be scheduled for immediate execution or at any late
#### Example
```python title="models.py"
from django.db import models
from core.choices import JobIntervalChoices
from netbox.models import NetBoxModel
from .jobs import MyTestJob
class MyModel(NetBoxModel):
foo = models.CharField()
def save(self, *args, **kwargs):
MyTestJob.enqueue_once(instance=self, interval=JobIntervalChoices.INTERVAL_HOURLY)
return super().save(*args, **kwargs)
def sync(self):
MyTestJob.enqueue(instance=self)
```
### System Jobs
!!! info "This feature was introduced in NetBox v4.2."
Some plugins may implement background jobs that are decoupled from the request/response cycle. Typical use cases would be housekeeping tasks or synchronization jobs. These can be registered as _system jobs_ using the `system_job()` decorator. The job interval must be passed as an integer (in minutes) when registering a system job. System jobs are scheduled automatically when the RQ worker (`manage.py rqworker`) is run.
#### Example
```python title="jobs.py"
from netbox.jobs import JobRunner
from core.choices import JobIntervalChoices
from netbox.jobs import JobRunner, system_job
from .models import MyModel
# Specify a predefined choice or an integer indicating
# the number of minutes between job executions
@system_job(interval=JobIntervalChoices.INTERVAL_HOURLY)
class MyHousekeepingJob(JobRunner):
class Meta:
name = "Housekeeping"
name = "My Housekeeping Job"
def run(self, *args, **kwargs):
# your logic goes here
MyModel.objects.filter(foo='bar').delete()
```
```python title="__init__.py"
from netbox.plugins import PluginConfig
!!! note
Ensure that any system jobs are imported on initialization. Otherwise, they won't be registered. This can be achieved by extending the PluginConfig's `ready()` method. For example:
class MyPluginConfig(PluginConfig):
```python
def ready(self):
super().ready()
from .jobs import MyHousekeepingJob
MyHousekeepingJob.setup(interval=60)
```
```
## Task queues

View File

@@ -18,6 +18,6 @@ backends = [MyDataBackend]
```
!!! tip
The path to the list of search indexes can be modified by setting `data_backends` in the PluginConfig instance.
The path to the list of data backends can be modified by setting `data_backends` in the PluginConfig instance.
::: netbox.data_backends.DataBackend

View File

@@ -1,7 +1,5 @@
# Event Types
!!! info "This feature was introduced in NetBox v4.1."
Plugins can register their own custom event types for use with NetBox [event rules](../../models/extras/eventrule.md). This is accomplished by calling the `register()` method on an instance of the `EventType` class. This can be done anywhere within the plugin. An example is provided below.
```python

View File

@@ -98,28 +98,30 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
### PluginConfig Attributes
| Name | Description |
|-----------------------|--------------------------------------------------------------------------------------------------------------------------|
| `name` | Raw plugin name; same as the plugin's source directory |
| `verbose_name` | Human-friendly name for the plugin |
| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) |
| `description` | Brief description of the plugin's purpose |
| `author` | Name of plugin's author |
| `author_email` | Author's public email address |
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
| `required_settings` | A list of any configuration parameters that **must** be defined by the user |
| `default_settings` | A dictionary of configuration parameters and their default values |
| `django_apps` | A list of additional Django apps to load alongside the plugin |
| `min_version` | Minimum version of NetBox with which the plugin is compatible |
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
| `queues` | A list of custom background task queues to create |
| `search_extensions` | The dotted path to the list of search index classes (default: `search.indexes`) |
| `data_backends` | The dotted path to the list of data source backend classes (default: `data_backends.backends`) |
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
| `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) |
| Name | Description |
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------|
| `name` | Raw plugin name; same as the plugin's source directory |
| `verbose_name` | Human-friendly name for the plugin |
| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) |
| `release_track` | An alternate release track (e.g. `dev` or `beta`) to which a release belongs |
| `description` | Brief description of the plugin's purpose |
| `author` | Name of plugin's author |
| `author_email` | Author's public email address |
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
| `required_settings` | A list of any configuration parameters that **must** be defined by the user |
| `default_settings` | A dictionary of configuration parameters and their default values |
| `django_apps` | A list of additional Django apps to load alongside the plugin |
| `min_version` | Minimum version of NetBox with which the plugin is compatible |
| `max_version` | Maximum version of NetBox with which the plugin is compatible |
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
| `queues` | A list of custom background task queues to create |
| `events_pipeline` | A list of handlers to add to [`EVENTS_PIPELINE`](../../configuration/miscellaneous.md#events_pipeline), identified by dotted paths |
| `search_extensions` | The dotted path to the list of search index classes (default: `search.indexes`) |
| `data_backends` | The dotted path to the list of data source backend classes (default: `data_backends.backends`) |
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
| `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) |
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.

View File

@@ -64,13 +64,14 @@ item1 = PluginMenuItem(
A `PluginMenuItem` has the following attributes:
| Attribute | Required | Description |
|---------------|----------|----------------------------------------------------------------------------------------------------------|
| `link` | Yes | Name of the URL path to which this menu item links |
| `link_text` | Yes | The text presented to the user |
| `permissions` | - | A list of permissions required to display this link |
| `staff_only` | - | Display only for users who have `is_staff` set to true (any specified permissions will also be required) |
| `buttons` | - | An iterable of PluginMenuButton instances to include |
| Attribute | Required | Description |
|-----------------|----------|----------------------------------------------------------------------------------------------------------|
| `link` | Yes | Name of the URL path to which this menu item links |
| `link_text` | Yes | The text presented to the user |
| `permissions` | - | A list of permissions required to display this link |
| `auth_required` | - | Display only for authenticated users |
| `staff_only` | - | Display only for users who have `is_staff` set to true (any specified permissions will also be required) |
| `buttons` | - | An iterable of PluginMenuButton instances to include |
## Menu Buttons

View File

@@ -1,7 +1,7 @@
# Staged Changes
!!! danger "Experimental Feature"
This feature is still under active development and considered experimental in nature. Its use in production is strongly discouraged at this time.
!!! danger "Deprecated Feature"
This feature has been deprecated in NetBox v4.2 and will be removed in a future release. Please consider using the [netbox-branching plugin](https://github.com/netboxlabs/netbox-branching), which provides much more robust functionality.
NetBox provides a programmatic API to stage the creation, modification, and deletion of objects without actually committing those changes to the active database. This can be useful for performing a "dry run" of bulk operations, or preparing a set of changes for administrative approval, for example.

View File

@@ -185,6 +185,9 @@ class MyView(generic.ObjectView):
)
```
!!! note "Changed in NetBox v4.2"
The `register_model_view()` function was extended in NetBox v4.2 to support registration of list views by passing `detail=False`.
::: utilities.views.register_model_view
::: utilities.views.ViewTab
@@ -203,8 +206,6 @@ Plugins can inject custom content into certain areas of core NetBox views. This
| `right_page()` | Object view | Inject content on the right side of the page |
| `full_width_page()` | Object view | Inject content across the entire bottom of the page |
!!! info "The `navbar()` and `alerts()` methods were introduced in NetBox v4.1."
Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however.
To control where the custom content is injected, plugin authors can specify an iterable of models by overriding the `models` attribute on the subclass. Extensions which do not specify a set of models will be invoked on every view, where supported.

View File

@@ -10,6 +10,14 @@ Minor releases are published in April, August, and December of each calendar yea
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
#### [Version 4.2](./version-4.2.md) (January 2025)
* Assign Multiple MAC Addresses per Interface ([#4867](https://github.com/netbox-community/netbox/issues/4867))
* Quick Add UI Widget ([#5858](https://github.com/netbox-community/netbox/issues/5858))
* VLAN Translation ([#7336](https://github.com/netbox-community/netbox/issues/7336))
* Virtual Circuits ([#13086](https://github.com/netbox-community/netbox/issues/13086))
* Q-in-Q Encapsulation ([#13428](https://github.com/netbox-community/netbox/issues/13428))
#### [Version 4.1](./version-4.1.md) (September 2024)
* Circuit Groups ([#7025](https://github.com/netbox-community/netbox/issues/7025))

View File

@@ -1,5 +1,17 @@
# NetBox v4.1
## v4.1.11 (2025-01-06)
### Bug Fixes
* [#17771](https://github.com/netbox-community/netbox/issues/17771) - Fix duplicate entries appearing on VLAN list when filtering by interface assignment
* [#18222](https://github.com/netbox-community/netbox/issues/18222) - Pass event rule action data to webhooks as context data
* [#18263](https://github.com/netbox-community/netbox/issues/18263) - Fix recalculation of cable paths when modifying cable terminations via the REST API
* [#18271](https://github.com/netbox-community/netbox/issues/18271) - Require only encryption _or_ authentication algorithm when creating an IPSec proposal via the REST API
* [#18289](https://github.com/netbox-community/netbox/issues/18289) - Enable ordering modules and module types by created & last updated times
---
## v4.1.10 (2024-12-23)
### Bug Fixes

View File

@@ -0,0 +1,177 @@
# NetBox v4.2
## v4.2.3 (2025-02-04)
### Enhancements
* [#18518](https://github.com/netbox-community/netbox/issues/18518) - Add a "hostname" `<meta>` tag to the page header
### Bug Fixes
* [#18497](https://github.com/netbox-community/netbox/issues/18497) - Fix unhandled `FieldDoesNotExist` exception when search results include virtual circuit
* [#18433](https://github.com/netbox-community/netbox/issues/18433) - Fix MAC address not shown as "primary for interface" in MAC address detail view
* [#18154](https://github.com/netbox-community/netbox/issues/18154) - Allow anonymous users to change default table preferences
* [#18515](https://github.com/netbox-community/netbox/issues/18515) - Fix Django `collectstatic` management command in debug mode with Redis not running
* [#18456](https://github.com/netbox-community/netbox/issues/18456) - Avoid duplicate MAC Address column in interface tables
* [#18447](https://github.com/netbox-community/netbox/issues/18447) - Fix `FieldError` exception when sorting interface tables on MAC Address columns
* [#18438](https://github.com/netbox-community/netbox/issues/18438) - Improve performance in IPAM migration `0072_prefix_cached_relations` when upgrading from v4.1 or earlier
* [#18436](https://github.com/netbox-community/netbox/issues/18436) - Reset primary MAC address when unassigning MAC address from interface
* [#18181](https://github.com/netbox-community/netbox/issues/18181) - Fix "Create & Add Another" workflow when adding IP addresses to interfaces
---
## v4.2.2 (2025-01-17)
### Bug Fixes
* [#18336](https://github.com/netbox-community/netbox/issues/18336) - Validate new rack height against installed devices when changing a rack's type
* [#18350](https://github.com/netbox-community/netbox/issues/18350) - Fix `FieldDoesNotExist` exception when global search results include a circuit termination
* [#18353](https://github.com/netbox-community/netbox/issues/18353) - Disable fetching of plugin catalog data when `ISOLATED_DEPLOYMENT` is enabled
* [#18362](https://github.com/netbox-community/netbox/issues/18362) - Avoid transmitting census data on every worker restart
* [#18363](https://github.com/netbox-community/netbox/issues/18363) - Fix support for assigning a MAC address to an interface via the REST API
* [#18368](https://github.com/netbox-community/netbox/issues/18368) - Restore missing attributes from REST API serializer for MAC addresses (`tags`, `created`, `last_updated`, and custom fields)
* [#18369](https://github.com/netbox-community/netbox/issues/18369) - Fix `TypeError` exception when rendering the system configuration view with one or more custom classes defined under `PROTECTION_RULES`
* [#18373](https://github.com/netbox-community/netbox/issues/18373) - Fix `AttributeError` exception when attempting to assign host devices to a cluster
* [#18376](https://github.com/netbox-community/netbox/issues/18376) - Fix the display of tagged VLANs in interfaces list for Q-in-Q interfaces
* [#18379](https://github.com/netbox-community/netbox/issues/18379) - Ensure RSS feed dashboard widget content is sanitized
* [#18392](https://github.com/netbox-community/netbox/issues/18392) - Virtual machines should not inherit config contexts assigned to locations
* [#18400](https://github.com/netbox-community/netbox/issues/18400) - Fix support for `STORAGE_BACKEND` configuration parameter
* [#18406](https://github.com/netbox-community/netbox/issues/18406) - Scope column headers in object lists should not be orderable
---
## v4.2.1 (2025-01-08)
### Bug Fixes
* [#18282](https://github.com/netbox-community/netbox/issues/18282) - Fix ordering of prefixes list by assigned VLAN
* [#18314](https://github.com/netbox-community/netbox/issues/18314) - Fix KeyError exception when rendering pre-saved dashboard (`requires_internet` missing)
* [#18316](https://github.com/netbox-community/netbox/issues/18316) - Fix AttributeError exception when global search results include prefixes and/or clusters
* [#18318](https://github.com/netbox-community/netbox/issues/18318) - Correct navigation breadcrumbs for module type UI view
* [#18324](https://github.com/netbox-community/netbox/issues/18324) - Correct filtering for certain related object listings
* [#18329](https://github.com/netbox-community/netbox/issues/18329) - Address upstream bug in GraphQL API where only one primary IP address is returned within a device/VM list
---
## v4.2.0 (2025-01-06)
### Breaking Changes
* Support for the Django admin UI has been completely removed. (The Django admin UI was disabled by default in NetBox v4.0.)
* This release drops support for PostgreSQL 12. PostgreSQL 13 or later is required to run this release.
* NetBox has adopted collation-based natural ordering for many models. This may alter the order in which some objects are listed by default.
* Automatic redirects from pre-v4.1 UI views for virtual disks have been removed.
* The `site` and `provider_network` foreign key fields on `circuits.CircuitTermination` have been replaced by the `termination` generic foreign key.
* The `site` foreign key field on `ipam.Prefix` has been replaced by the `scope` generic foreign key.
* The `site` foreign key field on `virtualization.Cluster` has been replaced by the `scope` generic foreign key.
* The `circuit` foreign key field on `circuits.CircuitGroupAssignment` has been replaced by the `member` generic foreign key.
* Obsolete nested REST API serializers have been removed. These were deprecated in NetBox v4.1 under [#17143](https://github.com/netbox-community/netbox/issues/17143).
### New Features
#### Assign Multiple MAC Addresses per Interface ([#4867](https://github.com/netbox-community/netbox/issues/4867))
MAC addresses are now managed as independent objects, rather than attributes on device and VM interfaces. NetBox now supports the assignment of multiple MAC addresses per interface, and allows a primary MAC address to be designated for each.
#### Quick Add UI Widget ([#5858](https://github.com/netbox-community/netbox/issues/5858))
A new UI widget has been introduced to enable conveniently creating new related objects while creating or editing an object. For instance, it is now possible to create and assign a new device role when creating or editing a device from within the device form.
#### VLAN Translation ([#7336](https://github.com/netbox-community/netbox/issues/7336))
User can now define policies which track the translation of VLAN IDs on IEEE 802.1Q-encapsulated interfaces. Translation policies can be reused across multiple interfaces.
#### Virtual Circuits ([#13086](https://github.com/netbox-community/netbox/issues/13086))
New models have been introduced to support the documentation of virtual circuits as an extension to the physical circuit modeling already supported. This enables users to accurately reflect point-to-point or multipoint virtual circuits atop infrastructure comprising physical circuits and cables.
#### Q-in-Q Encapsulation ([#13428](https://github.com/netbox-community/netbox/issues/13428))
NetBox now supports the designation of customer VLANs (CVLANs) and service VLANs (SVLANs) to support IEEE 802.1ad/Q-in-Q encapsulation. Each interface can now have it mode designated "Q-in-Q" and be assigned an SVLAN.
### Enhancements
* [#6414](https://github.com/netbox-community/netbox/issues/6414) - Prefixes can now be scoped by region, site group, site, or location
* [#7699](https://github.com/netbox-community/netbox/issues/7699) - Virtualization clusters can now be scoped by region, site group, site, or location
* [#9604](https://github.com/netbox-community/netbox/issues/9604) - The scope of a circuit termination now include a region, site group, site, location, or provider network
* [#10711](https://github.com/netbox-community/netbox/issues/10711) - Wireless LANs can now be scoped by region, site group, site, or location
* [#11279](https://github.com/netbox-community/netbox/issues/11279) - Improved the use of natural ordering for various models throughout the application
* [#12596](https://github.com/netbox-community/netbox/issues/12596) - Extended the virtualization clusters REST API endpoint to report on allocated VM resources
* [#16547](https://github.com/netbox-community/netbox/issues/16547) - Add a geographic distance field for circuits
* [#16783](https://github.com/netbox-community/netbox/issues/16783) - Add an operational status field for inventory items
* [#17195](https://github.com/netbox-community/netbox/issues/17195) - Add a color field for power outlets
### Plugins
* [#15093](https://github.com/netbox-community/netbox/issues/15093) - Introduced the `events_pipeline` configuration parameter, which allows plugins to hook into NetBox event processing
* [#16546](https://github.com/netbox-community/netbox/issues/16546) - NetBoxModel now provides a default `get_absolute_url()` method
* [#16971](https://github.com/netbox-community/netbox/issues/16971) - Plugins can now easily register system jobs to perform background tasks
* [#17029](https://github.com/netbox-community/netbox/issues/17029) - Registering a `PluginTemplateExtension` subclass for a single model has been deprecated (replace `model` with `models`)
* [#18023](https://github.com/netbox-community/netbox/issues/18023) - Extend `register_model_view()` to handle list views
### Other Changes
* [#16136](https://github.com/netbox-community/netbox/issues/16136) - Removed support for the Django admin UI
* [#17165](https://github.com/netbox-community/netbox/issues/17165) - All obsolete nested REST API serializers have been removed
* [#17472](https://github.com/netbox-community/netbox/issues/17472) - The legacy staged changes API has been deprecated, and will be removed in Netbox v4.3
* [#17476](https://github.com/netbox-community/netbox/issues/17476) - Upgrade to Django 5.1
* [#17752](https://github.com/netbox-community/netbox/issues/17752) - Bulk object import URL paths have been renamed from `*_import` to `*_bulk_import`
* [#17761](https://github.com/netbox-community/netbox/issues/17761) - Optional choice fields now store empty values as null (rather than empty strings) in the database
* [#18093](https://github.com/netbox-community/netbox/issues/18093) - Redirects for pre-v4.1 virtual disk UI views have been removed
### REST API Changes
* Added the following endpoints:
* `/api/circuits/virtual-circuits/`
* `/api/circuits/virtual-circuit-terminations/`
* `/api/dcim/mac-addresses/`
* `/api/ipam/vlan-translation-policies/`
* `/api/ipam/vlan-translation-rules/`
* circuits.Circuit
* Added the optional `distance` and `distance_unit` fields
* circuits.CircuitGroupAssignment
* Replaced the `circuit` field with `member_type` and `member_id` to support virtual circuit assignment
* circuits.CircuitTermination
* Removed the `site` & `provider_network` fields
* Added the `termination_type` & `termination_id` fields to facilitate termination assignment
* Added the read-only `termination` field
* dcim.Interface
* The `mac_address` field is now read-only
* Added the `primary_mac_address` relation to dcim.MACAddress
* Added the read-only `mac_addresses` list
* Added the `qinq_svlan` relation to ipam.VLAN
* Added the `vlan_translation_policy` relation to ipam.VLANTranslationPolicy
* Added `mode` choice "Q-in-Q"
* dcim.InventoryItem
* Added the optional `status` choice field
* dcim.Location
* Added the read-only `prefix_count` field
* dcim.PowerOutlet
* Added the optional `color` field
* dcim.Region
* Added the read-only `prefix_count` field
* dcim.SiteGroup
* Added the read-only `prefix_count` field
* ipam.Prefix
* Removed the `site` field
* Added the `scope_type` & `scope_id` fields to facilitate scope assignment
* Added the read-only `scope` field
* ipam.VLAN
* Added the optional `qinq_role` selection field
* Added the `qinq_svlan` recursive relation
* virtualization.Cluster
* Removed the `site` field
* Added the `scope_type` & `scope_id` fields to facilitate scope assignment
* Added the read-only `scope` field
* virtualization.Cluster
* Added the read-only fields `allocated_vcpus`, `allocated_memory`, and `allocated_disk`
* virtualization.VMInterface
* The `mac_address` field is now read-only
* Added the `primary_mac_address` relation to dcim.MACAddress
* Added the read-only `mac_addresses` list
* Added the `qinq_svlan` relation to ipam.VLAN
* Added the `vlan_translation_policy` relation to ipam.VLANTranslationPolicy
* Added `mode` choice "Q-in-Q"
* wireless.WirelessLAN
* Added the `scope_type` & `scope_id` fields to support scope assignment
* Added the read-only `scope` field

View File

@@ -174,6 +174,9 @@ nav:
- Provider: 'models/circuits/provider.md'
- Provider Account: 'models/circuits/provideraccount.md'
- Provider Network: 'models/circuits/providernetwork.md'
- Virtual Circuit: 'models/circuits/virtualcircuit.md'
- Virtual Circuit Termination: 'models/circuits/virtualcircuittermination.md'
- Virtual Circuit Type: 'models/circuits/virtualcircuittype.md'
- Core:
- DataFile: 'models/core/datafile.md'
- DataSource: 'models/core/datasource.md'
@@ -197,6 +200,7 @@ nav:
- InventoryItemRole: 'models/dcim/inventoryitemrole.md'
- InventoryItemTemplate: 'models/dcim/inventoryitemtemplate.md'
- Location: 'models/dcim/location.md'
- MACAddress: 'models/dcim/macaddress.md'
- Manufacturer: 'models/dcim/manufacturer.md'
- Module: 'models/dcim/module.md'
- ModuleBay: 'models/dcim/modulebay.md'
@@ -255,6 +259,8 @@ nav:
- ServiceTemplate: 'models/ipam/servicetemplate.md'
- VLAN: 'models/ipam/vlan.md'
- VLANGroup: 'models/ipam/vlangroup.md'
- VLANTranslationPolicy: 'models/ipam/vlantranslationpolicy.md'
- VLANTranslationRule: 'models/ipam/vlantranslationrule.md'
- VRF: 'models/ipam/vrf.md'
- Tenancy:
- Contact: 'models/tenancy/contact.md'
@@ -306,6 +312,7 @@ nav:
- git Cheat Sheet: 'development/git-cheat-sheet.md'
- Release Notes:
- Summary: 'release-notes/index.md'
- Version 4.2: 'release-notes/version-4.2.md'
- Version 4.1: 'release-notes/version-4.1.md'
- Version 4.0: 'release-notes/version-4.0.md'
- Version 3.7: 'release-notes/version-3.7.md'

View File

@@ -1,79 +0,0 @@
import warnings
from drf_spectacular.utils import extend_schema_serializer
from circuits.models import *
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
from .serializers_.nested import NestedProviderAccountSerializer
__all__ = [
'NestedCircuitSerializer',
'NestedCircuitTerminationSerializer',
'NestedCircuitTypeSerializer',
'NestedProviderNetworkSerializer',
'NestedProviderSerializer',
'NestedProviderAccountSerializer',
]
# TODO: Remove in v4.2
warnings.warn(
"Dedicated nested serializers will be removed in NetBox v4.2. Use Serializer(nested=True) instead.",
DeprecationWarning
)
#
# Provider networks
#
class NestedProviderNetworkSerializer(WritableNestedSerializer):
class Meta:
model = ProviderNetwork
fields = ['id', 'url', 'display_url', 'display', 'name']
#
# Providers
#
@extend_schema_serializer(
exclude_fields=('circuit_count',),
)
class NestedProviderSerializer(WritableNestedSerializer):
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = Provider
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
#
# Circuits
#
@extend_schema_serializer(
exclude_fields=('circuit_count',),
)
class NestedCircuitTypeSerializer(WritableNestedSerializer):
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = CircuitType
fields = ['id', 'url', 'display_url', 'display', 'name', 'slug', 'circuit_count']
class NestedCircuitSerializer(WritableNestedSerializer):
class Meta:
model = Circuit
fields = ['id', 'url', 'display_url', 'display', 'cid']
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
circuit = NestedCircuitSerializer()
class Meta:
model = CircuitTermination
fields = ['id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'cable', '_occupied']

View File

@@ -1,11 +1,20 @@
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices
from circuits.models import Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType
from dcim.api.serializers_.cables import CabledObjectSerializer
from dcim.api.serializers_.sites import SiteSerializer
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field
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,
)
from dcim.api.serializers_.device_components import InterfaceSerializer
from dcim.api.serializers_.cables import CabledObjectSerializer
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from netbox.choices import DistanceUnitChoices
from tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
__all__ = (
@@ -14,6 +23,9 @@ __all__ = (
'CircuitGroupSerializer',
'CircuitTerminationSerializer',
'CircuitTypeSerializer',
'VirtualCircuitSerializer',
'VirtualCircuitTerminationSerializer',
'VirtualCircuitTypeSerializer',
)
@@ -32,16 +44,32 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
site = SiteSerializer(nested=True, allow_null=True)
provider_network = ProviderNetworkSerializer(nested=True, allow_null=True)
termination_type = ContentTypeField(
queryset=ContentType.objects.filter(
model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES
),
allow_null=True,
required=False,
default=None
)
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
termination = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CircuitTermination
fields = [
'id', 'url', 'display_url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'xconnect_id', 'description',
'id', 'url', 'display_url', 'display', 'termination_type', 'termination_id', 'termination', 'port_speed',
'upstream_speed', 'xconnect_id', 'description',
]
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_termination(self, obj):
if obj.termination_id is None:
return None
serializer = get_serializer_for_model(obj.termination)
context = {'request': self.context['request']}
return serializer(obj.termination, nested=True, context=context).data
class CircuitGroupSerializer(NetBoxModelSerializer):
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
@@ -76,6 +104,7 @@ class CircuitSerializer(NetBoxModelSerializer):
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
status = ChoiceField(choices=CircuitStatusChoices, required=False)
type = CircuitTypeSerializer(nested=True)
distance_unit = ChoiceField(choices=DistanceUnitChoices, allow_blank=True, required=False, allow_null=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
@@ -85,33 +114,107 @@ class CircuitSerializer(NetBoxModelSerializer):
model = Circuit
fields = [
'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments',
'install_date', 'termination_date', 'commit_rate', 'description', 'distance', 'distance_unit',
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'assignments',
]
brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
circuit = CircuitSerializer(nested=True)
site = SiteSerializer(nested=True, required=False, allow_null=True)
provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True)
termination_type = ContentTypeField(
queryset=ContentType.objects.filter(
model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES
),
allow_null=True,
required=False,
default=None
)
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
termination = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CircuitTermination
fields = [
'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed',
'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end',
'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'termination_type', 'termination_id',
'termination', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'mark_connected',
'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated',
'_occupied',
]
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_termination(self, obj):
if obj.termination_id is None:
return None
serializer = get_serializer_for_model(obj.termination)
context = {'request': self.context['request']}
return serializer(obj.termination, nested=True, context=context).data
class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
circuit = CircuitSerializer(nested=True)
member_type = ContentTypeField(
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS)
)
member = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CircuitGroupAssignment
fields = [
'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
'id', 'url', 'display_url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority', 'tags',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'group', 'circuit', 'priority')
brief_fields = ('id', 'url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_member(self, obj):
if obj.member_id is None:
return None
serializer = get_serializer_for_model(obj.member)
context = {'request': self.context['request']}
return serializer(obj.member, nested=True, context=context).data
class VirtualCircuitTypeSerializer(NetBoxModelSerializer):
# Related object counts
virtual_circuit_count = RelatedObjectCountField('virtual_circuits')
class Meta:
model = VirtualCircuitType
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'virtual_circuit_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count')
class VirtualCircuitSerializer(NetBoxModelSerializer):
provider_network = ProviderNetworkSerializer(nested=True)
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
type = VirtualCircuitTypeSerializer(nested=True)
status = ChoiceField(choices=CircuitStatusChoices, required=False)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
class Meta:
model = VirtualCircuit
fields = [
'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'type', 'status',
'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description')
class VirtualCircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
virtual_circuit = VirtualCircuitSerializer(nested=True)
role = ChoiceField(choices=VirtualCircuitTerminationRoleChoices, required=False)
interface = InterfaceSerializer(nested=True)
class Meta:
model = VirtualCircuitTermination
fields = [
'id', 'url', 'display_url', 'display', 'virtual_circuit', 'role', 'interface', 'description', 'tags',
'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'virtual_circuit', 'role', 'interface', 'description')

View File

@@ -17,5 +17,10 @@ router.register('circuit-terminations', views.CircuitTerminationViewSet)
router.register('circuit-groups', views.CircuitGroupViewSet)
router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet)
# Virtual circuits
router.register('virtual-circuits', views.VirtualCircuitViewSet)
router.register('virtual-circuit-types', views.VirtualCircuitTypeViewSet)
router.register('virtual-circuit-terminations', views.VirtualCircuitTerminationViewSet)
app_name = 'circuits-api'
urlpatterns = router.urls

View File

@@ -93,3 +93,33 @@ class ProviderNetworkViewSet(NetBoxModelViewSet):
queryset = ProviderNetwork.objects.all()
serializer_class = serializers.ProviderNetworkSerializer
filterset_class = filtersets.ProviderNetworkFilterSet
#
# Virtual circuit types
#
class VirtualCircuitTypeViewSet(NetBoxModelViewSet):
queryset = VirtualCircuitType.objects.all()
serializer_class = serializers.VirtualCircuitTypeSerializer
filterset_class = filtersets.VirtualCircuitTypeFilterSet
#
# Virtual circuits
#
class VirtualCircuitViewSet(NetBoxModelViewSet):
queryset = VirtualCircuit.objects.all()
serializer_class = serializers.VirtualCircuitSerializer
filterset_class = filtersets.VirtualCircuitFilterSet
#
# Virtual circuit terminations
#
class VirtualCircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = VirtualCircuitTermination.objects.all()
serializer_class = serializers.VirtualCircuitTerminationSerializer
filterset_class = filtersets.VirtualCircuitTerminationFilterSet

View File

@@ -92,3 +92,19 @@ class CircuitPriorityChoices(ChoiceSet):
(PRIORITY_TERTIARY, _('Tertiary')),
(PRIORITY_INACTIVE, _('Inactive')),
]
#
# Virtual circuits
#
class VirtualCircuitTerminationRoleChoices(ChoiceSet):
ROLE_PEER = 'peer'
ROLE_HUB = 'hub'
ROLE_SPOKE = 'spoke'
CHOICES = [
(ROLE_PEER, _('Peer'), 'green'),
(ROLE_HUB, _('Hub'), 'blue'),
(ROLE_SPOKE, _('Spoke'), 'orange'),
]

View File

@@ -0,0 +1,12 @@
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',
)
CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS = Q(
app_label='circuits',
model__in=['circuit', 'virtualcircuit']
)

View File

@@ -1,13 +1,16 @@
import django_filters
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext as _
from dcim.filtersets import CabledObjectFilterSet
from dcim.models import Region, Site, SiteGroup
from dcim.models import Interface, Location, Region, Site, SiteGroup
from ipam.models import ASN
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
)
from .choices import *
from .models import *
@@ -20,43 +23,46 @@ __all__ = (
'ProviderNetworkFilterSet',
'ProviderAccountFilterSet',
'ProviderFilterSet',
'VirtualCircuitFilterSet',
'VirtualCircuitTerminationFilterSet',
'VirtualCircuitTypeFilterSet',
)
class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='circuits__terminations__site__region',
field_name='circuits__terminations___region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='circuits__terminations__site__region',
field_name='circuits__terminations___region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='circuits__terminations__site__group',
field_name='circuits__terminations___site_group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='circuits__terminations__site__group',
field_name='circuits__terminations___site_group',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='circuits__terminations__site',
field_name='circuits__terminations___site',
queryset=Site.objects.all(),
label=_('Site'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='circuits__terminations__site__slug',
field_name='circuits__terminations___site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site (slug)'),
@@ -173,7 +179,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
label=_('Provider account (account)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__provider_network',
field_name='terminations___provider_network',
queryset=ProviderNetwork.objects.all(),
label=_('Provider network (ID)'),
)
@@ -193,37 +199,37 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='terminations__site__region',
field_name='terminations___region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='terminations__site__region',
field_name='terminations___region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='terminations__site__group',
field_name='terminations___site_group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='terminations__site__group',
field_name='terminations___site_group',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site',
field_name='terminations___site',
queryset=Site.objects.all(),
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site__slug',
field_name='terminations___site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site (slug)'),
@@ -239,7 +245,9 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
class Meta:
model = Circuit
fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate')
fields = (
'id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
)
def search(self, queryset, name, value):
if not value.strip():
@@ -263,18 +271,60 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
queryset=Circuit.objects.all(),
label=_('Circuit'),
)
termination_type = ContentTypeFilter()
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_site_group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_site_group',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
field_name='_site',
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='site__slug',
field_name='_site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site (slug)'),
)
location_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
label=_('Location (ID)'),
)
location = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
to_field_name='slug',
label=_('Location (slug)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
field_name='_provider_network',
label=_('ProviderNetwork (ID)'),
)
provider_id = django_filters.ModelMultipleChoiceFilter(
@@ -292,8 +342,8 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
class Meta:
model = CircuitTermination
fields = (
'id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'mark_connected',
'pp_info', 'cable_end',
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description',
'mark_connected', 'pp_info', 'cable_end',
)
def search(self, queryset, name, value):
@@ -319,26 +369,36 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
method='search',
label=_('Search'),
)
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='circuit__provider',
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
member_type = ContentTypeFilter()
circuit = MultiValueCharFilter(
method='filter_circuit',
field_name='cid',
label=_('Circuit (CID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='circuit__provider__slug',
queryset=Provider.objects.all(),
to_field_name='slug',
label=_('Provider (slug)'),
)
circuit_id = django_filters.ModelMultipleChoiceFilter(
queryset=Circuit.objects.all(),
circuit_id = MultiValueNumberFilter(
method='filter_circuit',
field_name='pk',
label=_('Circuit (ID)'),
)
circuit = django_filters.ModelMultipleChoiceFilter(
field_name='circuit__cid',
queryset=Circuit.objects.all(),
to_field_name='cid',
label=_('Circuit (CID)'),
virtual_circuit = MultiValueCharFilter(
method='filter_virtual_circuit',
field_name='cid',
label=_('Virtual circuit (CID)'),
)
virtual_circuit_id = MultiValueNumberFilter(
method='filter_virtual_circuit',
field_name='pk',
label=_('Virtual circuit (ID)'),
)
provider = MultiValueCharFilter(
method='filter_provider',
field_name='slug',
label=_('Provider (name)'),
)
provider_id = MultiValueNumberFilter(
method='filter_provider',
field_name='pk',
label=_('Provider (ID)'),
)
group_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitGroup.objects.all(),
@@ -353,12 +413,173 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
class Meta:
model = CircuitGroupAssignment
fields = ('id', 'priority')
fields = ('id', 'member_id', 'priority')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(circuit__cid__icontains=value) |
Q(member__cid__icontains=value) |
Q(group__name__icontains=value)
)
def filter_circuit(self, queryset, name, value):
circuits = Circuit.objects.filter(**{f'{name}__in': value})
if not circuits.exists():
return queryset.none()
return queryset.filter(
Q(
member_type=ContentType.objects.get_for_model(Circuit),
member_id__in=circuits
)
)
def filter_virtual_circuit(self, queryset, name, value):
virtual_circuits = VirtualCircuit.objects.filter(**{f'{name}__in': value})
if not virtual_circuits.exists():
return queryset.none()
return queryset.filter(
Q(
member_type=ContentType.objects.get_for_model(VirtualCircuit),
member_id__in=virtual_circuits
)
)
def filter_provider(self, queryset, name, value):
providers = Provider.objects.filter(**{f'{name}__in': value})
if not providers.exists():
return queryset.none()
circuits = Circuit.objects.filter(provider__in=providers)
virtual_circuits = VirtualCircuit.objects.filter(provider_network__provider__in=providers)
return queryset.filter(
Q(
member_type=ContentType.objects.get_for_model(Circuit),
member_id__in=circuits
) |
Q(
member_type=ContentType.objects.get_for_model(VirtualCircuit),
member_id__in=virtual_circuits
)
)
class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta:
model = VirtualCircuitType
fields = ('id', 'name', 'slug', 'color', 'description')
class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_network__provider',
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider_network__provider__slug',
queryset=Provider.objects.all(),
to_field_name='slug',
label=_('Provider (slug)'),
)
provider_account_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account',
queryset=ProviderAccount.objects.all(),
label=_('Provider account (ID)'),
)
provider_account = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account__account',
queryset=Provider.objects.all(),
to_field_name='account',
label=_('Provider account (account)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
label=_('Provider network (ID)'),
)
type_id = django_filters.ModelMultipleChoiceFilter(
queryset=VirtualCircuitType.objects.all(),
label=_('Virtual circuit type (ID)'),
)
type = django_filters.ModelMultipleChoiceFilter(
field_name='type__slug',
queryset=VirtualCircuitType.objects.all(),
to_field_name='slug',
label=_('Virtual circuit type (slug)'),
)
status = django_filters.MultipleChoiceFilter(
choices=CircuitStatusChoices,
null_value=None
)
class Meta:
model = VirtualCircuit
fields = ('id', 'cid', 'description')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(cid__icontains=value) |
Q(description__icontains=value) |
Q(comments__icontains=value)
).distinct()
class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
q = django_filters.CharFilter(
method='search',
label=_('Search'),
)
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
queryset=VirtualCircuit.objects.all(),
label=_('Virtual circuit'),
)
role = django_filters.MultipleChoiceFilter(
choices=VirtualCircuitTerminationRoleChoices,
null_value=None
)
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_network__provider',
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_network__provider__slug',
queryset=Provider.objects.all(),
to_field_name='slug',
label=_('Provider (slug)'),
)
provider_account_id = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_account',
queryset=ProviderAccount.objects.all(),
label=_('Provider account (ID)'),
)
provider_account = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_circuit__provider_account__account',
queryset=ProviderAccount.objects.all(),
to_field_name='account',
label=_('Provider account (account)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
field_name='virtual_circuit__provider_network',
label=_('Provider network (ID)'),
)
interface_id = django_filters.ModelMultipleChoiceFilter(
queryset=Interface.objects.all(),
field_name='interface',
label=_('Interface (ID)'),
)
class Meta:
model = VirtualCircuitTermination
fields = ('id', 'interface_id', 'description')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(virtual_circuit__cid__icontains=value) |
Q(description__icontains=value)
).distinct()

View File

@@ -1,16 +1,25 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices
from circuits.choices import (
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices,
)
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
from circuits.models import *
from dcim.models import Site
from ipam.models import ASN
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import add_blank_choice
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet, TabbedGroups
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, NumberWithOptions
from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import (
ColorField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
from utilities.templatetags.builtins.filters import bettertitle
__all__ = (
'CircuitBulkEditForm',
@@ -21,6 +30,9 @@ __all__ = (
'ProviderBulkEditForm',
'ProviderAccountBulkEditForm',
'ProviderNetworkBulkEditForm',
'VirtualCircuitBulkEditForm',
'VirtualCircuitTerminationBulkEditForm',
'VirtualCircuitTypeBulkEditForm',
)
@@ -160,6 +172,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
options=CircuitCommitRateChoices
)
)
distance = forms.DecimalField(
label=_('Distance'),
min_value=0,
required=False
)
distance_unit = forms.ChoiceField(
label=_('Distance unit'),
choices=add_blank_choice(DistanceUnitChoices),
required=False,
initial=''
)
description = forms.CharField(
label=_('Description'),
max_length=100,
@@ -171,6 +194,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
fieldsets = (
FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
FieldSet('distance', 'distance_unit', name=_('Attributes')),
FieldSet('tenant', name=_('Tenancy')),
)
nullable_fields = (
@@ -184,15 +208,18 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
max_length=200,
required=False
)
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
required=False
termination_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
required=False,
label=_('Termination type')
)
provider_network = DynamicModelChoiceField(
label=_('Provider Network'),
queryset=ProviderNetwork.objects.all(),
required=False
termination = DynamicModelChoiceField(
label=_('Termination'),
queryset=Site.objects.none(), # Initial queryset
required=False,
disabled=True,
selector=True
)
port_speed = forms.IntegerField(
required=False,
@@ -212,15 +239,26 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
fieldsets = (
FieldSet(
'description',
TabbedGroups(
FieldSet('site', name=_('Site')),
FieldSet('provider_network', name=_('Provider Network')),
),
'termination_type', 'termination',
'mark_connected', name=_('Circuit Termination')
),
FieldSet('port_speed', 'upstream_speed', name=_('Termination Details')),
)
nullable_fields = ('description')
nullable_fields = ('description', 'termination')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if termination_type_id := get_field_value(self, 'termination_type'):
try:
termination_type = ContentType.objects.get(pk=termination_type_id)
model = termination_type.model_class()
self.fields['termination'].queryset = model.objects.all()
self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower
self.fields['termination'].disabled = False
self.fields['termination'].label = _(bettertitle(model._meta.verbose_name))
except ObjectDoesNotExist:
pass
class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
@@ -242,7 +280,7 @@ class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
circuit = DynamicModelChoiceField(
member = DynamicModelChoiceField(
label=_('Circuit'),
queryset=Circuit.objects.all(),
required=False
@@ -255,6 +293,88 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
model = CircuitGroupAssignment
fieldsets = (
FieldSet('circuit', 'priority'),
FieldSet('member', 'priority'),
)
nullable_fields = ('priority',)
class VirtualCircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
color = ColorField(
label=_('Color'),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
model = VirtualCircuitType
fieldsets = (
FieldSet('color', 'description'),
)
nullable_fields = ('color', 'description')
class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
required=False
)
provider_account = DynamicModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(),
required=False
)
type = DynamicModelChoiceField(
label=_('Type'),
queryset=VirtualCircuitType.objects.all(),
required=False
)
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(CircuitStatusChoices),
required=False,
initial=''
)
tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=100,
required=False
)
comments = CommentField()
model = VirtualCircuit
fieldsets = (
FieldSet('provider_network', 'provider_account', 'status', 'description', name=_('Virtual circuit')),
FieldSet('tenant', name=_('Tenancy')),
)
nullable_fields = (
'provider_account', 'tenant', 'description', 'comments',
)
class VirtualCircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
role = forms.ChoiceField(
label=_('Role'),
choices=add_blank_choice(VirtualCircuitTerminationRoleChoices),
required=False,
initial=''
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
model = VirtualCircuitTermination
fieldsets = (
FieldSet('role', 'description'),
)
nullable_fields = ('description',)

View File

@@ -1,12 +1,15 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from circuits.constants import *
from circuits.models import *
from dcim.models import Site
from dcim.models import Interface
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
__all__ = (
'CircuitImportForm',
@@ -18,6 +21,10 @@ __all__ = (
'ProviderImportForm',
'ProviderAccountImportForm',
'ProviderNetworkImportForm',
'VirtualCircuitImportForm',
'VirtualCircuitTerminationImportForm',
'VirtualCircuitTerminationImportRelatedForm',
'VirtualCircuitTypeImportForm',
)
@@ -94,6 +101,12 @@ class CircuitImportForm(NetBoxModelImportForm):
choices=CircuitStatusChoices,
help_text=_('Operational status')
)
distance_unit = CSVChoiceField(
label=_('Distance unit'),
choices=DistanceUnitChoices,
required=False,
help_text=_('Distance unit')
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
@@ -106,7 +119,7 @@ class CircuitImportForm(NetBoxModelImportForm):
model = Circuit
fields = [
'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
'commit_rate', 'description', 'comments', 'tags'
'commit_rate', 'distance', 'distance_unit', 'description', 'comments', 'tags'
]
@@ -120,17 +133,10 @@ class BaseCircuitTerminationImportForm(forms.ModelForm):
label=_('Termination'),
choices=CircuitTerminationSideChoices,
)
site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
to_field_name='name',
required=False
)
provider_network = CSVModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
to_field_name='name',
required=False
termination_type = CSVContentTypeField(
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
required=False,
label=_('Termination type (app & model)')
)
@@ -138,9 +144,12 @@ class CircuitTerminationImportRelatedForm(BaseCircuitTerminationImportForm):
class Meta:
model = CircuitTermination
fields = [
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
'circuit', 'term_side', 'termination_type', 'termination_id', 'port_speed', 'upstream_speed', 'xconnect_id',
'pp_info', 'description'
]
labels = {
'termination_id': _('Termination ID'),
}
class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTerminationImportForm):
@@ -148,9 +157,12 @@ class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTermination
class Meta:
model = CircuitTermination
fields = [
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
'circuit', 'term_side', 'termination_type', 'termination_id', 'port_speed', 'upstream_speed', 'xconnect_id',
'pp_info', 'description', 'tags'
]
labels = {
'termination_id': _('Termination ID'),
}
class CircuitGroupImportForm(NetBoxModelImportForm):
@@ -168,7 +180,101 @@ class CircuitGroupImportForm(NetBoxModelImportForm):
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
member_type = CSVContentTypeField(
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
label=_('Circuit type (app & model)')
)
priority = CSVChoiceField(
label=_('Priority'),
choices=CircuitPriorityChoices,
required=False
)
class Meta:
model = CircuitGroupAssignment
fields = ('circuit', 'group', 'priority')
fields = ('member_type', 'member_id', 'group', 'priority')
class VirtualCircuitTypeImportForm(NetBoxModelImportForm):
slug = SlugField()
class Meta:
model = VirtualCircuitType
fields = ('name', 'slug', 'color', 'description', 'tags')
class VirtualCircuitImportForm(NetBoxModelImportForm):
provider_network = CSVModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
to_field_name='name',
help_text=_('The network to which this virtual circuit belongs')
)
provider_account = CSVModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(),
to_field_name='account',
help_text=_('Assigned provider account (if any)'),
required=False
)
type = CSVModelChoiceField(
label=_('Type'),
queryset=VirtualCircuitType.objects.all(),
to_field_name='name',
help_text=_('Type of virtual circuit')
)
status = CSVChoiceField(
label=_('Status'),
choices=CircuitStatusChoices,
help_text=_('Operational status')
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned tenant')
)
class Meta:
model = VirtualCircuit
fields = [
'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'comments',
'tags',
]
class BaseVirtualCircuitTerminationImportForm(forms.ModelForm):
virtual_circuit = CSVModelChoiceField(
label=_('Virtual circuit'),
queryset=VirtualCircuit.objects.all(),
to_field_name='cid',
)
role = CSVChoiceField(
label=_('Role'),
choices=VirtualCircuitTerminationRoleChoices,
help_text=_('Operational role')
)
interface = CSVModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(),
to_field_name='pk',
)
class VirtualCircuitTerminationImportRelatedForm(BaseVirtualCircuitTerminationImportForm):
class Meta:
model = VirtualCircuitTermination
fields = [
'virtual_circuit', 'role', 'interface', 'description',
]
class VirtualCircuitTerminationImportForm(NetBoxModelImportForm, BaseVirtualCircuitTerminationImportForm):
class Meta:
model = VirtualCircuitTermination
fields = [
'virtual_circuit', 'role', 'interface', 'description', 'tags',
]

View File

@@ -1,12 +1,17 @@
from django import forms
from django.utils.translation import gettext as _
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices
from circuits.choices import (
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices,
VirtualCircuitTerminationRoleChoices,
)
from circuits.models import *
from dcim.models import Region, Site, SiteGroup
from dcim.models import Location, Region, Site, SiteGroup
from ipam.models import ASN
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms import add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DatePicker, NumberWithOptions
@@ -20,6 +25,9 @@ __all__ = (
'ProviderFilterForm',
'ProviderAccountFilterForm',
'ProviderNetworkFilterForm',
'VirtualCircuitFilterForm',
'VirtualCircuitTerminationFilterForm',
'VirtualCircuitTypeFilterForm',
)
@@ -114,7 +122,10 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', name=_('Attributes')),
FieldSet(
'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
name=_('Attributes')
),
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
@@ -188,6 +199,15 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
options=CircuitCommitRateChoices
)
)
distance = forms.DecimalField(
label=_('Distance'),
required=False,
)
distance_unit = forms.ChoiceField(
label=_('Distance unit'),
choices=add_blank_choice(DistanceUnitChoices),
required=False
)
tag = TagFilterField(model)
@@ -196,18 +216,29 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('circuit_id', 'term_side', name=_('Circuit')),
FieldSet('provider_id', 'provider_network_id', name=_('Provider')),
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
FieldSet('provider_id', name=_('Provider')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Termination')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id',
'site_group_id': '$site_group_id',
},
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
label=_('Location')
)
circuit_id = DynamicModelMultipleChoiceField(
queryset=Circuit.objects.all(),
required=False,
@@ -247,14 +278,14 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
model = CircuitGroupAssignment
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('provider_id', 'circuit_id', 'group_id', 'priority', name=_('Assignment')),
FieldSet('provider_id', 'member_id', 'group_id', 'priority', name=_('Assignment')),
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
)
circuit_id = DynamicModelMultipleChoiceField(
member_id = DynamicModelMultipleChoiceField(
queryset=Circuit.objects.all(),
required=False,
label=_('Circuit')
@@ -270,3 +301,93 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
required=False
)
tag = TagFilterField(model)
class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm):
model = VirtualCircuitType
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('color', name=_('Attributes')),
)
tag = TagFilterField(model)
color = ColorField(
label=_('Color'),
required=False
)
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
model = VirtualCircuit
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
FieldSet('type', 'status', name=_('Attributes')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
)
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
)
provider_account_id = DynamicModelMultipleChoiceField(
queryset=ProviderAccount.objects.all(),
required=False,
query_params={
'provider_id': '$provider_id'
},
label=_('Provider account')
)
provider_network_id = DynamicModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
required=False,
query_params={
'provider_id': '$provider_id'
},
label=_('Provider network')
)
type_id = DynamicModelMultipleChoiceField(
queryset=VirtualCircuitType.objects.all(),
required=False,
label=_('Type')
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=CircuitStatusChoices,
required=False
)
tag = TagFilterField(model)
class VirtualCircuitTerminationFilterForm(NetBoxModelFilterSetForm):
model = VirtualCircuitTermination
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('virtual_circuit_id', 'role', name=_('Virtual circuit')),
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
)
virtual_circuit_id = DynamicModelMultipleChoiceField(
queryset=VirtualCircuit.objects.all(),
required=False,
label=_('Virtual circuit')
)
role = forms.MultipleChoiceField(
label=_('Role'),
choices=VirtualCircuitTerminationRoleChoices,
required=False
)
provider_network_id = DynamicModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
required=False,
query_params={
'provider_id': '$provider_id'
},
label=_('Provider network')
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
)
tag = TagFilterField(model)

View File

@@ -1,14 +1,24 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
from circuits.choices import (
CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices, VirtualCircuitTerminationRoleChoices,
)
from circuits.constants import *
from circuits.models import *
from dcim.models import Site
from dcim.models import Interface, Site
from ipam.models import ASN
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
from utilities.forms.rendering import FieldSet, TabbedGroups
from utilities.forms.widgets import DatePicker, NumberWithOptions
from utilities.forms import get_field_value
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
)
from utilities.forms.rendering import FieldSet, InlineFields
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
from utilities.templatetags.builtins.filters import bettertitle
__all__ = (
'CircuitForm',
@@ -19,6 +29,9 @@ __all__ = (
'ProviderForm',
'ProviderAccountForm',
'ProviderNetworkForm',
'VirtualCircuitForm',
'VirtualCircuitTerminationForm',
'VirtualCircuitTypeForm',
)
@@ -45,7 +58,9 @@ class ProviderForm(NetBoxModelForm):
class ProviderAccountForm(NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all()
queryset=Provider.objects.all(),
selector=True,
quick_add=True
)
comments = CommentField()
@@ -59,7 +74,9 @@ class ProviderAccountForm(NetBoxModelForm):
class ProviderNetworkForm(NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all()
queryset=Provider.objects.all(),
selector=True,
quick_add=True
)
comments = CommentField()
@@ -92,7 +109,8 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
selector=True
selector=True,
quick_add=True
)
provider_account = DynamicModelChoiceField(
label=_('Provider account'),
@@ -103,12 +121,23 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
}
)
type = DynamicModelChoiceField(
queryset=CircuitType.objects.all()
queryset=CircuitType.objects.all(),
quick_add=True
)
comments = CommentField()
fieldsets = (
FieldSet('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
FieldSet(
'provider',
'provider_account',
'cid',
'type',
'status',
InlineFields('distance', 'distance_unit', label=_('Distance')),
'description',
'tags',
name=_('Circuit')
),
FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
@@ -117,7 +146,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
model = Circuit
fields = [
'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
'description', 'tenant_group', 'tenant', 'comments', 'tags',
'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'comments', 'tags',
]
widgets = {
'install_date': DatePicker(),
@@ -134,26 +163,24 @@ class CircuitTerminationForm(NetBoxModelForm):
queryset=Circuit.objects.all(),
selector=True
)
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
termination_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
widget=HTMXSelect(),
required=False,
selector=True
label=_('Termination type')
)
provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
termination = DynamicModelChoiceField(
label=_('Termination'),
queryset=Site.objects.none(), # Initial queryset
required=False,
disabled=True,
selector=True
)
fieldsets = (
FieldSet(
'circuit', 'term_side', 'description', 'tags',
TabbedGroups(
FieldSet('site', name=_('Site')),
FieldSet('provider_network', name=_('Provider Network')),
),
'termination_type', 'termination',
'mark_connected', name=_('Circuit Termination')
),
FieldSet('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', name=_('Termination Details')),
@@ -162,7 +189,7 @@ class CircuitTerminationForm(NetBoxModelForm):
class Meta:
model = CircuitTermination
fields = [
'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed', 'upstream_speed',
'circuit', 'term_side', 'termination_type', 'mark_connected', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info', 'description', 'tags',
]
widgets = {
@@ -174,6 +201,36 @@ class CircuitTerminationForm(NetBoxModelForm):
),
}
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {})
if instance is not None and instance.termination:
initial['termination'] = instance.termination
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if termination_type_id := get_field_value(self, 'termination_type'):
try:
termination_type = ContentType.objects.get(pk=termination_type_id)
model = termination_type.model_class()
self.fields['termination'].queryset = model.objects.all()
self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower
self.fields['termination'].disabled = False
self.fields['termination'].label = _(bettertitle(model._meta.verbose_name))
except ObjectDoesNotExist:
pass
if self.instance and termination_type_id != self.instance.termination_type_id:
self.initial['termination'] = None
def clean(self):
super().clean()
# Assign the selected termination (if any)
self.instance.termination = self.cleaned_data.get('termination')
class CircuitGroupForm(TenancyForm, NetBoxModelForm):
slug = SlugField()
@@ -195,14 +252,137 @@ class CircuitGroupAssignmentForm(NetBoxModelForm):
label=_('Group'),
queryset=CircuitGroup.objects.all(),
)
circuit = DynamicModelChoiceField(
member_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
widget=HTMXSelect(),
required=False,
label=_('Circuit type')
)
member = DynamicModelChoiceField(
label=_('Circuit'),
queryset=Circuit.objects.all(),
queryset=Circuit.objects.none(), # Initial queryset
required=False,
disabled=True,
selector=True
)
fieldsets = (
FieldSet('group', 'member_type', 'member', 'priority', 'tags', name=_('Group Assignment')),
)
class Meta:
model = CircuitGroupAssignment
fields = [
'group', 'circuit', 'priority', 'tags',
'group', 'member_type', 'priority', 'tags',
]
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {})
if instance is not None and instance.member:
initial['member'] = instance.member
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if member_type_id := get_field_value(self, 'member_type'):
try:
model = ContentType.objects.get(pk=member_type_id).model_class()
self.fields['member'].queryset = model.objects.all()
self.fields['member'].widget.attrs['selector'] = model._meta.label_lower
self.fields['member'].disabled = False
self.fields['member'].label = _(bettertitle(model._meta.verbose_name))
except ObjectDoesNotExist:
pass
if self.instance.pk and member_type_id != self.instance.member_type_id:
self.initial['member'] = None
def clean(self):
super().clean()
# Assign the selected circuit (if any)
self.instance.member = self.cleaned_data.get('member')
class VirtualCircuitTypeForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
FieldSet('name', 'slug', 'color', 'description', 'tags'),
)
class Meta:
model = VirtualCircuitType
fields = [
'name', 'slug', 'color', 'description', 'tags',
]
class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
selector=True
)
provider_account = DynamicModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(),
required=False
)
type = DynamicModelChoiceField(
queryset=VirtualCircuitType.objects.all(),
quick_add=True
)
comments = CommentField()
fieldsets = (
FieldSet(
'provider_network', 'provider_account', 'cid', 'type', 'status', 'description', 'tags',
name=_('Virtual circuit'),
),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
model = VirtualCircuit
fields = [
'cid', 'provider_network', 'provider_account', 'type', 'status', 'description', 'tenant_group', 'tenant',
'comments', 'tags',
]
class VirtualCircuitTerminationForm(NetBoxModelForm):
virtual_circuit = DynamicModelChoiceField(
label=_('Virtual circuit'),
queryset=VirtualCircuit.objects.all(),
selector=True
)
role = forms.ChoiceField(
choices=VirtualCircuitTerminationRoleChoices,
widget=HTMXSelect(),
label=_('Role')
)
interface = DynamicModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(),
selector=True,
query_params={
'kind': 'virtual',
'virtual_circuit_termination_id': 'null',
},
context={
'parent': 'device',
}
)
fieldsets = (
FieldSet('virtual_circuit', 'role', 'interface', 'description', 'tags'),
)
class Meta:
model = VirtualCircuitTermination
fields = [
'virtual_circuit', 'role', 'interface', 'description', 'tags',
]

View File

@@ -4,14 +4,17 @@ from circuits import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (
'CircuitTerminationFilter',
'CircuitFilter',
'CircuitGroupAssignmentFilter',
'CircuitGroupFilter',
'CircuitTerminationFilter',
'CircuitTypeFilter',
'ProviderFilter',
'ProviderAccountFilter',
'ProviderNetworkFilter',
'VirtualCircuitFilter',
'VirtualCircuitTerminationFilter',
'VirtualCircuitTypeFilter',
)
@@ -61,3 +64,21 @@ class ProviderAccountFilter(BaseFilterMixin):
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
class ProviderNetworkFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.VirtualCircuitType, lookups=True)
@autotype_decorator(filtersets.VirtualCircuitTypeFilterSet)
class VirtualCircuitTypeFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.VirtualCircuit, lookups=True)
@autotype_decorator(filtersets.VirtualCircuitFilterSet)
class VirtualCircuitFilter(BaseFilterMixin):
pass
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True)
@autotype_decorator(filtersets.VirtualCircuitTerminationFilterSet)
class VirtualCircuitTerminationFilter(BaseFilterMixin):
pass

View File

@@ -31,3 +31,12 @@ class CircuitsQuery:
provider_network: 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_termination: 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()

View File

@@ -1,4 +1,4 @@
from typing import Annotated, List
from typing import Annotated, List, Union
import strawberry
import strawberry_django
@@ -19,6 +19,9 @@ __all__ = (
'ProviderType',
'ProviderAccountType',
'ProviderNetworkType',
'VirtualCircuitTerminationType',
'VirtualCircuitType',
'VirtualCircuitTypeType',
)
@@ -59,13 +62,21 @@ class ProviderNetworkType(NetBoxObjectType):
@strawberry_django.type(
models.CircuitTermination,
fields='__all__',
exclude=('termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'),
filters=CircuitTerminationFilter
)
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
provider_network: Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')] | None
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
@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:
return self.termination
@strawberry_django.type(
@@ -106,9 +117,58 @@ class CircuitGroupType(OrganizationalObjectType):
@strawberry_django.type(
models.CircuitGroupAssignment,
fields='__all__',
exclude=('member_type', 'member_id'),
filters=CircuitGroupAssignmentFilter
)
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
circuit: Annotated["CircuitType", 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:
return self.member
@strawberry_django.type(
models.VirtualCircuitType,
fields='__all__',
filters=VirtualCircuitTypeFilter
)
class VirtualCircuitTypeType(OrganizationalObjectType):
color: str
virtual_circuits: List[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
@strawberry_django.type(
models.VirtualCircuitTermination,
fields='__all__',
filters=VirtualCircuitTerminationFilter
)
class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
virtual_circuit: Annotated[
"VirtualCircuitType",
strawberry.lazy('circuits.graphql.types')
] = strawberry_django.field(select_related=["virtual_circuit"])
interface: Annotated[
"InterfaceType",
strawberry.lazy('dcim.graphql.types')
] = strawberry_django.field(select_related=["interface"])
@strawberry_django.type(
models.VirtualCircuit,
fields='__all__',
filters=VirtualCircuitFilter
)
class VirtualCircuitType(NetBoxObjectType):
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]

View File

@@ -5,11 +5,9 @@ import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
dependencies = []
replaces = [
('circuits', '0001_initial'),
@@ -98,7 +96,12 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=100)),
('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='networks', to='circuits.provider')),
(
'provider',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, related_name='networks', to='circuits.provider'
),
),
],
options={
'ordering': ('provider', 'name'),

View File

@@ -4,7 +4,6 @@ import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('dcim', '0001_initial'),
('contenttypes', '0002_remove_content_type_name'),
@@ -58,32 +57,56 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='circuittermination',
name='_cable_peer_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='contenttypes.contenttype',
),
),
migrations.AddField(
model_name='circuittermination',
name='cable',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.cable'),
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.cable'
),
),
migrations.AddField(
model_name='circuittermination',
name='circuit',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.circuit'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.circuit'
),
),
migrations.AddField(
model_name='circuittermination',
name='provider_network',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='circuits.providernetwork'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='circuit_terminations',
to='circuits.providernetwork',
),
),
migrations.AddField(
model_name='circuittermination',
name='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.site'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='circuit_terminations',
to='dcim.site',
),
),
migrations.AddField(
model_name='circuit',
name='provider',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provider'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provider'
),
),
migrations.AddField(
model_name='circuit',
@@ -93,26 +116,50 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='circuit',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.tenant'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='circuits',
to='tenancy.tenant',
),
),
migrations.AddField(
model_name='circuit',
name='termination_a',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.circuittermination'),
field=models.ForeignKey(
blank=True,
editable=False,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='circuits.circuittermination',
),
),
migrations.AddField(
model_name='circuit',
name='termination_z',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.circuittermination'),
field=models.ForeignKey(
blank=True,
editable=False,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='circuits.circuittermination',
),
),
migrations.AddField(
model_name='circuit',
name='type',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.circuittype'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.circuittype'
),
),
migrations.AddConstraint(
model_name='providernetwork',
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_provider_name'),
constraint=models.UniqueConstraint(
fields=('provider', 'name'), name='circuits_providernetwork_provider_name'
),
),
migrations.AlterUniqueTogether(
name='providernetwork',

View File

@@ -5,7 +5,6 @@ import utilities.json
class Migration(migrations.Migration):
replaces = [
('circuits', '0003_extend_tag_support'),
('circuits', '0004_rename_cable_peer'),
@@ -14,7 +13,7 @@ class Migration(migrations.Migration):
('circuits', '0034_created_datetimefield'),
('circuits', '0035_provider_asns'),
('circuits', '0036_circuit_termination_date_tags_custom_fields'),
('circuits', '0037_new_cabling_models')
('circuits', '0037_new_cabling_models'),
]
dependencies = [

View File

@@ -6,13 +6,12 @@ import utilities.json
class Migration(migrations.Migration):
replaces = [
('circuits', '0038_cabling_cleanup'),
('circuits', '0039_unique_constraints'),
('circuits', '0040_provider_remove_deprecated_fields'),
('circuits', '0041_standardize_description_comments'),
('circuits', '0042_provideraccount')
('circuits', '0042_provideraccount'),
]
dependencies = [
@@ -51,11 +50,15 @@ class Migration(migrations.Migration):
),
migrations.AddConstraint(
model_name='circuittermination',
constraint=models.UniqueConstraint(fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'),
constraint=models.UniqueConstraint(
fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'
),
),
migrations.AddConstraint(
model_name='providernetwork',
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'),
constraint=models.UniqueConstraint(
fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'
),
),
migrations.RemoveField(
model_name='provider',
@@ -84,12 +87,20 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
(
'custom_field_data',
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)),
('account', models.CharField(max_length=100)),
('name', models.CharField(blank=True, max_length=100)),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='accounts', to='circuits.provider')),
(
'provider',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, related_name='accounts', to='circuits.provider'
),
),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
@@ -98,11 +109,17 @@ class Migration(migrations.Migration):
),
migrations.AddConstraint(
model_name='provideraccount',
constraint=models.UniqueConstraint(condition=models.Q(('name', ''), _negated=True), fields=('provider', 'name'), name='circuits_provideraccount_unique_provider_name'),
constraint=models.UniqueConstraint(
condition=models.Q(('name', ''), _negated=True),
fields=('provider', 'name'),
name='circuits_provideraccount_unique_provider_name',
),
),
migrations.AddConstraint(
model_name='provideraccount',
constraint=models.UniqueConstraint(fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'),
constraint=models.UniqueConstraint(
fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'
),
),
migrations.RemoveField(
model_name='provider',
@@ -111,7 +128,13 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='circuit',
name='provider_account',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='circuits',
to='circuits.provideraccount',
),
preserve_default=False,
),
migrations.AlterModelOptions(
@@ -120,6 +143,8 @@ class Migration(migrations.Migration):
),
migrations.AddConstraint(
model_name='circuit',
constraint=models.UniqueConstraint(fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'),
constraint=models.UniqueConstraint(
fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'
),
),
]

View File

@@ -5,7 +5,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0043_circuittype_color'),
('extras', '0119_notifications'),

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.0.9 on 2024-09-26 22:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0044_circuit_groups'),
]
operations = [
migrations.AddField(
model_name='circuit',
name='_abs_distance',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True),
),
migrations.AddField(
model_name='circuit',
name='distance',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
),
migrations.AddField(
model_name='circuit',
name='distance_unit',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -0,0 +1,39 @@
from django.db import migrations, models
def set_null_values(apps, schema_editor):
"""
Replace empty strings with null values.
"""
Circuit = apps.get_model('circuits', 'Circuit')
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
Circuit.objects.filter(distance_unit='').update(distance_unit=None)
CircuitGroupAssignment.objects.filter(priority='').update(priority=None)
CircuitTermination.objects.filter(cable_end='').update(cable_end=None)
class Migration(migrations.Migration):
dependencies = [
('circuits', '0045_circuit_distance'),
]
operations = [
migrations.AlterField(
model_name='circuit',
name='distance_unit',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='circuitgroupassignment',
name='priority',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='circuittermination',
name='cable_end',
field=models.CharField(blank=True, max_length=1, null=True),
),
migrations.RunPython(code=set_null_values, reverse_code=migrations.RunPython.noop),
]

View File

@@ -0,0 +1,53 @@
import django.db.models.deletion
from django.db import migrations, models
def copy_site_assignments(apps, schema_editor):
"""
Copy site ForeignKey values to the Termination GFK.
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
Site = apps.get_model('dcim', 'Site')
CircuitTermination.objects.filter(site__isnull=False).update(
termination_type=ContentType.objects.get_for_model(Site), termination_id=models.F('site_id')
)
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
CircuitTermination.objects.filter(provider_network__isnull=False).update(
termination_type=ContentType.objects.get_for_model(ProviderNetwork),
termination_id=models.F('provider_network_id'),
)
class Migration(migrations.Migration):
dependencies = [
('circuits', '0046_charfield_null_choices'),
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0193_poweroutlet_color'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='termination_id',
field=models.PositiveBigIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='circuittermination',
name='termination_type',
field=models.ForeignKey(
blank=True,
limit_choices_to=models.Q(
('model__in', ('region', 'sitegroup', 'site', 'location', 'providernetwork'))
),
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='+',
to='contenttypes.contenttype',
),
),
# Copy over existing site assignments
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
]

View File

@@ -0,0 +1,83 @@
import django.db.models.deletion
from django.db import migrations, models
def populate_denormalized_fields(apps, schema_editor):
"""
Copy site ForeignKey values to the Termination GFK.
"""
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
terminations = CircuitTermination.objects.filter(site__isnull=False).prefetch_related('site')
for termination in terminations:
termination._region_id = termination.site.region_id
termination._site_group_id = termination.site.group_id
termination._site_id = termination.site_id
# Note: Location cannot be set prior to migration
CircuitTermination.objects.bulk_update(terminations, ['_region', '_site_group', '_site'], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('circuits', '0047_circuittermination__termination'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='circuit_terminations',
to='dcim.location',
),
),
migrations.AddField(
model_name='circuittermination',
name='_region',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='circuit_terminations',
to='dcim.region',
),
),
migrations.AddField(
model_name='circuittermination',
name='_site',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='circuit_terminations',
to='dcim.site',
),
),
migrations.AddField(
model_name='circuittermination',
name='_site_group',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='circuit_terminations',
to='dcim.sitegroup',
),
),
# Populate denormalized FK values
migrations.RunPython(code=populate_denormalized_fields, reverse_code=migrations.RunPython.noop),
# Delete the site ForeignKey
migrations.RemoveField(
model_name='circuittermination',
name='site',
),
migrations.RenameField(
model_name='circuittermination',
old_name='provider_network',
new_name='_provider_network',
),
]

View File

@@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0048_circuitterminations_cached_relations'),
('dcim', '0197_natural_sort_collation'),
]
operations = [
migrations.AlterField(
model_name='provider',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
),
migrations.AlterField(
model_name='providernetwork',
name='name',
field=models.CharField(db_collation='natural_sort', max_length=100),
),
]

View File

@@ -0,0 +1,147 @@
import django.db.models.deletion
import taggit.managers
from django.db import migrations, models
import utilities.fields
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('circuits', '0049_natural_ordering'),
('dcim', '0196_qinq_svlan'),
('extras', '0122_charfield_null_choices'),
('tenancy', '0016_charfield_null_choices'),
]
operations = [
migrations.CreateModel(
name='VirtualCircuitType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(
blank=True,
default=dict,
encoder=utilities.json.CustomFieldJSONEncoder
)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)),
('color', utilities.fields.ColorField(blank=True, max_length=6)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'virtual circuit type',
'verbose_name_plural': 'virtual circuit types',
'ordering': ('name',),
},
),
migrations.CreateModel(
name='VirtualCircuit',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
(
'custom_field_data',
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)),
('cid', models.CharField(max_length=100)),
('status', models.CharField(default='active', max_length=50)),
(
'provider_account',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='virtual_circuits',
to='circuits.provideraccount',
),
),
(
'provider_network',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name='virtual_circuits',
to='circuits.providernetwork',
),
),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
(
'type',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name='virtual_circuits',
to='circuits.virtualcircuittype'
)
),
(
'tenant',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='virtual_circuits',
to='tenancy.tenant',
),
),
],
options={
'verbose_name': 'circuit',
'verbose_name_plural': 'circuits',
'ordering': ['provider_network', 'provider_account', 'cid'],
},
),
migrations.CreateModel(
name='VirtualCircuitTermination',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
(
'custom_field_data',
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
('role', models.CharField(default='peer', max_length=50)),
('description', models.CharField(blank=True, max_length=200)),
(
'interface',
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name='virtual_circuit_termination',
to='dcim.interface',
),
),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
(
'virtual_circuit',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='terminations',
to='circuits.virtualcircuit',
),
),
],
options={
'verbose_name': 'virtual circuit termination',
'verbose_name_plural': 'virtual circuit terminations',
'ordering': ['virtual_circuit', 'role', 'pk'],
},
),
migrations.AddConstraint(
model_name='virtualcircuit',
constraint=models.UniqueConstraint(
fields=('provider_network', 'cid'), name='circuits_virtualcircuit_unique_provider_network_cid'
),
),
migrations.AddConstraint(
model_name='virtualcircuit',
constraint=models.UniqueConstraint(
fields=('provider_account', 'cid'), name='circuits_virtualcircuit_unique_provideraccount_cid'
),
),
]

View File

@@ -0,0 +1,85 @@
import django.db.models.deletion
from django.db import migrations, models
def set_member_type(apps, schema_editor):
"""
Set member_type on any existing CircuitGroupAssignments to the content type for Circuit.
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
Circuit = apps.get_model('circuits', 'Circuit')
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
CircuitGroupAssignment.objects.update(
member_type=ContentType.objects.get_for_model(Circuit)
)
class Migration(migrations.Migration):
dependencies = [
('circuits', '0050_virtual_circuits'),
('contenttypes', '0002_remove_content_type_name'),
('extras', '0122_charfield_null_choices'),
]
operations = [
migrations.RemoveConstraint(
model_name='circuitgroupassignment',
name='circuits_circuitgroupassignment_unique_circuit_group',
),
migrations.AlterModelOptions(
name='circuitgroupassignment',
options={'ordering': ('group', 'member_type', 'member_id', 'priority', 'pk')},
),
# Change member_id to an integer field for the member GFK
migrations.RenameField(
model_name='circuitgroupassignment',
old_name='circuit',
new_name='member_id',
),
migrations.AlterField(
model_name='circuitgroupassignment',
name='member_id',
field=models.PositiveBigIntegerField(),
),
# Add content type pointer for the member GFK
migrations.AddField(
model_name='circuitgroupassignment',
name='member_type',
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
related_name='+',
to='contenttypes.contenttype',
blank=True,
null=True
),
preserve_default=False,
),
# Populate member_type for any existing assignments
migrations.RunPython(code=set_member_type, reverse_code=migrations.RunPython.noop),
# Disallow null values for member_type
migrations.AlterField(
model_name='circuitgroupassignment',
name='member_type',
field=models.ForeignKey(
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
on_delete=django.db.models.deletion.PROTECT,
related_name='+',
to='contenttypes.contenttype'
),
),
migrations.AddConstraint(
model_name='circuitgroupassignment',
constraint=models.UniqueConstraint(
fields=('member_type', 'member_id', 'group'),
name='circuits_circuitgroupassignment_unique_member_group'
),
),
]

View File

@@ -1,2 +1,3 @@
from .circuits import *
from .providers import *
from .virtual_circuits import *

View File

@@ -0,0 +1,23 @@
from django.utils.translation import gettext_lazy as _
from netbox.models import OrganizationalModel
from utilities.fields import ColorField
__all__ = (
'BaseCircuitType',
)
class BaseCircuitType(OrganizationalModel):
"""
Abstract base model to represent a type of physical or virtual circuit.
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band".
"""
color = ColorField(
verbose_name=_('color'),
blank=True
)
class Meta:
abstract = True

View File

@@ -1,13 +1,19 @@
from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from circuits.constants import *
from dcim.models import CabledObjectModel
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin
from utilities.fields import ColorField
from netbox.models.mixins import DistanceMixin
from netbox.models.features import (
ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin,
)
from .base import BaseCircuitType
__all__ = (
'Circuit',
@@ -18,26 +24,18 @@ __all__ = (
)
class CircuitType(OrganizationalModel):
class CircuitType(BaseCircuitType):
"""
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band".
"""
color = ColorField(
verbose_name=_('color'),
blank=True
)
def get_absolute_url(self):
return reverse('circuits:circuittype', args=[self.pk])
class Meta:
ordering = ('name',)
verbose_name = _('circuit type')
verbose_name_plural = _('circuit types')
class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel):
"""
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
@@ -61,7 +59,7 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
null=True
)
type = models.ForeignKey(
to='CircuitType',
to='circuits.CircuitType',
on_delete=models.PROTECT,
related_name='circuits'
)
@@ -113,6 +111,13 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
null=True
)
group_assignments = GenericRelation(
to='circuits.CircuitGroupAssignment',
content_type_field='member_type',
object_id_field='member_id',
related_query_name='circuit'
)
clone_fields = (
'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
'description',
@@ -140,9 +145,6 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
def __str__(self):
return self.cid
def get_absolute_url(self):
return reverse('circuits:circuit', args=[self.pk])
def get_status_color(self):
return CircuitStatusChoices.colors.get(self.status)
@@ -173,21 +175,26 @@ class CircuitGroup(OrganizationalModel):
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('circuits:circuitgroup', args=[self.pk])
class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
"""
Assignment of a Circuit to a CircuitGroup with an optional priority.
Assignment of a physical or virtual circuit to a CircuitGroup with an optional priority.
"""
circuit = models.ForeignKey(
Circuit,
on_delete=models.CASCADE,
related_name='assignments'
member_type = models.ForeignKey(
to='contenttypes.ContentType',
limit_choices_to=CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS,
on_delete=models.PROTECT,
related_name='+'
)
member_id = models.PositiveBigIntegerField(
verbose_name=_('member ID')
)
member = GenericForeignKey(
ct_field='member_type',
fk_field='member_id'
)
group = models.ForeignKey(
CircuitGroup,
to='circuits.CircuitGroup',
on_delete=models.CASCADE,
related_name='assignments'
)
@@ -195,19 +202,19 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
verbose_name=_('priority'),
max_length=50,
choices=CircuitPriorityChoices,
blank=True
blank=True,
null=True
)
prerequisite_models = (
'circuits.Circuit',
'circuits.CircuitGroup',
)
class Meta:
ordering = ('group', 'circuit', 'priority', 'pk')
ordering = ('group', 'member_type', 'member_id', 'priority', 'pk')
constraints = (
models.UniqueConstraint(
fields=('circuit', 'group'),
name='%(app_label)s_%(class)s_unique_circuit_group'
fields=('member_type', 'member_id', 'group'),
name='%(app_label)s_%(class)s_unique_member_group'
),
)
verbose_name = _('Circuit group assignment')
@@ -237,22 +244,24 @@ class CircuitTermination(
term_side = models.CharField(
max_length=1,
choices=CircuitTerminationSideChoices,
verbose_name=_('termination')
verbose_name=_('termination side')
)
site = models.ForeignKey(
to='dcim.Site',
termination_type = models.ForeignKey(
to='contenttypes.ContentType',
on_delete=models.PROTECT,
related_name='circuit_terminations',
limit_choices_to=Q(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
related_name='+',
blank=True,
null=True
)
provider_network = models.ForeignKey(
to='circuits.ProviderNetwork',
on_delete=models.PROTECT,
related_name='circuit_terminations',
termination_id = models.PositiveBigIntegerField(
blank=True,
null=True
)
termination = GenericForeignKey(
ct_field='termination_type',
fk_field='termination_id'
)
port_speed = models.PositiveIntegerField(
verbose_name=_('port speed (Kbps)'),
blank=True,
@@ -283,6 +292,43 @@ class CircuitTermination(
blank=True
)
# Cached associations to enable efficient filtering
_provider_network = models.ForeignKey(
to='circuits.ProviderNetwork',
on_delete=models.PROTECT,
related_name='circuit_terminations',
blank=True,
null=True
)
_location = models.ForeignKey(
to='dcim.Location',
on_delete=models.CASCADE,
related_name='circuit_terminations',
blank=True,
null=True
)
_site = models.ForeignKey(
to='dcim.Site',
on_delete=models.CASCADE,
related_name='circuit_terminations',
blank=True,
null=True
)
_region = models.ForeignKey(
to='dcim.Region',
on_delete=models.CASCADE,
related_name='circuit_terminations',
blank=True,
null=True
)
_site_group = models.ForeignKey(
to='dcim.SiteGroup',
on_delete=models.CASCADE,
related_name='circuit_terminations',
blank=True,
null=True
)
class Meta:
ordering = ['circuit', 'term_side']
constraints = (
@@ -304,10 +350,35 @@ class CircuitTermination(
super().clean()
# Must define either site *or* provider network
if self.site is None and self.provider_network is None:
raise ValidationError(_("A circuit termination must attach to either a site or a provider network."))
if self.site and self.provider_network:
raise ValidationError(_("A circuit termination cannot attach to both a site and a provider network."))
if self.termination is None:
raise ValidationError(_("A circuit termination must attach to termination."))
def save(self, *args, **kwargs):
# Cache objects associated with the terminating object (for filtering)
self.cache_related_objects()
super().save(*args, **kwargs)
def cache_related_objects(self):
self._provider_network = self._region = self._site_group = self._site = self._location = None
if self.termination_type:
termination_type = self.termination_type.model_class()
if termination_type == apps.get_model('dcim', 'region'):
self._region = self.termination
elif termination_type == apps.get_model('dcim', 'sitegroup'):
self._site_group = self.termination
elif termination_type == apps.get_model('dcim', 'site'):
self._region = self.termination.region
self._site_group = self.termination.group
self._site = self.termination
elif termination_type == apps.get_model('dcim', 'location'):
self._region = self.termination.site.region
self._site_group = self.termination.site.group
self._site = self.termination.site
self._location = self.termination
elif termination_type == apps.get_model('circuits', 'providernetwork'):
self._provider_network = self.termination
cache_related_objects.alters_data = True
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
@@ -321,7 +392,7 @@ class CircuitTermination(
def get_peer_termination(self):
peer_side = 'Z' if self.term_side == 'A' else 'A'
try:
return CircuitTermination.objects.prefetch_related('site').get(
return CircuitTermination.objects.prefetch_related('termination').get(
circuit=self.circuit,
term_side=peer_side
)

View File

@@ -1,6 +1,5 @@
from django.db import models
from django.db.models import Q
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from netbox.models import PrimaryModel
@@ -22,7 +21,8 @@ class Provider(ContactsMixin, PrimaryModel):
verbose_name=_('name'),
max_length=100,
unique=True,
help_text=_('Full name of the provider')
help_text=_('Full name of the provider'),
db_collation="natural_sort"
)
slug = models.SlugField(
verbose_name=_('slug'),
@@ -45,9 +45,6 @@ class Provider(ContactsMixin, PrimaryModel):
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('circuits:provider', args=[self.pk])
class ProviderAccount(ContactsMixin, PrimaryModel):
"""
@@ -91,9 +88,6 @@ class ProviderAccount(ContactsMixin, PrimaryModel):
return f'{self.account} ({self.name})'
return f'{self.account}'
def get_absolute_url(self):
return reverse('circuits:provideraccount', args=[self.pk])
class ProviderNetwork(PrimaryModel):
"""
@@ -102,7 +96,8 @@ class ProviderNetwork(PrimaryModel):
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100
max_length=100,
db_collation="natural_sort"
)
provider = models.ForeignKey(
to='circuits.Provider',
@@ -128,6 +123,3 @@ class ProviderNetwork(PrimaryModel):
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('circuits:providernetwork', args=[self.pk])

View File

@@ -0,0 +1,191 @@
from functools import cached_property
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
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, TagsMixin
from .base import BaseCircuitType
__all__ = (
'VirtualCircuit',
'VirtualCircuitTermination',
'VirtualCircuitType',
)
class VirtualCircuitType(BaseCircuitType):
"""
Like physical circuits, virtual circuits can be organized by their functional role. For example, a user might wish
to categorize virtual circuits by their technological nature or by product name.
"""
class Meta:
ordering = ('name',)
verbose_name = _('virtual circuit type')
verbose_name_plural = _('virtual circuit types')
class VirtualCircuit(PrimaryModel):
"""
A virtual connection between two or more endpoints, delivered across one or more physical circuits.
"""
cid = models.CharField(
max_length=100,
verbose_name=_('circuit ID'),
help_text=_('Unique circuit ID')
)
provider_network = models.ForeignKey(
to='circuits.ProviderNetwork',
on_delete=models.PROTECT,
related_name='virtual_circuits'
)
provider_account = models.ForeignKey(
to='circuits.ProviderAccount',
on_delete=models.PROTECT,
related_name='virtual_circuits',
blank=True,
null=True
)
type = models.ForeignKey(
to='circuits.VirtualCircuitType',
on_delete=models.PROTECT,
related_name='virtual_circuits'
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=CircuitStatusChoices,
default=CircuitStatusChoices.STATUS_ACTIVE
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='virtual_circuits',
blank=True,
null=True
)
group_assignments = GenericRelation(
to='circuits.CircuitGroupAssignment',
content_type_field='member_type',
object_id_field='member_id',
related_query_name='virtual_circuit'
)
clone_fields = (
'provider_network', 'provider_account', 'status', 'tenant', 'description',
)
prerequisite_models = (
'circuits.ProviderNetwork',
'circuits.VirtualCircuitType',
)
class Meta:
ordering = ['provider_network', 'provider_account', 'cid']
constraints = (
models.UniqueConstraint(
fields=('provider_network', 'cid'),
name='%(app_label)s_%(class)s_unique_provider_network_cid'
),
models.UniqueConstraint(
fields=('provider_account', 'cid'),
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
),
)
verbose_name = _('virtual circuit')
verbose_name_plural = _('virtual circuits')
def __str__(self):
return self.cid
def get_status_color(self):
return CircuitStatusChoices.colors.get(self.status)
def clean(self):
super().clean()
if self.provider_account and self.provider_network.provider != self.provider_account.provider:
raise ValidationError({
'provider_account': "The assigned account must belong to the provider of the assigned network."
})
@property
def provider(self):
return self.provider_network.provider
class VirtualCircuitTermination(
CustomFieldsMixin,
CustomLinksMixin,
TagsMixin,
ChangeLoggedModel
):
virtual_circuit = models.ForeignKey(
to='circuits.VirtualCircuit',
on_delete=models.CASCADE,
related_name='terminations'
)
role = models.CharField(
verbose_name=_('role'),
max_length=50,
choices=VirtualCircuitTerminationRoleChoices,
default=VirtualCircuitTerminationRoleChoices.ROLE_PEER
)
interface = models.OneToOneField(
to='dcim.Interface',
on_delete=models.CASCADE,
related_name='virtual_circuit_termination'
)
description = models.CharField(
verbose_name=_('description'),
max_length=200,
blank=True
)
class Meta:
ordering = ['virtual_circuit', 'role', 'pk']
verbose_name = _('virtual circuit termination')
verbose_name_plural = _('virtual circuit terminations')
def __str__(self):
return f'{self.virtual_circuit}: {self.get_role_display()} termination'
def get_absolute_url(self):
return reverse('circuits:virtualcircuittermination', args=[self.pk])
def get_role_color(self):
return VirtualCircuitTerminationRoleChoices.colors.get(self.role)
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
objectchange.related_object = self.virtual_circuit
return objectchange
@property
def parent_object(self):
return self.virtual_circuit
@cached_property
def peer_terminations(self):
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_PEER:
return self.virtual_circuit.terminations.exclude(pk=self.pk).filter(
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER
)
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_HUB:
return self.virtual_circuit.terminations.filter(
role=VirtualCircuitTerminationRoleChoices.ROLE_SPOKE
)
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_SPOKE:
return self.virtual_circuit.terminations.filter(
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB
)
def clean(self):
super().clean()
if self.interface and not self.interface.is_virtual:
raise ValidationError("Virtual circuits may be terminated only to virtual interfaces.")

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