Compare commits

...

120 Commits

Author SHA1 Message Date
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
Jeremy Stretch
c6452b33d8 Merge pull request #18267 from netbox-community/develop
Release v4.1.10
2024-12-23 11:42:29 -05:00
Jeremy Stretch
16917133b2 Merge branch 'master' into develop 2024-12-23 11:24:38 -05:00
Jeremy Stretch
28eada13d3 Release v4.1.10 2024-12-23 10:59:52 -05:00
Tobias Genannt
6ddd3cc779 #18260 - Add context managers to registry 2024-12-23 10:27:25 -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
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
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
499 changed files with 47409 additions and 29024 deletions

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.1.8
placeholder: v4.2.2
validations:
required: true
- type: dropdown
@@ -27,19 +27,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

@@ -22,24 +22,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.8
placeholder: v4.2.2
validations:
required: true
- type: dropdown

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

@@ -28,7 +28,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

@@ -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
@@ -101,7 +99,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 +132,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

@@ -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

@@ -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

@@ -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

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

@@ -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,29 @@ 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) |
| `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

@@ -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,7 +1,30 @@
# 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
* [#18260](https://github.com/netbox-community/netbox/issues/18260) - Fix object change logging
---
## v4.1.9 (2024-12-17)
!!! danger "Do Not Use"
This release contains a regression which breaks change logging. Please use release v4.1.10 instead.
### Enhancements
* [#17215](https://github.com/netbox-community/netbox/issues/17215) - Change the highlighted color of disabled interfaces in interface lists

View File

@@ -0,0 +1,157 @@
# NetBox v4.2
## 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,84 @@
# Generated by Django 5.0.9 on 2024-10-21 17:34
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'])
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.")

View File

@@ -34,7 +34,7 @@ class CircuitTerminationIndex(SearchIndex):
('port_speed', 2000),
('upstream_speed', 2000),
)
display_attrs = ('circuit', 'site', 'provider_network', 'description')
display_attrs = ('circuit', 'termination', 'description')
@register_search
@@ -80,3 +80,34 @@ class ProviderNetworkIndex(SearchIndex):
('comments', 5000),
)
display_attrs = ('provider', 'service_id', 'description')
@register_search
class VirtualCircuitIndex(SearchIndex):
model = models.VirtualCircuit
fields = (
('cid', 100),
('description', 500),
('comments', 5000),
)
display_attrs = ('provider', 'provider_network', 'provider_account', 'status', 'tenant', 'description')
@register_search
class VirtualCircuitTerminationIndex(SearchIndex):
model = models.VirtualCircuitTermination
fields = (
('description', 500),
)
display_attrs = ('virtual_circuit', 'role', 'description')
@register_search
class VirtualCircuitTypeIndex(SearchIndex):
model = models.VirtualCircuitType
fields = (
('name', 100),
('slug', 110),
('description', 500),
)
display_attrs = ('description',)

View File

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

View File

@@ -18,10 +18,8 @@ __all__ = (
CIRCUITTERMINATION_LINK = """
{% if value.site %}
<a href="{{ value.site.get_absolute_url }}">{{ value.site }}</a>
{% elif value.provider_network %}
<a href="{{ value.provider_network.get_absolute_url }}">{{ value.provider_network }}</a>
{% if value.termination %}
<a href="{{ value.termination.get_absolute_url }}">{{ value.termination }}</a>
{% endif %}
"""
@@ -44,9 +42,10 @@ class CircuitTypeTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = CircuitType
fields = (
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions',
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated',
'actions',
)
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
default_columns = ('pk', 'name', 'circuit_count', 'color', 'description')
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
@@ -62,13 +61,17 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
linkify=True,
verbose_name=_('Account')
)
type = tables.Column(
verbose_name=_('Type'),
linkify=True
)
status = columns.ChoiceFieldColumn()
termination_a = tables.TemplateColumn(
termination_a = columns.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
orderable=False,
verbose_name=_('Side A')
)
termination_z = tables.TemplateColumn(
termination_z = columns.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
orderable=False,
verbose_name=_('Side Z')
@@ -76,6 +79,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
commit_rate = CommitRateColumn(
verbose_name=_('Commit Rate')
)
distance = columns.DistanceColumn()
comments = columns.MarkdownColumn(
verbose_name=_('Comments')
)
@@ -109,22 +113,54 @@ class CircuitTerminationTable(NetBoxTable):
linkify=True,
accessor='circuit.provider'
)
term_side = tables.Column(
verbose_name=_('Side')
)
termination_type = columns.ContentTypeColumn(
verbose_name=_('Termination Type'),
)
termination = tables.Column(
verbose_name=_('Termination Point'),
linkify=True
)
# Termination types
site = tables.Column(
verbose_name=_('Site'),
linkify=True
linkify=True,
accessor='_site'
)
site_group = tables.Column(
verbose_name=_('Site Group'),
linkify=True,
accessor='_sitegroup'
)
region = tables.Column(
verbose_name=_('Region'),
linkify=True,
accessor='_region'
)
location = tables.Column(
verbose_name=_('Location'),
linkify=True,
accessor='_location'
)
provider_network = tables.Column(
verbose_name=_('Provider Network'),
linkify=True
linkify=True,
accessor='_provider_network'
)
class Meta(NetBoxTable.Meta):
model = CircuitTermination
fields = (
'pk', 'id', 'circuit', 'provider', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info', 'description', 'created', 'last_updated', 'actions',
'pk', 'id', 'circuit', 'provider', 'term_side', 'termination_type', 'termination', 'site_group', 'region',
'site', 'location', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
'description', 'created', 'last_updated', 'actions',
)
default_columns = (
'pk', 'id', 'circuit', 'provider', 'term_side', 'termination_type', 'termination', 'description',
)
default_columns = ('pk', 'id', 'circuit', 'provider', 'term_side', 'description')
class CircuitGroupTable(NetBoxTable):
@@ -156,11 +192,14 @@ class CircuitGroupAssignmentTable(NetBoxTable):
linkify=True
)
provider = tables.Column(
accessor='circuit__provider',
accessor='member__provider',
verbose_name=_('Provider'),
linkify=True
)
circuit = tables.Column(
member_type = columns.ContentTypeColumn(
verbose_name=_('Type')
)
member = tables.Column(
verbose_name=_('Circuit'),
linkify=True
)
@@ -174,6 +213,7 @@ class CircuitGroupAssignmentTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = CircuitGroupAssignment
fields = (
'pk', 'id', 'group', 'provider', 'circuit', 'priority', 'created', 'last_updated', 'actions', 'tags',
'pk', 'id', 'group', 'provider', 'member_type', 'member', 'priority', 'created', 'last_updated', 'actions',
'tags',
)
default_columns = ('pk', 'group', 'provider', 'circuit', 'priority')
default_columns = ('pk', 'group', 'provider', 'member_type', 'member', 'priority')

View File

@@ -0,0 +1,124 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from circuits.models import *
from netbox.tables import NetBoxTable, columns
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
__all__ = (
'VirtualCircuitTable',
'VirtualCircuitTerminationTable',
'VirtualCircuitTypeTable',
)
class VirtualCircuitTypeTable(NetBoxTable):
name = tables.Column(
linkify=True,
verbose_name=_('Name'),
)
color = columns.ColorColumn()
tags = columns.TagColumn(
url_name='circuits:virtualcircuittype_list'
)
virtual_circuit_count = columns.LinkedCountColumn(
viewname='circuits:virtualcircuit_list',
url_params={'type_id': 'pk'},
verbose_name=_('Circuits')
)
class Meta(NetBoxTable.Meta):
model = VirtualCircuitType
fields = (
'pk', 'id', 'name', 'virtual_circuit_count', 'color', 'description', 'slug', 'tags', 'created',
'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'virtual_circuit_count', 'color', 'description')
class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
cid = tables.Column(
linkify=True,
verbose_name=_('Circuit ID')
)
provider = tables.Column(
accessor=tables.A('provider_network__provider'),
verbose_name=_('Provider'),
linkify=True
)
provider_network = tables.Column(
linkify=True,
verbose_name=_('Provider network')
)
provider_account = tables.Column(
linkify=True,
verbose_name=_('Account')
)
type = tables.Column(
verbose_name=_('Type'),
linkify=True
)
status = columns.ChoiceFieldColumn()
termination_count = columns.LinkedCountColumn(
viewname='circuits:virtualcircuittermination_list',
url_params={'virtual_circuit_id': 'pk'},
verbose_name=_('Terminations')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments')
)
tags = columns.TagColumn(
url_name='circuits:virtualcircuit_list'
)
class Meta(NetBoxTable.Meta):
model = VirtualCircuit
fields = (
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
'termination_count', 'description',
)
class VirtualCircuitTerminationTable(NetBoxTable):
virtual_circuit = tables.Column(
verbose_name=_('Virtual circuit'),
linkify=True
)
provider = tables.Column(
accessor=tables.A('virtual_circuit__provider_network__provider'),
verbose_name=_('Provider'),
linkify=True
)
provider_network = tables.Column(
accessor=tables.A('virtual_circuit__provider_network'),
linkify=True,
verbose_name=_('Provider network')
)
provider_account = tables.Column(
linkify=True,
verbose_name=_('Account')
)
role = columns.ChoiceFieldColumn()
device = tables.Column(
accessor=tables.A('interface__device'),
linkify=True,
verbose_name=_('Device')
)
interface = tables.Column(
verbose_name=_('Interface'),
linkify=True
)
class Meta(NetBoxTable.Meta):
model = VirtualCircuitTermination
fields = (
'pk', 'id', 'virtual_circuit', 'provider', 'provider_network', 'provider_account', 'role', 'interfaces',
'description', 'created', 'last_updated', 'actions',
)
default_columns = (
'pk', 'id', 'virtual_circuit', 'role', 'device', 'interface', 'description',
)

View File

@@ -2,7 +2,8 @@ from django.urls import reverse
from circuits.choices import *
from circuits.models import *
from dcim.models import Site
from dcim.choices import InterfaceTypeChoices
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
from ipam.models import ASN, RIR
from utilities.testing import APITestCase, APIViewTestCases
@@ -120,9 +121,15 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
CircuitType.objects.bulk_create(circuit_types)
circuits = (
Circuit(cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
Circuit(cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
Circuit(cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
Circuit(
cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]
),
Circuit(
cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]
),
Circuit(
cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]
),
)
Circuit.objects.bulk_create(circuits)
@@ -181,10 +188,10 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
Circuit.objects.bulk_create(circuits)
circuit_terminations = (
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, site=sites[0]),
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, provider_network=provider_networks[0]),
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, site=sites[1]),
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, provider_network=provider_networks[1]),
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, termination=sites[0]),
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, termination=provider_networks[0]),
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, termination=sites[1]),
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, termination=provider_networks[1]),
)
CircuitTermination.objects.bulk_create(circuit_terminations)
@@ -192,13 +199,15 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
{
'circuit': circuits[2].pk,
'term_side': SIDE_A,
'site': sites[0].pk,
'termination_type': 'dcim.site',
'termination_id': sites[0].pk,
'port_speed': 200000,
},
{
'circuit': circuits[2].pk,
'term_side': SIDE_Z,
'provider_network': provider_networks[0].pk,
'termination_type': 'circuits.providernetwork',
'termination_id': provider_networks[0].pk,
'port_speed': 200000,
},
]
@@ -286,7 +295,7 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
model = CircuitGroupAssignment
brief_fields = ['circuit', 'display', 'group', 'id', 'priority', 'url']
brief_fields = ['display', 'group', 'id', 'member', 'member_id', 'member_type', 'priority', 'url']
bulk_update_data = {
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
}
@@ -321,17 +330,17 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
assignments = (
CircuitGroupAssignment(
group=circuit_groups[0],
circuit=circuits[0],
member=circuits[0],
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
),
CircuitGroupAssignment(
group=circuit_groups[1],
circuit=circuits[1],
member=circuits[1],
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
),
CircuitGroupAssignment(
group=circuit_groups[2],
circuit=circuits[2],
member=circuits[2],
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
),
)
@@ -340,17 +349,20 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
cls.create_data = [
{
'group': circuit_groups[3].pk,
'circuit': circuits[3].pk,
'member_type': 'circuits.circuit',
'member_id': circuits[3].pk,
'priority': CircuitPriorityChoices.PRIORITY_PRIMARY,
},
{
'group': circuit_groups[4].pk,
'circuit': circuits[4].pk,
'member_type': 'circuits.circuit',
'member_id': circuits[4].pk,
'priority': CircuitPriorityChoices.PRIORITY_SECONDARY,
},
{
'group': circuit_groups[5].pk,
'circuit': circuits[5].pk,
'member_type': 'circuits.circuit',
'member_id': circuits[5].pk,
'priority': CircuitPriorityChoices.PRIORITY_TERTIARY,
},
]
@@ -395,3 +407,290 @@ class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
'provider': providers[1].pk,
'description': 'New description',
}
class VirtualCircuitTypeTest(APIViewTestCases.APIViewTestCase):
model = VirtualCircuitType
brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url', 'virtual_circuit_count']
create_data = (
{
'name': 'Virtual Circuit Type 4',
'slug': 'virtual-circuit-type-4',
},
{
'name': 'Virtual Circuit Type 5',
'slug': 'virtual-circuit-type-5',
},
{
'name': 'Virtual Circuit Type 6',
'slug': 'virtual-circuit-type-6',
},
)
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
virtual_circuit_types = (
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
)
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
class VirtualCircuitTest(APIViewTestCases.APIViewTestCase):
model = VirtualCircuit
brief_fields = ['cid', 'description', 'display', 'id', 'provider_network', 'url']
bulk_update_data = {
'status': 'planned',
}
@classmethod
def setUpTestData(cls):
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = (
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
type=virtual_circuit_type,
cid='Virtual Circuit 1'
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
type=virtual_circuit_type,
cid='Virtual Circuit 2'
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
type=virtual_circuit_type,
cid='Virtual Circuit 3'
),
)
VirtualCircuit.objects.bulk_create(virtual_circuits)
cls.create_data = [
{
'cid': 'Virtual Circuit 4',
'provider_network': provider_network.pk,
'provider_account': provider_account.pk,
'type': virtual_circuit_type.pk,
'status': CircuitStatusChoices.STATUS_PLANNED,
},
{
'cid': 'Virtual Circuit 5',
'provider_network': provider_network.pk,
'provider_account': provider_account.pk,
'type': virtual_circuit_type.pk,
'status': CircuitStatusChoices.STATUS_PLANNED,
},
{
'cid': 'Virtual Circuit 6',
'provider_network': provider_network.pk,
'provider_account': provider_account.pk,
'type': virtual_circuit_type.pk,
'status': CircuitStatusChoices.STATUS_PLANNED,
},
]
class VirtualCircuitTerminationTest(APIViewTestCases.APIViewTestCase):
model = VirtualCircuitTermination
brief_fields = ['description', 'display', 'id', 'interface', 'role', 'url', 'virtual_circuit']
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
site = Site.objects.create(name='Site 1', slug='site-1')
devices = (
Device(site=site, name='hub', device_type=device_type, role=device_role),
Device(site=site, name='spoke1', device_type=device_type, role=device_role),
Device(site=site, name='spoke2', device_type=device_type, role=device_role),
Device(site=site, name='spoke3', device_type=device_type, role=device_role),
)
Device.objects.bulk_create(devices)
physical_interfaces = (
Interface(device=devices[0], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[1], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[2], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[3], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
)
Interface.objects.bulk_create(physical_interfaces)
virtual_interfaces = (
# Point-to-point VCs
Interface(
device=devices[0],
name='eth0.1',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[0],
name='eth0.2',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[0],
name='eth0.3',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[1],
name='eth0.1',
parent=physical_interfaces[1],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[2],
name='eth0.1',
parent=physical_interfaces[2],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[3],
name='eth0.1',
parent=physical_interfaces[3],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
# Hub and spoke VCs
Interface(
device=devices[0],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[1],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[2],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[3],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
)
Interface.objects.bulk_create(virtual_interfaces)
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = (
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 1',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 2',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 3',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 4',
type=virtual_circuit_type
),
)
VirtualCircuit.objects.bulk_create(virtual_circuits)
virtual_circuit_terminations = (
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[0],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[0]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[0],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[3]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[1],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[1]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[1],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[4]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[2],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[2]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[2],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[5]
),
)
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
cls.create_data = [
{
'virtual_circuit': virtual_circuits[3].pk,
'role': VirtualCircuitTerminationRoleChoices.ROLE_HUB,
'interface': virtual_interfaces[6].pk
},
{
'virtual_circuit': virtual_circuits[3].pk,
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
'interface': virtual_interfaces[7].pk
},
{
'virtual_circuit': virtual_circuits[3].pk,
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
'interface': virtual_interfaces[8].pk
},
{
'virtual_circuit': virtual_circuits[3].pk,
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
'interface': virtual_interfaces[9].pk
},
]

View File

@@ -3,8 +3,10 @@ from django.test import TestCase
from circuits.choices import *
from circuits.filtersets import *
from circuits.models import *
from dcim.models import Cable, Region, Site, SiteGroup
from dcim.choices import InterfaceTypeChoices
from dcim.models import Cable, Device, DeviceRole, DeviceType, Interface, Manufacturer, Region, Site, SiteGroup
from ipam.models import ASN, RIR
from netbox.choices import DistanceUnitChoices
from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests
@@ -69,10 +71,12 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Circuit.objects.bulk_create(circuits)
CircuitTermination.objects.bulk_create((
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'),
))
circuit_terminations = (
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A'),
CircuitTermination(circuit=circuits[1], termination=sites[0], term_side='A'),
)
for ct in circuit_terminations:
ct.save()
def test_q(self):
params = {'q': 'foobar1'}
@@ -222,24 +226,93 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = (
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE),
Circuit(
provider=providers[0],
provider_account=provider_accounts[0],
tenant=tenants[0],
type=circuit_types[0],
cid='Test Circuit 1',
install_date='2020-01-01',
termination_date='2021-01-01',
commit_rate=1000,
status=CircuitStatusChoices.STATUS_ACTIVE,
description='foobar1',
distance=10,
distance_unit=DistanceUnitChoices.UNIT_FOOT,
),
Circuit(
provider=providers[0],
provider_account=provider_accounts[0],
tenant=tenants[0],
type=circuit_types[0],
cid='Test Circuit 2',
install_date='2020-01-02',
termination_date='2021-01-02',
commit_rate=2000,
status=CircuitStatusChoices.STATUS_ACTIVE,
description='foobar2',
distance=20,
distance_unit=DistanceUnitChoices.UNIT_METER,
),
Circuit(
provider=providers[0],
provider_account=provider_accounts[1],
tenant=tenants[1],
type=circuit_types[0],
cid='Test Circuit 3',
install_date='2020-01-03',
termination_date='2021-01-03',
commit_rate=3000,
status=CircuitStatusChoices.STATUS_PLANNED,
distance=30,
distance_unit=DistanceUnitChoices.UNIT_METER,
),
Circuit(
provider=providers[1],
provider_account=provider_accounts[1],
tenant=tenants[1],
type=circuit_types[1],
cid='Test Circuit 4',
install_date='2020-01-04',
termination_date='2021-01-04',
commit_rate=4000,
status=CircuitStatusChoices.STATUS_PLANNED,
),
Circuit(
provider=providers[1],
provider_account=provider_accounts[2],
tenant=tenants[2],
type=circuit_types[1],
cid='Test Circuit 5',
install_date='2020-01-05',
termination_date='2021-01-05',
commit_rate=5000,
status=CircuitStatusChoices.STATUS_OFFLINE,
),
Circuit(
provider=providers[1],
provider_account=provider_accounts[2],
tenant=tenants[2],
type=circuit_types[1],
cid='Test Circuit 6',
install_date='2020-01-06',
termination_date='2021-01-06',
commit_rate=6000,
status=CircuitStatusChoices.STATUS_OFFLINE,
),
)
Circuit.objects.bulk_create(circuits)
circuit_terminations = ((
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A'),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A'),
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A'),
CircuitTermination(circuit=circuits[1], termination=sites[1], term_side='A'),
CircuitTermination(circuit=circuits[2], termination=sites[2], term_side='A'),
CircuitTermination(circuit=circuits[3], termination=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], termination=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], termination=provider_networks[2], term_side='A'),
))
CircuitTermination.objects.bulk_create(circuit_terminations)
for ct in circuit_terminations:
ct.save()
def test_q(self):
params = {'q': 'foobar1'}
@@ -289,6 +362,14 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_distance(self):
params = {'distance': [10, 20]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_distance_unit(self):
params = {'distance_unit': DistanceUnitChoices.UNIT_FOOT}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -374,19 +455,66 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Circuit.objects.bulk_create(circuits)
circuit_terminations = ((
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC', description='foobar1'),
CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF', description='foobar2'),
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
CircuitTermination(circuit=circuits[2], site=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'),
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
CircuitTermination(circuit=circuits[6], provider_network=provider_networks[0], term_side='A', mark_connected=True),
))
CircuitTermination.objects.bulk_create(circuit_terminations)
circuit_terminations = (
CircuitTermination(
circuit=circuits[0],
termination=sites[0],
term_side='A',
port_speed=1000,
upstream_speed=1000,
xconnect_id='ABC',
description='foobar1',
),
CircuitTermination(
circuit=circuits[0],
termination=sites[1],
term_side='Z',
port_speed=1000,
upstream_speed=1000,
xconnect_id='DEF',
description='foobar2',
),
CircuitTermination(
circuit=circuits[1],
termination=sites[1],
term_side='A',
port_speed=2000,
upstream_speed=2000,
xconnect_id='GHI',
),
CircuitTermination(
circuit=circuits[1],
termination=sites[2],
term_side='Z',
port_speed=2000,
upstream_speed=2000,
xconnect_id='JKL',
),
CircuitTermination(
circuit=circuits[2],
termination=sites[2],
term_side='A',
port_speed=3000,
upstream_speed=3000,
xconnect_id='MNO',
),
CircuitTermination(
circuit=circuits[2],
termination=sites[0],
term_side='Z',
port_speed=3000,
upstream_speed=3000,
xconnect_id='PQR',
),
CircuitTermination(circuit=circuits[3], termination=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], termination=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], termination=provider_networks[2], term_side='A'),
CircuitTermination(
circuit=circuits[6], termination=provider_networks[0], term_side='A', mark_connected=True
),
)
for ct in circuit_terminations:
ct.save()
Cable(a_terminations=[circuit_terminations[0]], b_terminations=[circuit_terminations[1]]).save()
@@ -520,7 +648,6 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
CircuitGroup(name='Circuit Group 4', slug='circuit-group-4'),
)
CircuitGroup.objects.bulk_create(circuit_groups)
@@ -528,43 +655,86 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
Provider(name='Provider 3', slug='provider-3'),
Provider(name='Provider 4', slug='provider-4'),
))
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuits = (
Circuit(cid='Circuit 1', provider=providers[0], type=circuittype),
Circuit(cid='Circuit 2', provider=providers[1], type=circuittype),
Circuit(cid='Circuit 3', provider=providers[2], type=circuittype),
Circuit(cid='Circuit 4', provider=providers[3], type=circuittype),
Circuit(cid='Circuit 1', provider=providers[0], type=circuit_type),
Circuit(cid='Circuit 2', provider=providers[1], type=circuit_type),
Circuit(cid='Circuit 3', provider=providers[2], type=circuit_type),
)
Circuit.objects.bulk_create(circuits)
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = (
VirtualCircuit(
provider_network=provider_networks[0],
cid='Virtual Circuit 1',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_networks[1],
cid='Virtual Circuit 2',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_networks[2],
cid='Virtual Circuit 3',
type=virtual_circuit_type
),
)
VirtualCircuit.objects.bulk_create(virtual_circuits)
assignments = (
CircuitGroupAssignment(
group=circuit_groups[0],
circuit=circuits[0],
member=circuits[0],
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
),
CircuitGroupAssignment(
group=circuit_groups[1],
circuit=circuits[1],
member=circuits[1],
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
),
CircuitGroupAssignment(
group=circuit_groups[2],
circuit=circuits[2],
member=circuits[2],
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
),
CircuitGroupAssignment(
group=circuit_groups[0],
member=virtual_circuits[0],
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
),
CircuitGroupAssignment(
group=circuit_groups[1],
member=virtual_circuits[1],
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
),
CircuitGroupAssignment(
group=circuit_groups[2],
member=virtual_circuits[2],
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
),
)
CircuitGroupAssignment.objects.bulk_create(assignments)
def test_group_id(self):
groups = CircuitGroup.objects.filter(name__in=['Circuit Group 1', 'Circuit Group 2'])
def test_group(self):
groups = CircuitGroup.objects.all()[:2]
params = {'group_id': [groups[0].pk, groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'group': [groups[0].slug, groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_circuit(self):
circuits = Circuit.objects.all()[:2]
@@ -573,12 +743,19 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'circuit': [circuits[0].cid, circuits[1].cid]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_virtual_circuit(self):
virtual_circuits = VirtualCircuit.objects.all()[:2]
params = {'virtual_circuit_id': [virtual_circuits[0].pk, virtual_circuits[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'virtual_circuit': [virtual_circuits[0].cid, virtual_circuits[1].cid]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_provider(self):
providers = Provider.objects.all()[:2]
params = {'provider_id': [providers[0].pk, providers[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'provider': [providers[0].slug, providers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
@@ -665,3 +842,347 @@ class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'provider': [providers[0].slug, providers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class VirtualCircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualCircuitType.objects.all()
filterset = VirtualCircuitTypeFilterSet
@classmethod
def setUpTestData(cls):
VirtualCircuitType.objects.bulk_create((
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1', description='foobar1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2', description='foobar2'),
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
))
def test_q(self):
params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_name(self):
params = {'name': ['Virtual Circuit Type 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_slug(self):
params = {'slug': ['virtual-circuit-type-1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualCircuit.objects.all()
filterset = VirtualCircuitFilterSet
@classmethod
def setUpTestData(cls):
tenant_groups = (
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
)
for tenantgroup in tenant_groups:
tenantgroup.save()
tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
)
Tenant.objects.bulk_create(tenants)
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
Provider(name='Provider 3', slug='provider-3'),
)
Provider.objects.bulk_create(providers)
provider_accounts = (
ProviderAccount(name='Provider Account 1', provider=providers[0], account='A'),
ProviderAccount(name='Provider Account 2', provider=providers[1], account='B'),
ProviderAccount(name='Provider Account 3', provider=providers[2], account='C'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
virtual_circuit_types = (
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
)
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
virutal_circuits = (
VirtualCircuit(
provider_network=provider_networks[0],
provider_account=provider_accounts[0],
tenant=tenants[0],
cid='Virtual Circuit 1',
type=virtual_circuit_types[0],
status=CircuitStatusChoices.STATUS_PLANNED,
description='virtualcircuit1',
),
VirtualCircuit(
provider_network=provider_networks[1],
provider_account=provider_accounts[1],
tenant=tenants[1],
cid='Virtual Circuit 2',
type=virtual_circuit_types[1],
status=CircuitStatusChoices.STATUS_ACTIVE,
description='virtualcircuit2',
),
VirtualCircuit(
provider_network=provider_networks[2],
provider_account=provider_accounts[2],
tenant=tenants[2],
cid='Virtual Circuit 3',
type=virtual_circuit_types[2],
status=CircuitStatusChoices.STATUS_DEPROVISIONING,
description='virtualcircuit3',
),
)
VirtualCircuit.objects.bulk_create(virutal_circuits)
def test_q(self):
params = {'q': 'virtualcircuit1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_cid(self):
params = {'cid': ['Virtual Circuit 1', 'Virtual Circuit 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_provider(self):
providers = Provider.objects.all()[:2]
params = {'provider_id': [providers[0].pk, providers[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'provider': [providers[0].slug, providers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_provider_account(self):
provider_accounts = ProviderAccount.objects.all()[:2]
params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_provider_network(self):
provider_networks = ProviderNetwork.objects.all()[:2]
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self):
virtual_circuit_types = VirtualCircuitType.objects.all()[:2]
params = {'type_id': [virtual_circuit_types[0].pk, virtual_circuit_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'type': [virtual_circuit_types[0].slug, virtual_circuit_types[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_status(self):
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['virtualcircuit1', 'virtualcircuit2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant(self):
tenants = Tenant.objects.all()[:2]
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class VirtualCircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualCircuitTermination.objects.all()
filterset = VirtualCircuitTerminationFilterSet
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
site = Site.objects.create(name='Site 1', slug='site-1')
devices = (
Device(site=site, name='Device 1', device_type=device_type, role=device_role),
Device(site=site, name='Device 2', device_type=device_type, role=device_role),
Device(site=site, name='Device 3', device_type=device_type, role=device_role),
)
Device.objects.bulk_create(devices)
virtual_interfaces = (
# Device 1
Interface(
device=devices[0],
name='eth0.1',
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[0],
name='eth0.2',
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
# Device 2
Interface(
device=devices[1],
name='eth0.1',
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[1],
name='eth0.2',
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
# Device 3
Interface(
device=devices[2],
name='eth0.1',
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[2],
name='eth0.2',
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
)
Interface.objects.bulk_create(virtual_interfaces)
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
Provider(name='Provider 3', slug='provider-3'),
)
Provider.objects.bulk_create(providers)
provider_networks = (
ProviderNetwork(provider=providers[0], name='Provider Network 1'),
ProviderNetwork(provider=providers[1], name='Provider Network 2'),
ProviderNetwork(provider=providers[2], name='Provider Network 3'),
)
ProviderNetwork.objects.bulk_create(provider_networks)
provider_accounts = (
ProviderAccount(provider=providers[0], account='Provider Account 1'),
ProviderAccount(provider=providers[1], account='Provider Account 2'),
ProviderAccount(provider=providers[2], account='Provider Account 3'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = (
VirtualCircuit(
provider_network=provider_networks[0],
provider_account=provider_accounts[0],
cid='Virtual Circuit 1',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_networks[1],
provider_account=provider_accounts[1],
cid='Virtual Circuit 2',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_networks[2],
provider_account=provider_accounts[2],
cid='Virtual Circuit 3',
type=virtual_circuit_type
),
)
VirtualCircuit.objects.bulk_create(virtual_circuits)
virtual_circuit_terminations = (
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[0],
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB,
interface=virtual_interfaces[0],
description='termination1'
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[0],
role=VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
interface=virtual_interfaces[3],
description='termination2'
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[1],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[1],
description='termination3'
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[1],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[4],
description='termination4'
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[2],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[2],
description='termination5'
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[2],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[5],
description='termination6'
),
)
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
def test_q(self):
params = {'q': 'termination1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self):
params = {'description': ['termination1', 'termination2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_virtual_circuit_id(self):
virtual_circuits = VirtualCircuit.objects.filter()[:2]
params = {'virtual_circuit_id': [virtual_circuits[0].pk, virtual_circuits[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_provider(self):
providers = Provider.objects.all()[:2]
params = {'provider_id': [providers[0].pk, providers[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'provider': [providers[0].slug, providers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_provider_network(self):
provider_networks = ProviderNetwork.objects.all()[:2]
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_provider_account(self):
provider_accounts = ProviderAccount.objects.all()[:2]
params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'provider_account': [provider_accounts[0].account, provider_accounts[1].account]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_interface(self):
interfaces = Interface.objects.all()[:2]
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@@ -1,12 +1,14 @@
import datetime
from django.contrib.contenttypes.models import ContentType
from django.test import override_settings
from django.urls import reverse
from circuits.choices import *
from circuits.models import *
from core.models import ObjectType
from dcim.models import Cable, Interface, Site
from dcim.choices import InterfaceTypeChoices
from dcim.models import Cable, Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
from ipam.models import ASN, RIR
from netbox.choices import ImportFormatChoices
from users.models import ObjectPermission
@@ -139,9 +141,15 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
CircuitType.objects.bulk_create(circuittypes)
circuits = (
Circuit(cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
Circuit(cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
Circuit(cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
Circuit(
cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]
),
Circuit(
cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]
),
Circuit(
cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]
),
)
Circuit.objects.bulk_create(circuits)
@@ -190,27 +198,31 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
def test_bulk_import_objects_with_terminations(self):
json_data = """
site = Site.objects.first()
json_data = f"""
[
{
{{
"cid": "Circuit 7",
"provider": "Provider 1",
"type": "Circuit Type 1",
"status": "active",
"description": "Testing Import",
"terminations": [
{
{{
"term_side": "A",
"site": "Site 1"
},
{
"termination_type": "dcim.site",
"termination_id": "{site.pk}"
}},
{{
"term_side": "Z",
"site": "Site 1"
}
"termination_type": "dcim.site",
"termination_id": "{site.pk}"
}}
]
}
}}
]
"""
initial_count = self._get_queryset().count()
data = {
'data': json_data,
@@ -227,10 +239,10 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
# Try GET with model-level permission
self.assertHttpStatus(self.client.get(self._get_url('import')), 200)
self.assertHttpStatus(self.client.get(self._get_url('bulk_import')), 200)
# Test POST with permission
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302)
self.assertHttpStatus(self.client.post(self._get_url('bulk_import'), data), 302)
self.assertEqual(self._get_queryset().count(), initial_count + 1)
@@ -359,24 +371,27 @@ class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
Circuit.objects.bulk_create(circuits)
circuit_terminations = (
CircuitTermination(circuit=circuits[0], term_side='A', site=sites[0]),
CircuitTermination(circuit=circuits[0], term_side='Z', site=sites[1]),
CircuitTermination(circuit=circuits[1], term_side='A', site=sites[0]),
CircuitTermination(circuit=circuits[1], term_side='Z', site=sites[1]),
CircuitTermination(circuit=circuits[0], term_side='A', termination=sites[0]),
CircuitTermination(circuit=circuits[0], term_side='Z', termination=sites[1]),
CircuitTermination(circuit=circuits[1], term_side='A', termination=sites[0]),
CircuitTermination(circuit=circuits[1], term_side='Z', termination=sites[1]),
)
CircuitTermination.objects.bulk_create(circuit_terminations)
for ct in circuit_terminations:
ct.save()
cls.form_data = {
'circuit': circuits[2].pk,
'term_side': 'A',
'site': sites[2].pk,
'termination_type': ContentType.objects.get_for_model(Site).pk,
'termination': sites[2].pk,
'description': 'New description',
}
site = sites[0].pk
cls.csv_data = (
"circuit,term_side,site,description",
"Circuit 3,A,Site 1,Foo",
"Circuit 3,Z,Site 1,Bar",
"circuit,term_side,termination_type,termination_id,description",
f"Circuit 3,A,dcim.site,{site},Foo",
f"Circuit 3,Z,dcim.site,{site},Bar",
)
cls.csv_update_data = (
@@ -453,6 +468,7 @@ class CircuitGroupAssignmentTestCase(
ViewTestCases.DeleteObjectViewTestCase,
ViewTestCases.ListObjectsViewTestCase,
ViewTestCases.BulkEditObjectsViewTestCase,
ViewTestCases.BulkImportObjectsViewTestCase,
ViewTestCases.BulkDeleteObjectsViewTestCase
):
model = CircuitGroupAssignment
@@ -482,17 +498,17 @@ class CircuitGroupAssignmentTestCase(
assignments = (
CircuitGroupAssignment(
group=circuit_groups[0],
circuit=circuits[0],
member=circuits[0],
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
),
CircuitGroupAssignment(
group=circuit_groups[1],
circuit=circuits[1],
member=circuits[1],
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
),
CircuitGroupAssignment(
group=circuit_groups[2],
circuit=circuits[2],
member=circuits[2],
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
),
)
@@ -502,11 +518,420 @@ class CircuitGroupAssignmentTestCase(
cls.form_data = {
'group': circuit_groups[3].pk,
'circuit': circuits[3].pk,
'member_type': ContentType.objects.get_for_model(Circuit).pk,
'member': circuits[3].pk,
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"member_type,member_id,group,priority",
f"circuits.circuit,{circuits[0].pk},{circuit_groups[3].pk},primary",
f"circuits.circuit,{circuits[1].pk},{circuit_groups[3].pk},secondary",
f"circuits.circuit,{circuits[2].pk},{circuit_groups[3].pk},tertiary",
)
cls.csv_update_data = (
"id,priority",
f"{assignments[0].pk},inactive",
f"{assignments[1].pk},inactive",
f"{assignments[2].pk},inactive",
)
cls.bulk_edit_data = {
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
}
class VirtualCircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = VirtualCircuitType
@classmethod
def setUpTestData(cls):
virtual_circuit_types = (
VirtualCircuitType(name='Virtual Circuit Type 1', slug='circuit-type-1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='circuit-type-2'),
VirtualCircuitType(name='Virtual Circuit Type 3', slug='circuit-type-3'),
)
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Virtual Circuit Type X',
'slug': 'virtual-circuit-type-x',
'description': 'A new virtual circuit type',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,slug",
"Virtual Circuit Type 4,circuit-type-4",
"Virtual Circuit Type 5,circuit-type-5",
"Virtual Circuit Type 6,circuit-type-6",
)
cls.csv_update_data = (
"id,name,description",
f"{virtual_circuit_types[0].pk},Virtual Circuit Type 7,New description7",
f"{virtual_circuit_types[1].pk},Virtual Circuit Type 8,New description8",
f"{virtual_circuit_types[2].pk},Virtual Circuit Type 9,New description9",
)
cls.bulk_edit_data = {
'description': 'Foo',
}
class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = VirtualCircuit
def setUp(self):
super().setUp()
self.add_permissions(
'circuits.add_virtualcircuittermination',
)
@classmethod
def setUpTestData(cls):
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_networks = (
ProviderNetwork(provider=provider, name='Provider Network 1'),
ProviderNetwork(provider=provider, name='Provider Network 2'),
)
ProviderNetwork.objects.bulk_create(provider_networks)
provider_accounts = (
ProviderAccount(provider=provider, account='Provider Account 1'),
ProviderAccount(provider=provider, account='Provider Account 2'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
virtual_circuit_types = (
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
)
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
virtual_circuits = (
VirtualCircuit(
provider_network=provider_networks[0],
provider_account=provider_accounts[0],
cid='Virtual Circuit 1',
type=virtual_circuit_types[0]
),
VirtualCircuit(
provider_network=provider_networks[0],
provider_account=provider_accounts[0],
cid='Virtual Circuit 2',
type=virtual_circuit_types[0]
),
VirtualCircuit(
provider_network=provider_networks[0],
provider_account=provider_accounts[0],
cid='Virtual Circuit 3',
type=virtual_circuit_types[0]
),
)
VirtualCircuit.objects.bulk_create(virtual_circuits)
device = create_test_device('Device 1')
interfaces = (
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
)
Interface.objects.bulk_create(interfaces)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'cid': 'Virtual Circuit X',
'provider_network': provider_networks[1].pk,
'provider_account': provider_accounts[1].pk,
'type': virtual_circuit_types[1].pk,
'status': CircuitStatusChoices.STATUS_PLANNED,
'description': 'A new virtual circuit',
'comments': 'Some comments',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"cid,provider_network,provider_account,type,status",
(
f"Virtual Circuit 4,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
f"{CircuitStatusChoices.STATUS_PLANNED}"
),
(
f"Virtual Circuit 5,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
f"{CircuitStatusChoices.STATUS_PLANNED}"
),
(
f"Virtual Circuit 6,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
f"{CircuitStatusChoices.STATUS_PLANNED}"
),
)
cls.csv_update_data = (
"id,cid,description,type,status",
(
f"{virtual_circuits[0].pk},Virtual Circuit A,New description,{virtual_circuit_types[1].name},"
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
),
(
f"{virtual_circuits[1].pk},Virtual Circuit B,New description,{virtual_circuit_types[1].name},"
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
),
(
f"{virtual_circuits[2].pk},Virtual Circuit C,New description,{virtual_circuit_types[1].name},"
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
),
)
cls.bulk_edit_data = {
'provider_network': provider_networks[1].pk,
'provider_account': provider_accounts[1].pk,
'type': virtual_circuit_types[1].pk,
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
'description': 'New description',
'comments': 'New comments',
}
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
def test_bulk_import_objects_with_terminations(self):
interfaces = Interface.objects.filter(type=InterfaceTypeChoices.TYPE_VIRTUAL)
json_data = f"""
[
{{
"cid": "Virtual Circuit 7",
"provider_network": "Provider Network 1",
"type": "Virtual Circuit Type 1",
"status": "active",
"terminations": [
{{
"role": "hub",
"interface": {interfaces[0].pk}
}},
{{
"role": "spoke",
"interface": {interfaces[1].pk}
}},
{{
"role": "spoke",
"interface": {interfaces[2].pk}
}}
]
}}
]
"""
initial_count = self._get_queryset().count()
data = {
'data': json_data,
'format': ImportFormatChoices.JSON,
}
# Assign model-level permission
obj_perm = ObjectPermission(
name='Test permission',
actions=['add']
)
obj_perm.save()
obj_perm.users.add(self.user)
obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
# Try GET with model-level permission
self.assertHttpStatus(self.client.get(self._get_url('bulk_import')), 200)
# Test POST with permission
self.assertHttpStatus(self.client.post(self._get_url('bulk_import'), data), 302)
self.assertEqual(self._get_queryset().count(), initial_count + 1)
class VirtualCircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = VirtualCircuitTermination
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
site = Site.objects.create(name='Site 1', slug='site-1')
devices = (
Device(site=site, name='hub', device_type=device_type, role=device_role),
Device(site=site, name='spoke1', device_type=device_type, role=device_role),
Device(site=site, name='spoke2', device_type=device_type, role=device_role),
Device(site=site, name='spoke3', device_type=device_type, role=device_role),
)
Device.objects.bulk_create(devices)
physical_interfaces = (
Interface(device=devices[0], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[1], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[2], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=devices[3], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
)
Interface.objects.bulk_create(physical_interfaces)
virtual_interfaces = (
# Point-to-point VCs
Interface(
device=devices[0],
name='eth0.1',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[0],
name='eth0.2',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[0],
name='eth0.3',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[1],
name='eth0.1',
parent=physical_interfaces[1],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[2],
name='eth0.1',
parent=physical_interfaces[2],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[3],
name='eth0.1',
parent=physical_interfaces[3],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
# Hub and spoke VCs
Interface(
device=devices[0],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[1],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[2],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
Interface(
device=devices[3],
name='eth0.9',
parent=physical_interfaces[0],
type=InterfaceTypeChoices.TYPE_VIRTUAL
),
)
Interface.objects.bulk_create(virtual_interfaces)
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
virtual_circuit_type = VirtualCircuitType.objects.create(
name='Virtual Circuit Type 1',
slug='virtual-circuit-type-1'
)
virtual_circuits = (
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 1',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 2',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 3',
type=virtual_circuit_type
),
VirtualCircuit(
provider_network=provider_network,
provider_account=provider_account,
cid='Virtual Circuit 4',
type=virtual_circuit_type
),
)
VirtualCircuit.objects.bulk_create(virtual_circuits)
virtual_circuit_terminations = (
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[0],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[0]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[0],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[3]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[1],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[1]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[1],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[4]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[2],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[2]
),
VirtualCircuitTermination(
virtual_circuit=virtual_circuits[2],
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
interface=virtual_interfaces[5]
),
)
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
cls.form_data = {
'virtual_circuit': virtual_circuits[3].pk,
'role': VirtualCircuitTerminationRoleChoices.ROLE_HUB,
'interface': virtual_interfaces[6].pk
}
cls.csv_data = (
"virtual_circuit,role,interface,description",
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_HUB},{virtual_interfaces[6].pk},Hub",
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[7].pk},Spoke 1",
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[8].pk},Spoke 2",
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[9].pk},Spoke 3",
)
cls.csv_update_data = (
"id,role,description",
f"{virtual_circuit_terminations[0].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
f"{virtual_circuit_terminations[1].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
f"{virtual_circuit_terminations[2].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
)
cls.bulk_edit_data = {
'description': 'New description',
}

View File

@@ -5,69 +5,71 @@ from . import views
app_name = 'circuits'
urlpatterns = [
# Providers
path('providers/', views.ProviderListView.as_view(), name='provider_list'),
path('providers/add/', views.ProviderEditView.as_view(), name='provider_add'),
path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'),
path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
path('providers/', include(get_model_urls('circuits', 'provider', detail=False))),
path('providers/<int:pk>/', include(get_model_urls('circuits', 'provider'))),
# Provider accounts
path('provider-accounts/', views.ProviderAccountListView.as_view(), name='provideraccount_list'),
path('provider-accounts/add/', views.ProviderAccountEditView.as_view(), name='provideraccount_add'),
path('provider-accounts/import/', views.ProviderAccountBulkImportView.as_view(), name='provideraccount_import'),
path('provider-accounts/edit/', views.ProviderAccountBulkEditView.as_view(), name='provideraccount_bulk_edit'),
path('provider-accounts/delete/', views.ProviderAccountBulkDeleteView.as_view(), name='provideraccount_bulk_delete'),
path('provider-accounts/', include(get_model_urls('circuits', 'provideraccount', detail=False))),
path('provider-accounts/<int:pk>/', include(get_model_urls('circuits', 'provideraccount'))),
# Provider networks
path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'),
path('provider-networks/add/', views.ProviderNetworkEditView.as_view(), name='providernetwork_add'),
path('provider-networks/import/', views.ProviderNetworkBulkImportView.as_view(), name='providernetwork_import'),
path('provider-networks/edit/', views.ProviderNetworkBulkEditView.as_view(), name='providernetwork_bulk_edit'),
path('provider-networks/delete/', views.ProviderNetworkBulkDeleteView.as_view(), name='providernetwork_bulk_delete'),
path('provider-networks/', include(get_model_urls('circuits', 'providernetwork', detail=False))),
path('provider-networks/<int:pk>/', include(get_model_urls('circuits', 'providernetwork'))),
# Circuit types
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
path('circuit-types/add/', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
path('circuit-types/edit/', views.CircuitTypeBulkEditView.as_view(), name='circuittype_bulk_edit'),
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
path('circuit-types/', include(get_model_urls('circuits', 'circuittype', detail=False))),
path('circuit-types/<int:pk>/', include(get_model_urls('circuits', 'circuittype'))),
# Circuits
path('circuits/', views.CircuitListView.as_view(), name='circuit_list'),
path('circuits/add/', views.CircuitEditView.as_view(), name='circuit_add'),
path('circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'),
path('circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
path('circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'),
path('circuits/', include(get_model_urls('circuits', 'circuit', detail=False))),
path(
'circuits/<int:pk>/terminations/swap/',
views.CircuitSwapTerminations.as_view(),
name='circuit_terminations_swap'
),
path('circuits/<int:pk>/', include(get_model_urls('circuits', 'circuit'))),
# Circuit terminations
path('circuit-terminations/', views.CircuitTerminationListView.as_view(), name='circuittermination_list'),
path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
path('circuit-terminations/import/', views.CircuitTerminationBulkImportView.as_view(), name='circuittermination_import'),
path('circuit-terminations/edit/', views.CircuitTerminationBulkEditView.as_view(), name='circuittermination_bulk_edit'),
path('circuit-terminations/delete/', views.CircuitTerminationBulkDeleteView.as_view(), name='circuittermination_bulk_delete'),
path('circuit-terminations/', include(get_model_urls('circuits', 'circuittermination', detail=False))),
path('circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'circuittermination'))),
# Circuit Groups
path('circuit-groups/', views.CircuitGroupListView.as_view(), name='circuitgroup_list'),
path('circuit-groups/add/', views.CircuitGroupEditView.as_view(), name='circuitgroup_add'),
path('circuit-groups/import/', views.CircuitGroupBulkImportView.as_view(), name='circuitgroup_import'),
path('circuit-groups/edit/', views.CircuitGroupBulkEditView.as_view(), name='circuitgroup_bulk_edit'),
path('circuit-groups/delete/', views.CircuitGroupBulkDeleteView.as_view(), name='circuitgroup_bulk_delete'),
path('circuit-groups/', include(get_model_urls('circuits', 'circuitgroup', detail=False))),
path('circuit-groups/<int:pk>/', include(get_model_urls('circuits', 'circuitgroup'))),
# Circuit Group Assignments
path('circuit-group-assignments/', views.CircuitGroupAssignmentListView.as_view(), name='circuitgroupassignment_list'),
path('circuit-group-assignments/add/', views.CircuitGroupAssignmentEditView.as_view(), name='circuitgroupassignment_add'),
path('circuit-group-assignments/import/', views.CircuitGroupAssignmentBulkImportView.as_view(), name='circuitgroupassignment_import'),
path('circuit-group-assignments/edit/', views.CircuitGroupAssignmentBulkEditView.as_view(), name='circuitgroupassignment_bulk_edit'),
path('circuit-group-assignments/delete/', views.CircuitGroupAssignmentBulkDeleteView.as_view(), name='circuitgroupassignment_bulk_delete'),
path('circuit-group-assignments/', include(get_model_urls('circuits', 'circuitgroupassignment', detail=False))),
path('circuit-group-assignments/<int:pk>/', include(get_model_urls('circuits', 'circuitgroupassignment'))),
# Virtual circuits
path('virtual-circuits/', views.VirtualCircuitListView.as_view(), name='virtualcircuit_list'),
path('virtual-circuits/add/', views.VirtualCircuitEditView.as_view(), name='virtualcircuit_add'),
path('virtual-circuits/import/', views.VirtualCircuitBulkImportView.as_view(), name='virtualcircuit_bulk_import'),
path('virtual-circuits/edit/', views.VirtualCircuitBulkEditView.as_view(), name='virtualcircuit_bulk_edit'),
path('virtual-circuits/delete/', views.VirtualCircuitBulkDeleteView.as_view(), name='virtualcircuit_bulk_delete'),
path('virtual-circuits/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuit'))),
path('virtual-circuit-types/', include(get_model_urls('circuits', 'virtualcircuittype', detail=False))),
path('virtual-circuit-types/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuittype'))),
# Virtual circuit terminations
path(
'virtual-circuit-terminations/',
views.VirtualCircuitTerminationListView.as_view(),
name='virtualcircuittermination_list',
),
path(
'virtual-circuit-terminations/add/',
views.VirtualCircuitTerminationEditView.as_view(),
name='virtualcircuittermination_add',
),
path(
'virtual-circuit-terminations/import/',
views.VirtualCircuitTerminationBulkImportView.as_view(),
name='virtualcircuittermination_bulk_import',
),
path(
'virtual-circuit-terminations/edit/',
views.VirtualCircuitTerminationBulkEditView.as_view(),
name='virtualcircuittermination_bulk_edit',
),
path(
'virtual-circuit-terminations/delete/',
views.VirtualCircuitTerminationBulkDeleteView.as_view(),
name='virtualcircuittermination_bulk_delete',
),
path('virtual-circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuittermination'))),
]

View File

@@ -17,6 +17,7 @@ from .models import *
# Providers
#
@register_model_view(Provider, 'list', path='', detail=False)
class ProviderListView(generic.ObjectListView):
queryset = Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider')
@@ -36,6 +37,7 @@ class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
}
@register_model_view(Provider, 'add', detail=False)
@register_model_view(Provider, 'edit')
class ProviderEditView(generic.ObjectEditView):
queryset = Provider.objects.all()
@@ -47,11 +49,13 @@ class ProviderDeleteView(generic.ObjectDeleteView):
queryset = Provider.objects.all()
@register_model_view(Provider, 'bulk_import', detail=False)
class ProviderBulkImportView(generic.BulkImportView):
queryset = Provider.objects.all()
model_form = forms.ProviderImportForm
@register_model_view(Provider, 'bulk_edit', path='edit', detail=False)
class ProviderBulkEditView(generic.BulkEditView):
queryset = Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider')
@@ -61,6 +65,7 @@ class ProviderBulkEditView(generic.BulkEditView):
form = forms.ProviderBulkEditForm
@register_model_view(Provider, 'bulk_delete', path='delete', detail=False)
class ProviderBulkDeleteView(generic.BulkDeleteView):
queryset = Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider')
@@ -78,6 +83,7 @@ class ProviderContactsView(ObjectContactsView):
# ProviderAccounts
#
@register_model_view(ProviderAccount, 'list', path='', detail=False)
class ProviderAccountListView(generic.ObjectListView):
queryset = ProviderAccount.objects.annotate(
count_circuits=count_related(Circuit, 'provider_account')
@@ -97,6 +103,7 @@ class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
}
@register_model_view(ProviderAccount, 'add', detail=False)
@register_model_view(ProviderAccount, 'edit')
class ProviderAccountEditView(generic.ObjectEditView):
queryset = ProviderAccount.objects.all()
@@ -108,12 +115,14 @@ class ProviderAccountDeleteView(generic.ObjectDeleteView):
queryset = ProviderAccount.objects.all()
@register_model_view(ProviderAccount, 'bulk_import', detail=False)
class ProviderAccountBulkImportView(generic.BulkImportView):
queryset = ProviderAccount.objects.all()
model_form = forms.ProviderAccountImportForm
table = tables.ProviderAccountTable
@register_model_view(ProviderAccount, 'bulk_edit', path='edit', detail=False)
class ProviderAccountBulkEditView(generic.BulkEditView):
queryset = ProviderAccount.objects.annotate(
count_circuits=count_related(Circuit, 'provider_account')
@@ -123,6 +132,7 @@ class ProviderAccountBulkEditView(generic.BulkEditView):
form = forms.ProviderAccountBulkEditForm
@register_model_view(ProviderAccount, 'bulk_delete', path='delete', detail=False)
class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
queryset = ProviderAccount.objects.annotate(
count_circuits=count_related(Circuit, 'provider_account')
@@ -140,6 +150,7 @@ class ProviderAccountContactsView(ObjectContactsView):
# Provider networks
#
@register_model_view(ProviderNetwork, 'list', path='', detail=False)
class ProviderNetworkListView(generic.ObjectListView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
@@ -158,7 +169,7 @@ class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
instance,
extra=(
(
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
Circuit.objects.restrict(request.user, 'view').filter(terminations___provider_network=instance),
'provider_network_id',
),
),
@@ -166,6 +177,7 @@ class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
}
@register_model_view(ProviderNetwork, 'add', detail=False)
@register_model_view(ProviderNetwork, 'edit')
class ProviderNetworkEditView(generic.ObjectEditView):
queryset = ProviderNetwork.objects.all()
@@ -177,11 +189,13 @@ class ProviderNetworkDeleteView(generic.ObjectDeleteView):
queryset = ProviderNetwork.objects.all()
@register_model_view(ProviderNetwork, 'bulk_import', detail=False)
class ProviderNetworkBulkImportView(generic.BulkImportView):
queryset = ProviderNetwork.objects.all()
model_form = forms.ProviderNetworkImportForm
@register_model_view(ProviderNetwork, 'bulk_edit', path='edit', detail=False)
class ProviderNetworkBulkEditView(generic.BulkEditView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
@@ -189,6 +203,7 @@ class ProviderNetworkBulkEditView(generic.BulkEditView):
form = forms.ProviderNetworkBulkEditForm
@register_model_view(ProviderNetwork, 'bulk_delete', path='delete', detail=False)
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
@@ -199,6 +214,7 @@ class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
# Circuit Types
#
@register_model_view(CircuitType, 'list', path='', detail=False)
class CircuitTypeListView(generic.ObjectListView):
queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
@@ -218,6 +234,7 @@ class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
}
@register_model_view(CircuitType, 'add', detail=False)
@register_model_view(CircuitType, 'edit')
class CircuitTypeEditView(generic.ObjectEditView):
queryset = CircuitType.objects.all()
@@ -229,11 +246,13 @@ class CircuitTypeDeleteView(generic.ObjectDeleteView):
queryset = CircuitType.objects.all()
@register_model_view(CircuitType, 'bulk_import', detail=False)
class CircuitTypeBulkImportView(generic.BulkImportView):
queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeImportForm
@register_model_view(CircuitType, 'bulk_edit', path='edit', detail=False)
class CircuitTypeBulkEditView(generic.BulkEditView):
queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
@@ -243,6 +262,7 @@ class CircuitTypeBulkEditView(generic.BulkEditView):
form = forms.CircuitTypeBulkEditForm
@register_model_view(CircuitType, 'bulk_delete', path='delete', detail=False)
class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
@@ -255,10 +275,10 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
# Circuits
#
@register_model_view(Circuit, 'list', path='', detail=False)
class CircuitListView(generic.ObjectListView):
queryset = Circuit.objects.prefetch_related(
'tenant__group', 'termination_a__site', 'termination_z__site',
'termination_a__provider_network', 'termination_z__provider_network',
'tenant__group', 'termination_a__termination', 'termination_z__termination',
)
filterset = filtersets.CircuitFilterSet
filterset_form = forms.CircuitFilterForm
@@ -270,6 +290,7 @@ class CircuitView(generic.ObjectView):
queryset = Circuit.objects.all()
@register_model_view(Circuit, 'add', detail=False)
@register_model_view(Circuit, 'edit')
class CircuitEditView(generic.ObjectEditView):
queryset = Circuit.objects.all()
@@ -281,6 +302,7 @@ class CircuitDeleteView(generic.ObjectDeleteView):
queryset = Circuit.objects.all()
@register_model_view(Circuit, 'bulk_import', detail=False)
class CircuitBulkImportView(generic.BulkImportView):
queryset = Circuit.objects.all()
model_form = forms.CircuitImportForm
@@ -296,20 +318,20 @@ class CircuitBulkImportView(generic.BulkImportView):
return data
@register_model_view(Circuit, 'bulk_edit', path='edit', detail=False)
class CircuitBulkEditView(generic.BulkEditView):
queryset = Circuit.objects.prefetch_related(
'termination_a__site', 'termination_z__site',
'termination_a__provider_network', 'termination_z__provider_network',
'tenant__group', 'termination_a__termination', 'termination_z__termination',
)
filterset = filtersets.CircuitFilterSet
table = tables.CircuitTable
form = forms.CircuitBulkEditForm
@register_model_view(Circuit, 'bulk_delete', path='delete', detail=False)
class CircuitBulkDeleteView(generic.BulkDeleteView):
queryset = Circuit.objects.prefetch_related(
'termination_a__site', 'termination_z__site',
'termination_a__provider_network', 'termination_z__provider_network',
'tenant__group', 'termination_a__termination', 'termination_z__termination',
)
filterset = filtersets.CircuitFilterSet
table = tables.CircuitTable
@@ -400,6 +422,7 @@ class CircuitContactsView(ObjectContactsView):
# Circuit terminations
#
@register_model_view(CircuitTermination, 'list', path='', detail=False)
class CircuitTerminationListView(generic.ObjectListView):
queryset = CircuitTermination.objects.all()
filterset = filtersets.CircuitTerminationFilterSet
@@ -412,6 +435,7 @@ class CircuitTerminationView(generic.ObjectView):
queryset = CircuitTermination.objects.all()
@register_model_view(CircuitTermination, 'add', detail=False)
@register_model_view(CircuitTermination, 'edit')
class CircuitTerminationEditView(generic.ObjectEditView):
queryset = CircuitTermination.objects.all()
@@ -423,11 +447,13 @@ class CircuitTerminationDeleteView(generic.ObjectDeleteView):
queryset = CircuitTermination.objects.all()
@register_model_view(CircuitTermination, 'bulk_import', detail=False)
class CircuitTerminationBulkImportView(generic.BulkImportView):
queryset = CircuitTermination.objects.all()
model_form = forms.CircuitTerminationImportForm
@register_model_view(CircuitTermination, 'bulk_edit', path='edit', detail=False)
class CircuitTerminationBulkEditView(generic.BulkEditView):
queryset = CircuitTermination.objects.all()
filterset = filtersets.CircuitTerminationFilterSet
@@ -435,6 +461,7 @@ class CircuitTerminationBulkEditView(generic.BulkEditView):
form = forms.CircuitTerminationBulkEditForm
@register_model_view(CircuitTermination, 'bulk_delete', path='delete', detail=False)
class CircuitTerminationBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitTermination.objects.all()
filterset = filtersets.CircuitTerminationFilterSet
@@ -449,6 +476,7 @@ register_model_view(CircuitTermination, 'trace', kwargs={'model': CircuitTermina
# Circuit Groups
#
@register_model_view(CircuitGroup, 'list', path='', detail=False)
class CircuitGroupListView(generic.ObjectListView):
queryset = CircuitGroup.objects.annotate(
circuit_group_assignment_count=count_related(CircuitGroupAssignment, 'group')
@@ -468,6 +496,7 @@ class CircuitGroupView(GetRelatedModelsMixin, generic.ObjectView):
}
@register_model_view(CircuitGroup, 'add', detail=False)
@register_model_view(CircuitGroup, 'edit')
class CircuitGroupEditView(generic.ObjectEditView):
queryset = CircuitGroup.objects.all()
@@ -479,11 +508,13 @@ class CircuitGroupDeleteView(generic.ObjectDeleteView):
queryset = CircuitGroup.objects.all()
@register_model_view(CircuitGroup, 'bulk_import', detail=False)
class CircuitGroupBulkImportView(generic.BulkImportView):
queryset = CircuitGroup.objects.all()
model_form = forms.CircuitGroupImportForm
@register_model_view(CircuitGroup, 'bulk_edit', path='edit', detail=False)
class CircuitGroupBulkEditView(generic.BulkEditView):
queryset = CircuitGroup.objects.all()
filterset = filtersets.CircuitGroupFilterSet
@@ -491,6 +522,7 @@ class CircuitGroupBulkEditView(generic.BulkEditView):
form = forms.CircuitGroupBulkEditForm
@register_model_view(CircuitGroup, 'bulk_delete', path='delete', detail=False)
class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitGroup.objects.all()
filterset = filtersets.CircuitGroupFilterSet
@@ -501,6 +533,7 @@ class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
# Circuit Groups
#
@register_model_view(CircuitGroupAssignment, 'list', path='', detail=False)
class CircuitGroupAssignmentListView(generic.ObjectListView):
queryset = CircuitGroupAssignment.objects.all()
filterset = filtersets.CircuitGroupAssignmentFilterSet
@@ -513,6 +546,7 @@ class CircuitGroupAssignmentView(generic.ObjectView):
queryset = CircuitGroupAssignment.objects.all()
@register_model_view(CircuitGroupAssignment, 'add', detail=False)
@register_model_view(CircuitGroupAssignment, 'edit')
class CircuitGroupAssignmentEditView(generic.ObjectEditView):
queryset = CircuitGroupAssignment.objects.all()
@@ -524,11 +558,13 @@ class CircuitGroupAssignmentDeleteView(generic.ObjectDeleteView):
queryset = CircuitGroupAssignment.objects.all()
@register_model_view(CircuitGroupAssignment, 'bulk_import', detail=False)
class CircuitGroupAssignmentBulkImportView(generic.BulkImportView):
queryset = CircuitGroupAssignment.objects.all()
model_form = forms.CircuitGroupAssignmentImportForm
@register_model_view(CircuitGroupAssignment, 'bulk_edit', path='edit', detail=False)
class CircuitGroupAssignmentBulkEditView(generic.BulkEditView):
queryset = CircuitGroupAssignment.objects.all()
filterset = filtersets.CircuitGroupAssignmentFilterSet
@@ -536,7 +572,175 @@ class CircuitGroupAssignmentBulkEditView(generic.BulkEditView):
form = forms.CircuitGroupAssignmentBulkEditForm
@register_model_view(CircuitGroupAssignment, 'bulk_delete', path='delete', detail=False)
class CircuitGroupAssignmentBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitGroupAssignment.objects.all()
filterset = filtersets.CircuitGroupAssignmentFilterSet
table = tables.CircuitGroupAssignmentTable
#
# Virtual circuit Types
#
@register_model_view(VirtualCircuitType, 'list', path='', detail=False)
class VirtualCircuitTypeListView(generic.ObjectListView):
queryset = VirtualCircuitType.objects.annotate(
virtual_circuit_count=count_related(VirtualCircuit, 'type')
)
filterset = filtersets.VirtualCircuitTypeFilterSet
filterset_form = forms.VirtualCircuitTypeFilterForm
table = tables.VirtualCircuitTypeTable
@register_model_view(VirtualCircuitType)
class VirtualCircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VirtualCircuitType.objects.all()
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
}
@register_model_view(VirtualCircuitType, 'add', detail=False)
@register_model_view(VirtualCircuitType, 'edit')
class VirtualCircuitTypeEditView(generic.ObjectEditView):
queryset = VirtualCircuitType.objects.all()
form = forms.VirtualCircuitTypeForm
@register_model_view(VirtualCircuitType, 'delete')
class VirtualCircuitTypeDeleteView(generic.ObjectDeleteView):
queryset = VirtualCircuitType.objects.all()
@register_model_view(VirtualCircuitType, 'bulk_import', detail=False)
class VirtualCircuitTypeBulkImportView(generic.BulkImportView):
queryset = VirtualCircuitType.objects.all()
model_form = forms.VirtualCircuitTypeImportForm
@register_model_view(VirtualCircuitType, 'bulk_edit', path='edit', detail=False)
class VirtualCircuitTypeBulkEditView(generic.BulkEditView):
queryset = VirtualCircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
)
filterset = filtersets.VirtualCircuitTypeFilterSet
table = tables.VirtualCircuitTypeTable
form = forms.VirtualCircuitTypeBulkEditForm
@register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualCircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
)
filterset = filtersets.VirtualCircuitTypeFilterSet
table = tables.VirtualCircuitTypeTable
#
# Virtual circuits
#
class VirtualCircuitListView(generic.ObjectListView):
queryset = VirtualCircuit.objects.annotate(
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
)
filterset = filtersets.VirtualCircuitFilterSet
filterset_form = forms.VirtualCircuitFilterForm
table = tables.VirtualCircuitTable
@register_model_view(VirtualCircuit)
class VirtualCircuitView(generic.ObjectView):
queryset = VirtualCircuit.objects.all()
@register_model_view(VirtualCircuit, 'edit')
class VirtualCircuitEditView(generic.ObjectEditView):
queryset = VirtualCircuit.objects.all()
form = forms.VirtualCircuitForm
@register_model_view(VirtualCircuit, 'delete')
class VirtualCircuitDeleteView(generic.ObjectDeleteView):
queryset = VirtualCircuit.objects.all()
class VirtualCircuitBulkImportView(generic.BulkImportView):
queryset = VirtualCircuit.objects.all()
model_form = forms.VirtualCircuitImportForm
additional_permissions = [
'circuits.add_virtualcircuittermination',
]
related_object_forms = {
'terminations': forms.VirtualCircuitTerminationImportRelatedForm,
}
def prep_related_object_data(self, parent, data):
data.update({'virtual_circuit': parent})
return data
class VirtualCircuitBulkEditView(generic.BulkEditView):
queryset = VirtualCircuit.objects.annotate(
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
)
filterset = filtersets.VirtualCircuitFilterSet
table = tables.VirtualCircuitTable
form = forms.VirtualCircuitBulkEditForm
class VirtualCircuitBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualCircuit.objects.annotate(
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
)
filterset = filtersets.VirtualCircuitFilterSet
table = tables.VirtualCircuitTable
#
# Virtual circuit terminations
#
class VirtualCircuitTerminationListView(generic.ObjectListView):
queryset = VirtualCircuitTermination.objects.all()
filterset = filtersets.VirtualCircuitTerminationFilterSet
filterset_form = forms.VirtualCircuitTerminationFilterForm
table = tables.VirtualCircuitTerminationTable
@register_model_view(VirtualCircuitTermination)
class VirtualCircuitTerminationView(generic.ObjectView):
queryset = VirtualCircuitTermination.objects.all()
@register_model_view(VirtualCircuitTermination, 'edit')
class VirtualCircuitTerminationEditView(generic.ObjectEditView):
queryset = VirtualCircuitTermination.objects.all()
form = forms.VirtualCircuitTerminationForm
@register_model_view(VirtualCircuitTermination, 'delete')
class VirtualCircuitTerminationDeleteView(generic.ObjectDeleteView):
queryset = VirtualCircuitTermination.objects.all()
class VirtualCircuitTerminationBulkImportView(generic.BulkImportView):
queryset = VirtualCircuitTermination.objects.all()
model_form = forms.VirtualCircuitTerminationImportForm
class VirtualCircuitTerminationBulkEditView(generic.BulkEditView):
queryset = VirtualCircuitTermination.objects.all()
filterset = filtersets.VirtualCircuitTerminationFilterSet
table = tables.VirtualCircuitTerminationTable
form = forms.VirtualCircuitTerminationBulkEditForm
class VirtualCircuitTerminationBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualCircuitTermination.objects.all()
filterset = filtersets.VirtualCircuitTerminationFilterSet
table = tables.VirtualCircuitTerminationTable

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