Compare commits

...

122 Commits

Author SHA1 Message Date
jeremystretch
f18f6e2856 Merge branch 'develop' into feature 2023-03-30 15:56:38 -04:00
jeremystretch
99b914689a Release v3.5-beta1 2023-03-30 15:55:12 -04:00
jeremystretch
b0f6cea4f3 Documentation cleanup 2023-03-30 15:45:32 -04:00
jeremystretch
bfd8e012dc Note breaking changes re: API schema 2023-03-30 14:36:02 -04:00
Arthur Hanson
ecd0c56554 Closes #9608: Move from drf-yasg to spectacular
Co-authored-by: arthanson <worldnomad@gmail.com>
Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-03-30 14:32:59 -04:00
jeremystretch
1be626e5ee Record OpenAPI spec for reference 2023-03-30 13:34:08 -04:00
jeremystretch
f36dfe3a11 Update changelog 2023-03-30 13:21:03 -04:00
jeremystretch
90527b799d #9416: Add view to reset user's dashboard 2023-03-30 12:46:06 -04:00
jeremystretch
6e6e8fa2d9 Set user agent for RSS feed requests 2023-03-30 12:44:36 -04:00
jeremystretch
424b336536 Fixes #12061: Improve handling of insufficient permissions for widget content 2023-03-30 10:03:41 -04:00
jeremystretch
06dec6a2d9 Fixes #12046: Fix URL parsing for git data sources 2023-03-30 09:07:56 -04:00
jeremystretch
f965608791 Fixes #11660: Catch RQ timeout exception when syncing data sources 2023-03-30 08:51:47 -04:00
jeremystretch
c4891fe105 Closes #12085: Add a file source view for reports 2023-03-29 16:58:35 -04:00
Jeremy Stretch
715592547c #12081: Script & report cleanup (#12091)
* start() and terminate() methods on Job should call save()

* Fix display of associated jobs

* Introduce get_latest_jobs() method on JobsMixin

* Update messaging when no reports/scripts exist

* Catch ImportErrors when rendering report/script lists

* Fix loading of nested modules

* Fix URLs for nested scripts/reports
2023-03-29 16:51:55 -04:00
jeremystretch
177668dca5 Update changelog for #9047 2023-03-29 08:31:40 -04:00
Daniel Sheppard
9d709c84e7 Closes: #9047 - Add Provider Accounts (#12057)
* #9047 - ProviderAccount

* #9047 - Move to new selector types

* #9047 - Re-introduce provider FK to Circuit model

* #9047 - Fix broken tests

* Misc cleanup

* Revert errant change

* Fix tests

* Update circuit filter form

---------

Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-03-29 08:27:11 -04:00
Jeremy Stretch
d2a694a878 Closes #12068: Establish a direct relationship from jobs to objects (#12075)
* Reference database object by GFK when running scripts & reports via UI

* Reference database object by GFK when running scripts & reports via API

* Remove old enqueue_job() method

* Enable filtering jobs by object

* Introduce ObjectJobsView

* Add tabbed views for report & script jobs

* Add object_id to JobSerializer

* Move generic relation to JobsMixin

* Clean up old naming
2023-03-28 15:47:09 -04:00
jeremystretch
15590f1f48 Merge branch 'develop' into feature 2023-03-28 14:19:08 -04:00
jeremystretch
61e2073775 #12067: Fix ordering for JobResult replication 2023-03-27 14:25:42 -04:00
jeremystretch
82080ef491 Remove the old JobResult model 2023-03-27 14:20:13 -04:00
jeremystretch
b3d2020045 Replicate JobResults to new Job model 2023-03-27 14:20:13 -04:00
jeremystretch
34236ff468 Update release notes 2023-03-27 14:20:13 -04:00
jeremystretch
7b258dc11f Rename JOBRESULT_RETENTION to JOB_RETENTION 2023-03-27 14:20:13 -04:00
jeremystretch
40572b543f Rename JobResult to Job and move to core 2023-03-27 14:20:13 -04:00
jeremystretch
669cfe8952 Closes #12062: Avoid caching invalid RSS feed content 2023-03-27 09:29:51 -04:00
jeremystretch
37aa07eea1 Clean up navigation menu 2023-03-24 22:01:41 -04:00
jeremystretch
ea1801bad3 Refactor extras.models 2023-03-24 21:25:22 -04:00
jeremystretch
5c0ce96b6f Reorganize management commands 2023-03-24 21:11:10 -04:00
Jeremy Stretch
f7a2eb8aef Closes #11890: Sync/upload reports & scripts (#12059)
* Initial work on #11890

* Consolidate get_scripts() and get_reports() functions

* Introduce proxy models for script & report modules

* Add add/delete views for reports & scripts

* Add deletion links for modules

* Enable resolving scripts/reports from module class

* Remove get_modules() utility function

* Show results in report/script lists

* Misc cleanup

* Fix file uploads

* Support automatic migration for submodules

* Fix module child ordering

* Template cleanup

* Remove ManagedFile views

* Move is_script(), is_report() into extras.utils

* Fix URLs for nested reports & scripts

* Misc cleanup
2023-03-24 21:00:36 -04:00
jeremystretch
9c5f4163af Update release notes 2023-03-23 08:44:42 -04:00
Arthur Hanson
c57d71a9db 11291 optimize GraphQL queries (#11943)
* 11291 initial optimize graphql queries

* 11291 add optimizer to schemas

* 11291 cleanup fields.py

* 11291 fix fragment query
2023-03-23 08:42:18 -04:00
jeremystretch
e176c7d906 #9416: cleanup & widget improvements 2023-03-22 15:44:01 -04:00
jeremystretch
a098c3b0c1 #11558: Introduce GIT_PATH configuration setting 2023-03-22 11:25:03 -04:00
jeremystretch
2fc79af4c7 #11558: Cleanup & docs 2023-03-22 09:20:44 -04:00
jeremystretch
00088cba6d #11559: Add device config API endpoint & cleanup 2023-03-21 17:00:06 -04:00
jeremystretch
d6afc125e5 #11693: Clean up ExportTemplate model form 2023-03-21 14:15:37 -04:00
jeremystretch
c35e7b8ee5 #11584: Add bulk edit view & tests 2023-03-21 13:47:08 -04:00
jeremystretch
1e493f6f28 #10729: Cleanup 2023-03-21 09:36:21 -04:00
jeremystretch
402d5d955f Documentation cleanup 2023-03-20 16:37:47 -04:00
jeremystretch
64f6eea579 #10054: Trigger change event after updating form field value 2023-03-20 16:06:06 -04:00
jeremystretch
9ce29f9edc #9653: Cleanup 2023-03-20 16:00:08 -04:00
jeremystretch
ecb8f1599d #11558: Set data_synced time under model's clean() 2023-03-20 15:45:48 -04:00
jeremystretch
2b3b9517d2 #9073: Fix form behavior when disassociating a ConfigContext from a DataFile 2023-03-20 15:42:23 -04:00
jeremystretch
08bdb54cb4 #11558: Disable sync button if RQ worker not running 2023-03-20 15:12:11 -04:00
jeremystretch
13d604d44e Fix rendering of form textareas 2023-03-20 13:18:09 -04:00
jeremystretch
9219397208 #8958: Declare constants for event types 2023-03-20 13:04:05 -04:00
jeremystretch
6e93c3574c #8749: Cleanup 2023-03-20 12:42:26 -04:00
jeremystretch
0455654f71 #8272: Cleanup & add filter for bridge_id 2023-03-20 11:40:45 -04:00
jeremystretch
3b9fda0169 Merge branch 'develop' into feature 2023-03-20 08:54:22 -04:00
jeremystretch
0ddd7b0e45 Disable bulk update test for L2VPN terminations 2023-03-17 10:09:57 -04:00
Jeremy Stretch
206d578bc3 Closes #10242: Redirect to filtered objects list after bulk import (#12001)
* Redirect user to filtered objects list after bulk import

* Remove obsolete table attribute from bulk import views
2023-03-17 09:23:40 -04:00
jeremystretch
6e4c4c4342 Closes #11494: Enable filtering objects by create/update request IDs 2023-03-16 16:29:43 -04:00
Arthur
5b81986bb3 11955 remove csvdatafield csvfilefield 2023-03-16 15:17:37 -04:00
jeremystretch
43bba935c3 Remove unused template 2023-03-15 14:08:56 -04:00
jeremystretch
da7293524e #10054: Simplify cable forms 2023-03-15 14:01:01 -04:00
Jeremy Stretch
cacc418cd5 Closes: #11781: Add support for Amazon S3 remote data sources (#11986)
* Add boto3 as a dependency

* Add Amazon S3 backend for remote data sources

* Update docs to include Amazon S3 support
2023-03-15 12:11:52 -04:00
jeremystretch
5cd3ad0b12 Cleanup & docs 2023-03-14 15:44:16 -04:00
Arthur
f7150645a1 8749 clone custom fields 2023-03-14 15:44:16 -04:00
jeremystretch
ef7c66de34 Fix CSV import tests 2023-03-14 15:31:26 -04:00
jeremystretch
1446b07f8c Closes #11780: Enable loading import data from remote sources 2023-03-14 14:53:26 -04:00
Jeremy Stretch
8bd0a2ef9d Closes #11826: RSS feed widget (#11976)
* Add feedparser as a dependency

* Introduce RSSFeedWidget

* Clean up widget templates
2023-03-14 11:59:27 -04:00
jeremystretch
af63ac693e Closes #11893: Convert ChangeLogWidget for use with all models 2023-03-14 08:39:18 -04:00
jeremystretch
6a696d9ed7 Closes #11968: Add navigation menu buttons to create device & VM components 2023-03-13 15:15:38 -04:00
Jeremy Stretch
d1f76bec37 Closes #10054: Implement advanced UI controls for object selection (#11952)
* WIP

* WIP

* WIP

* Make object selector functional

* Replace extraneous form fields with selector widgets

* Avoid overlap with filterset field names

* Show checkmarks next to visibile filters

* Update results automatically when searching

* Include selector for device/VM component parent fields

* Use selector for filtering VLAN group/site

* Limit selector to 100 results
2023-03-13 12:44:26 -04:00
jeremystretch
2a9178af12 Merge branch 'develop' into feature 2023-03-13 11:58:37 -04:00
jeremystretch
a717ddf5e6 Changelog for #8272, #8958 2023-03-10 09:00:28 -05:00
jeremystretch
e015da9e4c Undo change to default status for object type_* fields on Webhook 2023-03-07 18:01:32 -05:00
jeremystretch
4de64d783e Add trigger_webhooks() to JobResult 2023-03-07 18:01:32 -05:00
jeremystretch
a8c331f88a Replace JobResult.set_status() with terminate() 2023-03-07 18:01:32 -05:00
jeremystretch
697feed257 Add type_job_start & type_job_end to Webhook 2023-03-07 18:01:32 -05:00
kkthxbye-code
3260ae76f1 Move update_interface_bridges to a utility function 2023-03-07 18:00:24 -05:00
kkthxbye-code
a74ae46f86 Add bridge to InterfaceTemplate 2023-03-07 18:00:24 -05:00
jeremystretch
c44eb65993 Closes #11325: Move help_texts from model forms to models 2023-03-01 17:31:54 -05:00
jeremystretch
536b46158a Add mark_utilized to IPRange 2023-03-01 09:27:06 -05:00
jeremystretch
8a08d3621b Closes #10374: Require unique tenant names & slugs per group (not globally) 2023-02-28 20:03:41 -05:00
Jeremy Stretch
5517963b24 Closes #10729: Add date & time custom field type (#11857)
* Add datetime custom field type

* Update custom field tests
2023-02-28 13:33:40 -05:00
Jeremy Stretch
7994073687 Closes #8550: Implement ASN ranges (#11835)
* Move ASN to a separate module

* Move ASNField from dcim to ipam

* Introduce ASNRange model

* Add relationship from ASN to ASNRange

* Add an available-asns API endpoint

* Add RIR assignment for ASNRange

* Add standard tests

* Move child ASNs to a tabbed view

* Remove FK on ASN to ASNRange

* Add tests for provisioning available ASNs

* Add docs for ASNRange
2023-02-27 16:36:05 -05:00
jeremystretch
e4e4d0c0ec Fixes #11753: Avoid re-initializing SlimSelects 2023-02-25 12:09:06 -05:00
jeremystretch
64291f731b Update change log 2023-02-24 16:46:01 -05:00
Jeremy Stretch
084a2cc52c Closes #9416: Dashboard widgets (#11823)
* Replace masonry with gridstack

* Initial work on dashboard widgets

* Implement function to save dashboard layout

* Define a default dashboard

* Clean up widgets

* Implement widget configuration views & forms

* Permit merging dict value with existing dict in user config

* Add widget deletion view

* Enable HTMX for widget configuration

* Implement view to add dashboard widgets

* ObjectCountsWidget: Identify models by app_label & name

* Add color customization to dashboard widgets

* Introduce Dashboard model to store user dashboard layout & config

* Clean up utility functions

* Remove hard-coded API URL

* Use fixed grid cell height

* Add modal close button

* Clean up dashboard views

* Rebuild JS
2023-02-24 16:04:00 -05:00
Arthur Hanson
36771e821c 10520 remove Napalm code references (#11768)
* 10520 remove all Napalm code references

* 10520 remove lldp

* 10520 remove config, status - rebuild js

* 10520 re-add config parameters

* 10520 re-add serializer

* 10520 update docs
2023-02-24 15:38:50 -05:00
jeremystretch
927371b908 Adjust inspector to accommodate non-detail views 2023-02-24 13:54:39 -05:00
jeremystretch
574b5551a0 Clean up model & registry documentation 2023-02-19 20:09:52 -05:00
jeremystretch
c109daf1d8 Clean up the application registry 2023-02-19 20:09:52 -05:00
jeremystretch
c84f0de8f8 #11625: Employ HTMX form rendering for device & VM interfaces 2023-02-19 20:09:52 -05:00
jeremystretch
368e774ceb Add index for (source, path) to DataFile 2023-02-19 20:09:52 -05:00
Jeremy Stretch
73a7a2d27a Closes #11559: Implement config template rendering (#11769)
* WIP

* Add config_template field to Device

* Pre-fetch referenced templates

* Correct up_to_date callable

* Add config_template FK to Device

* Update & merge migrations

* Add config_template FK to Platform

* Add tagging support for ConfigTemplate

* Catch exceptions when rendering device templates in UI

* Refactor ConfigTemplate.render()

* Add support for returning plain text content

* Add ConfigTemplate model documentation

* Add feature documentation for config rendering
2023-02-19 20:09:52 -05:00
jeremystretch
db4e00d394 #11765: Avoid setting netbox-static-select on SelectMultiple with size 2023-02-19 20:09:52 -05:00
Jeremy Stretch
b9bd96f0c7 Closes #11765: Remove StaticSelect & StaticSelectMultiple (#11767)
* Remove StaticSelect, StaticSelectMultiple form widgets

* Tag custom ChoiceField, MultipleChoiceField classes for removal in v3.6
2023-02-19 20:09:51 -05:00
kkthxbye-code
c73829fe92 Fix issues with the ContactAssignmentListView 2023-02-19 20:09:51 -05:00
kkthxbye-code
81b8046d1d Fixes #9653 - Add default_platform to DeviceType 2023-02-19 20:09:51 -05:00
jeremystretch
a1c9f7a2c6 DataFile should not inherit from ChangeLoggingMixin 2023-02-19 20:09:51 -05:00
jeremystretch
62509c20da Check for change records only if objects being deleted support change logging 2023-02-19 20:09:51 -05:00
jeremystretch
96a79c2126 Closes #11737: ChangeLoggedModel should inherit WebhooksMixin 2023-02-19 20:09:51 -05:00
kkthxbye-code
8d68b6a2e6 Fixes #11694 - Remove obsolete SmallTextarea widget 2023-02-19 20:09:51 -05:00
jeremystretch
c8faca01f1 Changelog for #11693 2023-02-19 20:09:51 -05:00
jeremystretch
b267cbae36 Merge migrations 2023-02-19 20:09:51 -05:00
jeremystretch
ac87ce733d Closes #11693: Enable remote data synchronization for export templates 2023-02-19 20:09:51 -05:00
Jeremy Stretch
678a7d17df Closes #9073: Remote data support for config contexts (#11692)
* WIP

* Add bulk sync view for config contexts

* Introduce 'sync' permission for synced data models

* Docs & cleanup

* Remove unused method

* Add a REST API endpoint to synchronize config context data
2023-02-19 20:09:51 -05:00
jeremystretch
664132281e Fixes #11659: Include all relevant DataFile attributes during bulk update 2023-02-19 20:09:51 -05:00
jeremystretch
0be633d624 #11558: Fix URL display under data source view 2023-02-19 20:09:51 -05:00
Jeremy Stretch
d8784d4155 Closes #11558: Add support for remote data sources (#11646)
* WIP

* WIP

* Add git sync

* Fix file hashing

* Add last_synced to DataSource

* Build out UI & API resources

* Add status field to DataSource

* Add UI control to sync data source

* Add API endpoint to sync data sources

* Fix display of DataSource job results

* DataSource password should be write-only

* General cleanup

* Add data file UI view

* Punt on HTTP, FTP support for now

* Add DataSource URL validation

* Add HTTP proxy support to git fetcher

* Add management command to sync data sources

* DataFile REST API endpoints should be read-only

* Refactor fetch methods into backend classes

* Replace auth & git branch fields with general-purpose parameters

* Fix last_synced time

* Render discrete form fields for backend parameters

* Enable dynamic edit form for DataSource

* Register DataBackend classes in application registry

* Add search indexers for DataSource, DataFile

* Add single & bulk delete views for DataFile

* Add model documentation

* Convert DataSource to a primary model

* Introduce pre_sync & post_sync signals

* Clean up migrations

* Rename url to source_url

* Clean up filtersets

* Add API & filterset tests

* Add view tests

* Add initSelect() to HTMX refresh handler

* Render DataSourceForm fieldsets dynamically

* Update compiled static resources
2023-02-19 20:09:51 -05:00
jeremystretch
e65b2a9fb3 Closes #11625: Add HTMX support to ObjectEditView 2023-02-19 20:09:51 -05:00
jeremystretch
7accdd52d8 Closes #11611: Refactor API viewset classes and introduce NetBoxReadOnlyModelViewSet 2023-02-19 20:09:51 -05:00
jeremystretch
2669068429 #11517: Standardize display of contact assignments 2023-02-19 20:09:51 -05:00
jeremystretch
157bf89e89 Closes #11584: Add a list view for contact assignments 2023-02-19 20:09:51 -05:00
jeremystretch
6e264562ee Use embedded tables for importing/export VRFs & L2VPNs under route target view 2023-02-19 20:09:51 -05:00
jeremystretch
2525eefefd Move rack reservations panel to separate tab 2023-02-19 20:09:51 -05:00
jeremystretch
0f6995e92a Use embedded table to show assigned services under object view 2023-02-19 20:09:51 -05:00
jeremystretch
8f7c100e22 Standard related object links across all models 2023-02-19 20:09:51 -05:00
jeremystretch
48e5b395b2 Standardize linking to related objects in tables 2023-02-19 20:09:51 -05:00
jeremystretch
94797bb956 Standardize related model display for organizational models 2023-02-19 20:09:51 -05:00
jeremystretch
91b81d51da Standardize related model display for nested models 2023-02-19 20:09:51 -05:00
jeremystretch
0c9e7aa074 Clean up related objects for sites, tenants 2023-02-19 20:09:51 -05:00
jeremystretch
48d6d7279d Changelog for #11440 2023-02-19 20:09:51 -05:00
kkthxbye
8e94eb67d2 Add the enabled filed to InterfaceTemplate 2023-02-19 20:09:51 -05:00
Jeremy Stretch
1a2dae3471 Closes #8184: Enable HTMX for embedded tables (#11518)
* Enable HTMX rendering for embedded tables

* Start converting embedded tables to use HTMX (WIP)

* Additional table conversions (WIP)

* Standardize HTMX usage for nested group models

* Enable HTMX for additional emebedded tables

* Fix HTMX table rendering for ObjectChildrenView

* Standardize usage of inc/panel_table.html

* Hide selection boxes in embedded tables
2023-02-19 20:09:51 -05:00
jeremystretch
f74a2536f1 Closes #11254: Introduce the X-Request-ID HTTP header to annotate the unique ID of each request for change logging 2023-02-19 20:09:51 -05:00
jeremystretch
ef3ac25406 Remove old feature version notices 2023-02-19 20:09:51 -05:00
jeremystretch
0b4ea14e9a Closes #11489: Refactor & combine core middleware 2023-02-19 20:09:51 -05:00
jeremystretch
2381317eb3 Closes #10604: Remove unused extra_tabs block from object.html generic template 2023-02-19 20:09:51 -05:00
jeremystretch
e19ce69238 Closes #10923: Remove unused NetBoxModelCSVForm class 2023-02-19 20:09:51 -05:00
518 changed files with 178407 additions and 7886 deletions

View File

@@ -2,6 +2,10 @@
# https://github.com/mozilla/bleach
bleach<6.0
# Python client for Amazon AWS API
# https://github.com/boto/boto3
boto3
# The Python web framework on which NetBox is built
# https://github.com/django/django
Django<4.2
@@ -62,9 +66,13 @@ django-timezone-field
# https://github.com/encode/django-rest-framework
djangorestframework
# Swagger/OpenAPI schema generation for REST APIs
# https://github.com/axnsan12/drf-yasg
drf-yasg[validation]
# Sane and flexible OpenAPI 3 schema generation for Django REST framework.
# https://github.com/tfranzel/drf-spectacular
drf-spectacular
# RSS feed parser
# https://github.com/kurtmckee/feedparser
feedparser
# Django wrapper for Graphene (GraphQL support)
# https://github.com/graphql-python/graphene-django

95605
contrib/openapi2.json Normal file

File diff suppressed because it is too large Load Diff

69695
contrib/openapi2.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ NetBox includes a `housekeeping` management command that should be run nightly.
* Clearing expired authentication sessions from the database
* Deleting changelog records older than the configured [retention time](../configuration/miscellaneous.md#changelog_retention)
* Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#jobresult_retention)
* Deleting job result records older than the configured [retention time](../configuration/miscellaneous.md#job_retention)
* Check for new NetBox releases (if [`RELEASE_CHECK_URL`](../configuration/miscellaneous.md#release_check_url) is set)
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.

View File

@@ -58,8 +58,6 @@ Additionally, where multiple permissions have been assigned for an object type,
### User Token
!!! info "This feature was introduced in NetBox v3.3"
When defining a permission constraint, administrators may use the special token `$user` to reference the current user at the time of evaluation. This can be helpful to restrict users to editing only their own journal entries, for example. Such a constraint might be defined as:
```json

View File

@@ -26,7 +26,7 @@ Some configuration parameters are primarily controlled via NetBox's admin interf
* [`DEFAULT_USER_PREFERENCES`](./default-values.md#default_user_preferences)
* [`ENFORCE_GLOBAL_UNIQUE`](./miscellaneous.md#enforce_global_unique)
* [`GRAPHQL_ENABLED`](./miscellaneous.md#graphql_enabled)
* [`JOBRESULT_RETENTION`](./miscellaneous.md#jobresult_retention)
* [`JOB_RETENTION`](./miscellaneous.md#job_retention)
* [`MAINTENANCE_MODE`](./miscellaneous.md#maintenance_mode)
* [`MAPS_URL`](./miscellaneous.md#maps_url)
* [`MAX_PAGE_SIZE`](./miscellaneous.md#max_page_size)

View File

@@ -87,14 +87,16 @@ Setting this to False will disable the GraphQL API.
---
## JOBRESULT_RETENTION
## JOB_RETENTION
!!! tip "Dynamic Configuration Parameter"
!!! note
This parameter was renamed from `JOBRESULT_RETENTION` in NetBox v3.5.
Default: 90
The number of days to retain job results (scripts and reports). Set this to `0` to retain
job results in the database indefinitely.
The number of days to retain job results (scripts and reports). Set this to `0` to retain job results in the database indefinitely.
!!! warning
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.

View File

@@ -1,5 +1,7 @@
# NAPALM Parameters
!!! **Note:** As of NetBox v3.5, NAPALM integration has been moved to a plugin and these configuration parameters are now deprecated.
## NAPALM_USERNAME
## NAPALM_PASSWORD

View File

@@ -73,6 +73,14 @@ Determines if localization features are enabled or not. This should only be enab
---
## GIT_PATH
Default: `git`
The system path to the `git` executable, used by the synchronization backend for remote git repositories.
---
## HTTP_PROXIES
Default: None

View File

@@ -16,6 +16,7 @@ Custom fields may be created by navigating to Customization > Custom Fields. Net
* Decimal: A fixed-precision decimal number (4 decimal places)
* Boolean: True or false
* Date: A date in ISO 8601 format (YYYY-MM-DD)
* Date & time: A date and time in ISO 8601 format (YYYY-MM-DD HH:MM:SS)
* URL: This will be presented as a link in the web UI
* JSON: Arbitrary data stored in JSON format
* Selection: A selection of one of several pre-defined custom choices
@@ -35,18 +36,12 @@ The filter logic controls how values are matched when filtering objects by the c
### Grouping
!!! note
This feature was introduced in NetBox v3.3.
Related custom fields can be grouped together within the UI by assigning each the same group name. When at least one custom field for an object type has a group defined, it will appear under the group heading within the custom fields panel under the object view. All custom fields with the same group name will appear under that heading. (Note that the group names must match exactly, or each will appear as a separate heading.)
This parameter has no effect on the API representation of custom field data.
### Visibility
!!! note
This feature was introduced in NetBox v3.3.
When creating a custom field, there are three options for UI visibility. These control how and whether the custom field is displayed within the NetBox UI.
* **Read/write** (default): The custom field is included when viewing and editing objects.

View File

@@ -108,8 +108,6 @@ commit_default = False
Set the maximum allowed runtime for the script. If not set, `RQ_DEFAULT_TIMEOUT` will be used.
!!! info "This feature was introduced in v3.2.1"
## Accessing Request Data
Details of the current HTTP request (the one being made to execute the script) are available as the instance attribute `self.request`. This can be used to infer, for example, the user executing the script and the client IP address:

View File

@@ -95,8 +95,6 @@ A human-friendly description of what your report does.
Set the maximum allowed runtime for the report. If not set, `RQ_DEFAULT_TIMEOUT` will be used.
!!! info "This feature was introduced in v3.2.1"
## Logging
The following methods are available to log results within a report:

View File

@@ -8,6 +8,14 @@ The registry can be inspected by importing `registry` from `extras.registry`.
## Stores
### `data_backends`
A dictionary mapping data backend types to their respective classes. These are used to interact with [remote data sources](../models/core/datasource.md).
### `denormalized_fields`
Stores registration made using `netbox.denormalized.register()`. For each model, a list of related models and their field mappings is maintained to facilitate automatic updates.
### `model_features`
A dictionary of particular features (e.g. custom fields) mapped to the NetBox models which support them, arranged by app. For example:
@@ -20,38 +28,23 @@ A dictionary of particular features (e.g. custom fields) mapped to the NetBox mo
...
},
'webhooks': {
...
'extras': ['configcontext', 'tag', ...],
'dcim': ['site', 'rack', 'devicetype', ...],
},
...
}
```
### `plugin_menu_items`
Supported model features are listed in the [features matrix](./models.md#features-matrix).
Navigation menu items provided by NetBox plugins. Each plugin is registered as a key with the list of menu items it provides. An example:
### `plugins`
```python
{
'Plugin A': (
<MenuItem>, <MenuItem>, <MenuItem>,
),
'Plugin B': (
<MenuItem>, <MenuItem>, <MenuItem>,
),
}
```
This store maintains all registered items for plugins, such as navigation menus, template extensions, etc.
### `plugin_template_extensions`
### `search`
Plugin content that gets embedded into core NetBox templates. The store comprises NetBox models registered as dictionary keys, each pointing to a list of applicable template extension classes that exist. An example:
A dictionary mapping each model (identified by its app and label) to its search index class, if one has been registered for it.
```python
{
'dcim.site': [
<TemplateExtension>, <TemplateExtension>, <TemplateExtension>,
],
'dcim.rack': [
<TemplateExtension>, <TemplateExtension>,
],
}
```
### `views`
A hierarchical mapping of registered views for each model. Mappings are added using the `register_model_view()` decorator, and URLs paths can be generated from these using `get_model_urls()`.

View File

@@ -2,38 +2,44 @@
## Model Types
A NetBox model represents a discrete object type such as a device or IP address. Per [Django convention](https://docs.djangoproject.com/en/stable/topics/db/models/), each model is defined as a Python class and has its own SQL table. All NetBox data models can be categorized by type.
A NetBox model represents a discrete object type such as a device or IP address. Per [Django convention](https://docs.djangoproject.com/en/stable/topics/db/models/), each model is defined as a Python class and has its own table in the PostgreSQL database. All NetBox data models can be categorized by type.
The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/) framework can be used to reference models within the database. A ContentType instance references a model by its `app_label` and `name`: For example, the Site model is referred to as `dcim.site`. The content type combined with an object's primary key form a globally unique identifier for the object (e.g. `dcim.site:123`).
The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/) framework is used to map Django models to database tables. A ContentType instance references a model by its `app_label` and `name`: For example, the Site model within the DCIM app is referred to as `dcim.site`. The content type combined with an object's primary key form a globally unique identifier for the object (e.g. `dcim.site:123`).
### Features Matrix
* [Change logging](../features/change-logging.md) - Changes to these objects are automatically recorded in the change log
* [Webhooks](../integrations/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects
* [Custom fields](../customization/custom-fields.md) - These models support the addition of user-defined fields
* [Export templates](../customization/export-templates.md) - Users can create custom export templates for these models
* [Tagging](../models/extras/tag.md) - The models can be tagged with user-defined tags
* [Journaling](../features/journaling.md) - These models support persistent historical commentary
* Nesting - These models can be nested recursively to create a hierarchy
Depending on its classification, each NetBox model may support various features which enhance its operation. Each feature is enabled by inheriting from its designated mixin class, and some features also make use of the [application registry](./application-registry.md#model_features).
| Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting |
| ------------------ | ---------------- | ---------------- |------------------| ---------------- | ---------------- | ---------------- | ---------------- |
| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | |
| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | :material-check: |
| Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
| Component Template | :material-check: | :material-check: | | | | | |
| Feature | Feature Mixin | Registry Key | Description |
|------------------------------------------------------------|-------------------------|--------------------|--------------------------------------------------------------------------------|
| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | - | Changes to these objects are automatically recorded in the change log |
| Cloning | `CloningMixin` | - | Provides the `clone()` method to prepare a copy |
| [Custom fields](../customization/custom-fields.md) | `CustomFieldsMixin` | `custom_fields` | These models support the addition of user-defined fields |
| [Custom links](../customization/custom-links.md) | `CustomLinksMixin` | `custom_links` | These models support the assignment of custom links |
| [Custom validation](../customization/custom-validation.md) | `CustomValidationMixin` | - | Supports the enforcement of custom validation rules |
| [Export templates](../customization/export-templates.md) | `ExportTemplatesMixin` | `export_templates` | Users can create custom export templates for these models |
| [Job results](../features/background-jobs.md) | `JobsMixin` | `jobs` | Users can create custom export templates for these models |
| [Journaling](../features/journaling.md) | `JournalingMixin` | `journaling` | These models support persistent historical commentary |
| [Synchronized data](../integrations/synchronized-data.md) | `SyncedDataMixin` | `synced_data` | Certain model data can be automatically synchronized from a remote data source |
| [Tagging](../models/extras/tag.md) | `TagsMixin` | `tags` | The models can be tagged with user-defined tags |
| [Webhooks](../integrations/webhooks.md) | `WebhooksMixin` | `webhooks` | NetBox is capable of generating outgoing webhooks for these objects |
## Models Index
### Primary Models
These are considered the "core" application models which are used to model network infrastructure.
* [circuits.Circuit](../models/circuits/circuit.md)
* [circuits.Provider](../models/circuits/provider.md)
* [circuits.ProviderAccount](../models/circuits/provideracount.md)
* [circuits.ProviderNetwork](../models/circuits/providernetwork.md)
* [core.DataSource](../models/core/datasource.md)
* [dcim.Cable](../models/dcim/cable.md)
* [dcim.Device](../models/dcim/device.md)
* [dcim.DeviceType](../models/dcim/devicetype.md)
* [dcim.Module](../models/dcim/module.md)
* [dcim.ModuleType](../models/dcim/moduletype.md)
* [dcim.PowerFeed](../models/dcim/powerfeed.md)
* [dcim.PowerPanel](../models/dcim/powerpanel.md)
* [dcim.Rack](../models/dcim/rack.md)
@@ -47,10 +53,10 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
* [ipam.IPAddress](../models/ipam/ipaddress.md)
* [ipam.IPRange](../models/ipam/iprange.md)
* [ipam.L2VPN](../models/ipam/l2vpn.md)
* [ipam.L2VPNTermination](../models/ipam/l2vpntermination.md)
* [ipam.Prefix](../models/ipam/prefix.md)
* [ipam.RouteTarget](../models/ipam/routetarget.md)
* [ipam.Service](../models/ipam/service.md)
* [ipam.ServiceTemplate](../models/ipam/servicetemplate.md)
* [ipam.VLAN](../models/ipam/vlan.md)
* [ipam.VRF](../models/ipam/vrf.md)
* [tenancy.Contact](../models/tenancy/contact.md)
@@ -62,6 +68,8 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
### Organizational Models
Organization models are used to organize and classify primary models.
* [circuits.CircuitType](../models/circuits/circuittype.md)
* [dcim.DeviceRole](../models/dcim/devicerole.md)
* [dcim.Manufacturer](../models/dcim/manufacturer.md)
@@ -76,6 +84,8 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
### Nested Group Models
Nested group models behave like organizational model, but self-nest within a recursive hierarchy. For example, the Region model can be used to represent a hierarchy of countries, states, and cities.
* [dcim.Location](../models/dcim/location.md) (formerly RackGroup)
* [dcim.Region](../models/dcim/region.md)
* [dcim.SiteGroup](../models/dcim/sitegroup.md)
@@ -85,12 +95,15 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
### Component Models
Component models represent individual physical or virtual components belonging to a device or virtual machine.
* [dcim.ConsolePort](../models/dcim/consoleport.md)
* [dcim.ConsoleServerPort](../models/dcim/consoleserverport.md)
* [dcim.DeviceBay](../models/dcim/devicebay.md)
* [dcim.FrontPort](../models/dcim/frontport.md)
* [dcim.Interface](../models/dcim/interface.md)
* [dcim.InventoryItem](../models/dcim/inventoryitem.md)
* [dcim.ModuleBay](../models/dcim/modulebay.md)
* [dcim.PowerOutlet](../models/dcim/poweroutlet.md)
* [dcim.PowerPort](../models/dcim/powerport.md)
* [dcim.RearPort](../models/dcim/rearport.md)
@@ -98,11 +111,15 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
### Component Template Models
These function as templates to effect the replication of device and virtual machine components. Component template models support a limited feature set, including change logging, custom validation, and webhooks.
* [dcim.ConsolePortTemplate](../models/dcim/consoleporttemplate.md)
* [dcim.ConsoleServerPortTemplate](../models/dcim/consoleserverporttemplate.md)
* [dcim.DeviceBayTemplate](../models/dcim/devicebaytemplate.md)
* [dcim.FrontPortTemplate](../models/dcim/frontporttemplate.md)
* [dcim.InterfaceTemplate](../models/dcim/interfacetemplate.md)
* [dcim.InventoryItemTemplate](../models/dcim/inventoryitemtemplate.md)
* [dcim.ModuleBayTemplate](../models/dcim/modulebaytemplate.md)
* [dcim.PowerOutletTemplate](../models/dcim/poweroutlettemplate.md)
* [dcim.PowerPortTemplate](../models/dcim/powerporttemplate.md)
* [dcim.RearPortTemplate](../models/dcim/rearporttemplate.md)

View File

@@ -29,7 +29,7 @@ A SearchIndex subclass defines both its model and a list of two-tuples specifyin
| 60 | Unique serialized attribute (per related object) | Device.serial |
| 100 | Primary human identifier | Device.name, Circuit.cid, Cable.label |
| 110 | Slug | Site.slug |
| 200 | Secondary identifier | Provider.account, DeviceType.part_number |
| 200 | Secondary identifier | ProviderAccount.account, DeviceType.part_number |
| 300 | Highly unique descriptive attribute | CircuitTermination.xconnect_id, IPAddress.dns_name |
| 500 | Description | Site.description |
| 1000 | Custom field default | - |

View File

@@ -36,6 +36,8 @@ To learn more about this feature, check out the [webhooks documentation](../inte
To learn more about this feature, check out the [NAPALM documentation](../integrations/napalm.md).
As of NetBox v3.5, NAPALM integration has been moved to a plugin. Please see the [netbox_napalm_plugin](https://github.com/netbox-community/netbox-napalm) for installation instructions.
## Prometheus Metrics
NetBox includes a special `/metrics` view which exposes metrics for a [Prometheus](https://prometheus.io/) scraper, powered by the open source [django-prometheus](https://github.com/korfuri/django-prometheus) library. To learn more about this feature, check out the [Prometheus metrics documentation](../integrations/prometheus-metrics.md).

View File

@@ -0,0 +1,13 @@
# Background Jobs
NetBox includes the ability to execute certain functions as background tasks. These include:
* [Report](../customization/reports.md) execution
* [Custom script](../customization/custom-scripts.md) execution
* Synchronization of [remote data sources](../integrations/synchronized-data.md)
Additionally, NetBox plugins can enqueue their own background tasks. This is accomplished using the [Job model](../models/core/job.md). Background tasks are executed by the `rqworker` process(es).
## Scheduled Jobs
Background jobs can be configured to run immediately, or at a set time in the future. Scheduled jobs can also be configured to repeat at a set interval.

View File

@@ -1,9 +1,13 @@
# Change Logging
Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object taken both before and after the change is saved to the database, along with meta data including the current time and the user associated with the change. These records form a persistent record of changes both for each individual object as well as NetBox as a whole. The global change log can be viewed by navigating to Other > Change Log.
Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object taken both before and after the change is saved to the database, along with metadata including the current time and the user associated with the change. These records form a persistent record of changes both for each individual object as well as NetBox as a whole. The global change log can be viewed by navigating to Other > Change Log.
A serialized representation of the instance being modified is included in JSON format. This is similar to how objects are conveyed within the REST API, but does not include any nested representations. For instance, the `tenant` field of a site will record only the tenant's ID, not a representation of the tenant.
When a request is made, a UUID is generated and attached to any change records resulting from that request. For example, editing three objects in bulk will create a separate change record for each (three in total), and each of those objects will be associated with the same UUID. This makes it easy to identify all the change records resulting from a particular request.
Change records are exposed in the API via the read-only endpoint `/api/extras/object-changes/`. They may also be exported via the web UI in CSV format.
## Correlating Changes by Request
Every request made to NetBox is assigned a random unique ID that can be used to correlate change records. For example, if you change the status of three sites using the UI's bulk edit feature, you will see three new change records (one for each site) all referencing the same request ID. This shows that all three changes were made as part of the same request.

View File

@@ -5,13 +5,15 @@ NetBox is ideal for managing your network's transit and peering providers and ci
```mermaid
flowchart TD
ASN --> Provider
Provider --> ProviderNetwork & Circuit
Provider --> ProviderNetwork & ProviderAccount & Circuit
ProviderAccount --> Circuit
CircuitType --> Circuit
click ASN "../../models/circuits/asn/"
click Circuit "../../models/circuits/circuit/"
click CircuitType "../../models/circuits/circuittype/"
click Provider "../../models/circuits/provider/"
click ProviderAccount "../../models/circuits/provideraccount/"
click ProviderNetwork "../../models/circuits/providernetwork/"
```
@@ -25,7 +27,7 @@ Sometimes you'll need to model provider networks into which you don't have full
A circuit is a physical connection between two points, which is installed and maintained by an external provider. For example, an Internet connection delivered as a fiber optic cable would be modeled as a circuit in NetBox.
Each circuit is associated with a provider and assigned a circuit ID, which must be unique to that provider. A circuit is also assigned a user-defined type, operational status, and various other operating characteristics.
Each circuit is associated with a provider and assigned a circuit ID, which must be unique to that provider. A circuit is also assigned a user-defined type, operational status, and various other operating characteristics. Provider accounts can also be employed to further categorize circuits belonging to a common provider: These may represent different business units or technologies.
Each circuit may have up to two terminations (A and Z) defined. Each termination can be associated with a particular site or provider network. In the case of the former, a cable can be connected between the circuit termination and a device component to map its physical connectivity.

View File

@@ -0,0 +1,79 @@
# Configuration Rendering
One of the critical aspects of operating a network is ensuring that every network node is configured correctly. By leveraging configuration templates and [context data](./context-data.md), NetBox can render complete configuration files for each device on your network.
```mermaid
flowchart TD
ConfigContext & ConfigTemplate --> Config{{Rendered configuration}}
click ConfigContext "../../models/extras/configcontext/"
click ConfigTemplate "../../models/extras/configtemplate/"
```
## Configuration Templates
Configuration templates are written in the [Jinja2 templating language](https://jinja.palletsprojects.com/), and may be automatically populated from remote data sources. Context data is applied to a template during rendering to output a complete configuration file. Below is an example Jinja2 template which renders a simple network switch configuration file.
```jinja2
{% extends 'base.j2' %}
{% block content %}
system {
host-name {{ device.name }};
domain-name example.com;
time-zone UTC;
authentication-order [ password radius ];
ntp {
{% for server in ntp_servers %}
server {{ server }};
{% endfor %}
}
}
{% for interface in device.interfaces.all() %}
{% include 'common/interface.j2' %}
{% endfor %}
{% endblock %}
```
When rendered for a specific NetBox device, the template's `device` variable will be populated with the device instance, and `ntp_servers` will be pulled from the device's available context data. The resulting output will be a valid configuration segment that can be applied directly to a compatible network device.
## Rendering Templates
### Device Configurations
NetBox provides a REST API endpoint specifically for rendering the default configuration template for a specific device. This is accomplished by sending a POST request to the device's unique URL, optionally including additional context data.
```no-highlight
curl -X POST \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
http://netbox:8000/api/dcim/devices/123/render-config/ \
--data '{
"extra_data": "abc123"
}'
```
This request will trigger resolution of the device's preferred config template in the following order:
* The config template assigned to the individual device
* The config template assigned to the device's role
* The config template assigned to the device's platform
If no config template has been assigned to any of these three objects, the request will fail.
### General Purpose Use
NetBox config templates can also be rendered without being tied to any specific device, using a separate general purpose REST API endpoint. Any data included with a POST request to this endpoint will be passed as context data for the template.
```no-highlight
curl -X POST \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
http://netbox:8000/api/extras/config-templates/123/render/ \
--data '{
"foo": "abc",
"bar": 123
}'
```

View File

@@ -31,6 +31,7 @@ The following models support the assignment of contacts:
* circuits.Circuit
* circuits.Provider
* circuits.ProviderAccount
* dcim.Device
* dcim.Location
* dcim.Manufacturer

View File

@@ -11,6 +11,8 @@ Configuration context data (or "config contexts" for short) is a powerful featur
}
```
Context data can be consumed by remote API clients, or it can be employed natively to render [configuration templates](./configuration-rendering.md).
Config contexts can be computed for objects based on the following criteria:
| Type | Devices | Virtual Machines |

View File

@@ -0,0 +1,23 @@
# Synchronized Data
!!! info "This feature was introduced in NetBox v3.5."
Several models in NetBox support the automatic synchronization of local data from a designated remote source. For example, [configuration templates](./configuration-rendering.md) defined in NetBox can source their content from text files stored in a remote git repository. This accomplished using the core [data source](../models/core/datasource.md) and [data file](../models/core/datafile.md) models.
To enable remote data synchronization, the NetBox administrator first designates one or more remote data sources. NetBox currently supports the following source types:
* Git repository
* Amazon S3 bucket (or compatible product)
* Local disk path
(Local disk paths are considered "remote" in this context as they exist outside NetBox's database. These paths could also be mapped to external network shares.)
Each type of remote source has its own configuration parameters. For instance, a git source will ask the user to specify a branch and authentication credentials. Once the source has been created, a synchronization job is run to automatically replicate remote files in the local database.
The following NetBox models can be associated with replicated data files:
* Config contexts
* Config templates
* Export templates
Once a data has been designated for a local instance, its data will be replaced with the content of the replicated file. When the replicated file is updated in the future (via synchronization jobs), the local instance will be flagged as having out-of-date data. A user can then synchronize these objects individually or in bulk to effect the update. This two-stgae process ensures that automated synchronization tasks do not immediately affect production data.

View File

@@ -56,7 +56,7 @@ Below is the (rough) recommended order in which NetBox objects should be created
4. Manufacturers, device types, and module types
5. Platforms and device roles
6. Devices and modules
7. Providers and provider networks
7. Providers, provider accounts, and provider networks
8. Circuit types and circuits
9. Wireless LAN groups and wireless LANs
10. Route targets and VRFs

View File

@@ -28,12 +28,12 @@ This section entails the installation and configuration of a local PostgreSQL da
host all all ::1/128 md5
```
Once PostgreSQL has been installed, start the service and enable it to run at boot:
Once PostgreSQL has been installed, start the service and enable it to run at boot:
```no-highlight
sudo systemctl start postgresql
sudo systemctl enable postgresql
```
```no-highlight
sudo systemctl start postgresql
sudo systemctl enable postgresql
```
Before continuing, verify that you have installed PostgreSQL 11 or later:

View File

@@ -199,14 +199,6 @@ When you have finished modifying the configuration, remember to save the file.
All Python packages required by NetBox are listed in `requirements.txt` and will be installed automatically. NetBox also supports some optional packages. If desired, these packages must be listed in `local_requirements.txt` within the NetBox root directory.
### NAPALM
Integration with the [NAPALM automation](../integrations/napalm.md) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device.
```no-highlight
sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt"
```
### Remote File Storage
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storage_backend) in `configuration.py`.

View File

@@ -1,6 +1,6 @@
# Installation
The installation instructions provided here have been tested to work on Ubuntu 20.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
The installation instructions provided here have been tested to work on Ubuntu 22.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
<iframe width="560" height="315" src="https://www.youtube.com/embed/_y5JRiW_PLM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

View File

@@ -1,74 +1,3 @@
# NAPALM
NetBox supports integration with the [NAPALM automation](https://github.com/napalm-automation/napalm) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally.
The NetBox UI will display tabs for status, LLDP neighbors, and configuration under the device view if the following conditions are met:
* Device status is "Active"
* A primary IP has been assigned to the device
* A platform with a NAPALM driver has been assigned
* The authenticated user has the `dcim.napalm_read_device` permission
!!! note
To enable this integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm) for more information.
Below is an example REST API request and response:
```no-highlight
GET /api/dcim/devices/1/napalm/?method=get_environment
{
"get_environment": {
...
}
}
```
!!! note
To make NAPALM requests via the NetBox REST API, a NetBox user must have assigned a permission granting the `napalm_read` action for the device object type.
## Authentication
By default, the [`NAPALM_USERNAME`](../configuration/napalm.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/napalm.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
```
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
-H "X-NAPALM-Username: foo" \
-H "X-NAPALM-Password: bar"
```
## Method Support
The list of supported NAPALM methods depends on the [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html#general-support-matrix) configured for the platform of a device. Because there is no granular mechanism in place for limiting potentially disruptive requests, NetBox supports only read-only [get](https://napalm.readthedocs.io/en/latest/support/index.html#getters-support-matrix) methods.
## Multiple Methods
It is possible to request the output of multiple NAPALM methods in a single API request by passing multiple `method` parameters. For example:
```no-highlight
GET /api/dcim/devices/1/napalm/?method=get_ntp_servers&method=get_ntp_peers
{
"get_ntp_servers": {
...
},
"get_ntp_peers": {
...
}
}
```
## Optional Arguments
The behavior of NAPALM drivers can be adjusted according to the [optional arguments](https://napalm.readthedocs.io/en/latest/support/index.html#optional-arguments). NetBox exposes those arguments using headers prefixed with `X-NAPALM-`. For example, the SSH port is changed to 2222 in this API call:
```
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
-H "X-NAPALM-port: 2222"
```
As of NetBox v3.5, NAPALM integration has been moved to a plugin. Please see the [netbox_napalm_plugin](https://github.com/netbox-community/netbox-napalm) for installation instructions. **Note:** All previously entered NAPALM configuration data will be saved and automatically imported by the new plugin.

View File

@@ -584,12 +584,8 @@ Additionally, a token can be set to expire at a specific time. This can be usefu
#### Client IP Restriction
!!! note
This feature was introduced in NetBox v3.3.
Each API token can optionally be restricted by client IP address. If one or more allowed IP prefixes/addresses is defined for a token, authentication will fail for any client connecting from an IP address outside the defined range(s). This enables restricting the use a token to a specific client. (By default, any client IP address is permitted.)
### Authenticating to the API
An authentication token is attached to a request by setting the `Authorization` header to the string `Token` followed by a space and the user's token:
@@ -657,3 +653,28 @@ Note that we are _not_ passing an existing REST API token with this request. If
"description": ""
}
```
## HTTP Headers
### `API-Version`
This header specifies the API version in use. This will always match the version of NetBox installed. For example, NetBox v3.4.2 will report an API version of `3.4`.
### `X-Request-ID`
!!! info "This feature was introduced in NetBox v3.5."
This header specifies the unique ID assigned to the received API request. It can be very handy for correlating a request with change records. For example, after creating several new objects, you can filter against the object changes API endpoint to retrieve the resulting change records:
```
GET /api/extras/object-changes/?request_id=e39c84bc-f169-4d5f-bc1c-94487a1b18b5
```
The request ID can also be used to filter many objects directly, to return those created or updated by a certain request:
```
GET /api/dcim/sites/?created_by_request=e39c84bc-f169-4d5f-bc1c-94487a1b18b5
```
!!! note
This header is included with _all_ NetBox responses, although it is most practical when working with an API.

View File

@@ -0,0 +1,9 @@
# Synchronized Data
Some NetBox models support automatic synchronization of certain attributes from remote [data sources](../models/core/datasource.md), such as a git repository hosted on GitHub or GitLab. Data from the authoritative remote source is synchronized locally in NetBox as [data files](../models/core/datafile.md).
The following features support the use of synchronized data:
* [Configuration templates](../features/configuration-rendering.md)
* [Configuration context data](../features/context-data.md)
* [Export templates](../customization/export-templates.md)

View File

@@ -8,6 +8,10 @@ A circuit represents a physical point-to-point data connection, typically used t
The [provider](./provider.md) to which this circuit belongs.
### Provider Account
Circuits may optionally be assigned to a specific [provider account](./provideraccount.md).
### Circuit ID
An identifier for this circuit. This must be unique to the assigned provider. (Circuits assigned to different providers may have the same circuit ID.)

View File

@@ -12,21 +12,10 @@ A unique human-friendly name.
A unique URL-friendly identifier. (This value can be used for filtering.)
### ASN
The AS number assigned to this provider.
!!! warning "Legacy field"
This field is being removed in NetBox v3.4. Users are highly encouraged to use the [ASN model](../ipam/asn.md) to track AS number assignment for providers.
### ASNs
The [AS numbers](../ipam/asn.md) assigned to this provider (optional).
### Account Number
The administrative account identifier tied to this provider for your organization.
### Portal URL
The URL for the provider's customer service portal.

View File

@@ -0,0 +1,17 @@
# Provider Accounts
This model can be used to represent individual accounts associated with a provider.
## Fields
### Provider
The [provider](./provider.md) the account belongs to.
### Name
A human-friendly name, unique to the provider.
### Account Number
The administrative account identifier tied to this provider for your organization.

View File

@@ -0,0 +1,25 @@
# Data Files
A data file object is the representation in NetBox's database of some file belonging to a remote [data source](./datasource.md). Data files are synchronized automatically, and cannot be modified locally (although they can be deleted).
## Fields
### Source
The [data source](./datasource.md) to which this file belongs.
### Path
The path to the file, relative to its source's URL. For example, a file at `/opt/config-data/routing/bgp/peer.yaml` with a source URL of `file:///opt/config-data/` would have its path set to `routing/bgp/peer.yaml`.
### Last Updated
The date and time at which the file most recently updated from its source. Note that this attribute is updated only when the file's contents have been modified. Re-synchronizing the data source will not update this timestamp if the upstream file's data has not changed.
### Size
The file's size, in bytes.
### Hash
A [SHA256 hash](https://en.wikipedia.org/wiki/SHA-2) of the file's data. This can be compared to a hash taken from the original file to determine whether any changes have been made.

View File

@@ -0,0 +1,49 @@
# Data Sources
A data source represents some external repository of data which NetBox can consume, such as a git repository. Files within the data source are synchronized to NetBox by saving them in the database as [data file](./datafile.md) objects.
## Fields
### Name
The data source's human-friendly name.
### Type
The type of data source. Supported options include:
* Local directory
* git repository
* Amazon S3 bucket
### URL
The URL identifying the remote source. Some examples are included below.
| Type | Example URL |
|-----------|----------------------------------------------------|
| Local | file:///path/to/my/data/ |
| git | https://github.com/my-organization/my-repo |
| Amazon S3 | https://s3.us-east-2.amazonaws.com/my-bucket-name/ |
### Status
The source's current synchronization status. Note that this cannot be set manually: It is updated automatically when the source is synchronized.
### Enabled
If false, synchronization will be disabled.
### Ignore Rules
A set of rules (one per line) identifying filenames to ignore during synchronization. Some examples are provided below. See Python's [`fnmatch()` documentation](https://docs.python.org/3/library/fnmatch.html) for a complete reference.
| Rule | Description |
|----------------|------------------------------------------|
| `README` | Ignore any files named `README` |
| `*.txt` | Ignore any files with a `.txt` extension |
| `data???.json` | Ignore e.g. `data123.json` |
### Last Synced
The date and time at which the source was most recently synchronized successfully.

54
docs/models/core/job.md Normal file
View File

@@ -0,0 +1,54 @@
# Jobs
The Job model is used to schedule and record the execution of [background tasks](../../features/background-jobs.md).
## Fields
### Name
The name or other identifier of the NetBox object with which the job is associated.
## Object Type
The type of object (model) associated with this job.
### Created
The date and time at which the job itself was created.
### Scheduled
The date and time at which the job is/was scheduled to execute (if not submitted for immediate execution at the time of creation).
### Interval
The interval (in minutes) at which a scheduled job should re-execute.
### Completed
The date and time at which the job completed (if complete).
### User
The user who created the job.
### Status
The job's current status. Potential values include:
| Value | Description |
|-------|-------------|
| Pending | Awaiting execution by an RQ worker process |
| Scheduled | Scheduled for a future date/time |
| Running | Currently executing |
| Completed | Successfully completed |
| Failed | The job did not complete successfully |
| Errored | An unexpected error was encountered during execution |
### Data
Any data associated with the execution of the job, such as log output.
### Job ID
The job's UUID, used for unique identification within a queue.

View File

@@ -72,6 +72,10 @@ The device's operational status.
A device may be associated with a particular [platform](./platform.md) to indicate its operating system. Note that only platforms assigned to the associated manufacturer (or to no manufacturer) will be available for selection.
### Configuration Template
The [configuration template](../extras/configtemplate.md) from which the configuration for this device can be rendered. If set, this will override any config template referenced by the device's role or platform.
### Primary IPv4 & IPv6 Addresses
Each device may designate one primary IPv4 address and/or one primary IPv6 address for management purposes.

View File

@@ -19,3 +19,7 @@ The color used when displaying the role in the NetBox UI.
### VM Role
If selected, this role may be assigned to [virtual machines](../virtualization/virtualmachine.md)
### Configuration Template
The default [configuration template](../extras/configtemplate.md) for devices assigned to this role.

View File

@@ -21,6 +21,10 @@ The model number assigned to this device type by its manufacturer. Must be uniqu
A unique URL-friendly representation of the model identifier. (This value can be used for filtering.)
### Default Platform
If defined, devices instantiated from this type will automatically inherit the selected platform. (This assignment can be changed after the device has been created.)
### Part Number
An alternative part number to uniquely identify the device type.

View File

@@ -22,6 +22,10 @@ A unique URL-friendly identifier. (This value can be used for filtering.)
If designated, this platform will be available for use only to devices assigned to this [manufacturer](./manufacturer.md). This can be handy e.g. for limiting network operating systems to use on hardware produced by the relevant vendor. However, it should not be used when defining general-purpose software platforms.
### Configuration Template
The default [configuration template](../extras/configtemplate.md) for devices assigned to this platform.
### NAPALM Driver
The [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html) associated with this platform.

View File

@@ -18,6 +18,10 @@ A numeric value which influences the order in which context data is merged. Cont
The context data expressed in JSON format.
### Data File
Config context data may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify local data for the config context: It will be populated automatically from the data file.
### Is Active
If not selected, this config context will be excluded from rendering. This can be convenient to temporarily disable a config context.

View File

@@ -0,0 +1,29 @@
# Configuration Templates
Configuration templates can be used to render [device](../dcim/device.md) configurations from [context data](../../features/context-data.md). Templates are written in the [Jinja2 language](https://jinja.palletsprojects.com/) and can be associated with devices roles, platforms, and/or individual devices.
Context data is made available to [devices](../dcim/device.md) and/or [virtual machines](../virtualization/virtualmachine.md) based on their relationships to other objects in NetBox. For example, context data can be associated only with devices assigned to a particular site, or only to virtual machines in a certain cluster.
See the [configuration rendering documentation](../../features/configuration-rendering.md) for more information.
## Fields
### Name
A unique human-friendly name.
### Weight
A numeric value which influences the order in which context data is merged. Contexts with a lower weight are merged before those with a higher weight.
### Data File
Template code may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify template code: It will be populated automatically from the data file.
### Template Code
Jinja2 template code, if being defined locally rather than replicated from a data file.
### Environment Parameters
A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior.

View File

@@ -82,6 +82,10 @@ The default value to populate for the custom field when creating new objects (op
For choice and multi-choice custom fields only. A comma-delimited list of the available choices.
### Cloneable
If enabled, values from this field will be automatically pre-populated when cloning existing objects.
### Minimum Value
For numeric custom fields only. The minimum valid value (optional).

View File

@@ -12,6 +12,10 @@ The name of the export template. This will appear in the "export" dropdown list
The type of NetBox object to which the export template applies.
### Data File
Template code may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify local content for the template: It will be populated automatically from the data file.
### Template Code
Jinja2 template code for rendering the exported data.

View File

@@ -22,11 +22,13 @@ If not selected, the webhook will be inactive.
The events which will trigger the webhook. At least one event type must be selected.
| Name | Description |
|-----------|--------------------------------------|
| Creations | A new object has been created |
| Updates | An existing object has been modified |
| Deletions | An object has been deleted |
| Name | Description |
|------------|--------------------------------------|
| Creations | A new object has been created |
| Updates | An existing object has been modified |
| Deletions | An object has been deleted |
| Job starts | A job for an object starts |
| Job ends | A job for an object terminates |
### URL
@@ -58,6 +60,10 @@ Jinja2 template for a custom request body, if desired. If not defined, NetBox wi
A secret string used to prove authenticity of the request (optional). This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key.
### 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, the webhook will not be sent. A webhook that does not define any conditions will _always_ trigger.
### SSL Verification
Controls whether validation of the receiver's SSL certificate is enforced when HTTPS is used.

View File

@@ -1,8 +1,8 @@
# ASN
# ASNs
An Autonomous System Number (ASN) is a numeric identifier used in the BGP protocol to identify which [autonomous system](https://en.wikipedia.org/wiki/Autonomous_system_%28Internet%29) a particular prefix is originating and transiting through. NetBox support both 32- and 64- ASNs.
ASNs must be globally unique within NetBox, must each may be assigned to multiple [sites](../dcim/site.md).
ASNs must be globally unique within NetBox, and may be allocated from within a [defined range](./asnrange.md). Each ASN may be assigned to multiple [sites](../dcim/site.md).
## Fields

View File

@@ -0,0 +1,21 @@
# ASN Ranges
Ranges can be defined to group [AS numbers](./asn.md) numerically and to facilitate their automatic provisioning. Each range must be assigned to a [RIR](./rir.md).
## Fields
### Name
A unique human-friendly name for the range.
### Slug
A unique URL-friendly identifier. (This value can be used for filtering.)
### RIR
The [Regional Internet Registry](./rir.md) or similar authority responsible for the allocation of AS numbers within this range.
### Start & End
The starting and ending numeric boundaries of the range (inclusive).

View File

@@ -28,3 +28,7 @@ The IP range's operational status. Note that the status of a range does _not_ ha
!!! tip
Additional statuses may be defined by setting `IPRange.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Mark Utilized
If enabled, the IP range will be considered 100% utilized regardless of how many IP addresses are defined within it. This is useful for documenting DHCP ranges, for example.

View File

@@ -6,11 +6,11 @@ A tenant represents a discrete grouping of resources used for administrative pur
### Name
A unique human-friendly name.
A human-friendly name, unique to the assigned group.
### Slug
A unique URL-friendly identifier. (This value can be used for filtering.)
A URL-friendly identifier, unique to the assigned group. (This value can be used for filtering.)
### Group

View File

@@ -70,9 +70,6 @@ class MyModelImportForm(NetBoxModelImportForm):
fields = ('name', 'status', 'site', 'comments')
```
!!! note "Previously NetBoxModelCSVForm"
This form class was previously named `NetBoxModelCSVForm`. It was renamed in NetBox v3.4 to convey support for JSON and YAML formats in addition to CSV. The `NetBoxModelCSVForm` class has been retained for backward compatibility and functions exactly the same as `NetBoxModelImportForm`. However, plugin authors should be aware that this backward compatability will be removed in NetBox v3.5.
### `NetBoxModelBulkEditForm`
This form facilitates editing multiple objects in bulk. Unlike a model form, this form does not have a child `Meta` class, and must explicitly define each field. All fields in a bulk edit form are generally declared with `required=False`.
@@ -170,6 +167,9 @@ In addition to the [form fields provided by Django](https://docs.djangoproject.c
## Choice Fields
!!! warning "Obsolete Fields"
NetBox's custom `ChoiceField` and `MultipleChoiceField` classes are no longer necessary thanks to improvements made to the user interface. Django's native form fields can be used instead. These custom field classes will be removed in NetBox v3.6.
::: utilities.forms.ChoiceField
options:
members: false

View File

@@ -2,9 +2,6 @@
## Menus
!!! note
This feature was introduced in NetBox v3.4.
A plugin can register its own submenu as part of NetBox's navigation menu. This is done by defining a variable named `menu` in `navigation.py`, pointing to an instance of the `PluginMenu` class. Each menu must define a label and grouped menu items (discussed below), and may optionally specify an icon. An example is shown below.
```python title="navigation.py"

View File

@@ -1,8 +1,5 @@
# Search
!!! note
This feature was introduced in NetBox v3.4.
Plugins can define and register their own models to extend NetBox's core search functionality. Typically, a plugin will include a file named `search.py`, which holds all search indexes for its models (see the example below).
```python

View File

@@ -3,9 +3,6 @@
!!! 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.
!!! note
This feature was introduced in NetBox v3.4.
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.
To begin staging changes, first create a [branch](../../models/extras/branch.md):

View File

@@ -74,7 +74,6 @@ This template is used by the `ObjectView` generic view to display a single objec
| `breadcrumbs` | - | Breadcrumb list items (HTML `<li>` elements) |
| `object_identifier` | - | A unique identifier (string) for the object |
| `extra_controls` | - | Additional action buttons to display |
| `extra_tabs` | - | Additional tabs to include |
#### Context

View File

@@ -157,9 +157,6 @@ These views are provided to enable or enhance certain NetBox model features, suc
### Additional Tabs
!!! note
This feature was introduced in NetBox v3.4.
Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`:
```python

View File

@@ -0,0 +1,113 @@
# NetBox v3.5
## v3.5-beta1 (2023-03-30)
### Breaking Changes
* The JobResult model has been moved from the `extras` app to `core` and renamed to Job. Accordingly, its REST API endpoint has been moved from `/api/extras/job-results/` to `/api/core/jobs/`.
* The `obj_type` field on the Job model (previously JobResult) has been renamed to `object_type` for consistency with other models.
* The `JOBRESULT_RETENTION` configuration parameter has been renamed to `JOB_RETENTION`.
* The REST API schema is now generated using the OpenAPI 3.0 spec
* The URLs for the REST API schema documentation have changed:
* `/api/docs/` is now `/api/schema/swagger-ui/`
* `/api/redoc/` is now `/api/schema/redoc/`
### New Features
#### Customizable Dashboard ([#9416](https://github.com/netbox-community/netbox/issues/9416))
The static home view has been replaced with a fully customizable dashboard. Users can construct and rearrange their own personal dashboard to convey the information most pertinent to them. Supported widgets include object statistics, configurable object lists, RSS feeds, and notes, and we expect to continue adding new widgets over time.
#### Remote Data Sources ([#11558](https://github.com/netbox-community/netbox/issues/11558))
NetBox now has the ability to synchronize arbitrary data from external sources through the new [DataSource](../models/core/datasource.md) and [DataFile](../models/core/datafile.md) models. Synchronized files are stored in the PostgreSQL database, and may be referenced and consumed by other NetBox models, such as export templates and config contexts. Currently, replication from local filesystem paths, git repositories, and Amazon S3 buckets is supported, and we expect to introduce additional backends in the near future.
#### Configuration Template Rendering ([#11559](https://github.com/netbox-community/netbox/issues/11559))
This release introduces the ability to render device configurations from Jinja2 templates natively within NetBox, via both the UI and REST API. The new [ConfigTemplate](../models/extras/configtemplate.md) model stores template code (which may be defined locally or sourced from remote data files). The rendering engine passes data gleaned from both config contexts and request parameters to generate complete configurations suitable for direct application to network devices.
#### NAPALM Plugin ([#10520](https://github.com/netbox-community/netbox/issues/10520))
The NAPALM integration feature found in previous NetBox releases has been moved from the core application to a [dedicated plugin](https://github.com/netbox-community/netbox-napalm). This allows greater control over the feature's configuration and will unlock additional potential as a separate project.
#### ASN Ranges ([#8550](https://github.com/netbox-community/netbox/issues/8550))
A new ASN range model has been introduced to facilitate the provisioning of new autonomous system numbers from within a prescribed range. For example, an administrator might define an ASN range of 65000-65099 to be used for internal site identification. This includes a REST API endpoint suitable for automatic provisioning, very similar to the allocation of available prefixes and IP addresses.
#### Provider Accounts ([#9047](https://github.com/netbox-community/netbox/issues/9047))
A new model has been introduced to represent individual accounts within a common circuit provider. This replaces the `account` field on the provider model, enabling users to track multiple accounts per provider. New provider account instances will be created automatically during upgrade for all providers which currently have an account assigned. The assignment of individual circuits to a provider account remains optional.
#### Job-Triggered Webhooks ([#8958](https://github.com/netbox-community/netbox/issues/8958))
Two new webhook trigger events have been introduced: `job_start` and `job_end`. These enable users to configure webhook to trigger when a background job starts or ends, respectively. This new functionality can be used, for example, to inform a remote system when a custom script has been executed.
### Enhancements
* [#7947](https://github.com/netbox-community/netbox/issues/7947) - Enable marking IP ranges as fully utilized
* [#8184](https://github.com/netbox-community/netbox/issues/8184) - Employ HTMX to dynamically render tables listing related objects
* [#8272](https://github.com/netbox-community/netbox/issues/8272) - Support bridge relationships among device type interfaces
* [#8749](https://github.com/netbox-community/netbox/issues/8749) - Support replicating custom field values when cloning an object
* [#9073](https://github.com/netbox-community/netbox/issues/9073) - Enable syncing config context data from remote sources
* [#9653](https://github.com/netbox-community/netbox/issues/9653) - Enable setting a default platform for device types
* [#10054](https://github.com/netbox-community/netbox/issues/10054) - Introduce advanced object selector for UI forms
* [#10242](https://github.com/netbox-community/netbox/issues/10242) - Redirect to filtered objects list after bulk import
* [#10374](https://github.com/netbox-community/netbox/issues/10374) - Require unique tenant names & slugs per group
* [#10729](https://github.com/netbox-community/netbox/issues/10729) - Add date & time custom field type
* [#11254](https://github.com/netbox-community/netbox/issues/11254) - Introduce the `X-Request-ID` HTTP header to annotate the unique ID of each request for change logging
* [#11291](https://github.com/netbox-community/netbox/issues/11291) - Optimized GraphQL API request handling
* [#11440](https://github.com/netbox-community/netbox/issues/11440) - Add an `enabled` field for device type interfaces
* [#11494](https://github.com/netbox-community/netbox/issues/11494) - Enable filtering objects by create/update request IDs
* [#11517](https://github.com/netbox-community/netbox/issues/11517) - Standardize the inclusion of related objects across the entire UI
* [#11584](https://github.com/netbox-community/netbox/issues/11584) - Add a list view for contact assignments
* [#11625](https://github.com/netbox-community/netbox/issues/11625) - Add HTMX support to ObjectEditView
* [#11693](https://github.com/netbox-community/netbox/issues/11693) - Enable syncing export template content from remote sources
* [#11780](https://github.com/netbox-community/netbox/issues/11780) - Enable loading import data from remote sources
* [#11790](https://github.com/netbox-community/netbox/issues/11790) - Create database indexes for all generic foreign keys
* [#11968](https://github.com/netbox-community/netbox/issues/11968) - Add navigation menu buttons to create device & VM components
* [#12068](https://github.com/netbox-community/netbox/issues/12068) - Enable generic foreign key relationships from jobs to NetBox objects
* [#12085](https://github.com/netbox-community/netbox/issues/12085) - Add a file source view for reports
### Other Changes
* [#10604](https://github.com/netbox-community/netbox/issues/10604) - Remove unused `extra_tabs` block from `object.html` generic template
* [#10923](https://github.com/netbox-community/netbox/issues/10923) - Remove unused `NetBoxModelCSVForm` class (replaced by `NetBoxModelImportForm`)
* [#11489](https://github.com/netbox-community/netbox/issues/11489) - Consoldated several middleware classes
* [#11611](https://github.com/netbox-community/netbox/issues/11611) - Refactor API viewset classes and introduce NetBoxReadOnlyModelViewSet
* [#11694](https://github.com/netbox-community/netbox/issues/11694) - Remove obsolete `SmallTextarea` form widget
* [#11737](https://github.com/netbox-community/netbox/issues/11737) - `ChangeLoggedModel` now inherits `WebhooksMixin`
* [#11765](https://github.com/netbox-community/netbox/issues/11765) - Retire the `StaticSelect` and `StaticSelectMultiple` form widgets
* [#11955](https://github.com/netbox-community/netbox/issues/11955) - Remove the unused `CSVDataField` and `CSVFileField` classes
* [#12067](https://github.com/netbox-community/netbox/issues/12067) - Move & rename `extras.JobResult` to `core.Job`
### REST API Changes
* All API responses now include a `X-Request-ID` HTTP header indicating the request's unique ID
* Introduced new endpoints:
* `/api/circuits/provider-accounts/`
* `/api/core/data-files/`
* `/api/core/data-sources/`
* `/api/dcim/device/<id>/render-config/`
* `/api/extras/config-templates/`
* `/api/ipam/asn-ranges/`
* Removed existing endpoints:
* `/api/dcim/device/<id>/napalm/`
* circuits.Circuit
* Added the optional `account` foreign key to ProviderAccount
* circuits.Provider
* Removed the `account` field
* dcim.DeviceType
* Added `default_platform` foreign key (optional)
* dcim.InterfaceTemplate
* Added `enabled` boolean field
* Added optional `bridge` foreign key (optional)
* extras.ConfigContext
* Added `data_source`, `data_file`, `data_path`, and `data_synced` fields to enable syncing data from remote sources
* extras.ExportTemplate
* Added `data_source`, `data_file`, `data_path`, and `data_synced` fields to enable syncing content from remote sources
* extras.Webhook
* Added `type_job_start` and `type_job_end` boolean fields
* ipam.ASN
* The `rir` field now fully represents the assigned RIR (if any)
* ipam.IPRange
* Added the `mark_utilized` boolean field (default: false)

View File

@@ -74,8 +74,11 @@ nav:
- Contacts: 'features/contacts.md'
- Search: 'features/search.md'
- Context Data: 'features/context-data.md'
- Configuration Rendering: 'features/configuration-rendering.md'
- Synchronized Data: 'features/synchronized-data.md'
- Change Logging: 'features/change-logging.md'
- Journaling: 'features/journaling.md'
- Background Jobs: 'features/background-jobs.md'
- Auth & Permissions: 'features/authentication-permissions.md'
- API & Integration: 'features/api-integration.md'
- Customization: 'features/customization.md'
@@ -116,6 +119,7 @@ nav:
- REST API: 'integrations/rest-api.md'
- GraphQL API: 'integrations/graphql-api.md'
- Webhooks: 'integrations/webhooks.md'
- Synchronized Data: 'integrations/synchronized-data.md'
- NAPALM: 'integrations/napalm.md'
- Prometheus Metrics: 'integrations/prometheus-metrics.md'
- Plugins:
@@ -151,7 +155,12 @@ nav:
- Circuit Termination: 'models/circuits/circuittermination.md'
- Circuit Type: 'models/circuits/circuittype.md'
- Provider: 'models/circuits/provider.md'
- Provider Account: 'models/circuits/provideraccount.md'
- Provider Network: 'models/circuits/providernetwork.md'
- Core:
- DataFile: 'models/core/datafile.md'
- DataSource: 'models/core/datasource.md'
- Job: 'models/core/job.md'
- DCIM:
- Cable: 'models/dcim/cable.md'
- ConsolePort: 'models/dcim/consoleport.md'
@@ -196,6 +205,7 @@ nav:
- Extras:
- Branch: 'models/extras/branch.md'
- ConfigContext: 'models/extras/configcontext.md'
- ConfigTemplate: 'models/extras/configtemplate.md'
- CustomField: 'models/extras/customfield.md'
- CustomLink: 'models/extras/customlink.md'
- ExportTemplate: 'models/extras/exporttemplate.md'
@@ -207,6 +217,7 @@ nav:
- Webhook: 'models/extras/webhook.md'
- IPAM:
- ASN: 'models/ipam/asn.md'
- ASNRange: 'models/ipam/asnrange.md'
- Aggregate: 'models/ipam/aggregate.md'
- FHRPGroup: 'models/ipam/fhrpgroup.md'
- FHRPGroupAssignment: 'models/ipam/fhrpgroupassignment.md'
@@ -259,6 +270,7 @@ nav:
- git Cheat Sheet: 'development/git-cheat-sheet.md'
- Release Notes:
- Summary: 'release-notes/index.md'
- Version 3.5: 'release-notes/version-3.5.md'
- Version 3.4: 'release-notes/version-3.4.md'
- Version 3.3: 'release-notes/version-3.3.md'
- Version 3.2: 'release-notes/version-3.2.md'

View File

@@ -1,3 +1,5 @@
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
from drf_spectacular.types import OpenApiTypes
from rest_framework import serializers
from circuits.models import *
@@ -9,6 +11,7 @@ __all__ = [
'NestedCircuitTypeSerializer',
'NestedProviderNetworkSerializer',
'NestedProviderSerializer',
'NestedProviderAccountSerializer',
]
@@ -28,6 +31,9 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
# Providers
#
@extend_schema_serializer(
exclude_fields=('circuit_count',),
)
class NestedProviderSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
circuit_count = serializers.IntegerField(read_only=True)
@@ -37,10 +43,25 @@ class NestedProviderSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
#
# Provider Accounts
#
class NestedProviderAccountSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
class Meta:
model = ProviderAccount
fields = ['id', 'url', 'display', 'name', 'account']
#
# Circuits
#
@extend_schema_serializer(
exclude_fields=('circuit_count',),
)
class NestedCircuitTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True)

View File

@@ -18,6 +18,12 @@ from .nested_serializers import *
class ProviderSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
accounts = SerializedPKRelatedField(
queryset=ProviderAccount.objects.all(),
serializer=NestedProviderAccountSerializer,
required=False,
many=True
)
asns = SerializedPKRelatedField(
queryset=ASN.objects.all(),
serializer=NestedASNSerializer,
@@ -31,11 +37,27 @@ class ProviderSerializer(NetBoxModelSerializer):
class Meta:
model = Provider
fields = [
'id', 'url', 'display', 'name', 'slug', 'account', 'description', 'comments', 'asns', 'tags',
'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags',
'custom_fields', 'created', 'last_updated', 'circuit_count',
]
#
# Provider Accounts
#
class ProviderAccountSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
provider = NestedProviderSerializer()
class Meta:
model = ProviderAccount
fields = [
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
#
# Provider networks
#
@@ -70,8 +92,8 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
site = NestedSiteSerializer()
provider_network = NestedProviderNetworkSerializer()
site = NestedSiteSerializer(allow_null=True)
provider_network = NestedProviderNetworkSerializer(allow_null=True)
class Meta:
model = CircuitTermination
@@ -84,18 +106,19 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
class CircuitSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
provider = NestedProviderSerializer()
provider_account = NestedProviderAccountSerializer()
status = ChoiceField(choices=CircuitStatusChoices, required=False)
type = NestedCircuitTypeSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
termination_a = CircuitCircuitTerminationSerializer(read_only=True)
termination_z = CircuitCircuitTerminationSerializer(read_only=True)
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
class Meta:
model = Circuit
fields = [
'id', 'url', 'display', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date',
'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
'id', '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',
]

View File

@@ -7,14 +7,13 @@ router.APIRootView = views.CircuitsRootView
# Providers
router.register('providers', views.ProviderViewSet)
router.register('provider-accounts', views.ProviderAccountViewSet)
router.register('provider-networks', views.ProviderNetworkViewSet)
# Circuits
router.register('circuit-types', views.CircuitTypeViewSet)
router.register('circuits', views.CircuitViewSet)
router.register('circuit-terminations', views.CircuitTerminationViewSet)
# Provider networks
router.register('provider-networks', views.ProviderNetworkViewSet)
app_name = 'circuits-api'
urlpatterns = router.urls

View File

@@ -46,7 +46,7 @@ class CircuitTypeViewSet(NetBoxModelViewSet):
class CircuitViewSet(NetBoxModelViewSet):
queryset = Circuit.objects.prefetch_related(
'type', 'tenant', 'provider', 'termination_a', 'termination_z'
'type', 'tenant', 'provider', 'provider_account', 'termination_a', 'termination_z'
).prefetch_related('tags')
serializer_class = serializers.CircuitSerializer
filterset_class = filtersets.CircuitFilterSet
@@ -65,6 +65,16 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
brief_prefetch_fields = ['circuit']
#
# Provider accounts
#
class ProviderAccountViewSet(NetBoxModelViewSet):
queryset = ProviderAccount.objects.prefetch_related('provider', 'tags')
serializer_class = serializers.ProviderAccountSerializer
filterset_class = filtersets.ProviderAccountFilterSet
#
# Provider networks
#

View File

@@ -16,6 +16,7 @@ __all__ = (
'CircuitTerminationFilterSet',
'CircuitTypeFilterSet',
'ProviderNetworkFilterSet',
'ProviderAccountFilterSet',
'ProviderFilterSet',
)
@@ -66,7 +67,34 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class Meta:
model = Provider
fields = ['id', 'name', 'slug', 'account']
fields = ['id', 'name', 'slug']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(accounts__account__icontains=value) |
Q(accounts__name__icontains=value) |
Q(comments__icontains=value)
)
class ProviderAccountFilterSet(NetBoxModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider__slug',
queryset=Provider.objects.all(),
to_field_name='slug',
label=_('Provider (slug)'),
)
class Meta:
model = ProviderAccount
fields = ['id', 'name', 'account', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -75,7 +103,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
Q(name__icontains=value) |
Q(account__icontains=value) |
Q(comments__icontains=value)
)
).distinct()
class ProviderNetworkFilterSet(NetBoxModelFilterSet):
@@ -123,6 +151,11 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
to_field_name='slug',
label=_('Provider (slug)'),
)
provider_account_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account',
queryset=ProviderAccount.objects.all(),
label=_('ProviderAccount (ID)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__provider_network',
queryset=ProviderNetwork.objects.all(),

View File

@@ -8,13 +8,13 @@ from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
StaticSelect,
)
__all__ = (
'CircuitBulkEditForm',
'CircuitTypeBulkEditForm',
'ProviderBulkEditForm',
'ProviderAccountBulkEditForm',
'ProviderNetworkBulkEditForm',
)
@@ -25,11 +25,6 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
label=_('ASNs'),
required=False
)
account = forms.CharField(
max_length=30,
required=False,
label=_('Account number')
)
description = forms.CharField(
max_length=200,
required=False
@@ -40,10 +35,32 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
model = Provider
fieldsets = (
(None, ('asns', 'account', )),
(None, ('asns', 'description')),
)
nullable_fields = (
'asns', 'account', 'description', 'comments',
'asns', 'description', 'comments',
)
class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
required=False
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
label=_('Comments')
)
model = ProviderAccount
fieldsets = (
(None, ('provider', 'description')),
)
nullable_fields = (
'description', 'comments',
)
@@ -96,11 +113,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
queryset=Provider.objects.all(),
required=False
)
provider_account = DynamicModelChoiceField(
queryset=ProviderAccount.objects.all(),
required=False,
query_params={
'provider': '$provider'
}
)
status = forms.ChoiceField(
choices=add_blank_choice(CircuitStatusChoices),
required=False,
initial='',
widget=StaticSelect()
initial=''
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
@@ -129,7 +152,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
model = Circuit
fieldsets = (
('Circuit', ('provider', 'type', 'status', 'description')),
('Service Parameters', ('install_date', 'termination_date', 'commit_rate')),
('Service Parameters', ('provider_account', 'install_date', 'termination_date', 'commit_rate')),
('Tenancy', ('tenant',)),
)
nullable_fields = (

View File

@@ -13,6 +13,7 @@ __all__ = (
'CircuitTerminationImportForm',
'CircuitTypeImportForm',
'ProviderImportForm',
'ProviderAccountImportForm',
'ProviderNetworkImportForm',
)
@@ -23,7 +24,21 @@ class ProviderImportForm(NetBoxModelImportForm):
class Meta:
model = Provider
fields = (
'name', 'slug', 'account', 'description', 'comments', 'tags',
'name', 'slug', 'description', 'comments', 'tags',
)
class ProviderAccountImportForm(NetBoxModelImportForm):
provider = CSVModelChoiceField(
queryset=Provider.objects.all(),
to_field_name='name',
help_text=_('Assigned provider')
)
class Meta:
model = ProviderAccount
fields = (
'provider', 'name', 'account', 'description', 'comments', 'tags',
)
@@ -47,9 +62,6 @@ class CircuitTypeImportForm(NetBoxModelImportForm):
class Meta:
model = CircuitType
fields = ('name', 'slug', 'description', 'tags')
help_texts = {
'name': _('Name of circuit type'),
}
class CircuitImportForm(NetBoxModelImportForm):
@@ -58,6 +70,11 @@ class CircuitImportForm(NetBoxModelImportForm):
to_field_name='name',
help_text=_('Assigned provider')
)
provider_account = CSVModelChoiceField(
queryset=ProviderAccount.objects.all(),
to_field_name='name',
help_text=_('Assigned provider account')
)
type = CSVModelChoiceField(
queryset=CircuitType.objects.all(),
to_field_name='name',
@@ -77,8 +94,8 @@ class CircuitImportForm(NetBoxModelImportForm):
class Meta:
model = Circuit
fields = [
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
'description', 'comments', 'tags'
'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
'commit_rate', 'description', 'comments', 'tags'
]

View File

@@ -7,12 +7,13 @@ from dcim.models import Region, Site, SiteGroup
from ipam.models import ASN
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms import DatePicker, DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField
from utilities.forms import DatePicker, DynamicModelMultipleChoiceField, TagFilterField
__all__ = (
'CircuitFilterForm',
'CircuitTypeFilterForm',
'ProviderFilterForm',
'ProviderAccountFilterForm',
'ProviderNetworkFilterForm',
)
@@ -56,6 +57,23 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model)
class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
model = ProviderAccount
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('provider_id', 'account')),
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
)
account = forms.CharField(
required=False
)
tag = TagFilterField(model)
class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
model = ProviderNetwork
fieldsets = (
@@ -83,7 +101,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
model = Circuit
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Provider', ('provider_id', 'provider_network_id')),
('Provider', ('provider_id', 'provider_account_id', 'provider_network_id')),
('Attributes', ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
@@ -99,6 +117,14 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
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,
@@ -107,7 +133,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
},
label=_('Provider network')
)
status = MultipleChoiceField(
status = forms.MultipleChoiceField(
choices=CircuitStatusChoices,
required=False
)

View File

@@ -1,13 +1,12 @@
from django.utils.translation import gettext as _
from circuits.models import *
from dcim.models import Region, Site, SiteGroup
from dcim.models import Site
from ipam.models import ASN
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms import (
CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SelectSpeedWidget, SlugField,
StaticSelect,
)
__all__ = (
@@ -15,6 +14,7 @@ __all__ = (
'CircuitTerminationForm',
'CircuitTypeForm',
'ProviderForm',
'ProviderAccountForm',
'ProviderNetworkForm',
)
@@ -30,17 +30,26 @@ class ProviderForm(NetBoxModelForm):
fieldsets = (
('Provider', ('name', 'slug', 'asns', 'description', 'tags')),
('Support Info', ('account',)),
)
class Meta:
model = Provider
fields = [
'name', 'slug', 'account', 'asns', 'description', 'comments', 'tags',
'name', 'slug', 'asns', 'description', 'comments', 'tags',
]
class ProviderAccountForm(NetBoxModelForm):
provider = DynamicModelChoiceField(
queryset=Provider.objects.all()
)
comments = CommentField()
class Meta:
model = ProviderAccount
fields = [
'provider', 'name', 'account', 'description', 'comments', 'tags',
]
help_texts = {
'name': _("Full name of the provider"),
}
class ProviderNetworkForm(NetBoxModelForm):
@@ -78,7 +87,15 @@ class CircuitTypeForm(NetBoxModelForm):
class CircuitForm(TenancyForm, NetBoxModelForm):
provider = DynamicModelChoiceField(
queryset=Provider.objects.all()
queryset=Provider.objects.all(),
selector=True
)
provider_account = DynamicModelChoiceField(
queryset=ProviderAccount.objects.all(),
required=False,
query_params={
'provider_id': '$provider',
}
)
type = DynamicModelChoiceField(
queryset=CircuitType.objects.all()
@@ -86,7 +103,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
fieldsets = (
('Circuit', ('provider', 'cid', 'type', 'status', 'description', 'tags')),
('Circuit', ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')),
('Service Parameters', ('install_date', 'termination_date', 'commit_rate')),
('Tenancy', ('tenant_group', 'tenant')),
)
@@ -94,15 +111,10 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
class Meta:
model = Circuit
fields = [
'cid', 'type', 'provider', 'status', 'install_date', 'termination_date', 'commit_rate', 'description',
'tenant_group', 'tenant', 'comments', 'tags',
'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
'description', 'tenant_group', 'tenant', 'comments', 'tags',
]
help_texts = {
'cid': _("Unique circuit ID"),
'commit_rate': _("Committed rate"),
}
widgets = {
'status': StaticSelect(),
'install_date': DatePicker(),
'termination_date': DatePicker(),
'commit_rate': SelectSpeedWidget(),
@@ -110,71 +122,28 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
class CircuitTerminationForm(NetBoxModelForm):
provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
required=False,
initial_params={
'circuits': '$circuit'
}
)
circuit = DynamicModelChoiceField(
queryset=Circuit.objects.all(),
query_params={
'provider_id': '$provider',
},
)
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site_group = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
selector=True
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
query_params={
'region_id': '$region',
'group_id': '$site_group',
},
required=False
)
provider_network_provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
required=False,
label='Provider',
initial_params={
'networks': 'provider_network'
}
selector=True
)
provider_network = DynamicModelChoiceField(
queryset=ProviderNetwork.objects.all(),
query_params={
'provider_id': '$provider_network_provider',
},
required=False
required=False,
selector=True
)
class Meta:
model = CircuitTermination
fields = [
'provider', 'circuit', 'term_side', 'region', 'site_group', 'site', 'provider_network_provider',
'provider_network', 'mark_connected', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
'description', 'tags',
'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info', 'description', 'tags',
]
help_texts = {
'port_speed': _("Physical circuit speed"),
'xconnect_id': _("ID of the local cross-connect"),
'pp_info': _("Patch panel ID and port number(s)")
}
widgets = {
'term_side': StaticSelect(),
'port_speed': SelectSpeedWidget(),
'upstream_speed': SelectSpeedWidget(),
}

View File

@@ -1,21 +1,41 @@
import graphene
from circuits import models
from netbox.graphql.fields import ObjectField, ObjectListField
from .types import *
from utilities.graphql_optimizer import gql_query_optimizer
class CircuitsQuery(graphene.ObjectType):
circuit = ObjectField(CircuitType)
circuit_list = ObjectListField(CircuitType)
def resolve_circuit_list(root, info, **kwargs):
return gql_query_optimizer(models.Circuit.objects.all(), info)
circuit_termination = ObjectField(CircuitTerminationType)
circuit_termination_list = ObjectListField(CircuitTerminationType)
def resolve_circuit_termination_list(root, info, **kwargs):
return gql_query_optimizer(models.CircuitTermination.objects.all(), info)
circuit_type = ObjectField(CircuitTypeType)
circuit_type_list = ObjectListField(CircuitTypeType)
def resolve_circuit_type_list(root, info, **kwargs):
return gql_query_optimizer(models.CircuitType.objects.all(), info)
provider = ObjectField(ProviderType)
provider_list = ObjectListField(ProviderType)
def resolve_provider_list(root, info, **kwargs):
return gql_query_optimizer(models.Provider.objects.all(), info)
provider_account = ObjectField(ProviderAccountType)
provider_account_list = ObjectListField(ProviderAccountType)
provider_network = ObjectField(ProviderNetworkType)
provider_network_list = ObjectListField(ProviderNetworkType)
def resolve_provider_network_list(root, info, **kwargs):
return gql_query_optimizer(models.ProviderNetwork.objects.all(), info)

View File

@@ -10,6 +10,7 @@ __all__ = (
'CircuitType',
'CircuitTypeType',
'ProviderType',
'ProviderAccountType',
'ProviderNetworkType',
)
@@ -45,6 +46,14 @@ class ProviderType(NetBoxObjectType, ContactsMixin):
filterset_class = filtersets.ProviderFilterSet
class ProviderAccountType(NetBoxObjectType):
class Meta:
model = models.ProviderAccount
fields = '__all__'
filterset_class = filtersets.ProviderAccountFilterSet
class ProviderNetworkType(NetBoxObjectType):
class Meta:

View File

@@ -1,4 +1,4 @@
import dcim.fields
import ipam.fields
from utilities.json import CustomFieldJSONEncoder
from django.db import migrations, models
import django.db.models.deletion
@@ -77,7 +77,7 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
('asn', dcim.fields.ASNField(blank=True, null=True)),
('asn', ipam.fields.ASNField(blank=True, null=True)),
('account', models.CharField(blank=True, max_length=30)),
('portal_url', models.URLField(blank=True)),
('noc_contact', models.TextField(blank=True)),

View File

@@ -0,0 +1,91 @@
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import utilities.json
def create_provideraccounts_from_providers(apps, schema_editor):
"""
Migrate Account in Provider model to separate account model
"""
Provider = apps.get_model('circuits', 'Provider')
ProviderAccount = apps.get_model('circuits', 'ProviderAccount')
provider_accounts = []
for provider in Provider.objects.all():
if provider.account:
provider_accounts.append(ProviderAccount(
provider=provider,
account=provider.account
))
ProviderAccount.objects.bulk_create(provider_accounts, batch_size=100)
def restore_providers_from_provideraccounts(apps, schema_editor):
"""
Restore Provider account values from auto-generated ProviderAccounts
"""
ProviderAccount = apps.get_model('circuits', 'ProviderAccount')
provider_accounts = ProviderAccount.objects.order_by('pk')
for provideraccount in provider_accounts:
if provider_accounts.filter(provider=provideraccount.provider)[0] == provideraccount:
provideraccount.provider.account = provideraccount.account
provideraccount.provider.save()
class Migration(migrations.Migration):
dependencies = [
('extras', '0084_staging'),
('circuits', '0041_standardize_description_comments'),
]
operations = [
migrations.CreateModel(
name='ProviderAccount',
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)),
('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')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ('provider', 'account'),
},
),
migrations.AddConstraint(
model_name='provideraccount',
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'),
),
migrations.RunPython(
create_provideraccounts_from_providers, restore_providers_from_provideraccounts
),
migrations.RemoveField(
model_name='provider',
name='account',
),
migrations.AddField(
model_name='circuit',
name='provider_account',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount', null=True, blank=True),
preserve_default=False,
),
migrations.AlterModelOptions(
name='circuit',
options={'ordering': ['provider', 'provider_account', 'cid']},
),
migrations.AddConstraint(
model_name='circuit',
constraint=models.UniqueConstraint(fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'),
),
]

View File

@@ -1,4 +1,3 @@
from django.apps import apps
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
@@ -10,7 +9,6 @@ from dcim.models import CabledObjectModel
from netbox.models import (
ChangeLoggedModel, CustomFieldsMixin, CustomLinksMixin, OrganizationalModel, PrimaryModel, TagsMixin,
)
from netbox.models.features import WebhooksMixin
__all__ = (
'Circuit',
@@ -31,18 +29,26 @@ class CircuitType(OrganizationalModel):
class Circuit(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. Circuit port speed and commit rate are measured
in Kbps.
circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
ProviderAccount. Circuit port speed and commit rate are measured in Kbps.
"""
cid = models.CharField(
max_length=100,
verbose_name='Circuit ID'
verbose_name='Circuit ID',
help_text=_("Unique circuit ID")
)
provider = models.ForeignKey(
to='circuits.Provider',
on_delete=models.PROTECT,
related_name='circuits'
)
provider_account = models.ForeignKey(
to='circuits.ProviderAccount',
on_delete=models.PROTECT,
related_name='circuits',
blank=True,
null=True
)
type = models.ForeignKey(
to='CircuitType',
on_delete=models.PROTECT,
@@ -73,7 +79,9 @@ class Circuit(PrimaryModel):
commit_rate = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name='Commit rate (Kbps)')
verbose_name='Commit rate (Kbps)',
help_text=_("Committed rate")
)
# Generic relations
contacts = GenericRelation(
@@ -102,7 +110,8 @@ class Circuit(PrimaryModel):
)
clone_fields = (
'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description',
'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
'description',
)
prerequisite_models = (
'circuits.CircuitType',
@@ -110,12 +119,16 @@ class Circuit(PrimaryModel):
)
class Meta:
ordering = ['provider', 'cid']
ordering = ['provider', 'provider_account', 'cid']
constraints = (
models.UniqueConstraint(
fields=('provider', 'cid'),
name='%(app_label)s_%(class)s_unique_provider_cid'
),
models.UniqueConstraint(
fields=('provider_account', 'cid'),
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
),
)
def __str__(self):
@@ -127,12 +140,17 @@ class Circuit(PrimaryModel):
def get_status_color(self):
return CircuitStatusChoices.colors.get(self.status)
def clean(self):
super().clean()
if self.provider_account and self.provider != self.provider_account.provider:
raise ValidationError({'provider_account': "The assigned account must belong to the assigned provider."})
class CircuitTermination(
CustomFieldsMixin,
CustomLinksMixin,
TagsMixin,
WebhooksMixin,
ChangeLoggedModel,
CabledObjectModel
):
@@ -163,7 +181,8 @@ class CircuitTermination(
port_speed = models.PositiveIntegerField(
verbose_name='Port speed (Kbps)',
blank=True,
null=True
null=True,
help_text=_("Physical circuit speed")
)
upstream_speed = models.PositiveIntegerField(
blank=True,
@@ -174,12 +193,14 @@ class CircuitTermination(
xconnect_id = models.CharField(
max_length=50,
blank=True,
verbose_name='Cross-connect ID'
verbose_name='Cross-connect ID',
help_text=_("ID of the local cross-connect")
)
pp_info = models.CharField(
max_length=100,
blank=True,
verbose_name='Patch panel/port(s)'
verbose_name='Patch panel/port(s)',
help_text=_("Patch panel ID and port number(s)")
)
description = models.CharField(
max_length=200,

View File

@@ -1,12 +1,15 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.db.models import Q
from django.urls import reverse
from django.utils.translation import gettext as _
from netbox.models import PrimaryModel
__all__ = (
'ProviderNetwork',
'Provider',
'ProviderAccount',
)
@@ -17,7 +20,8 @@ class Provider(PrimaryModel):
"""
name = models.CharField(
max_length=100,
unique=True
unique=True,
help_text=_("Full name of the provider")
)
slug = models.SlugField(
max_length=100,
@@ -28,20 +32,13 @@ class Provider(PrimaryModel):
related_name='providers',
blank=True
)
account = models.CharField(
max_length=30,
blank=True,
verbose_name='Account number'
)
# Generic relations
contacts = GenericRelation(
to='tenancy.ContactAssignment'
)
clone_fields = (
'account',
)
clone_fields = ()
class Meta:
ordering = ['name']
@@ -53,6 +50,54 @@ class Provider(PrimaryModel):
return reverse('circuits:provider', args=[self.pk])
class ProviderAccount(PrimaryModel):
"""
This is a discrete account within a provider. Each Circuit belongs to a Provider Account.
"""
provider = models.ForeignKey(
to='circuits.Provider',
on_delete=models.PROTECT,
related_name='accounts'
)
account = models.CharField(
max_length=100,
verbose_name='Account ID'
)
name = models.CharField(
max_length=100,
blank=True
)
# Generic relations
contacts = GenericRelation(
to='tenancy.ContactAssignment'
)
clone_fields = ('provider', )
class Meta:
ordering = ('provider', 'account')
constraints = (
models.UniqueConstraint(
fields=('provider', 'account'),
name='%(app_label)s_%(class)s_unique_provider_account'
),
models.UniqueConstraint(
fields=('provider', 'name'),
name='%(app_label)s_%(class)s_unique_provider_name',
condition=~Q(name="")
),
)
def __str__(self):
if self.name:
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):
"""
This represents a provider network which exists outside of NetBox, the details of which are unknown or

View File

@@ -39,12 +39,20 @@ class ProviderIndex(SearchIndex):
model = models.Provider
fields = (
('name', 100),
('account', 200),
('description', 500),
('comments', 5000),
)
class ProviderAccountIndex(SearchIndex):
model = models.ProviderAccount
fields = (
('name', 100),
('account', 200),
('comments', 5000),
)
@register_search
class ProviderNetworkIndex(SearchIndex):
model = models.ProviderNetwork

View File

@@ -1,4 +1,5 @@
import django_tables2 as tables
from circuits.models import *
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
@@ -28,7 +29,9 @@ class CircuitTypeTable(NetBoxTable):
tags = columns.TagColumn(
url_name='circuits:circuittype_list'
)
circuit_count = tables.Column(
circuit_count = columns.LinkedCountColumn(
viewname='circuits:circuit_list',
url_params={'type_id': 'pk'},
verbose_name='Circuits'
)
@@ -48,6 +51,10 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
provider = tables.Column(
linkify=True
)
provider_account = tables.Column(
linkify=True,
verbose_name='Account'
)
status = columns.ChoiceFieldColumn()
termination_a = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
@@ -66,9 +73,9 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Circuit
fields = (
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'tenant_group', 'termination_a', 'termination_z',
'install_date', 'termination_date', 'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created',
'last_updated',
'pk', 'id', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'tenant_group',
'termination_a', 'termination_z', 'install_date', 'termination_date', 'commit_rate', 'description',
'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',

View File

@@ -7,6 +7,7 @@ from netbox.tables import NetBoxTable, columns
__all__ = (
'ProviderTable',
'ProviderAccountTable',
'ProviderNetworkTable',
)
@@ -15,6 +16,16 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
accounts = columns.ManyToManyColumn(
linkify_item=True,
verbose_name='Accounts'
)
account_count = columns.LinkedCountColumn(
accessor=tables.A('accounts__count'),
viewname='circuits:provideraccount_list',
url_params={'account_id': 'pk'},
verbose_name='Account Count'
)
asns = columns.ManyToManyColumn(
linkify_item=True,
verbose_name='ASNs'
@@ -39,10 +50,38 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Provider
fields = (
'pk', 'id', 'name', 'asns', 'account', 'asn_count', 'circuit_count', 'description', 'comments', 'contacts',
'tags', 'created', 'last_updated',
'pk', 'id', 'name', 'accounts', 'account_count', 'asns', 'asn_count', 'circuit_count', 'description',
'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'account', 'circuit_count')
default_columns = ('pk', 'name', 'account_count', 'circuit_count')
class ProviderAccountTable(ContactsColumnMixin, NetBoxTable):
account = tables.Column(
linkify=True
)
name = tables.Column()
provider = tables.Column(
linkify=True
)
circuit_count = columns.LinkedCountColumn(
accessor=Accessor('count_circuits'),
viewname='circuits:circuit_list',
url_params={'provider_account_id': 'pk'},
verbose_name='Circuits'
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='circuits:provideraccount_list'
)
class Meta(NetBoxTable.Meta):
model = ProviderAccount
fields = (
'pk', 'id', 'account', 'name', 'provider', 'circuit_count', 'comments', 'contacts', 'tags', 'created',
'last_updated',
)
default_columns = ('pk', 'account', 'name', 'provider', 'circuit_count')
class ProviderNetworkTable(NetBoxTable):

View File

@@ -20,7 +20,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
model = Provider
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
bulk_update_data = {
'account': '1234',
'comments': 'New comments',
}
@classmethod
@@ -106,6 +106,12 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
)
Provider.objects.bulk_create(providers)
provider_accounts = (
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
ProviderAccount(name='Provider Account 2', provider=providers[1], account='2345'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
circuit_types = (
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
@@ -113,9 +119,9 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
CircuitType.objects.bulk_create(circuit_types)
circuits = (
Circuit(cid='Circuit 1', provider=providers[0], type=circuit_types[0]),
Circuit(cid='Circuit 2', provider=providers[0], type=circuit_types[0]),
Circuit(cid='Circuit 3', provider=providers[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)
@@ -123,16 +129,19 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
{
'cid': 'Circuit 4',
'provider': providers[1].pk,
'provider_account': provider_accounts[1].pk,
'type': circuit_types[1].pk,
},
{
'cid': 'Circuit 5',
'provider': providers[1].pk,
'provider_account': provider_accounts[1].pk,
'type': circuit_types[1].pk,
},
{
'cid': 'Circuit 6',
'provider': providers[1].pk,
'provider_account': provider_accounts[1].pk,
'type': circuit_types[1].pk,
},
]
@@ -197,6 +206,49 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
}
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
model = ProviderAccount
brief_fields = ['account', 'display', 'id', 'name', 'url']
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
)
Provider.objects.bulk_create(providers)
provider_accounts = (
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
ProviderAccount(name='Provider Account 2', provider=providers[0], account='2345'),
ProviderAccount(name='Provider Account 3', provider=providers[0], account='3456'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
cls.create_data = [
{
'name': 'Provider Account 4',
'provider': providers[0].pk,
'account': '4567',
},
{
'name': 'Provider Account 5',
'provider': providers[0].pk,
'account': '5678',
},
{
'name': 'Provider Account 6',
'provider': providers[0].pk,
'account': '6789',
},
]
cls.bulk_update_data = {
'provider': providers[1].pk,
'description': 'New description',
}
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
model = ProviderNetwork
brief_fields = ['display', 'id', 'name', 'url']

View File

@@ -25,11 +25,11 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
ASN.objects.bulk_create(asns)
providers = (
Provider(name='Provider 1', slug='provider-1', account='1234'),
Provider(name='Provider 2', slug='provider-2', account='2345'),
Provider(name='Provider 3', slug='provider-3', account='3456'),
Provider(name='Provider 4', slug='provider-4', account='4567'),
Provider(name='Provider 5', slug='provider-5', account='5678'),
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'),
Provider(name='Provider 5', slug='provider-5'),
)
Provider.objects.bulk_create(providers)
providers[0].asns.set([asns[0]])
@@ -64,8 +64,8 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
CircuitType.objects.bulk_create(circuit_types)
circuits = (
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1'),
Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 1'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'),
Circuit(provider=providers[1], type=circuit_types[1], cid='Circuit 2'),
)
Circuit.objects.bulk_create(circuits)
@@ -87,10 +87,6 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'asn_id': [asns[0].pk, asns[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_account(self):
params = {'account': ['1234', '2345']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -193,9 +189,17 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
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[1]),
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
@@ -204,12 +208,12 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = (
Circuit(provider=providers[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], 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], 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], 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], 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], 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'),
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.objects.bulk_create(circuits)
@@ -246,6 +250,11 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'provider': [provider.slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
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)
def test_provider_network(self):
provider_networks = ProviderNetwork.objects.all()[:2]
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
@@ -445,3 +454,44 @@ class ProviderNetworkTestCase(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 ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ProviderAccount.objects.all()
filterset = ProviderAccountFilterSet
@classmethod
def setUpTestData(cls):
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], description='foobar1', account='1234'),
ProviderAccount(name='Provider Account 2', provider=providers[1], description='foobar2', account='2345'),
ProviderAccount(name='Provider Account 3', provider=providers[2], account='3456'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
def test_name(self):
params = {'name': ['Provider Account 1', 'Provider Account 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_account(self):
params = {'account': ['1234', '3456']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
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)

View File

@@ -38,7 +38,6 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'name': 'Provider X',
'slug': 'provider-x',
'asns': [asns[6].pk, asns[7].pk],
'account': '1234',
'comments': 'Another provider',
'tags': [t.pk for t in tags],
}
@@ -58,7 +57,6 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
cls.bulk_edit_data = {
'account': '5678',
'comments': 'New comments',
}
@@ -124,6 +122,12 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
Provider.objects.bulk_create(providers)
provider_accounts = (
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
ProviderAccount(name='Provider Account 2', provider=providers[1], account='2345'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
circuittypes = (
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
@@ -131,9 +135,9 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
CircuitType.objects.bulk_create(circuittypes)
circuits = (
Circuit(cid='Circuit 1', provider=providers[0], type=circuittypes[0]),
Circuit(cid='Circuit 2', provider=providers[0], type=circuittypes[0]),
Circuit(cid='Circuit 3', provider=providers[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)
@@ -143,6 +147,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.form_data = {
'cid': 'Circuit X',
'provider': providers[1].pk,
'provider_account': provider_accounts[1].pk,
'type': circuittypes[1].pk,
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
'tenant': None,
@@ -155,10 +160,10 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
"cid,provider,type,status",
"Circuit 4,Provider 1,Circuit Type 1,active",
"Circuit 5,Provider 1,Circuit Type 1,active",
"Circuit 6,Provider 1,Circuit Type 1,active",
"cid,provider,provider_account,type,status",
"Circuit 4,Provider 1,Provider Account 1,Circuit Type 1,active",
"Circuit 5,Provider 1,Provider Account 1,Circuit Type 1,active",
"Circuit 6,Provider 1,Provider Account 1,Circuit Type 1,active",
)
cls.csv_update_data = (
@@ -170,6 +175,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.bulk_edit_data = {
'provider': providers[1].pk,
'provider_account': provider_accounts[1].pk,
'type': circuittypes[1].pk,
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
'tenant': None,
@@ -179,6 +185,57 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
class ProviderAccountTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = ProviderAccount
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
)
Provider.objects.bulk_create(providers)
provider_accounts = (
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
ProviderAccount(name='Provider Account 2', provider=providers[0], account='2345'),
ProviderAccount(name='Provider Account 3', provider=providers[0], account='3456'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Provider Account X',
'provider': providers[1].pk,
'account': 'XXXX',
'description': 'A new provider network',
'comments': 'Longer description goes here',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,provider,account,description",
"Provider Account 4,Provider 1,4567,Foo",
"Provider Account 5,Provider 1,5678,Bar",
"Provider Account 6,Provider 1,6789,Baz",
)
cls.csv_update_data = (
"id,name,account,description",
f"{provider_accounts[0].pk},Provider Network 7,7890,New description7",
f"{provider_accounts[1].pk},Provider Network 8,8901,New description8",
f"{provider_accounts[2].pk},Provider Network 9,9012,New description9",
)
cls.bulk_edit_data = {
'provider': providers[1].pk,
'description': 'New description',
'comments': 'New comments',
}
class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = ProviderNetwork

View File

@@ -14,6 +14,14 @@ urlpatterns = [
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
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/<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'),

View File

@@ -30,17 +30,13 @@ class ProviderView(generic.ObjectView):
queryset = Provider.objects.all()
def get_extra_context(self, request, instance):
circuits = Circuit.objects.restrict(request.user, 'view').filter(
provider=instance
).prefetch_related(
'tenant__group', 'termination_a__site', 'termination_z__site',
'termination_a__provider_network', 'termination_z__provider_network',
related_models = (
(ProviderAccount.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
(Circuit.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
)
circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
circuits_table.configure(request)
return {
'circuits_table': circuits_table,
'related_models': related_models,
}
@@ -58,7 +54,6 @@ class ProviderDeleteView(generic.ObjectDeleteView):
class ProviderBulkImportView(generic.BulkImportView):
queryset = Provider.objects.all()
model_form = forms.ProviderImportForm
table = tables.ProviderTable
class ProviderBulkEditView(generic.BulkEditView):
@@ -78,6 +73,67 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
table = tables.ProviderTable
#
# ProviderAccounts
#
class ProviderAccountListView(generic.ObjectListView):
queryset = ProviderAccount.objects.annotate(
count_circuits=count_related(Circuit, 'provider_account')
)
filterset = filtersets.ProviderAccountFilterSet
filterset_form = forms.ProviderAccountFilterForm
table = tables.ProviderAccountTable
@register_model_view(ProviderAccount)
class ProviderAccountView(generic.ObjectView):
queryset = ProviderAccount.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(Circuit.objects.restrict(request.user, 'view').filter(provider_account=instance), 'provider_account_id'),
)
return {
'related_models': related_models,
}
@register_model_view(ProviderAccount, 'edit')
class ProviderAccountEditView(generic.ObjectEditView):
queryset = ProviderAccount.objects.all()
form = forms.ProviderAccountForm
@register_model_view(ProviderAccount, 'delete')
class ProviderAccountDeleteView(generic.ObjectDeleteView):
queryset = ProviderAccount.objects.all()
class ProviderAccountBulkImportView(generic.BulkImportView):
queryset = ProviderAccount.objects.all()
model_form = forms.ProviderAccountImportForm
table = tables.ProviderAccountTable
class ProviderAccountBulkEditView(generic.BulkEditView):
queryset = ProviderAccount.objects.annotate(
count_circuits=count_related(Circuit, 'provider_account')
)
filterset = filtersets.ProviderAccountFilterSet
table = tables.ProviderAccountTable
form = forms.ProviderAccountBulkEditForm
class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
queryset = ProviderAccount.objects.annotate(
count_circuits=count_related(Circuit, 'provider_account')
)
filterset = filtersets.ProviderAccountFilterSet
table = tables.ProviderAccountTable
#
# Provider networks
#
@@ -94,18 +150,15 @@ class ProviderNetworkView(generic.ObjectView):
queryset = ProviderNetwork.objects.all()
def get_extra_context(self, request, instance):
circuits = Circuit.objects.restrict(request.user, 'view').filter(
Q(termination_a__provider_network=instance.pk) |
Q(termination_z__provider_network=instance.pk)
).prefetch_related(
'tenant__group', 'termination_a__site', 'termination_z__site',
'termination_a__provider_network', 'termination_z__provider_network',
related_models = (
(
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
'providernetwork_id',
),
)
circuits_table = tables.CircuitTable(circuits, user=request.user)
circuits_table.configure(request)
return {
'circuits_table': circuits_table,
'related_models': related_models,
}
@@ -123,7 +176,6 @@ class ProviderNetworkDeleteView(generic.ObjectDeleteView):
class ProviderNetworkBulkImportView(generic.BulkImportView):
queryset = ProviderNetwork.objects.all()
model_form = forms.ProviderNetworkImportForm
table = tables.ProviderNetworkTable
class ProviderNetworkBulkEditView(generic.BulkEditView):
@@ -157,12 +209,12 @@ class CircuitTypeView(generic.ObjectView):
queryset = CircuitType.objects.all()
def get_extra_context(self, request, instance):
circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance)
circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('type',))
circuits_table.configure(request)
related_models = (
(Circuit.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
)
return {
'circuits_table': circuits_table,
'related_models': related_models,
}
@@ -180,7 +232,6 @@ class CircuitTypeDeleteView(generic.ObjectDeleteView):
class CircuitTypeBulkImportView(generic.BulkImportView):
queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeImportForm
table = tables.CircuitTypeTable
class CircuitTypeBulkEditView(generic.BulkEditView):
@@ -232,7 +283,6 @@ class CircuitDeleteView(generic.ObjectDeleteView):
class CircuitBulkImportView(generic.BulkImportView):
queryset = Circuit.objects.all()
model_form = forms.CircuitImportForm
table = tables.CircuitTable
additional_permissions = [
'circuits.add_circuittermination',
]

View File

View File

@@ -0,0 +1,41 @@
from rest_framework import serializers
from core.choices import JobStatusChoices
from core.models import *
from netbox.api.fields import ChoiceField
from netbox.api.serializers import WritableNestedSerializer
from users.api.nested_serializers import NestedUserSerializer
__all__ = (
'NestedDataFileSerializer',
'NestedDataSourceSerializer',
'NestedJobSerializer',
)
class NestedDataSourceSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:datasource-detail')
class Meta:
model = DataSource
fields = ['id', 'url', 'display', 'name']
class NestedDataFileSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:datafile-detail')
class Meta:
model = DataFile
fields = ['id', 'url', 'display', 'path']
class NestedJobSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
status = ChoiceField(choices=JobStatusChoices)
user = NestedUserSerializer(
read_only=True
)
class Meta:
model = Job
fields = ['url', 'created', 'completed', 'user', 'status']

224
netbox/core/api/schema.py Normal file
View File

@@ -0,0 +1,224 @@
import re
import typing
from drf_spectacular.extensions import (
OpenApiSerializerFieldExtension,
OpenApiViewExtension,
)
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.plumbing import (
ComponentRegistry,
ResolvedComponent,
build_basic_type,
build_media_type_object,
build_object_type,
is_serializer,
)
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema
from rest_framework.relations import ManyRelatedField
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
from netbox.api.serializers import WritableNestedSerializer
# see netbox.api.routers.NetBoxRouter
BULK_ACTIONS = ("bulk_destroy", "bulk_partial_update", "bulk_update")
WRITABLE_ACTIONS = ("PATCH", "POST", "PUT")
class FixTimeZoneSerializerField(OpenApiSerializerFieldExtension):
target_class = 'timezone_field.rest_framework.TimeZoneSerializerField'
def map_serializer_field(self, auto_schema, direction):
return build_basic_type(OpenApiTypes.STR)
class ChoiceFieldFix(OpenApiSerializerFieldExtension):
target_class = 'netbox.api.fields.ChoiceField'
def map_serializer_field(self, auto_schema, direction):
if direction == 'request':
return build_basic_type(OpenApiTypes.STR)
elif direction == "response":
return build_object_type(
properties={
"value": build_basic_type(OpenApiTypes.STR),
"label": build_basic_type(OpenApiTypes.STR),
}
)
class NetBoxAutoSchema(AutoSchema):
"""
Overrides to drf_spectacular.openapi.AutoSchema to fix following issues:
1. bulk serializers cause operation_id conflicts with non-bulk ones
2. bulk operations should specify a list
3. bulk operations don't have filter params
4. bulk operations don't have pagination
5. bulk delete should specify input
"""
writable_serializers = {}
@property
def is_bulk_action(self):
if hasattr(self.view, "action") and self.view.action in BULK_ACTIONS:
return True
else:
return False
def get_operation_id(self):
"""
bulk serializers cause operation_id conflicts with non-bulk ones
bulk operations cause id conflicts in spectacular resulting in numerous:
Warning: operationId "xxx" has collisions [xxx]. "resolving with numeral suffixes"
code is modified from drf_spectacular.openapi.AutoSchema.get_operation_id
"""
if self.is_bulk_action:
tokenized_path = self._tokenize_path()
# replace dashes as they can be problematic later in code generation
tokenized_path = [t.replace('-', '_') for t in tokenized_path]
if self.method == 'GET' and self._is_list_view():
# this shouldn't happen, but keeping it here to follow base code
action = 'list'
else:
# action = self.method_mapping[self.method.lower()]
# use bulk name so partial_update -> bulk_partial_update
action = self.view.action.lower()
if not tokenized_path:
tokenized_path.append('root')
if re.search(r'<drf_format_suffix\w*:\w+>', self.path_regex):
tokenized_path.append('formatted')
return '_'.join(tokenized_path + [action])
# if not bulk - just return normal id
return super().get_operation_id()
def get_request_serializer(self) -> typing.Any:
# bulk operations should specify a list
serializer = super().get_request_serializer()
if self.is_bulk_action:
return type(serializer)(many=True)
# handle mapping for Writable serializers - adapted from dansheps original code
# for drf-yasg
if serializer is not None and self.method in WRITABLE_ACTIONS:
writable_class = self.get_writable_class(serializer)
if writable_class is not None:
if hasattr(serializer, "child"):
child_serializer = self.get_writable_class(serializer.child)
serializer = writable_class(context=serializer.context, child=child_serializer)
else:
serializer = writable_class(context=serializer.context)
return serializer
def get_response_serializers(self) -> typing.Any:
# bulk operations should specify a list
response_serializers = super().get_response_serializers()
if self.is_bulk_action:
return type(response_serializers)(many=True)
return response_serializers
def get_serializer_ref_name(self, serializer):
# from drf-yasg.utils
"""Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer')
:param serializer: Serializer instance
:return: Serializer's ``ref_name`` or ``None`` for inline serializer
:rtype: str or None
"""
serializer_meta = getattr(serializer, 'Meta', None)
serializer_name = type(serializer).__name__
if hasattr(serializer_meta, 'ref_name'):
ref_name = serializer_meta.ref_name
elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer):
ref_name = None
else:
ref_name = serializer_name
if ref_name.endswith('Serializer'):
ref_name = ref_name[: -len('Serializer')]
return ref_name
def get_writable_class(self, serializer):
properties = {}
fields = {} if hasattr(serializer, 'child') else serializer.fields
for child_name, child in fields.items():
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
properties[child_name] = None
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
properties[child_name] = None
if not properties:
return None
if type(serializer) not in self.writable_serializers:
writable_name = 'Writable' + type(serializer).__name__
meta_class = getattr(type(serializer), 'Meta', None)
if meta_class:
ref_name = 'Writable' + self.get_serializer_ref_name(serializer)
writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name})
properties['Meta'] = writable_meta
self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties)
writable_class = self.writable_serializers[type(serializer)]
return writable_class
def get_filter_backends(self):
# bulk operations don't have filter params
if self.is_bulk_action:
return []
return super().get_filter_backends()
def _get_paginator(self):
# bulk operations don't have pagination
if self.is_bulk_action:
return None
return super()._get_paginator()
def _get_request_body(self, direction='request'):
# bulk delete should specify input
if (not self.is_bulk_action) or (self.method != 'DELETE'):
return super()._get_request_body(direction)
# rest from drf_spectacular.openapi.AutoSchema._get_request_body
# but remove the unsafe method check
request_serializer = self.get_request_serializer()
if isinstance(request_serializer, dict):
content = []
request_body_required = True
for media_type, serializer in request_serializer.items():
schema, partial_request_body_required = self._get_request_for_media_type(serializer, direction)
examples = self._get_examples(serializer, direction, media_type)
if schema is None:
continue
content.append((media_type, schema, examples))
request_body_required &= partial_request_body_required
else:
schema, request_body_required = self._get_request_for_media_type(request_serializer, direction)
if schema is None:
return None
content = [
(media_type, schema, self._get_examples(request_serializer, direction, media_type))
for media_type in self.map_parsers()
]
request_body = {
'content': {
media_type: build_media_type_object(schema, examples) for media_type, schema, examples in content
}
}
if request_body_required:
request_body['required'] = request_body_required
return request_body

View File

@@ -0,0 +1,72 @@
from rest_framework import serializers
from core.choices import *
from core.models import *
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
from users.api.nested_serializers import NestedUserSerializer
from .nested_serializers import *
__all__ = (
'DataFileSerializer',
'DataSourceSerializer',
'JobSerializer',
)
class DataSourceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='core-api:datasource-detail'
)
type = ChoiceField(
choices=DataSourceTypeChoices
)
status = ChoiceField(
choices=DataSourceStatusChoices,
read_only=True
)
# Related object counts
file_count = serializers.IntegerField(
read_only=True
)
class Meta:
model = DataSource
fields = [
'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
'parameters', 'ignore_rules', 'created', 'last_updated', 'file_count',
]
class DataFileSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='core-api:datafile-detail'
)
source = NestedDataSourceSerializer(
read_only=True
)
class Meta:
model = DataFile
fields = [
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
]
class JobSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
user = NestedUserSerializer(
read_only=True
)
status = ChoiceField(choices=JobStatusChoices, read_only=True)
object_type = ContentTypeField(
read_only=True
)
class Meta:
model = Job
fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
'started', 'completed', 'user', 'data', 'job_id',
]

16
netbox/core/api/urls.py Normal file
View File

@@ -0,0 +1,16 @@
from netbox.api.routers import NetBoxRouter
from . import views
router = NetBoxRouter()
router.APIRootView = views.CoreRootView
# Data sources
router.register('data-sources', views.DataSourceViewSet)
router.register('data-files', views.DataFileViewSet)
# Jobs
router.register('jobs', views.JobViewSet)
app_name = 'core-api'
urlpatterns = router.urls

58
netbox/core/api/views.py Normal file
View File

@@ -0,0 +1,58 @@
from django.shortcuts import get_object_or_404
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.routers import APIRootView
from rest_framework.viewsets import ReadOnlyModelViewSet
from core import filtersets
from core.models import *
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
from utilities.utils import count_related
from . import serializers
class CoreRootView(APIRootView):
"""
Core API root view
"""
def get_view_name(self):
return 'Core'
class DataSourceViewSet(NetBoxModelViewSet):
queryset = DataSource.objects.annotate(
file_count=count_related(DataFile, 'source')
)
serializer_class = serializers.DataSourceSerializer
filterset_class = filtersets.DataSourceFilterSet
@action(detail=True, methods=['post'])
def sync(self, request, pk):
"""
Enqueue a job to synchronize the DataSource.
"""
if not request.user.has_perm('extras.sync_datasource'):
raise PermissionDenied("Syncing data sources requires the core.sync_datasource permission.")
datasource = get_object_or_404(DataSource, pk=pk)
datasource.enqueue_sync_job(request)
serializer = serializers.DataSourceSerializer(datasource, context={'request': request})
return Response(serializer.data)
class DataFileViewSet(NetBoxReadOnlyModelViewSet):
queryset = DataFile.objects.defer('data').prefetch_related('source')
serializer_class = serializers.DataFileSerializer
filterset_class = filtersets.DataFileFilterSet
class JobViewSet(ReadOnlyModelViewSet):
"""
Retrieve a list of job results
"""
queryset = Job.objects.prefetch_related('user')
serializer_class = serializers.JobSerializer
filterset_class = filtersets.JobFilterSet

9
netbox/core/apps.py Normal file
View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
name = "core"
def ready(self):
from . import data_backends, search
from core.api import schema # noqa: E402

78
netbox/core/choices.py Normal file
View File

@@ -0,0 +1,78 @@
from django.utils.translation import gettext as _
from utilities.choices import ChoiceSet
#
# Data sources
#
class DataSourceTypeChoices(ChoiceSet):
LOCAL = 'local'
GIT = 'git'
AMAZON_S3 = 'amazon-s3'
CHOICES = (
(LOCAL, _('Local'), 'gray'),
(GIT, _('Git'), 'blue'),
(AMAZON_S3, _('Amazon S3'), 'blue'),
)
class DataSourceStatusChoices(ChoiceSet):
NEW = 'new'
QUEUED = 'queued'
SYNCING = 'syncing'
COMPLETED = 'completed'
FAILED = 'failed'
CHOICES = (
(NEW, _('New'), 'blue'),
(QUEUED, _('Queued'), 'orange'),
(SYNCING, _('Syncing'), 'cyan'),
(COMPLETED, _('Completed'), 'green'),
(FAILED, _('Failed'), 'red'),
)
#
# Managed files
#
class ManagedFileRootPathChoices(ChoiceSet):
SCRIPTS = 'scripts' # settings.SCRIPTS_ROOT
REPORTS = 'reports' # settings.REPORTS_ROOT
CHOICES = (
(SCRIPTS, _('Scripts')),
(REPORTS, _('Reports')),
)
#
# Jobs
#
class JobStatusChoices(ChoiceSet):
STATUS_PENDING = 'pending'
STATUS_SCHEDULED = 'scheduled'
STATUS_RUNNING = 'running'
STATUS_COMPLETED = 'completed'
STATUS_ERRORED = 'errored'
STATUS_FAILED = 'failed'
CHOICES = (
(STATUS_PENDING, 'Pending', 'cyan'),
(STATUS_SCHEDULED, 'Scheduled', 'gray'),
(STATUS_RUNNING, 'Running', 'blue'),
(STATUS_COMPLETED, 'Completed', 'green'),
(STATUS_ERRORED, 'Errored', 'red'),
(STATUS_FAILED, 'Failed', 'red'),
)
TERMINAL_STATE_CHOICES = (
STATUS_COMPLETED,
STATUS_ERRORED,
STATUS_FAILED,
)

View File

@@ -0,0 +1,191 @@
import logging
import os
import re
import subprocess
import tempfile
from contextlib import contextmanager
from pathlib import Path
from urllib.parse import quote, urlunparse, urlparse
import boto3
from botocore.config import Config as Boto3Config
from django import forms
from django.conf import settings
from django.utils.translation import gettext as _
from netbox.registry import registry
from .choices import DataSourceTypeChoices
from .exceptions import SyncError
__all__ = (
'LocalBackend',
'GitBackend',
)
logger = logging.getLogger('netbox.data_backends')
def register_backend(name):
"""
Decorator for registering a DataBackend class.
"""
def _wrapper(cls):
registry['data_backends'][name] = cls
return cls
return _wrapper
class DataBackend:
parameters = {}
def __init__(self, url, **kwargs):
self.url = url
self.params = kwargs
@property
def url_scheme(self):
return urlparse(self.url).scheme.lower()
@contextmanager
def fetch(self):
raise NotImplemented()
@register_backend(DataSourceTypeChoices.LOCAL)
class LocalBackend(DataBackend):
@contextmanager
def fetch(self):
logger.debug(f"Data source type is local; skipping fetch")
local_path = urlparse(self.url).path # Strip file:// scheme
yield local_path
@register_backend(DataSourceTypeChoices.GIT)
class GitBackend(DataBackend):
parameters = {
'username': forms.CharField(
required=False,
label=_('Username'),
widget=forms.TextInput(attrs={'class': 'form-control'})
),
'password': forms.CharField(
required=False,
label=_('Password'),
widget=forms.TextInput(attrs={'class': 'form-control'})
),
'branch': forms.CharField(
required=False,
label=_('Branch'),
widget=forms.TextInput(attrs={'class': 'form-control'})
)
}
@contextmanager
def fetch(self):
local_path = tempfile.TemporaryDirectory()
# Add authentication credentials to URL (if specified)
username = self.params.get('username')
password = self.params.get('password')
if username and password:
# Add username & password to URL
parsed = urlparse(self.url)
url = f'{parsed.scheme}://{quote(username)}:{quote(password)}@{parsed.netloc}{parsed.path}'
else:
url = self.url
# Compile git arguments
args = [settings.GIT_PATH, 'clone', '--depth', '1']
if branch := self.params.get('branch'):
args.extend(['--branch', branch])
args.extend([url, local_path.name])
# Prep environment variables
env_vars = {}
if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'):
env_vars['http_proxy'] = settings.HTTP_PROXIES.get(self.url_scheme)
logger.debug(f"Cloning git repo: {' '.join(args)}")
try:
subprocess.run(args, check=True, capture_output=True, env=env_vars)
except FileNotFoundError as e:
raise SyncError(
f"Unable to fetch: git executable not found. Check that the git executable exists at the "
f"configured path: {settings.GIT_PATH}"
)
except subprocess.CalledProcessError as e:
raise SyncError(f"Fetching remote data failed: {e.stderr}")
yield local_path.name
local_path.cleanup()
@register_backend(DataSourceTypeChoices.AMAZON_S3)
class S3Backend(DataBackend):
parameters = {
'aws_access_key_id': forms.CharField(
label=_('AWS access key ID'),
widget=forms.TextInput(attrs={'class': 'form-control'})
),
'aws_secret_access_key': forms.CharField(
label=_('AWS secret access key'),
widget=forms.TextInput(attrs={'class': 'form-control'})
),
}
REGION_REGEX = r's3\.([a-z0-9-]+)\.amazonaws\.com'
@contextmanager
def fetch(self):
local_path = tempfile.TemporaryDirectory()
# Build the S3 configuration
s3_config = Boto3Config(
proxies=settings.HTTP_PROXIES,
)
# Initialize the S3 resource and bucket
aws_access_key_id = self.params.get('aws_access_key_id')
aws_secret_access_key = self.params.get('aws_secret_access_key')
s3 = boto3.resource(
's3',
region_name=self._region_name,
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
config=s3_config
)
bucket = s3.Bucket(self._bucket_name)
# Download all files within the specified path
for obj in bucket.objects.filter(Prefix=self._remote_path):
local_filename = os.path.join(local_path.name, obj.key)
# Build local path
Path(os.path.dirname(local_filename)).mkdir(parents=True, exist_ok=True)
bucket.download_file(obj.key, local_filename)
yield local_path.name
local_path.cleanup()
@property
def _region_name(self):
domain = urlparse(self.url).netloc
if m := re.match(self.REGION_REGEX, domain):
return m.group(1)
return None
@property
def _bucket_name(self):
url_path = urlparse(self.url).path.lstrip('/')
return url_path.split('/')[0]
@property
def _remote_path(self):
url_path = urlparse(self.url).path.lstrip('/')
if '/' in url_path:
return url_path.split('/', 1)[1]
return ''

View File

@@ -0,0 +1,2 @@
class SyncError(Exception):
pass

124
netbox/core/filtersets.py Normal file
View File

@@ -0,0 +1,124 @@
from django.db.models import Q
from django.utils.translation import gettext as _
import django_filters
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
from .choices import *
from .models import *
__all__ = (
'DataFileFilterSet',
'DataSourceFilterSet',
'JobFilterSet',
)
class DataSourceFilterSet(NetBoxModelFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=DataSourceTypeChoices,
null_value=None
)
status = django_filters.MultipleChoiceFilter(
choices=DataSourceStatusChoices,
null_value=None
)
class Meta:
model = DataSource
fields = ('id', 'name', 'enabled')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value) |
Q(comments__icontains=value)
)
class DataFileFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter(
method='search'
)
source_id = django_filters.ModelMultipleChoiceFilter(
queryset=DataSource.objects.all(),
label=_('Data source (ID)'),
)
source = django_filters.ModelMultipleChoiceFilter(
field_name='source__name',
queryset=DataSource.objects.all(),
to_field_name='name',
label=_('Data source (name)'),
)
class Meta:
model = DataFile
fields = ('id', 'path', 'last_updated', 'size', 'hash')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(path__icontains=value)
)
class JobFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
label=_('Search'),
)
created = django_filters.DateTimeFilter()
created__before = django_filters.DateTimeFilter(
field_name='created',
lookup_expr='lte'
)
created__after = django_filters.DateTimeFilter(
field_name='created',
lookup_expr='gte'
)
scheduled = django_filters.DateTimeFilter()
scheduled__before = django_filters.DateTimeFilter(
field_name='scheduled',
lookup_expr='lte'
)
scheduled__after = django_filters.DateTimeFilter(
field_name='scheduled',
lookup_expr='gte'
)
started = django_filters.DateTimeFilter()
started__before = django_filters.DateTimeFilter(
field_name='started',
lookup_expr='lte'
)
started__after = django_filters.DateTimeFilter(
field_name='started',
lookup_expr='gte'
)
completed = django_filters.DateTimeFilter()
completed__before = django_filters.DateTimeFilter(
field_name='completed',
lookup_expr='lte'
)
completed__after = django_filters.DateTimeFilter(
field_name='completed',
lookup_expr='gte'
)
status = django_filters.MultipleChoiceFilter(
choices=JobStatusChoices,
null_value=None
)
class Meta:
model = Job
fields = ('id', 'object_type', 'object_id', 'name', 'interval', 'status', 'user')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(user__username__icontains=value) |
Q(name__icontains=value)
)

View File

@@ -0,0 +1,4 @@
from .bulk_edit import *
from .bulk_import import *
from .filtersets import *
from .model_forms import *

View File

@@ -0,0 +1,46 @@
from django import forms
from django.utils.translation import gettext as _
from core.choices import DataSourceTypeChoices
from core.models import *
from netbox.forms import NetBoxModelBulkEditForm
from utilities.forms import add_blank_choice, BulkEditNullBooleanSelect, CommentField
__all__ = (
'DataSourceBulkEditForm',
)
class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
type = forms.ChoiceField(
choices=add_blank_choice(DataSourceTypeChoices),
required=False,
initial=''
)
enabled = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect(),
label=_('Enforce unique space')
)
description = forms.CharField(
max_length=200,
required=False
)
comments = CommentField(
label=_('Comments')
)
parameters = forms.JSONField(
required=False
)
ignore_rules = forms.CharField(
required=False,
widget=forms.Textarea()
)
model = DataSource
fieldsets = (
(None, ('type', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules')),
)
nullable_fields = (
'description', 'description', 'parameters', 'comments', 'parameters', 'ignore_rules',
)

View File

@@ -0,0 +1,15 @@
from core.models import *
from netbox.forms import NetBoxModelImportForm
__all__ = (
'DataSourceImportForm',
)
class DataSourceImportForm(NetBoxModelImportForm):
class Meta:
model = DataSource
fields = (
'name', 'type', 'source_url', 'enabled', 'description', 'comments', 'parameters', 'ignore_rules',
)

View File

@@ -0,0 +1,115 @@
from django import forms
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from core.choices import *
from core.models import *
from extras.forms.mixins import SavedFiltersMixin
from extras.utils import FeatureQuery
from netbox.forms import NetBoxModelFilterSetForm
from utilities.forms import (
APISelectMultiple, BOOLEAN_WITH_BLANK_CHOICES, ContentTypeChoiceField, DateTimePicker,
DynamicModelMultipleChoiceField, FilterForm,
)
__all__ = (
'DataFileFilterForm',
'DataSourceFilterForm',
'JobFilterForm',
)
class DataSourceFilterForm(NetBoxModelFilterSetForm):
model = DataSource
fieldsets = (
(None, ('q', 'filter_id')),
('Data Source', ('type', 'status')),
)
type = forms.MultipleChoiceField(
choices=DataSourceTypeChoices,
required=False
)
status = forms.MultipleChoiceField(
choices=DataSourceStatusChoices,
required=False
)
enabled = forms.NullBooleanField(
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
class DataFileFilterForm(NetBoxModelFilterSetForm):
model = DataFile
fieldsets = (
(None, ('q', 'filter_id')),
('File', ('source_id',)),
)
source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
required=False,
label=_('Data source')
)
class JobFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
('Attributes', ('object_type', 'status')),
('Creation', (
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
'started__after', 'completed__before', 'completed__after', 'user',
)),
)
object_type = ContentTypeChoiceField(
label=_('Object Type'),
queryset=ContentType.objects.filter(FeatureQuery('jobs').get_query()),
required=False,
)
status = forms.MultipleChoiceField(
choices=JobStatusChoices,
required=False
)
created__after = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
created__before = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
scheduled__after = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
scheduled__before = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
started__after = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
started__before = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
completed__after = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
completed__before = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
user = DynamicModelMultipleChoiceField(
queryset=User.objects.all(),
required=False,
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
)
)

View File

@@ -0,0 +1,111 @@
import copy
from django import forms
from core.models import *
from extras.forms.mixins import SyncedDataMixin
from netbox.forms import NetBoxModelForm
from netbox.registry import registry
from utilities.forms import CommentField, get_field_value
__all__ = (
'DataSourceForm',
'ManagedFileForm',
)
class DataSourceForm(NetBoxModelForm):
comments = CommentField()
class Meta:
model = DataSource
fields = [
'name', 'type', 'source_url', 'enabled', 'description', 'comments', 'ignore_rules', 'tags',
]
widgets = {
'type': forms.Select(
attrs={
'hx-get': '.',
'hx-include': '#form_fields input',
'hx-target': '#form_fields',
}
),
'ignore_rules': forms.Textarea(
attrs={
'rows': 5,
'class': 'font-monospace',
'placeholder': '.cache\n*.txt'
}
),
}
@property
def fieldsets(self):
fieldsets = [
('Source', ('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules')),
]
if self.backend_fields:
fieldsets.append(
('Backend Parameters', self.backend_fields)
)
return fieldsets
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Determine the selected backend type
backend_type = get_field_value(self, 'type')
backend = registry['data_backends'].get(backend_type)
# Add backend-specific form fields
self.backend_fields = []
for name, form_field in backend.parameters.items():
field_name = f'backend_{name}'
self.backend_fields.append(field_name)
self.fields[field_name] = copy.copy(form_field)
if self.instance and self.instance.parameters:
self.fields[field_name].initial = self.instance.parameters.get(name)
def save(self, *args, **kwargs):
parameters = {}
for name in self.fields:
if name.startswith('backend_'):
parameters[name[8:]] = self.cleaned_data[name]
self.instance.parameters = parameters
return super().save(*args, **kwargs)
class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
upload_file = forms.FileField(
required=False
)
fieldsets = (
('File Upload', ('upload_file',)),
('Data Source', ('data_source', 'data_file')),
)
class Meta:
model = ManagedFile
fields = ('data_source', 'data_file')
def clean(self):
super().clean()
if self.cleaned_data.get('upload_file') and self.cleaned_data.get('data_file'):
raise forms.ValidationError("Cannot upload a file and sync from an existing file")
if not self.cleaned_data.get('upload_file') and not self.cleaned_data.get('data_file'):
raise forms.ValidationError("Must upload a file or select a data file to sync")
return self.cleaned_data
def save(self, *args, **kwargs):
# If a file was uploaded, save it to disk
if self.cleaned_data['upload_file']:
self.instance.file_path = self.cleaned_data['upload_file'].name
with open(self.instance.full_path, 'wb+') as new_file:
new_file.write(self.cleaned_data['upload_file'].read())
return super().save(*args, **kwargs)

View File

View File

@@ -0,0 +1,20 @@
import graphene
from core import models
from netbox.graphql.fields import ObjectField, ObjectListField
from .types import *
from utilities.graphql_optimizer import gql_query_optimizer
class CoreQuery(graphene.ObjectType):
data_file = ObjectField(DataFileType)
data_file_list = ObjectListField(DataFileType)
def resolve_data_file_list(root, info, **kwargs):
return gql_query_optimizer(models.DataFile.objects.all(), info)
data_source = ObjectField(DataSourceType)
data_source_list = ObjectListField(DataSourceType)
def resolve_data_source_list(root, info, **kwargs):
return gql_query_optimizer(models.DataSource.objects.all(), info)

View File

@@ -0,0 +1,21 @@
from core import filtersets, models
from netbox.graphql.types import BaseObjectType, NetBoxObjectType
__all__ = (
'DataFileType',
'DataSourceType',
)
class DataFileType(BaseObjectType):
class Meta:
model = models.DataFile
exclude = ('data',)
filterset_class = filtersets.DataFileFilterSet
class DataSourceType(NetBoxObjectType):
class Meta:
model = models.DataSource
fields = '__all__'
filterset_class = filtersets.DataSourceFilterSet

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