Compare commits

...

166 Commits

Author SHA1 Message Date
Jeremy Stretch
839e5082a8 Misc cleanup
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-07-25 15:45:47 -04:00
Jeremy Stretch
68d39065ce Finish adding tests for changelog message functionality 2025-07-25 14:35:27 -04:00
Jeremy Stretch
7470147c1a Tweak test to generate expected number of change records 2025-07-25 14:33:56 -04:00
Jeremy Stretch
5bed9e8667 Add missing mixins for changelog message support 2025-07-25 14:22:04 -04:00
Jeremy Stretch
3fba47eb08 Incorporate changelog messages for object bulk view tests 2025-07-25 13:00:01 -04:00
Jeremy Stretch
6acde0f432 Incorporate changelog messages for object view tests
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
2025-07-24 17:38:11 -04:00
Jeremy Stretch
f17438132a Introduce get_random_string() utility function for tests 2025-07-24 16:25:27 -04:00
Jeremy Stretch
084f640566 Add ChangeLoggingMixin to necesssary model forms 2025-07-24 16:24:44 -04:00
Jeremy Stretch
a5d6173372 Fix changelog message support for VirtualChassis 2025-07-24 16:24:06 -04:00
Jeremy Stretch
5ab696e55b Add documentation 2025-07-24 14:41:37 -04:00
Jeremy Stretch
bdb0e5720d Enable changelog message support for bulk deletions 2025-07-24 14:02:44 -04:00
Jeremy Stretch
1615a369f9 Fix changelog_message assignment 2025-07-24 13:41:15 -04:00
Jeremy Stretch
ac26665f29 Add REST API support for changelog messages (WIP)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
2025-07-23 08:37:25 -04:00
Jeremy Stretch
f600429b7e Add message support for bulk import/update
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
2025-07-22 14:49:55 -04:00
Jeremy Stretch
0514bb4e60 Fix bulk operation templates 2025-07-18 14:17:14 -04:00
Jeremy Stretch
2044802586 Cosmetic improvements to form fields 2025-07-18 11:19:39 -04:00
Jeremy Stretch
0703fe7852 Add changelog message support for bulk edit & bulk delete 2025-07-18 10:31:42 -04:00
Jeremy Stretch
1b11895c90 Fix tests 2025-07-18 09:43:21 -04:00
Jeremy Stretch
8d5436876e Enable changelog messages for single object operations 2025-07-18 09:43:20 -04:00
Jeremy Stretch
5e5c46f77c Set max length on changelog message 2025-07-18 09:42:37 -04:00
Jeremy Stretch
aaaf346e5f Add message field to ObjectChange model 2025-07-18 09:42:37 -04:00
Jeremy Stretch
cebc56e5cc Closes #19891: Bulk operation jobs (#19897)
* Add background_job toggle to BulkEditForm

* Account for bug fix in v4.3.4

* Enable background jobs for bulk edit & bulk delete

* Move background_job field to a mixin

* Cosmetic improvements

* Misc cleanup

* Fix BackgroundJobMixin
2025-07-18 08:24:38 -05:00
Jeremy Stretch
7f2b744a53 Closes #18528: Introduce HOSTNAME config parameter (#19894)
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-07-17 10:09:30 -07:00
Jeremy Stretch
733dd81f0e Closes #19738: Deprecate the direct assignment of a VLAN to a site (#19904) 2025-07-17 08:45:56 -05:00
Jeremy Stretch
32fb3869a4 Closes #19829: Move object types REST API endpoint to core app
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
2025-07-16 14:54:03 -04:00
Jeremy Stretch
c5ffab0c28 Closes #18349: Replace houskeeping management command with a system job (#19815) 2025-07-16 14:50:11 -04:00
Jeremy Stretch
5f8a4f6c43 Merge branch 'main' into feature
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
2025-07-16 09:52:58 -04:00
github-actions
b88b5b0b1b Update source translation strings
Some checks failed
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
2025-07-16 05:06:12 +00:00
Jason Novinger
6eeb382512 Release v4.3.4 (#19887)
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-07-15 12:56:11 -05:00
Jeremy Stretch
e5d6c71171 Fixes #19633: Log all evaluations of invalid event rule conditions (#19885)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
* flush_events() should catch only import errors

* Fixes #19633: Log all evaluations of invalid event rule conditions

* Correct comment
2025-07-15 10:25:25 -05:00
Jeremy Stretch
f777bfee2e Fixes #19876: Remove Markdown rendering from CustomFieldChoiceSet description field (#19877) 2025-07-15 07:55:26 -07:00
bctiemann
8b63eb64c1 Merge pull request #19860 from netbox-community/19839-nested-object-parent-export
Fixes #19839: Enable export of parent assignment for recursively nested objects
2025-07-15 08:42:43 -04:00
Jason Novinger
cff29f9551 Fixes #19413: Group custom fields in filter tab
Replaced manual rendering of custom fields in the filter tab with the
`render_custom_fields` template tag. This change ensures that custom fields are
properly grouped, addressing the issue where they were previously displayed
without their associated groups.
2025-07-15 08:41:38 -04:00
github-actions
a5c0cae112 Update source translation strings 2025-07-15 05:05:26 +00:00
Peter
2a27e475e4 Fixes #19828: Add L2VPNTerminationType to InterfaceType (#19879)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
Co-authored-by: swoga <3697291+swoga@users.noreply.github.com>
2025-07-14 14:42:53 -05:00
Jason Novinger
44efa037cc Fixes #19800: ModuleType import supports associating ModuleTypeProfile (#19803)
* Fixes #19800: ModuleType import supports associating ModuleTypeProfile

* Fixes up ModuleTypeTestCase to include bulk import testing

Also includes an additional regression assertion.

* Address PR feedback

I ultimately left the extra asserts in for test_bulk_import_objects_with_permissionsince
since the parent test is currently only testing against number of
objects successfully imported. Will file a follow up FR to improve that
test.
2025-07-14 15:22:52 -04:00
Jeremy Stretch
6c17629159 Fixes #19841: Add white background to upgrade paths image 2025-07-14 15:08:27 -04:00
Jeremy Stretch
f13d028c98 Fixes #19827: Enforce uniqueness for device role names & slugs (#19859) 2025-07-14 09:13:44 -07:00
bctiemann
f5d32b1bf1 Closes: #19793 - Nav menu link customization (#19794)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
* Support menu items that are callables

* Fix quote on add button

* Clarify docstring to differentiate link and url

* Back out support for callables but keep alternate prerendered url param

* Make url a property on MenuItem/PluginMenuItem etc, overridable via a setter

* Use reverse_lazy instead of reverse

* Use reverse_lazy instead of reverse
2025-07-14 10:39:24 -04:00
Jeremy Stretch
f05897d61a Closes #18811: Match full-form IPv6 addresses in global search (#19873)
* Closes #18811: Match full-form IPv6 addresses in global search

* Fix typo
2025-07-14 09:28:30 -05:00
Jeremy Stretch
21a840c32e Closes #19816: Implement a logging mechanism for background jobs (#19838)
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
* Initial work on #19816

* Use TZ-aware timestamps

* Deserialize JobLogEntry timestamp

* Repurpose RQJobStatusColumn to display job entry level badges

* Misc cleanup

* Test logging

* Refactor HTML templates

* Update documentation
2025-07-14 08:52:50 -05:00
Luke Anderson
b5421f1cd6 Fixes #19870: Correct Documentation Formatting for Public Demo Instance URL 2025-07-14 08:45:26 -04:00
Jeremy Stretch
23cc4f1c41 Fixes #19839: Enable export of parent assignment for recursively nested objects 2025-07-10 12:41:11 -04:00
Jeremy Stretch
875a641687 Closes #19589: Background job for bulk operations (#19804)
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
* Initial work on #19589

* Add tooling for handling background requests

* UI notification should link to enqueued job

* Use an informative name for the job

* Disable background jobs for file uploads
2025-07-10 09:32:35 -05:00
Jeremy Stretch
6022433a40 Closes #19134: Allow negative values for interface TX power (#19847)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
2025-07-09 10:17:41 -07:00
Jeremy Stretch
878c624eaf Closes #19722: Extend the object types REST API endpoint (#19826) 2025-07-09 08:43:24 -07:00
Olexandr88
9c2cd66162 Update README.md
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-07-09 10:53:40 -04:00
github-actions
f61a2964c8 Update source translation strings 2025-07-09 05:04:52 +00:00
Jeremy Stretch
90e8a61670 Closes #19739: Add a user preference for CSV delimiter in table exports (#19824)
* Closes #19739: Add a user preference for CSV delimiter in table exports

* Pass custom delimiter when exporting entire table
2025-07-08 14:11:40 -05:00
Jason Novinger
ee94fb0b94 Closes #19550: Enhancement: Refactor rack elevations template for lazy loading /dcim/rack-elevations/ (#19823)
* Refactor rack elevation template to use htmx for dynamic loading and improved user experience

* rework to prevent dup loading

* Update netbox/templates/dcim/inc/rack_elevation.html

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

* Update netbox/templates/dcim/inc/rack_elevation.html

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

* Move inline styles to styles/custom/racks.css

---------

Co-authored-by: tony.nealon@wholesailnetworks.com <tony.nealon@wholesailnetworks.com>
Co-authored-by: tbotnz <tonynealon1989@gmail.com>
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-07-08 11:20:04 -04:00
Harry
8fb8f4c75b Closes #19571: Create expansion_card.json (#19689)
* Create expansion_card.json

* Update 0206_load_module_type_profiles.py

* Update expansion_card.json

Fixed
2025-07-08 08:27:48 -05:00
Jeremy Stretch
a1cd81ff35 Closes #17413: Permit identical names for platforms belonging to different manufacturers (#19814) 2025-07-07 10:38:01 -07:00
github-actions
e33793dc82 Update source translation strings 2025-07-03 05:04:46 +00:00
Jeremy Stretch
3b8841ee3b Fixes #19806: Introduce JobFailed exception to allow marking background jobs as failed (#19807) 2025-07-02 14:02:49 -05:00
Jeremy Stretch
ce12de8b6d Closes #19231: Add bulk renaming support for all models (#19795)
* Closes #19231: Add bulk renaming support for all models

* Introduce a template filter for getattr()

* Extend BulkRenameView to support arbitrary field names

* Address bulk renaming support for remaining models

* Bulk rename URL resolution should fail silently

* Update documentation

* Fix bulk button rendering for HTMX requests
2025-07-02 13:35:34 -05:00
dieck
ea4c205a37 Upgrade documentation: have git fetch new tags
fixes #19778
2025-07-02 13:59:56 -04:00
Jeremy Stretch
601a77ac73 Closes #19735: Implement reuable bulk operations classes (#19774)
* Initial work on #19735

* Work in progress

* Remove ClusterRemoveDevicesView (anti-pattern)

* Misc cleanup

* Fix has_bulk_actions

* Fix has_bulk_actions for ObjectChildrenView

* Restore clone button

* Misc cleanup

* Clean up custom bulk actions

* Rename individual object actions

* Collapse into a single template tag

* Fix support for legacy action dicts

* Rename bulk attr to multi

* clone_button tag should fail silently if view name is invalid

* Clean up action buttons

* Fix export button label

* Replace clone_button with an ObjectAction

* Create object actions for adding device/VM components

* Move core_sync.html to core app

* Remove extra_bulk_buttons from template doc
2025-06-30 13:03:07 -04:00
github-actions
2a5d3abafb Update source translation strings 2025-06-27 05:03:03 +00:00
Jeremy Stretch
71e6ea5785 Release v4.3.3 2025-06-26 14:42:03 -04:00
Jason Novinger
0a9887b42f Fixes #19745: properly check IP assignment to FHRPGroup
- Expands the logic in ServiceImportForm.clean() to handle properly
  validation of FHRPGroup assignments and maintain the existing
  [VM]Interface validation checks.
- Includes an extension to ServiceTestCase.csv_data to act as a
  regression test for this behavior.
2025-06-26 12:09:14 -04:00
Tobias Genannt
3ecf29d797 Fixes #17719: User settings for table stripe (#19526)
* Fixes #17719: User setting table stripe

* Tweak user preference name

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-06-26 12:03:17 -04:00
Jason Novinger
c48e4f590e Fixes #19640: restores device/vm FHRPGroupAssignment graphql filters (#19712)
* Fixes #19640: restores device/vm FHRPGroupAssignment graphql filters

* Add docstring for device_filter helper function
2025-06-26 12:00:56 -04:00
github-actions
aee83a434a Update source translation strings 2025-06-26 05:02:35 +00:00
Arthur Hanson
a17699d261 19644 Make atomic use correct database instead of default (#19651)
* 19644 set atomic transactions to appropriate database

* 19644 set atomic transactions for Job Script run

* 19644 set atomic transactions to appropriate database

* 19644 set atomic transactions to appropriate database

* 19644 fix review comments

* 19644 fix review comments
2025-06-25 15:00:26 -04:00
Jeremy Stretch
f97d07a11c Update README & contributing guide (#19727) 2025-06-20 07:56:45 -07:00
github-actions
1fd3d390ae Update source translation strings 2025-06-20 05:02:37 +00:00
Omripresent
7dab7d730d Fixes: #19492: Add Save Button to Script Output Window (#19721)
* Add condition to ScriptResultView.get function to generate a download
file of job output if job is completed

* Update template script_result.html adding a download button to trigger
output download in ScriptResultView.get

* Simplify conditional logic; tweak timestamp format

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-06-19 13:31:54 -04:00
Jason Novinger
c660f1c019 Fixes #19702: add NotificationGroup.event_rules GenericRelation
The collector we use to notify users about dependent object that will be
deleted does handle GFKs. However, a GenericRelation must be set up on
the other end.
2025-06-19 09:41:40 -04:00
github-actions
334b45f55a Update source translation strings 2025-06-17 05:02:05 +00:00
Martin Hauser
e6c1cebd34 Closes #19499 - Add WirelessLink Bulk Import Support by Device and Interface Names (#19679) 2025-06-16 11:19:56 -07:00
Arthur Hanson
a9af541e81 Fixes #19529: fix CLI running of scripts (#19698)
* 19529 fix custom script path

* 19529 fix custom script path

* 19529 fix custom script path

* 19529 fix custom script path

* 19680 add object_change migrator

* 19680 optimize migration

* 19680 optimize migration
2025-06-16 07:17:38 -05:00
github-actions
f706572113 Update source translation strings 2025-06-14 05:02:08 +00:00
Arthur Hanson
6a6286777c Fixes #19680 fix deletion dependency order for GenericRelations (#19681)
* #19680 fix deletion dependency order for GenericRelations

* 19680 add test

* 19680 fix Collector and test

* 19680 put on changeloggingmixin

* 19680 cleanup

* 19680 cleanup

* 19680 cleanup

* 19680 skip changelog update for deleted objects

* 19680 remove print
2025-06-13 16:08:59 -05:00
Omripresent
afeddee10d Fixes #19687: Treat cellular interface type as not connectable (#19691)
* Add cellular interface types to WIRELESS_IFACE_TYPES const
Add cable termination test for cellular interface

* Add regression tag to cellular test
2025-06-12 09:49:09 -05:00
Arthur Hanson
a48bee2a2e 19555 fix script API validation for scheduled_at (#19693)
* 19555 fix script API validation for scheduled_at

* 19555 fix script API validation for scheduled_at
2025-06-11 12:41:45 -05:00
github-actions
b9db6ebd63 Update source translation strings 2025-06-11 05:02:55 +00:00
Martin Hauser
9e0493c64c Closes #17183 - Add Object Types Field to Tag Bulk Import Form (#19639) 2025-06-10 09:13:59 -07:00
hblandford
e3509c092a Closes #19684: Update pyproject.toml version to 4.3.2 (#19688)
Co-authored-by: Hugh Blandford <hugh.blandford@gmail.com>
2025-06-10 09:56:55 -05:00
bctiemann
762cfc7d10 Merge pull request #19672 from netbox-community/19659-service-form-initial-data
Fixes #19659: Populate initial device/VM selection for "add a service" button
2025-06-10 08:49:23 -04:00
bctiemann
522f80ed9d Merge pull request #19642 from pheus/17420-add-plugins-content-type-removal-instructions
Closes #17420 - Add Instructions for Cleaning up Content Types after Uninstalling a Plugin
2025-06-10 08:39:16 -04:00
github-actions
fd6062de75 Update source translation strings 2025-06-10 05:02:15 +00:00
gizmonicus
c872cce59f Fixes: #19616: configuration_example.py has inaccurate STORAGE_BACKEND examples (#19657) 2025-06-09 11:14:52 -07:00
Jeremy Stretch
dc8267d890 Fixes #19673: Ignore custom field references when compiling table prefetches (#19674) 2025-06-09 11:12:48 -07:00
Aaron
2bfb9f4ed0 Fixes #19617: Inconsistent styling of Connect buttons (#19682) 2025-06-09 10:21:28 -04:00
Martin Hauser
dda0a55e5e fix(ipam): Correct usage of the queryset.none method (#19678)
Ensures the `queryset.none()` method is called properly with
parentheses. This fixes a potential issue where the method would not
execute as intended, improving the stability and correctness of the
filter logic.
2025-06-09 07:45:40 -05:00
Martin Hauser
2680f855ff fix(wireless): Correct validation error field reference
Fixes the reference from `interface_a` to `interface_b` in the
validation error message for WirelessLink. Ensures the correct field is
indicated during validation errors.
2025-06-06 15:27:06 -04:00
Jeremy Stretch
6ca791850a Closes #19668: Remove obsolete docs publication step from release checklist (#19675) 2025-06-06 13:26:43 -05:00
Jeremy Stretch
43df06f210 Fixes #19667: Fix TypeError exception when creating a new module profile type with no schema (#19671) 2025-06-06 13:25:19 -05:00
Jeremy Stretch
7e6b1bbd79 Fixes #19659: Populate initial device/VM selection for 'add a service' button 2025-06-06 12:26:05 -04:00
Jeremy Stretch
0e68901022 Release v4.3.2 (#19656)
* Release v4.3.2

* Correct strawberry-graphql version
2025-06-05 15:56:06 -04:00
Jeremy Stretch
179c06ec20 Closes #19627: Object change migrators (#19628)
* Initial work on ObjectChange data migrations

* Fix migration bug

* Add migrators for MAC address assignments

* Update reverting kwarg; allow pop() to fail

* Cross-reference MAC address migrators

* Split migrator logic across migrations

* Add missing migrator
2025-06-05 11:47:59 -04:00
mr1716
bd8cf64ded #19638 Update Django URLs To Stable Version (#19649)
* Update Django URLs To Stable Version

* Update docs/configuration/security.md

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-06-05 11:38:07 -04:00
mr1716
67b42710ef #19634 Update values to ensure consistency when referencing values set (#19635)
* Update values to ensure consistency when referencing values see\t

* Update required-parameters.md For Updated Django Link

* Update required-parameters.md to fix Django link

* Update error-reporting.md Remove Number Formatting

* Update docs/configuration/error-reporting.md

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-06-05 10:03:22 -04:00
github-actions
67d62a2089 Update source translation strings 2025-06-05 05:02:10 +00:00
Jason Novinger
e24fa2ee4d Fixes #19610: FieldError when sorting Tunnel Termination on tenant (#19612) 2025-06-04 13:50:12 -07:00
bctiemann
5fe5b2e7c4 Merge pull request #19630 from netbox-community/19599-user-changelog-sorting
Fixes #19599: Prevent exception when sorting user's recent activity
2025-06-04 16:17:34 -04:00
Martin Hauser
d68f42140f Closes #19535 - Add Project Stanza to pyproject toml (#19643)
* feat(project): Add project metadata to pyproject.toml

Introduces project metadata, including name, version, authors, and
description, to `pyproject.toml` for enhanced package definition.
Also includes URLs for source code, documentation, and issue tracking.

* docs(release): Add checklist item for Python versions in pyproject.toml

Include step to update minimum and supported Python versions
in the project metadata file as part of the release process.

* docs(release): Update checklist to include pyproject.toml versioning

Add a step to update the version in `pyproject.toml` alongside
`release.yaml`.

* feat(project): Update pyproject.toml for best practices

Refreshes metadata to resolve deprecations and follow packaging best
practices. Updates include description, license, Python versions,
classifiers, maintainers, and repository URLs for improved compliance.

* fix(project): Update repository URL key in pyproject.toml

Replaces the 'Repository' key with 'Source' in accordance with updated
metadata conventions. This ensures compliance with modern best
practices for project metadata.

* fix(project): Specify Python 3 :: Only in classifiers

Updates the Python version classifier in `pyproject.toml` to indicate
support exclusively for Python 3. This change ensures clarity in the
supported Python versions for the project metadata.
2025-06-04 14:37:18 -05:00
Jason Novinger
95d0ca56a7 Fixes #19487: fix ordering issues with CircuitTerminationTable/TunnelTerminationTable configuration (#19600)
* Fixes #19487: make CircuitTermination.termination GFK not orderable

* Add test to ensure no more broken sorting for CircuitTerminationTable

* Fix CircuitTerminationTable.site_group accessor

* Make TunnelTerminationTable.termination GFK field non-orderable
2025-06-04 11:48:23 -05:00
Martin Hauser
716acaa657 docs(plugins): Add guide for cleaning up Content Types
Provides instructions for removing stale Content Types and related
Permissions after uninstalling a plugin. Includes steps for identifying
and safely deleting stale entries to prevent issues in the permissions
management UI.
2025-06-04 17:58:29 +02:00
github-actions
ecb8656723 Update source translation strings 2025-06-04 05:02:13 +00:00
Marcus Weiner
065511fca2 Allow filtering IP addresses by family in GraphQL (#19621) 2025-06-03 11:15:55 -05:00
Jeremy Stretch
77f0eeb7bf Fixes #19587: Occupied filter should match on interfaces terminating a wireless link (#19631) 2025-06-03 07:34:39 -07:00
mr1716
f45b671fc9 #19619 update documentation for consistency (#19620)
* Update system.md For Capitalization Consistency

* Update security.md For Consistency

* Update system.md To Improve Consistency

* Update security.md for Consistency

* Update docs/configuration/security.md

* Update docs/configuration/system.md

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-06-03 09:13:10 -04:00
Arthur Hanson
b1cbdbe079 19623 show description on provider account detail view (#19629)
* 19623 show description on provider account detail view

* Fix indentation

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-06-03 09:03:44 -04:00
Jeremy Stretch
e5e7a66cb9 Apply fix to user view as well 2025-06-03 08:58:16 -04:00
Jeremy Stretch
357ae44cde Fixes #19599: Prevent exception when sorting user's recent activity 2025-06-02 15:14:11 -04:00
mr1716
b62f2347c5 Closes #19611: Update index.md To Spell Acronym First Time It's Found (#19614)
* Update index.md To Spell Acronym First Time It's Found

* Update index.md to make lower case
2025-05-30 13:57:22 -05:00
bctiemann
0c6726d40f Merge pull request #19570 from netbox-community/19490-jinja-template-fails-with-empty-include
Fixes #19490: restores nesting behavior of DataSource-based ConfigTemplate
2025-05-28 09:00:20 -04:00
Jason Novinger
cc099e86e1 Fixes #19520: restores ability to set Prefix.scope via API (#19588) 2025-05-27 10:32:36 -07:00
Alexander Haase
a97b438b7e Fixes #19530: Overhaul documentation for plugin views (#19530) 2025-05-27 10:07:48 -07:00
Jason Novinger
d7672ab260 Fixes #19490: restores nesting behavior of DataSource-based ConfigTemplates
The ability to render nested templates was accidentally removed with the
implementation of #17653, which normalized the behavior of various Jinja2
template rendering actions.

This fix restores that behavior while retaining the normalized behavior.
This fix also includes regression tests to ensure this behavior is not
removed accidentally again in the future.
2025-05-23 16:34:22 -05:00
github-actions
b3d318cbe1 Update source translation strings 2025-05-23 05:02:08 +00:00
bctiemann
2804359cdd Merge pull request #19527 from Omripresent/fix-19496
Fixes #19496: Page error on config render with empty output
2025-05-22 08:09:58 -04:00
bctiemann
e8d08c4d38 Merge pull request #19485 from Omripresent/main
Fixes: #19475 - VM Interface VLAN availibility when cluster and VLAN group scope is dcim.location
2025-05-21 20:24:45 -04:00
bctiemann
98d9e7f8d5 Merge pull request #19516 from larsen0815/patch-2
Fixes #19502: Improve upgrade instructions
2025-05-21 17:58:03 -04:00
Jeremy Stretch
51d046b1f5 Closes #19521: Clean up test suite output (#19524) 2025-05-21 09:57:32 -07:00
github-actions
88565e8f68 Update source translation strings 2025-05-20 05:02:15 +00:00
Jason Novinger
a2a8779ebc Fixes #19415: Increased Circuit/WirelessLink distance upper limit (#19495)
* Fixes #19415: Increased Circuit/WirelessLink absolute distance upper limit

Also adds form validation that provides a useful message to the user
rather than a 500 error with potentially little information.

* Include forgotten migration files

* Remove unnecessary comments

* Remove more unnecessary comments

* Addresses PR feedback

* Gah, remove django migration header comment

* Clean up new has_field_errors mechanism, fix issue with ObjectAttribute

* Address PR feedback, revert changes to render_fieldset template tag
2025-05-19 08:38:30 -04:00
Jason Novinger
03ff535772 Fixes #19510: Re-adds IPAddressType.assigned filter 2025-05-16 15:30:07 -04:00
Omri Abu
e6d364b250 Initilize error_message to empty string
Update template branching for empty template render output
2025-05-16 14:46:43 -04:00
Omripresent
be07f222f6 Merge branch 'netbox-community:main' into main 2025-05-16 14:40:35 -04:00
larsen0815
21f5fe873c Fixes typo 2025-05-16 15:09:52 +02:00
larsen0815
83dc65acb5 Improve upgrade instructions 2025-05-16 12:19:07 +02:00
github-actions
b6c8502408 Update source translation strings 2025-05-16 05:02:28 +00:00
Aaron
4795fab16f Fixes #19486: Fix connection card rendering for Console Server Ports (#19498)
This fixes a visual anomaly with the console server port details page, where cards are inadvertantly nested inside each other.
2025-05-15 09:40:03 -05:00
github-actions
de2e2b5c82 Update source translation strings 2025-05-15 05:02:17 +00:00
Jeremy Stretch
cf7ab43f39 Closes #19493: Change filter() to filter_type() (#19494) 2025-05-14 08:34:25 -07:00
Renato Almeida de Oliveira
1700a9265c Closes: #19200 Add Virtual Chassis name to pane on Device View (#19369) 2025-05-14 08:28:11 -04:00
Aaron Queen
39b03abe72 Use colored labels when displaying virtual circuit types 2025-05-14 08:03:46 -04:00
github-actions
b497b85665 Update source translation strings 2025-05-14 05:02:15 +00:00
Omri Abu
0d29e5776c Update get_for_virtualmachine to support lookup by cluster location
scope
Update test case to include location scoped cluster
2025-05-13 22:20:54 -04:00
Jeremy Stretch
cbe14b76c0 Release v4.3.1 2025-05-13 15:44:10 -04:00
Jeremy Stretch
3d1334a798 Fixes #19464: Fix bulk editing of inventory items from device view (#19477) 2025-05-13 10:23:02 -07:00
Jeremy Stretch
408550d3c7 Fixes #19463: Hide button dropdown for tables which do not support saved configs (#19481) 2025-05-13 10:22:15 -07:00
bctiemann
6b9b5c4184 Merge pull request #19456 from netbox-community/19444-contact-groups-changelog
Fixes #19444: Fix change logging for contact group assignments
2025-05-13 11:39:59 -04:00
Jeremy Stretch
59dce87ba0 Fixes #19465: Fix ability to clear assigned prefix scope in UI (#19479) 2025-05-13 10:21:06 -05:00
bctiemann
f6a85775d7 Merge pull request #19480 from netbox-community/19472-vdc-device-column
Fixes #19472: Fix device column rendering in virtual device contexts table
2025-05-13 11:12:55 -04:00
Jeremy Stretch
33887e7c69 Fixes #19472: Fix devie column rendering in virtual device contexts table 2025-05-13 10:46:41 -04:00
github-actions
b57ceca2fd Update source translation strings 2025-05-13 05:02:09 +00:00
bctiemann
8e13f2a9ec Merge pull request #19443 from netbox-community/19440-migration-connections
Fixes #19440: Ensure data migrations use the correct database connection
2025-05-12 14:13:31 -04:00
bctiemann
6af4f5d7ee Merge pull request #19400 from netbox-community/19397-graphql-IPRangeFilter-role
Fixes #19397: Fix filtering IP ranges by role in GraphQL API
2025-05-12 14:05:13 -04:00
bctiemann
6054f8197d Merge pull request #19418 from netbox-community/19381-script
19381 fix data file script sync
2025-05-12 11:32:16 -04:00
github-actions
fc98294812 Update source translation strings 2025-05-10 05:02:10 +00:00
Martin Hauser
4b58678823 feat(dcim): Add 2.5 Gbps and 5 Gbps options to InterfaceSpeedChoices (#19445)
Extend `InterfaceSpeedChoices` to include 2.5 Gbps and 5 Gbps values.
This improves support for modern interface speeds and enhances API data
validation.
2025-05-09 14:02:30 -05:00
Jeremy Stretch
abeed474f6 Fixes #19444: Fix changeloggin for contact group assignments 2025-05-09 14:21:02 -04:00
Martin Hauser
d1303f49e6 Fixes #19432 - Update PostgreSQL Version in Programming Error Message (#19446) 2025-05-09 07:38:47 -07:00
Martin Hauser
127452f4d5 feat(search): Add search index for tags
Introduces a search index for the Tag model to enable global search for
Tags. Includes fields for name, slug, and description with corresponding
weight values. Display attributes are limited to the description field.

Fixes #17073
2025-05-09 08:55:05 -04:00
github-actions
2979067b65 Update source translation strings 2025-05-09 05:02:08 +00:00
Abraham Vegh
6c07aeeded Add 1000BASE-SX interface type 2025-05-08 15:45:03 -04:00
Jeremy Stretch
76aa255f07 Fixes #19440: Ensure data migrations use the correct database connection 2025-05-08 14:53:52 -04:00
dianbofa
0c04a8d301 feat(core): Add queue_name parameter to Job.enqueue() method (#19424) 2025-05-08 08:39:55 -07:00
Corubba
6665810a6d Fixes #19361: Fix wrong graphql field data-types (#19373) 2025-05-07 08:29:52 -07:00
Jason Novinger
8baf15771a Fixes #17107: Circuit to Provider Network cabling visual bug 2025-05-07 09:28:02 -04:00
github-actions
045417c45c Update source translation strings 2025-05-07 05:02:11 +00:00
Arthur
aac333a6d4 19381 fix data file script sync 2025-05-06 11:50:02 -07:00
Andrey Tikhonov
145ee11a3f Fixes #19309: N+1 problem on /interfaces, /ip-addresses and /prefixes requests (#19304)
* Fixes N+1 problem on /interfaces, /ip-addresses and /prefixes requests

* remove extra .all()

* more prefetch for IPAddressViewSet
2025-05-06 11:47:44 -05:00
github-actions
94618a9dfb Update source translation strings 2025-05-06 05:02:20 +00:00
mr1716
21e813cee2 #19404 Deduplicate IP Range API Serializer (#19405) 2025-05-05 14:31:12 -05:00
Étienne Brunel
2c014bade5 fix: Set qinq_role allow_null to True 2025-05-05 10:16:05 -04:00
mr1716
b17bfef7e5 Fixes #19370: Update documentation default values (#19374)
* Update security.md for default values

* Update plugins.md documentation default formatting

* Tweak punctuation

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-05-05 09:33:59 -04:00
Arthur Hanson
88f7b6508c 19380 call configure on embedded tables (#19390)
* 19380 call configure on  embedded tables

* 19380 call configure on  embedded tables

* 19380 call configure on  embedded tables
2025-05-05 09:29:32 -04:00
Jeremy Stretch
bd4f1e7d2f Fixes #19397: Fix filtering IP ranges by role in GraphQL API 2025-05-05 08:41:46 -04:00
Jeremy Stretch
6e49cee718 Fixes #19376: Fix FieldDoesNotExist exception when global search results include a contact (#19389) 2025-05-02 14:24:08 -05:00
Jeremy Stretch
4868818576 Fixes #19375: Fix table configuration after applying a saved table config (#19385) 2025-05-02 10:13:28 -07:00
Jeremy Stretch
7cd5dc0c84 Closes #19383: Extend security policy to provide guidance on compliance reporting 2025-05-02 10:20:57 -04:00
github-actions
aea51df06c Update source translation strings 2025-05-02 05:02:18 +00:00
371 changed files with 59809 additions and 50348 deletions

View File

@@ -15,7 +15,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.3.0
placeholder: v4.3.4
validations:
required: true
- type: dropdown

View File

@@ -27,7 +27,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.3.0
placeholder: v4.3.4
validations:
required: true
- type: dropdown

View File

@@ -8,7 +8,7 @@
</h3>
<h3>
:jigsaw: <a href="#jigsaw-creating-plugins">Create a plugin</a> &middot;
:rescue_worker_helmet: <a href="#rescue_worker_helmet-become-a-maintainer">Become a maintainer</a> &middot;
:briefcase: <a href="#briefcase-looking-for-a-job">Work with us!</a> &middot;
:heart: <a href="#heart-other-ways-to-contribute">Other ideas</a>
</h3>
</div>
@@ -109,21 +109,9 @@ Do you have an idea for something you'd like to build in NetBox, but might not b
Check out our [plugin development tutorial](https://github.com/netbox-community/netbox-plugin-tutorial) to get started!
## :rescue_worker_helmet: Become a Maintainer
## :briefcase: Looking for a Job?
We're always looking for motivated individuals to join the maintainers team and help drive NetBox's long-term development. Some of our most sought-after skills include:
* Python development with a strong focus on the [Django](https://www.djangoproject.com/) framework
* Expertise working with PostgreSQL databases
* Javascript & TypeScript proficiency
* A knack for web application design (HTML & CSS)
* Familiarity with git and software development best practices
* Excellent attention to detail
* Working experience in the field of network operations & engineering
We generally ask that maintainers dedicate around four hours of work to the project each week on average, which includes both hands-on development and project management tasks such as issue triage. Maintainers are also encouraged (but not required) to attend our bi-weekly Zoom call to catch up on recent items.
Interested? You can contact our lead maintainer, Jeremy Stretch, at jeremy@netbox.dev or on the [NetDev Community Slack](https://netdev.chat/). We'd love to have you on the team!
At [NetBox Labs](https://netboxlabs.com/), we're always looking for highly skilled and motivated people to join our team. While NetBox is a core part of our product lineup, we have an ever-expanding suite of solutions serving the network automation space. Check out our [current openings](https://netboxlabs.com/careers/) to see if you might be a fit!
## :heart: Other Ways to Contribute

View File

@@ -6,9 +6,9 @@
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=main" alt="CI status" /></a>
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
<p>
<strong><a href="https://github.com/netbox-community/netbox/">NetBox Community</a></strong> |
<strong><a href="https://netboxlabs.com/community/">NetBox Community</a></strong> |
<strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> |
<strong><a href="https://netboxlabs.com/netbox-enterprise/">NetBox Enterprise</a></strong>
</p>

View File

@@ -14,6 +14,12 @@ Administrators are encouraged to adhere to industry best practices concerning th
* Prohibit access to your database from clients other than the NetBox application
* Keep your deployment updated to the most recent stable release
## Compliance Reporting
Please note that security compliance reports (e.g. SOC 2) are provided by NetBox Labs only to customers using NetBox Cloud or NetBox Enterprise. They are not available to users of self-hosted NetBox Community Edition.
If you would like to consider upgrading to NetBox Cloud or Enterprise, please contact `sales@netboxlabs.com`.
## Reporting a Suspected Vulnerability
If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so by emailing `security@netboxlabs.com`. Please ensure that your report meets all the following conditions:

View File

@@ -14,6 +14,10 @@ django-debug-toolbar
# https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst
django-filter
# Django Debug Toolbar extension for GraphiQL
# https://github.com/flavors/django-graphiql-debug-toolbar/blob/main/CHANGES.rst
django-graphiql-debug-toolbar
# HTMX utilities for Django
# https://django-htmx.readthedocs.io/en/latest/changelog.html
django-htmx
@@ -108,6 +112,7 @@ nh3
# Fork of PIL (Python Imaging Library) for image processing
# https://github.com/python-pillow/Pillow/releases
# https://pillow.readthedocs.io/en/stable/releasenotes/
Pillow
# PostgreSQL database adapter for Python
@@ -126,21 +131,22 @@ requests
# https://github.com/rq/rq/blob/master/CHANGES.md
rq
# Social authentication framework
# https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md
social-auth-core
# Django app for social-auth-core
# https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md
social-auth-app-django
# Social authentication framework
# https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md
social-auth-core
# Strawberry GraphQL
# https://github.com/strawberry-graphql/strawberry/blob/main/CHANGELOG.md
strawberry-graphql
# Strawberry GraphQL Django extension
# https://github.com/strawberry-graphql/strawberry-django/releases
strawberry-graphql-django
# See #19771
strawberry-graphql-django==0.60.0
# SVG image rendering (used for rack elevations)
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst

View File

@@ -329,6 +329,7 @@
"100base-tx",
"100base-t1",
"1000base-t",
"1000base-sx",
"1000base-lx",
"1000base-tx",
"2.5gbase-t",

View File

@@ -1,17 +0,0 @@
[Unit]
Description=NetBox Housekeeping Service
Documentation=https://docs.netbox.dev/
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=netbox
Group=netbox
WorkingDirectory=/opt/netbox
ExecStart=/opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py housekeeping
[Install]
WantedBy=multi-user.target

View File

@@ -1,9 +0,0 @@
#!/bin/sh
# This shell script invokes NetBox's housekeeping management command, which
# intended to be run nightly. This script can be copied into your system's
# daily cron directory (e.g. /etc/cron.daily), or referenced directly from
# within the cron configuration file.
#
# If NetBox has been installed into a nonstandard location, update the paths
# below.
/opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py housekeeping

View File

@@ -1,13 +0,0 @@
[Unit]
Description=NetBox Housekeeping Timer
Documentation=https://docs.netbox.dev/
After=network-online.target
Wants=network-online.target
[Timer]
OnCalendar=daily
AccuracySec=1h
Persistent=true
[Install]
WantedBy=multi-user.target

View File

@@ -1,49 +0,0 @@
# Housekeeping
NetBox includes a `housekeeping` management command that should be run nightly. This command handles:
* 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#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`.
## Scheduling
### Using Cron
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.
```shell
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
```
!!! note
On Debian-based systems, be sure to omit the `.sh` file extension when linking to the script from within a cron directory. Otherwise, the task may not run.
### Using Systemd
First, create symbolic links for the systemd service and timer files. Link the existing service and timer files from the `/opt/netbox/contrib/` directory to the `/etc/systemd/system/` directory:
```bash
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.service /etc/systemd/system/netbox-housekeeping.service
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.timer /etc/systemd/system/netbox-housekeeping.timer
```
Then, reload the systemd configuration and enable the timer to start automatically at boot:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now netbox-housekeeping.timer
```
Check the status of your timer by running:
```bash
sudo systemctl list-timers --all
```
This command will show a list of all timers, including your `netbox-housekeeping.timer`. Make sure the timer is active and properly scheduled.
That's it! Your NetBox housekeeping service is now configured to run daily using systemd.

View File

@@ -4,7 +4,7 @@
Default: `None`
Defines a Sentry data source name (DSN) for automated error reporting. `SENTRY_ENABLED` must be True for this parameter to take effect. For example:
Defines a Sentry data source name (DSN) for automated error reporting. `SENTRY_ENABLED` must be `True` for this parameter to take effect. For example:
```
SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
@@ -16,7 +16,7 @@ SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0"
Default: `False`
Set to True to enable automatic error reporting via [Sentry](https://sentry.io/).
Set to `True` to enable automatic error reporting via [Sentry](https://sentry.io/).
!!! note
The `sentry-sdk` Python package is required to enable Sentry integration.

View File

@@ -6,7 +6,7 @@
Default: `True`
Setting this to False will disable the GraphQL API.
Setting this to `False` will disable the GraphQL API.
---

View File

@@ -57,7 +57,7 @@ Sets content for the top banner in the user interface.
Default: `True`
Enables anonymous census reporting. To opt out of census reporting, set this to False.
Enables anonymous census reporting. To opt out of census reporting, set this to `False`.
This data enables the project maintainers to estimate how many NetBox deployments exist and track the adoption of new versions over time. Census reporting effects a single HTTP request each time a worker starts. The only data reported by this function are the NetBox version, Python version, and a pseudorandom unique identifier.
@@ -102,7 +102,7 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
Default: `True`
By default, NetBox will prevent the creation of duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This validation can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to False.
By default, NetBox will prevent the creation of duplicate prefixes and IP addresses in the global table (that is, those which are not assigned to any VRF). This validation can be disabled by setting `ENFORCE_GLOBAL_UNIQUE` to `False`.
---
@@ -143,7 +143,7 @@ The number of days to retain job results (scripts and reports). Set this to `0`
Default: `False`
Setting this to True will display a "maintenance mode" banner at the top of every page. Additionally, NetBox will no longer update a user's "last active" time upon login. This is to allow new logins when the database is in a read-only state. Recording of login times will resume when maintenance mode is disabled.
Setting this to `True` will display a "maintenance mode" banner at the top of every page. Additionally, NetBox will no longer update a user's "last active" time upon login. This is to allow new logins when the database is in a read-only state. Recording of login times will resume when maintenance mode is disabled.
---
@@ -181,7 +181,7 @@ Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Pr
Default: `False`
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to prefer IPv4 instead.
When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to `True` to prefer IPv4 instead.
---

View File

@@ -35,7 +35,7 @@ Note that a plugin must be listed in `PLUGINS` for its configuration to take eff
## PLUGINS_CATALOG_CONFIG
Default: Empty
Default: `{}` (Empty)
This parameter controls how individual plugins are displayed in the plugins catalog under Admin > System > Plugins. Adding a plugin to the `hidden` list will omit that plugin from the catalog. Adding a plugin to the `static` list will display the plugin, but not link to the plugin details or upgrade instructions.

View File

@@ -1,6 +1,6 @@
# Remote Authentication Settings
The configuration parameters listed here control remote authentication for NetBox. Note that `REMOTE_AUTH_ENABLED` must be true in order for these settings to take effect.
The configuration parameters listed here control remote authentication for NetBox. Note that `REMOTE_AUTH_ENABLED` must be `True` in order for these settings to take effect.
---
@@ -8,7 +8,7 @@ The configuration parameters listed here control remote authentication for NetBo
Default: `False`
If true, NetBox will automatically create groups specified in the `REMOTE_AUTH_GROUP_HEADER` header if they don't already exist. (Requires `REMOTE_AUTH_ENABLED`.)
If `True`, NetBox will automatically create groups specified in the `REMOTE_AUTH_GROUP_HEADER` header if they don't already exist. (Requires `REMOTE_AUTH_ENABLED`.)
---
@@ -16,7 +16,7 @@ If true, NetBox will automatically create groups specified in the `REMOTE_AUTH_G
Default: `False`
If true, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
If `True`, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.)
---
@@ -43,7 +43,7 @@ The list of groups to assign a new user account when created using remote authen
Default: `{}` (Empty dictionary)
A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED` as True and `REMOTE_AUTH_GROUP_SYNC_ENABLED` as False.)
A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED` as `True` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` as `False`.)
---

View File

@@ -2,12 +2,12 @@
## ALLOWED_HOSTS
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attacks](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
This is a list of valid fully-qualified domain names (FQDNs) and/or IP addresses that can be used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different; for example, when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server. To help guard against [HTTP Host header attacks](https://docs.djangoproject.com/en/stable/topics/security/#host-headers-virtual-hosting), NetBox will not permit access to the server via any other hostnames (or IPs).
!!! note
This parameter must always be defined as a list or tuple, even if only a single value is provided.
The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts POST requests to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, sets `USE_X_FORWARDED_HOST` to true, which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts)).
The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts POST requests to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, sets `USE_X_FORWARDED_HOST` to `True`, which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts)).
Example:

View File

@@ -5,7 +5,7 @@
Default: `False`
!!! note
The default value of this parameter changed from true to false in NetBox v4.3.0.
The default value of this parameter changed from `True` to `False` in NetBox v4.3.0.
If disabled, the values of API tokens will not be displayed after each token's initial creation. A user **must** record the value of a token prior to its creation, or it will be lost. Note that this affects _all_ users, regardless of assigned permissions.
@@ -52,7 +52,7 @@ Although it is not recommended, the default validation rules can be disabled by
Default: `False`
If True, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below).
If `True`, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below).
---
@@ -62,7 +62,7 @@ If True, cross-origin resource sharing (CORS) requests will be accepted from all
These settings specify a list of origins that are authorized to make cross-site API requests. Use
`CORS_ORIGIN_WHITELIST` to define a list of exact hostnames, or `CORS_ORIGIN_REGEX_WHITELIST` to define a set of regular
expressions. (These settings have no effect if `CORS_ORIGIN_ALLOW_ALL` is True.) For example:
expressions. (These settings have no effect if `CORS_ORIGIN_ALLOW_ALL` is `True`.) For example:
```python
CORS_ORIGIN_WHITELIST = [
@@ -84,7 +84,7 @@ The name of the cookie to use for the cross-site request forgery (CSRF) authenti
Default: `False`
If true, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection.
If `True`, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection.
---
@@ -92,7 +92,7 @@ If true, the cookie employed for cross-site request forgery (CSRF) protection wi
Default: `[]`
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://).
Defines a list of trusted origins for unsafe (e.g. `POST`) requests. This is a pass-through to Django's [`CSRF_TRUSTED_ORIGINS`](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-trusted-origins) setting. Note that each host listed must specify a scheme (e.g. `http://` or `https://).
```python
CSRF_TRUSTED_ORIGINS = (
@@ -135,7 +135,7 @@ DEFAULT_PERMISSIONS = {
## EXEMPT_VIEW_PERMISSIONS
Default: Empty list
Default: `[]` (Empty list)
A list of NetBox models to exempt from the enforcement of view permissions. Models listed here will be viewable by all users, both authenticated and anonymous.
@@ -164,7 +164,7 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
Default: `False`
If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
If `True`, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely.
@@ -191,7 +191,7 @@ The lifetime (in seconds) of the authentication cookie issued to a NetBox user u
## LOGIN_FORM_HIDDEN
Default: False
Default: `False`
Option to hide the login form when only SSO authentication is in use.
@@ -212,7 +212,7 @@ The view name or URL to which a user is redirected after logging out.
Default: `False`
If true, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain.
If `True`, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain.
---
@@ -220,7 +220,7 @@ If true, the `includeSubDomains` directive will be included in the HTTP Strict T
Default: `False`
If true, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar.
If `True`, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar.
---
@@ -236,7 +236,7 @@ If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict
Default: `False`
If true, all non-HTTPS requests will be automatically redirected to use HTTPS.
If `True`, all non-HTTPS requests will be automatically redirected to use HTTPS.
!!! warning
Ensure that your frontend HTTP daemon has been configured to forward the HTTP scheme correctly before enabling this option. An incorrectly configured frontend may result in a looping redirect.
@@ -255,7 +255,7 @@ The name used for the session cookie. See the [Django documentation](https://doc
Default: `False`
If true, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection.
If `True`, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection.
---

View File

@@ -72,6 +72,16 @@ Email is sent from NetBox only for critical events or if configured for [logging
---
## HOSTNAME
!!! info "This parameter was introduced in NetBox v4.4."
Default: System hostname
The hostname displayed in the user interface identifying the system on which NetBox is running. If not defined, this defaults to the system hostname as reported by Python's `platform.node()`.
---
## HTTP_PROXIES
Default: `None`
@@ -95,7 +105,7 @@ Default: `('127.0.0.1', '::1')`
A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For
example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
addresses (and [`DEBUG`](./development.md#debug) is true).
addresses (and [`DEBUG`](./development.md#debug) is `True`).
---
@@ -103,7 +113,7 @@ addresses (and [`DEBUG`](./development.md#debug) is true).
Default: `False`
Set this configuration parameter to True for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
Set this configuration parameter to `True` for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
!!! note
If Internet access is available via a proxy, set [`HTTP_PROXIES`](#http_proxies) instead.
@@ -114,7 +124,7 @@ Set this configuration parameter to True for NetBox deployments which do not hav
Default: `{}`
A dictionary of custom jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example:
A dictionary of custom Jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example:
```python
def uppercase(x):
@@ -158,6 +168,8 @@ LOGGING = {
* `netbox.<app>.<model>` - Generic form for model-specific log messages
* `netbox.auth.*` - Authentication events
* `netbox.api.views.*` - Views which handle business logic for the REST API
* `netbox.event_rules` - Event rules
* `netbox.jobs.*` - Background jobs
* `netbox.reports.*` - Report execution (`module.name`)
* `netbox.scripts.*` - Custom script execution (`module.name`)
* `netbox.views.*` - Views which handle business logic for the web UI

View File

@@ -147,7 +147,7 @@ For UI development you will need to review the [Web UI Development Guide](web-ui
## Populating Demo Data
Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. (This sample data is used to populate the public demo instance at <https://demo.netbox.dev>.)
Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. This sample data is used to populate the [public demo instance](https://demo.netbox.dev).
The demo data is provided in JSON format and loaded into an empty database using Django's `loaddata` management command. Consult the demo data repo's `README` file for complete instructions on populating the data.

View File

@@ -53,6 +53,8 @@ If a new Django release is adopted or other major dependencies (Python, PostgreS
* Update the installation guide (`docs/installation/index.md`) with the new minimum versions.
* Update the upgrade guide (`docs/installation/upgrading.md`) for the current version accordingly.
* Update the minimum PostgreSQL version in the programming error template (`netbox/templates/exceptions/programming_error.html`).
* Update the minimum and supported Python versions in the project metadata file (`pyproject.toml`)
### Manually Perform a New Install
@@ -164,7 +166,8 @@ Then, compile these portable (`.po`) files for use in the application:
### Update Version and Changelog
* Update the version number and date in `netbox/release.yaml`. Add or remove the designation (e.g. `beta1`) if applicable.
* Update the version number and published date in `netbox/release.yaml`. Add or remove the designation (e.g. `beta1`) if applicable.
* Copy the version number from `release.yaml` to `pyproject.toml` in the project root.
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
* Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release.
@@ -190,15 +193,3 @@ Create a [new release](https://github.com/netbox-community/netbox/releases/new)
* **Description:** Copy from the pull request body, then promote the `###` headers to `##` ones
Once created, the release will become available for users to install.
### Update the Public Documentation
After a release has been published, the public NetBox documentation needs to be updated. This is accomplished by running two actions on the [netboxlabs-docs](https://github.com/netboxlabs/netboxlabs-docs) repository.
First, run the `build-site` action, by navigating to Actions > build-site > Run workflow. This process compiles the documentation along with an overlay for integration with the documentation portal at <https://netboxlabs.com/docs>. The job should take about two minutes.
Once the documentation files have been compiled, they must be published by running the `deploy-kinsta` action. Select the desired deployment environment (staging or production) and specify `latest` as the deploy tag.
Clear the CDN cache from the [Kinsta](https://my.kinsta.com/) portal. Navigate to _Sites_ / _NetBox Labs_ / _Live_, select _Cache_ in the left-nav, click the _Clear Cache_ button, and confirm the clear operation.
Finally, verify that the documentation at <https://netboxlabs.com/docs/netbox/en/stable/> has been updated.

View File

@@ -2,9 +2,9 @@
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)
* Housekeeping tasks
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).

View File

@@ -8,6 +8,12 @@ When a request is made, a UUID is generated and attached to any change records r
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.
## User Messages
!!! info "This feature was introduced in NetBox v4.4."
When creating, modifying, or deleting an object in NetBox, a user has the option of recording an arbitrary message that will appear in the change record. This can be helpful to capture additional context, such as the reason for the change.
## 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

@@ -9,7 +9,7 @@ NetBox is the leading solution for modeling and documenting modern networks. By
## :material-server-network: Built for Networks
Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
Unlike general-purpose configuration management databases (CMDBs), NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
* Hierarchical regions, sites, and locations
* Racks, devices, and device components

View File

@@ -108,7 +108,7 @@ Open `configuration.py` with your preferred editor to begin configuring NetBox.
### ALLOWED_HOSTS
This is a list of the valid hostnames and IP addresses by which this server can be reached. You must specify at least one name or IP address. (Note that this does not restrict the locations from which NetBox may be accessed: It is merely for [HTTP host header validation](https://docs.djangoproject.com/en/3.0/topics/security/#host-headers-virtual-hosting).)
This is a list of the valid hostnames and IP addresses by which this server can be reached. You must specify at least one name or IP address. (Note that this does not restrict the locations from which NetBox may be accessed: It is merely for [HTTP host header validation](https://docs.djangoproject.com/en/stable/topics/security/#host-headers-virtual-hosting).)
```python
ALLOWED_HOSTS = ['netbox.example.com', '192.0.2.123']
@@ -264,18 +264,6 @@ cd /opt/netbox/netbox
python3 manage.py createsuperuser
```
## Schedule the Housekeeping Task
NetBox includes a `housekeeping` management command that handles some recurring cleanup tasks, such as clearing out old sessions and expired change records. Although this command may be run manually, it is recommended to configure a scheduled job using the system's `cron` daemon or a similar utility.
A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to or linked from your system's daily cron task directory, or included within the crontab directly. (If installing NetBox into a nonstandard path, be sure to update the system paths within this script first.)
```shell
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
```
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
## Test the Application
At this point, we should be able to run NetBox's development server for testing. We can check by starting a development instance locally.

View File

@@ -28,7 +28,7 @@ NetBox ships with a default configuration file for uWSGI. To use it, copy `/opt/
sudo cp /opt/netbox/contrib/uwsgi.ini /opt/netbox/uwsgi.ini
```
While the provided configuration should suffice for most initial installations, you may wish to edit this file to change the bound IP address and/or port number, or to make performance-related adjustments. See [the uWSGI documentation](https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html) for the available configuration parameters and take a minute to review the [Things to know](https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) page. Django also provides [additional documentation](https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/uwsgi/) on configuring uWSGI with a Django app.
While the provided configuration should suffice for most initial installations, you may wish to edit this file to change the bound IP address and/or port number, or to make performance-related adjustments. See [the uWSGI documentation](https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html) for the available configuration parameters and take a minute to review the [Things to know](https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) page. Django also provides [additional documentation](https://docs.djangoproject.com/en/stable/howto/deployment/wsgi/uwsgi/) on configuring uWSGI with a Django app.
## systemd Setup

View File

@@ -122,7 +122,7 @@ sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/
### Option B: Check Out a Git Release
This guide assumes that NetBox is installed at `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following command:
This guide assumes that NetBox is installed in `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following command:
```
git ls-remote --tags https://github.com/netbox-community/netbox.git \
@@ -134,6 +134,8 @@ git ls-remote --tags https://github.com/netbox-community/netbox.git \
Check out the desired release by specifying its tag. For example:
```
cd /opt/netbox && \
sudo git fetch --tags && \
sudo git checkout v4.2.7
```
@@ -181,13 +183,3 @@ Finally, restart the gunicorn and RQ services:
```no-highlight
sudo systemctl restart netbox netbox-rq
```
## 6. Verify Housekeeping Scheduling
If upgrading from a release prior to NetBox v3.0, check that a cron task (or similar scheduled process) has been configured to run NetBox's nightly housekeeping command. A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be linked from your system's daily cron task directory, or included within the crontab directly. (If NetBox has been installed in a nonstandard path, be sure to update the system paths within this script first.)
```shell
sudo ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
```
See the [housekeeping documentation](../administration/housekeeping.md) for further details.

View File

@@ -608,6 +608,28 @@ http://netbox/api/dcim/sites/ \
!!! note
The bulk deletion of objects is an all-or-none operation, meaning that if NetBox fails to delete any of the specified objects (e.g. due a dependency by a related object), the entire operation will be aborted and none of the objects will be deleted.
## Changelog Messages
!!! info "This feature was introduced in NetBox v4.4."
Most objects in NetBox support [change logging](../features/change-logging.md), which generates a detailed record each time an object is created, modified, or deleted. Beginning in NetBox v4.4, users can attach a message to the change record as well. This is accomplished via the REST API by including a `changelog_message` field in the object representation.
For example, the following API request will create a new site and record a message in the resulting changelog entry:
```no-highlight
curl -s -X POST \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
http://netbox/api/dcim/site/ \
--data '{
"name": "Site A",
"slug": "site-a",
"changelog_message": "Adding a site for ticket #4137"
}'
```
This approach works when creating, modifying, or deleting objects, either individually or in bulk.
## Uploading Files
As JSON does not support the inclusion of binary data, files cannot be uploaded using JSON-formatted API requests. Instead, we can use form data encoding to attach a local file.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -10,11 +10,11 @@ The assignment of platforms to devices is an optional feature, and may be disreg
### Name
A unique human-friendly name.
A human-friendly name for the platform. Must be unique per manufacturer.
### Slug
A unique URL-friendly identifier. (This value can be used for filtering.)
A URL-friendly identifier; must be unique per manufacturer. (This value can be used for filtering.)
### Manufacturer

View File

@@ -25,6 +25,9 @@ The user-defined functional [role](./role.md) assigned to the VLAN.
### VLAN Group or Site
!!! warning "Site assignment is deprecated"
The assignment of individual VLANs directly to a site has been deprecated. This ability will be removed in a future NetBox release. Users are strongly encouraged to utilize VLAN groups, which have the added benefit of supporting the assignment of a VLAN to multiple sites.
The [VLAN group](./vlangroup.md) or [site](../dcim/site.md) to which the VLAN is assigned.
### Q-in-Q Role

View File

@@ -15,7 +15,6 @@ A background job implements a basic [Job](../../models/core/job.md) executor for
```python title="jobs.py"
from netbox.jobs import JobRunner
class MyTestJob(JobRunner):
class Meta:
name = "My Test Job"
@@ -25,6 +24,8 @@ class MyTestJob(JobRunner):
# your logic goes here
```
Completed jobs will have their status updated to "completed" by default, or "errored" if an unhandled exception was raised by the `run()` method. To intentionally mark a job as failed, raise the `core.exceptions.JobFailed` exception. (Note that "failed" differs from "errored" in that a failure may be expected under certain conditions, whereas an error is not.)
You can schedule the background job from within your code (e.g. from a model's `save()` method or a view) by calling `MyTestJob.enqueue()`. This method passes through all arguments to `Job.enqueue()`. However, no `name` argument must be passed, as the background job name will be used instead.
!!! tip
@@ -38,6 +39,27 @@ You can schedule the background job from within your code (e.g. from a model's `
This is the human-friendly names of your background job. If omitted, the class name will be used.
### Logging
!!! info "This feature was introduced in NetBox v4.4."
A Python logger is instantiated by the runner for each job. It can be utilized within a job's `run()` method as needed:
```python
def run(self, *args, **kwargs):
obj = MyModel.objects.get(pk=kwargs.get('pk'))
self.logger.info("Retrieved object {obj}")
```
Four of the standard Python logging levels are supported:
* `debug()`
* `info()`
* `warning()`
* `error()`
Log entries recorded using the runner's logger will be saved in the job's log in the database in addition to being processed by other [system logging handlers](../../configuration/system.md#logging).
### Scheduled Jobs
As described above, jobs can be scheduled for immediate execution or at any later time using the `enqueue()` method. However, for management purposes, the `enqueue_once()` method allows a job to be scheduled exactly once avoiding duplicates. If a job is already scheduled for a particular instance, a second one won't be scheduled, respecting thread safety. An example use case would be to schedule a periodic task that is bound to an instance in general, but not to any event of that instance (such as updates). The parameters of the `enqueue_once()` method are identical to those of `enqueue()`.

View File

@@ -22,7 +22,7 @@ from netbox.plugins import PluginConfig
### ContentType renamed to ObjectType
NetBox's proxy model for Django's [ContentType model](https://docs.djangoproject.com/en/5.0/ref/contrib/contenttypes/#the-contenttype-model) has been renamed to ObjectType for clarity. In general, plugins should use the ObjectType proxy when referencing content types, as it includes several custom manager methods. The one exception to this is when defining [generic foreign keys](https://docs.djangoproject.com/en/5.0/ref/contrib/contenttypes/#generic-relations): The ForeignKey field used for a GFK should point to Django's native ContentType.
NetBox's proxy model for Django's [ContentType model](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#the-contenttype-model) has been renamed to ObjectType for clarity. In general, plugins should use the ObjectType proxy when referencing content types, as it includes several custom manager methods. The one exception to this is when defining [generic foreign keys](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#generic-relations): The ForeignKey field used for a GFK should point to Django's native ContentType.
Additionally, plugin maintainers are strongly encouraged to adopt the "object type" terminology for field and filter names wherever feasible to be consistent with NetBox core (however this is not required for compatibility).

View File

@@ -1,6 +1,6 @@
# Views
## Writing Views
## Writing Basic Views
If your plugin will provide its own page or pages within the NetBox web UI, you'll need to define views. A view is a piece of business logic which performs an action and/or renders a page when a request is made to a particular URL. HTML content is rendered using a [template](./templates.md). Views are typically defined in `views.py`, and URL patterns in `urls.py`.
@@ -47,9 +47,13 @@ A URL pattern has three components:
This makes our view accessible at the URL `/plugins/animal-sounds/random/`. (Remember, our `AnimalSoundsConfig` class sets our plugin's base URL to `animal-sounds`.) Viewing this URL should show the base NetBox template with our custom content inside it.
## NetBox Model Views
NetBox provides several generic view classes and additional helper functions, to simplify the implementation of plugin logic. These are recommended to be used whenever possible to keep the maintenance overhead of plugins low.
### View Classes
NetBox provides several generic view classes (documented below) to facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use.
Generic view classes (documented below) facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use.
| View Class | Description |
|----------------------|--------------------------------------------------------|
@@ -60,23 +64,57 @@ NetBox provides several generic view classes (documented below) to facilitate co
| `ObjectListView` | View a list of objects |
| `BulkImportView` | Import a set of new objects |
| `BulkEditView` | Edit multiple objects |
| `BulkRenameView` | Rename multiple objects |
| `BulkDeleteView` | Delete multiple objects |
!!! warning
Please note that only the classes which appear in this documentation are currently supported. Although other classes may be present within the `views.generic` module, they are not yet supported for use by plugins.
#### Example Usage
### URL registration
The NetBox URL registration process has two parts:
1. View classes can be decorated with `@register_model_view()`. This registers a new URL for the model.
2. All of a model's URLs can be included in `urls.py` using the `get_model_urls()` function. This call is usually required twice: once to import general views for the model and again to import model detail views tied to the object's primary key.
::: utilities.views.register_model_view
!!! note "Changed in NetBox v4.2"
In NetBox v4.2, the `register_model_view()` function was extended to support the registration of list views by passing `detail=False`.
::: utilities.urls.get_model_urls
!!! note "Changed in NetBox v4.2"
In NetBox v4.2, the `get_model_urls()` function was extended to support retrieving registered general model views (e.g. for listing objects) by passing `detail=False`.
### Example Usage
```python
# views.py
from netbox.views.generic import ObjectEditView
from utilities.views import register_model_view
from .models import Thing
@register_model_view(Thing, name='add', detail=False)
@register_model_view(Thing, name='edit')
class ThingEditView(ObjectEditView):
queryset = Thing.objects.all()
template_name = 'myplugin/thing.html'
...
```
```python
# urls.py
from django.urls import include, path
from utilities.urls import get_model_urls
urlpatterns = [
path('thing/', include(get_model_urls('myplugin', 'thing', detail=False))),
path('thing/<int:pk>/', include(get_model_urls('myplugin', 'thing'))),
...
]
```
## Object Views
Below are the class definitions for NetBox's object views. These views handle CRUD actions for individual objects. The view, add/edit, and delete views each inherit from `BaseObjectView`, which is not intended to be used directly.
@@ -134,6 +172,10 @@ Below are the class definitions for NetBox's multi-object views. These views han
options:
members: false
::: netbox.views.generic.BulkRenameView
options:
members: false
::: netbox.views.generic.BulkDeleteView
options:
members:
@@ -143,6 +185,9 @@ Below are the class definitions for NetBox's multi-object views. These views han
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
!!! note
These feature views are automatically registered for all models that implement the respective feature. There is usually no need to override them. However, if that's the case, the URL must be registered manually in `urls.py` instead of using the `register_model_view()` function or decorator.
::: netbox.views.generic.ObjectChangeLogView
options:
members:
@@ -157,7 +202,7 @@ These views are provided to enable or enhance certain NetBox model features, suc
### Additional Tabs
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`, and add it to the template context dict:
Plugins can "attach" a custom view to a 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`, and add it to the template context dict:
```python
from dcim.models import Site
@@ -185,11 +230,6 @@ class MyView(generic.ObjectView):
)
```
!!! note "Changed in NetBox v4.2"
The `register_model_view()` function was extended in NetBox v4.2 to support registration of list views by passing `detail=False`.
::: utilities.views.register_model_view
::: utilities.views.ViewTab
### Extra Template Content

View File

@@ -86,3 +86,69 @@ netbox=> DELETE FROM django_migrations WHERE app='pluginname';
!!! warning
Exercise extreme caution when altering Django system tables. Users are strongly encouraged to perform a backup of their database immediately before taking these actions.
## Clean Up Content Types and Permissions
After removing a plugin and its database tables, you may find that object type references (`ContentTypes`) created by the plugin still appear in the permissions management section (e.g., when editing permissions in the NetBox UI).
This happens because the `django_content_type` table retains entries for the models that the plugin registered with Django.
!!! warning
Please use caution when removing `ContentTypes`. It is strongly recommended to **back up your database** before making these changes.
**Identify Stale Content Types:**
Open the Django shell to inspect lingering `ContentType` entries related to the removed plugin.
Typically, the Content Type's `app_label` matches the plugins name.
```no-highlight
$ cd /opt/netbox/
$ source /opt/netbox/venv/bin/activate
(venv) $ python3 netbox/manage.py nbshell
```
Then, in the shell:
```no-highlight
from django.contrib.contenttypes.models import ContentType
# Replace 'pluginname' with your plugin's actual name
stale_types = ContentType.objects.filter(app_label="pluginname")
for ct in stale_types:
print(ct)
### ^^^ These will be removed, make sure its ok
```
!!! warning
Review the output carefully and confirm that each listed Content Type is related to the plugin you removed.
**Remove Stale Content Types and Related Permissions:**
Next, check for any permissions associated with these Content Types:
```no-highlight
from django.contrib.auth.models import Permission
for ct in stale_types:
perms = Permission.objects.filter(content_type=ct)
print(list(perms))
```
If there are related Permissions, you can remove them safely:
```no-highlight
for ct in stale_types:
Permission.objects.filter(content_type=ct).delete()
```
After removing any related permissions, delete the Content Type entries:
```no-highlight
stale_types.delete()
```
**Restart NetBox:**
After making these changes, restart the NetBox service to ensure all changes are reflected.
```no-highlight
sudo systemctl restart netbox
```

View File

@@ -434,7 +434,7 @@ A new management command has been added: `manage.py housekeeping`. This command
* Delete change log records which have surpassed the configured retention period (if configured)
* Check for new NetBox releases (if enabled)
A convenience script for calling this command via an automated scheduler has been included at `/contrib/netbox-housekeeping.sh`. Please see the [housekeeping documentation](../administration/housekeeping.md) for further details.
A convenience script for calling this command via an automated scheduler has been included at `/contrib/netbox-housekeeping.sh`. Please see the housekeeping documentation for further details.
#### Custom Queue Support for Plugins ([#6651](https://github.com/netbox-community/netbox/issues/6651))

View File

@@ -1,3 +1,111 @@
# NetBox v4.3
## v4.3.4 (2025-07-15)
### Enhancements
* [#18811](https://github.com/netbox-community/netbox/issues/18811) - Match expanded form IPv6 addresses in global search
* [#19550](https://github.com/netbox-community/netbox/issues/19550) - Enable lazy loading for rack elevations
* [#19571](https://github.com/netbox-community/netbox/issues/19571) - Add a default module type profile for expansion cards
* [#19793](https://github.com/netbox-community/netbox/issues/19793) - Support custom dynamic navigation menu links
* [#19828](https://github.com/netbox-community/netbox/issues/19828) - Expose L2VPN termination in interface GraphQL response
### Bug Fixes
* [#19413](https://github.com/netbox-community/netbox/issues/19413) - Custom fields should be grouped in filter forms
* [#19633](https://github.com/netbox-community/netbox/issues/19633) - Introduce InvalidCondition exception and log all evaluations of invalid event rule conditions
* [#19800](https://github.com/netbox-community/netbox/issues/19800) - Module type bulk import should support profile assignment
* [#19806](https://github.com/netbox-community/netbox/issues/19806) - Introduce JobFailed exception to allow marking background jobs as failed
* [#19827](https://github.com/netbox-community/netbox/issues/19827) - Enforce uniqueness for device role names & slugs
* [#19839](https://github.com/netbox-community/netbox/issues/19839) - Enable export of parent assignment for recursively nested objects
* [#19876](https://github.com/netbox-community/netbox/issues/19876) - Remove Markdown rendering from CustomFieldChoiceSet description field
---
## v4.3.3 (2025-06-26)
### Enhancements
* [#17183](https://github.com/netbox-community/netbox/issues/17183) - Enable associating tags with object types during bulk import
* [#17719](https://github.com/netbox-community/netbox/issues/17719) - Introduce a user preference for table row striping
* [#19492](https://github.com/netbox-community/netbox/issues/19492) - Add a UI button to download the output of an executed custom script
* [#19499](https://github.com/netbox-community/netbox/issues/19499) - Support qualifying interfaces by parent device when bulk importing wireless links
### Bug Fixes
* [#19529](https://github.com/netbox-community/netbox/issues/19529) - Fix support for running custom scripts via the `runscript` management command
* [#19555](https://github.com/netbox-community/netbox/issues/19555) - Fix support for `schedule_at` when invoking a custom script via the REST API
* [#19617](https://github.com/netbox-community/netbox/issues/19617) - Ensure consistent styling of "connect" buttons in UI
* [#19640](https://github.com/netbox-community/netbox/issues/19640) - Restore ability to filter FHRP group assignments by device/VM in GraphQL API
* [#19644](https://github.com/netbox-community/netbox/issues/19644) - Atomic transactions should always employ database routing
* [#19659](https://github.com/netbox-community/netbox/issues/19659) - Populate initial device/VM selection for "add a service" button
* [#19665](https://github.com/netbox-community/netbox/issues/19665) - Correct field reference in wireless link model validation
* [#19667](https://github.com/netbox-community/netbox/issues/19667) - Fix `TypeError` exception when creating a new module profile type with no schema
* [#19673](https://github.com/netbox-community/netbox/issues/19673) - Ignore custom field references when compiling table prefetches
* [#19677](https://github.com/netbox-community/netbox/issues/19677) - Fix exception when passing null value to `present_in_vrf` filter
* [#19680](https://github.com/netbox-community/netbox/issues/19680) - Correct chronological ordering of change records resulting from device deletions
* [#19687](https://github.com/netbox-community/netbox/issues/19687) - Cellular interface types should be considered non-connectable
* [#19702](https://github.com/netbox-community/netbox/issues/19702) - Fix `DoesNotExist` exception when deleting a notification group with an associated event rule
* [#19745](https://github.com/netbox-community/netbox/issues/19745) - Fix bulk import of services with IP addresses assigned to FHRP groups
---
## v4.3.2 (2025-06-05)
### Enhancements
* [#19200](https://github.com/netbox-community/netbox/issues/19200) - Display assigned virtual chassis (if any) on device view
* [#19461](https://github.com/netbox-community/netbox/issues/19461) - Add color backgrounds for virtual circuit types
* [#19605](https://github.com/netbox-community/netbox/issues/19605) - Enable filtering IP addresses by family in GraphQL API
* [#19627](https://github.com/netbox-community/netbox/issues/19627) - Introduce object change migrators
### Bug Fixes
* [#19415](https://github.com/netbox-community/netbox/issues/19415) - Increase maximum supported distance for circuits and wireless links
* [#19475](https://github.com/netbox-community/netbox/issues/19475) - VLANs belonging to the same location as a VM's cluster should be eligible for assignment to interfaces on that VM
* [#19486](https://github.com/netbox-community/netbox/issues/19486) - Fix connection card rendering for console server ports
* [#19487](https://github.com/netbox-community/netbox/issues/19487) - Fix `FieldError` exception when ordering circuit or tunnel terminations by the terminating object
* [#19490](https://github.com/netbox-community/netbox/issues/19490) - Fix inclusion support for config templates populated via a data source
* [#19496](https://github.com/netbox-community/netbox/issues/19496) - Fix `AttributeError` exception when rendering a config template with no output
* [#19510](https://github.com/netbox-community/netbox/issues/19510) - Restore GraphQL API filtering for assigned IP addresses
* [#19520](https://github.com/netbox-community/netbox/issues/19520) - Restore ability to alter prefix scope via the REST API
* [#19587](https://github.com/netbox-community/netbox/issues/19587) - The `occupied` filter should include interfaces terminating a wireless link
* [#19599](https://github.com/netbox-community/netbox/issues/19599) - Fix `AttributeError` exception when sorting change history under user view
* [#19610](https://github.com/netbox-community/netbox/issues/19610) - Fix `FieldError` exception when sorting tunnel terminations by tenant
* [#19623](https://github.com/netbox-community/netbox/issues/19623) - Display description under provider account view
---
## v4.3.1 (2025-05-13)
### Enhancements
* [#17073](https://github.com/netbox-community/netbox/issues/17073) - Enable global search for tags
* [#18419](https://github.com/netbox-community/netbox/issues/18419) - Enable specifying a queue name when calling `Job.enqueue()`
* [#19416](https://github.com/netbox-community/netbox/issues/19416) - Add the 1000BASE-SX interface type
* [#19434](https://github.com/netbox-community/netbox/issues/19434) - Add pre-populated interface speed choices for 2.5 and 5 Gbps
### Bug Fixes
* [#17107](https://github.com/netbox-community/netbox/issues/17107) - Fix cosmetic issue in cable traces ending at a provider network
* [#19309](https://github.com/netbox-community/netbox/issues/19309) - Improve REST API query performance for prefixes and IP addresses
* [#19361](https://github.com/netbox-community/netbox/issues/19361) - Fix incorrect GraphQL object types
* [#19375](https://github.com/netbox-community/netbox/issues/19375) - Fix table configuration after applying a saved table config
* [#19376](https://github.com/netbox-community/netbox/issues/19376) - Fix `FieldDoesNotExist` exception when global search results include a contact
* [#19380](https://github.com/netbox-community/netbox/issues/19380) - Fix column selections for child object tables
* [#19381](https://github.com/netbox-community/netbox/issues/19381) - Fix syncing of custom scripts from a remote data source
* [#19396](https://github.com/netbox-community/netbox/issues/19396) - Enable nullifying VLAN `qinq_role` via the REST API
* [#19397](https://github.com/netbox-community/netbox/issues/19397) - Correct enum type for IPRangeFilter in GraphQL API
* [#19432](https://github.com/netbox-community/netbox/issues/19432) - Update minimum required PostgreSQL version referenced by server error page
* [#19440](https://github.com/netbox-community/netbox/issues/19440) - Ensure data migrations use the correct database connection
* [#19444](https://github.com/netbox-community/netbox/issues/19444) - Fix change logging for contact group assignments
* [#19463](https://github.com/netbox-community/netbox/issues/19463) - Hide button dropdown for tables which do not support saved configs
* [#19464](https://github.com/netbox-community/netbox/issues/19464) - Fix bulk editing of inventory items from device view
* [#19465](https://github.com/netbox-community/netbox/issues/19465) - Fix ability to clear assigned prefix scope in UI
* [#19472](https://github.com/netbox-community/netbox/issues/19472) - Fix device column rendering in virtual device contexts table
---
## v4.3.0 (2025-05-01)
### Breaking Changes

View File

@@ -158,7 +158,6 @@ nav:
- Okta: 'administration/authentication/okta.md'
- Permissions: 'administration/permissions.md'
- Error Reporting: 'administration/error-reporting.md'
- Housekeeping: 'administration/housekeeping.md'
- Replicating NetBox: 'administration/replicating-netbox.md'
- NetBox Shell: 'administration/netbox-shell.md'
- Data Model:

View File

@@ -191,12 +191,10 @@ class ProfileView(LoginRequiredMixin, View):
def get(self, request):
# Compile changelog table
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
user=request.user
).prefetch_related(
'changed_object_type'
)[:20]
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=request.user)[:20]
changelog_table = ObjectChangeTable(changelog)
changelog_table.orderable = False
changelog_table.configure(request)
return render(request, self.template_name, {
'changelog_table': changelog_table,

View File

@@ -16,6 +16,7 @@ from utilities.forms import get_field_value
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
)
from utilities.forms.mixins import DistanceValidationMixin
from utilities.forms.rendering import FieldSet, InlineFields
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
from utilities.templatetags.builtins.filters import bettertitle
@@ -105,7 +106,7 @@ class CircuitTypeForm(NetBoxModelForm):
]
class CircuitForm(TenancyForm, NetBoxModelForm):
class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),

View File

@@ -41,7 +41,7 @@ __all__ = (
)
@strawberry_django.filter(models.CircuitTermination, lookups=True)
@strawberry_django.filter_type(models.CircuitTermination, lookups=True)
class CircuitTerminationFilter(
BaseObjectTypeFilterMixin,
CustomFieldsFilterMixin,
@@ -87,7 +87,7 @@ class CircuitTerminationFilter(
)
@strawberry_django.filter(models.Circuit, lookups=True)
@strawberry_django.filter_type(models.Circuit, lookups=True)
class CircuitFilter(
ContactFilterMixin,
ImageAttachmentFilterMixin,
@@ -121,17 +121,17 @@ class CircuitFilter(
)
@strawberry_django.filter(models.CircuitType, lookups=True)
@strawberry_django.filter_type(models.CircuitType, lookups=True)
class CircuitTypeFilter(BaseCircuitTypeFilterMixin):
pass
@strawberry_django.filter(models.CircuitGroup, lookups=True)
@strawberry_django.filter_type(models.CircuitGroup, lookups=True)
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
pass
@strawberry_django.filter(models.CircuitGroupAssignment, lookups=True)
@strawberry_django.filter_type(models.CircuitGroupAssignment, lookups=True)
class CircuitGroupAssignmentFilter(
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
):
@@ -148,7 +148,7 @@ class CircuitGroupAssignmentFilter(
)
@strawberry_django.filter(models.Provider, lookups=True)
@strawberry_django.filter_type(models.Provider, lookups=True)
class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field()
@@ -158,7 +158,7 @@ class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
)
@strawberry_django.filter(models.ProviderAccount, lookups=True)
@strawberry_django.filter_type(models.ProviderAccount, lookups=True)
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -168,7 +168,7 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ProviderNetwork, lookups=True)
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
class ProviderNetworkFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@@ -178,12 +178,12 @@ class ProviderNetworkFilter(PrimaryModelFilterMixin):
service_id: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.VirtualCircuitType, lookups=True)
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin):
pass
@strawberry_django.filter(models.VirtualCircuit, lookups=True)
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
cid: FilterLookup[str] | None = strawberry_django.filter_field()
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@@ -206,7 +206,7 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
)
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True)
@strawberry_django.filter_type(models.VirtualCircuitTermination, lookups=True)
class VirtualCircuitTerminationFilter(
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
):

View File

@@ -8,10 +8,11 @@ def set_null_values(apps, schema_editor):
Circuit = apps.get_model('circuits', 'Circuit')
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
db_alias = schema_editor.connection.alias
Circuit.objects.filter(distance_unit='').update(distance_unit=None)
CircuitGroupAssignment.objects.filter(priority='').update(priority=None)
CircuitTermination.objects.filter(cable_end='').update(cable_end=None)
Circuit.objects.using(db_alias).filter(distance_unit='').update(distance_unit=None)
CircuitGroupAssignment.objects.using(db_alias).filter(priority='').update(priority=None)
CircuitTermination.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
class Migration(migrations.Migration):

View File

@@ -1,4 +1,5 @@
import django.db.models.deletion
from django.contrib.contenttypes.models import ContentType
from django.db import migrations, models
@@ -8,14 +9,15 @@ def copy_site_assignments(apps, schema_editor):
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
Site = apps.get_model('dcim', 'Site')
db_alias = schema_editor.connection.alias
CircuitTermination.objects.filter(site__isnull=False).update(
CircuitTermination.objects.using(db_alias).filter(site__isnull=False).update(
termination_type=ContentType.objects.get_for_model(Site), termination_id=models.F('site_id')
)
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
CircuitTermination.objects.filter(provider_network__isnull=False).update(
CircuitTermination.objects.using(db_alias).filter(provider_network__isnull=False).update(
termination_type=ContentType.objects.get_for_model(ProviderNetwork),
termination_id=models.F('provider_network_id'),
)
@@ -48,3 +50,26 @@ class Migration(migrations.Migration):
# Copy over existing site assignments
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
]
def oc_circuittermination_termination(objectchange, reverting):
site_ct = ContentType.objects.get_by_natural_key('dcim', 'site').pk
provider_network_ct = ContentType.objects.get_by_natural_key('circuits', 'providernetwork').pk
for data in (objectchange.prechange_data, objectchange.postchange_data):
if data is None:
continue
if site_id := data.get('site'):
data.update({
'termination_type': site_ct,
'termination_id': site_id,
})
elif provider_network_id := data.get('provider_network'):
data.update({
'termination_type': provider_network_ct,
'termination_id': provider_network_id,
})
objectchange_migrators = {
'circuits.circuittermination': oc_circuittermination_termination,
}

View File

@@ -7,15 +7,20 @@ def populate_denormalized_fields(apps, schema_editor):
Copy site ForeignKey values to the Termination GFK.
"""
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
db_alias = schema_editor.connection.alias
terminations = CircuitTermination.objects.filter(site__isnull=False).prefetch_related('site')
terminations = CircuitTermination.objects.using(db_alias).filter(site__isnull=False).prefetch_related('site')
for termination in terminations:
termination._region_id = termination.site.region_id
termination._site_group_id = termination.site.group_id
termination._site_id = termination.site_id
# Note: Location cannot be set prior to migration
CircuitTermination.objects.bulk_update(terminations, ['_region', '_site_group', '_site'], batch_size=100)
CircuitTermination.objects.using(db_alias).bulk_update(
terminations,
['_region', '_site_group', '_site'],
batch_size=100
)
class Migration(migrations.Migration):
@@ -81,3 +86,15 @@ class Migration(migrations.Migration):
new_name='_provider_network',
),
]
def oc_circuittermination_remove_fields(objectchange, reverting):
for data in (objectchange.prechange_data, objectchange.postchange_data):
if data is not None:
data.pop('site', None)
data.pop('provider_network', None)
objectchange_migrators = {
'circuits.circuittermination': oc_circuittermination_remove_fields,
}

View File

@@ -1,4 +1,5 @@
import django.db.models.deletion
from django.contrib.contenttypes.models import ContentType
from django.db import migrations, models
@@ -9,8 +10,9 @@ def set_member_type(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')
Circuit = apps.get_model('circuits', 'Circuit')
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
db_alias = schema_editor.connection.alias
CircuitGroupAssignment.objects.update(
CircuitGroupAssignment.objects.using(db_alias).update(
member_type=ContentType.objects.get_for_model(Circuit)
)
@@ -81,3 +83,21 @@ class Migration(migrations.Migration):
),
),
]
def oc_circuitgroupassignment_member(objectchange, reverting):
circuit_ct = ContentType.objects.get_by_natural_key('circuits', 'circuit').pk
for data in (objectchange.prechange_data, objectchange.postchange_data):
if data is None:
continue
if circuit_id := data.get('circuit'):
data.update({
'member_type': circuit_ct,
'member_id': circuit_id,
})
data.pop('circuit', None)
objectchange_migrators = {
'circuits.circuitgroupassignment': oc_circuitgroupassignment_member,
}

View File

@@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0051_virtualcircuit_group_assignment'),
]
operations = [
migrations.AlterField(
model_name='circuit',
name='_abs_distance',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=13, null=True),
),
]

View File

@@ -120,7 +120,8 @@ class CircuitTerminationTable(NetBoxTable):
)
termination = tables.Column(
verbose_name=_('Termination Point'),
linkify=True
linkify=True,
orderable=False,
)
# Termination types
@@ -132,7 +133,7 @@ class CircuitTerminationTable(NetBoxTable):
site_group = tables.Column(
verbose_name=_('Site Group'),
linkify=True,
accessor='_sitegroup'
accessor='_site_group'
)
region = tables.Column(
verbose_name=_('Region'),

View File

@@ -54,9 +54,8 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
linkify=True,
verbose_name=_('Account')
)
type = tables.Column(
type = columns.ColoredLabelColumn(
verbose_name=_('Type'),
linkify=True
)
status = columns.ChoiceFieldColumn()
termination_count = columns.LinkedCountColumn(

View File

@@ -0,0 +1,23 @@
from django.test import RequestFactory, tag, TestCase
from circuits.models import CircuitTermination
from circuits.tables import CircuitTerminationTable
@tag('regression')
class CircuitTerminationTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
terminations = CircuitTermination.objects.all()
disallowed = {'actions', }
orderable_columns = [
column.name for column in CircuitTerminationTable(terminations).columns
if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get("/")
for col in orderable_columns:
for dir in ('-', ''):
table = CircuitTerminationTable(terminations)
table.order_by = f'{dir}{col}'
table.as_html(fake_request)

View File

@@ -1,10 +1,11 @@
from django.contrib import messages
from django.db import transaction
from django.db import router, transaction
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _
from dcim.views import PathTraceView
from ipam.models import ASN
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
from netbox.views import generic
from utilities.forms import ConfirmationForm
from utilities.query import count_related
@@ -79,6 +80,11 @@ class ProviderBulkEditView(generic.BulkEditView):
form = forms.ProviderBulkEditForm
@register_model_view(Provider, 'bulk_rename', path='rename', detail=False)
class ProviderBulkRenameView(generic.BulkRenameView):
queryset = Provider.objects.all()
@register_model_view(Provider, 'bulk_delete', path='delete', detail=False)
class ProviderBulkDeleteView(generic.BulkDeleteView):
queryset = Provider.objects.annotate(
@@ -141,6 +147,11 @@ class ProviderAccountBulkEditView(generic.BulkEditView):
form = forms.ProviderAccountBulkEditForm
@register_model_view(ProviderAccount, 'bulk_rename', path='rename', detail=False)
class ProviderAccountBulkRenameView(generic.BulkRenameView):
queryset = ProviderAccount.objects.all()
@register_model_view(ProviderAccount, 'bulk_delete', path='delete', detail=False)
class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
queryset = ProviderAccount.objects.annotate(
@@ -212,6 +223,11 @@ class ProviderNetworkBulkEditView(generic.BulkEditView):
form = forms.ProviderNetworkBulkEditForm
@register_model_view(ProviderNetwork, 'bulk_rename', path='rename', detail=False)
class ProviderNetworkBulkRenameView(generic.BulkRenameView):
queryset = ProviderNetwork.objects.all()
@register_model_view(ProviderNetwork, 'bulk_delete', path='delete', detail=False)
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
queryset = ProviderNetwork.objects.all()
@@ -271,6 +287,11 @@ class CircuitTypeBulkEditView(generic.BulkEditView):
form = forms.CircuitTypeBulkEditForm
@register_model_view(CircuitType, 'bulk_rename', path='rename', detail=False)
class CircuitTypeBulkRenameView(generic.BulkRenameView):
queryset = CircuitType.objects.all()
@register_model_view(CircuitType, 'bulk_delete', path='delete', detail=False)
class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitType.objects.annotate(
@@ -337,6 +358,12 @@ class CircuitBulkEditView(generic.BulkEditView):
form = forms.CircuitBulkEditForm
@register_model_view(Circuit, 'bulk_rename', path='rename', detail=False)
class CircuitBulkRenameView(generic.BulkRenameView):
queryset = Circuit.objects.all()
field_name = 'cid'
@register_model_view(Circuit, 'bulk_delete', path='delete', detail=False)
class CircuitBulkDeleteView(generic.BulkDeleteView):
queryset = Circuit.objects.prefetch_related(
@@ -384,7 +411,7 @@ class CircuitSwapTerminations(generic.ObjectEditView):
if termination_a and termination_z:
# Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint
with transaction.atomic():
with transaction.atomic(using=router.db_for_write(CircuitTermination)):
termination_a.term_side = '_'
termination_a.save()
termination_z.term_side = 'A'
@@ -432,6 +459,7 @@ class CircuitTerminationListView(generic.ObjectListView):
filterset = filtersets.CircuitTerminationFilterSet
filterset_form = forms.CircuitTerminationFilterForm
table = tables.CircuitTerminationTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(CircuitTermination)
@@ -526,6 +554,11 @@ class CircuitGroupBulkEditView(generic.BulkEditView):
form = forms.CircuitGroupBulkEditForm
@register_model_view(CircuitGroup, 'bulk_rename', path='rename', detail=False)
class CircuitGroupBulkRenameView(generic.BulkRenameView):
queryset = CircuitGroup.objects.all()
@register_model_view(CircuitGroup, 'bulk_delete', path='delete', detail=False)
class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitGroup.objects.all()
@@ -543,6 +576,7 @@ class CircuitGroupAssignmentListView(generic.ObjectListView):
filterset = filtersets.CircuitGroupAssignmentFilterSet
filterset_form = forms.CircuitGroupAssignmentFilterForm
table = tables.CircuitGroupAssignmentTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(CircuitGroupAssignment)
@@ -635,6 +669,11 @@ class VirtualCircuitTypeBulkEditView(generic.BulkEditView):
form = forms.VirtualCircuitTypeBulkEditForm
@register_model_view(VirtualCircuitType, 'bulk_rename', path='rename', detail=False)
class VirtualCircuitTypeBulkRenameView(generic.BulkRenameView):
queryset = VirtualCircuitType.objects.all()
@register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualCircuitType.objects.annotate(
@@ -697,6 +736,12 @@ class VirtualCircuitBulkEditView(generic.BulkEditView):
form = forms.VirtualCircuitBulkEditForm
@register_model_view(VirtualCircuit, 'bulk_rename', path='rename', detail=False)
class VirtualCircuitulkRenameView(generic.BulkRenameView):
queryset = VirtualCircuit.objects.all()
field_name = 'cid'
class VirtualCircuitBulkDeleteView(generic.BulkDeleteView):
queryset = VirtualCircuit.objects.annotate(
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
@@ -714,6 +759,7 @@ class VirtualCircuitTerminationListView(generic.ObjectListView):
filterset = filtersets.VirtualCircuitTerminationFilterSet
filterset_form = forms.VirtualCircuitTerminationFilterForm
table = tables.VirtualCircuitTerminationTable
actions = (AddObject, BulkImport, BulkExport, BulkEdit, BulkDelete)
@register_model_view(VirtualCircuitTermination)

View File

@@ -1,4 +1,5 @@
from .serializers_.change_logging import *
from .serializers_.data import *
from .serializers_.jobs import *
from .serializers_.object_types import *
from .serializers_.tasks import *

View File

@@ -44,7 +44,8 @@ class ObjectChangeSerializer(BaseModelSerializer):
model = ObjectChange
fields = [
'id', 'url', 'display_url', 'display', 'time', 'user', 'user_name', 'request_id', 'action',
'changed_object_type', 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
'changed_object_type', 'changed_object_id', 'changed_object', 'message', 'prechange_data',
'postchange_data',
]
@extend_schema_field(serializers.JSONField(allow_null=True))

View File

@@ -23,6 +23,6 @@ class JobSerializer(BaseModelSerializer):
model = Job
fields = [
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled',
'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id',
'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id', 'log_entries',
]
brief_fields = ('url', 'created', 'completed', 'user', 'status')

View File

@@ -0,0 +1,47 @@
import inspect
from django.urls import NoReverseMatch, reverse
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from core.models import ObjectType
from netbox.api.serializers import BaseModelSerializer
from utilities.views import get_viewname
__all__ = (
'ObjectTypeSerializer',
)
class ObjectTypeSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objecttype-detail')
app_name = serializers.CharField(source='app_verbose_name', read_only=True)
model_name = serializers.CharField(source='model_verbose_name', read_only=True)
model_name_plural = serializers.CharField(source='model_verbose_name_plural', read_only=True)
is_plugin_model = serializers.BooleanField(read_only=True)
rest_api_endpoint = serializers.SerializerMethodField()
description = serializers.SerializerMethodField()
class Meta:
model = ObjectType
fields = [
'id', 'url', 'display', 'app_label', 'app_name', 'model', 'model_name', 'model_name_plural',
'is_plugin_model', 'rest_api_endpoint', 'description',
]
@extend_schema_field(OpenApiTypes.STR)
def get_rest_api_endpoint(self, obj):
if not (model := obj.model_class()):
return
if viewname := get_viewname(model, action='list', rest_api=True):
try:
return reverse(viewname)
except NoReverseMatch:
return
@extend_schema_field(OpenApiTypes.STR)
def get_description(self, obj):
if not (model := obj.model_class()):
return
return inspect.getdoc(model)

View File

@@ -10,6 +10,7 @@ router.register('data-sources', views.DataSourceViewSet)
router.register('data-files', views.DataFileViewSet)
router.register('jobs', views.JobViewSet)
router.register('object-changes', views.ObjectChangeViewSet)
router.register('object-types', views.ObjectTypeViewSet)
router.register('background-queues', views.BackgroundQueueViewSet, basename='rqqueue')
router.register('background-workers', views.BackgroundWorkerViewSet, basename='rqworker')
router.register('background-tasks', views.BackgroundTaskViewSet, basename='rqtask')

View File

@@ -17,6 +17,7 @@ from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs, requeue_rq_jo
from django_rq.queues import get_redis_connection
from django_rq.utils import get_statistics
from django_rq.settings import QUEUES_LIST
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import LimitOffsetListPagination
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
@@ -85,6 +86,16 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
filterset_class = filtersets.ObjectChangeFilterSet
class ObjectTypeViewSet(ReadOnlyModelViewSet):
"""
Read-only list of ObjectTypes.
"""
permission_classes = [IsAuthenticatedOrLoginNotRequired]
queryset = ObjectType.objects.order_by('app_label', 'model')
serializer_class = serializers.ObjectTypeSerializer
filterset_class = filtersets.ObjectTypeFilterSet
class BaseRQViewSet(viewsets.ViewSet):
"""
Base class for RQ view sets. Provides a list() method. Subclasses must implement get_data().

View File

@@ -4,23 +4,31 @@ from django.utils.translation import gettext_lazy as _
from rq.job import JobStatus
__all__ = (
'JOB_LOG_ENTRY_LEVELS',
'RQ_TASK_STATUSES',
)
@dataclass
class Status:
class Badge:
label: str
color: str
RQ_TASK_STATUSES = {
JobStatus.QUEUED: Status(_('Queued'), 'cyan'),
JobStatus.FINISHED: Status(_('Finished'), 'green'),
JobStatus.FAILED: Status(_('Failed'), 'red'),
JobStatus.STARTED: Status(_('Started'), 'blue'),
JobStatus.DEFERRED: Status(_('Deferred'), 'gray'),
JobStatus.SCHEDULED: Status(_('Scheduled'), 'purple'),
JobStatus.STOPPED: Status(_('Stopped'), 'orange'),
JobStatus.CANCELED: Status(_('Cancelled'), 'yellow'),
JobStatus.QUEUED: Badge(_('Queued'), 'cyan'),
JobStatus.FINISHED: Badge(_('Finished'), 'green'),
JobStatus.FAILED: Badge(_('Failed'), 'red'),
JobStatus.STARTED: Badge(_('Started'), 'blue'),
JobStatus.DEFERRED: Badge(_('Deferred'), 'gray'),
JobStatus.SCHEDULED: Badge(_('Scheduled'), 'purple'),
JobStatus.STOPPED: Badge(_('Stopped'), 'orange'),
JobStatus.CANCELED: Badge(_('Cancelled'), 'yellow'),
}
JOB_LOG_ENTRY_LEVELS = {
'debug': Badge(_('Debug'), 'gray'),
'info': Badge(_('Info'), 'blue'),
'warning': Badge(_('Warning'), 'orange'),
'error': Badge(_('Error'), 'red'),
}

View File

@@ -0,0 +1,21 @@
import logging
from dataclasses import dataclass, field
from datetime import datetime
from django.utils import timezone
__all__ = (
'JobLogEntry',
)
@dataclass
class JobLogEntry:
level: str
message: str
timestamp: datetime = field(default_factory=timezone.now)
@classmethod
def from_logrecord(cls, record: logging.LogRecord):
return cls(record.levelname.lower(), record.msg)

View File

@@ -1,9 +1,19 @@
from django.core.exceptions import ImproperlyConfigured
class SyncError(Exception):
pass
__all__ = (
'IncompatiblePluginError',
'JobFailed',
'SyncError',
)
class IncompatiblePluginError(ImproperlyConfigured):
pass
class JobFailed(Exception):
pass
class SyncError(Exception):
pass

View File

@@ -1,9 +1,8 @@
import django_filters
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext as _
import django_filters
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
from netbox.utils import get_data_backend_choices
from users.models import User
@@ -17,6 +16,7 @@ __all__ = (
'DataSourceFilterSet',
'JobFilterSet',
'ObjectChangeFilterSet',
'ObjectTypeFilterSet',
)
@@ -134,6 +134,25 @@ class JobFilterSet(BaseFilterSet):
)
class ObjectTypeFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label=_('Search'),
)
class Meta:
model = ObjectType
fields = ('id', 'app_label', 'model')
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(app_label__icontains=value) |
Q(model__icontains=value)
)
class ObjectChangeFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
@@ -167,7 +186,8 @@ class ObjectChangeFilterSet(BaseFilterSet):
return queryset
return queryset.filter(
Q(user_name__icontains=value) |
Q(object_repr__icontains=value)
Q(object_repr__icontains=value) |
Q(message__icontains=value)
)

View File

@@ -23,7 +23,7 @@ __all__ = (
)
@strawberry_django.filter(models.DataFile, lookups=True)
@strawberry_django.filter_type(models.DataFile, lookups=True)
class DataFileFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field()
created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
@@ -39,7 +39,7 @@ class DataFileFilter(BaseFilterMixin):
hash: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DataSource, lookups=True)
@strawberry_django.filter_type(models.DataSource, lookups=True)
class DataSourceFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
type: FilterLookup[str] | None = strawberry_django.filter_field()
@@ -56,7 +56,7 @@ class DataSourceFilter(PrimaryModelFilterMixin):
)
@strawberry_django.filter(models.ObjectChange, lookups=True)
@strawberry_django.filter_type(models.ObjectChange, lookups=True)
class ObjectChangeFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field()
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
@@ -82,7 +82,7 @@ class ObjectChangeFilter(BaseFilterMixin):
)
@strawberry_django.filter(DjangoContentType, lookups=True)
@strawberry_django.filter_type(DjangoContentType, lookups=True)
class ContentTypeFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field()
app_label: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@@ -1,13 +1,20 @@
import logging
import requests
import sys
from datetime import timedelta
from importlib import import_module
import requests
from django.conf import settings
from django.core.cache import cache
from django.utils import timezone
from packaging import version
from core.models import Job, ObjectChange
from netbox.config import Config
from netbox.jobs import JobRunner, system_job
from netbox.search.backends import search_backend
from utilities.proxy import resolve_proxies
from .choices import DataSourceStatusChoices, JobIntervalChoices
from .exceptions import SyncError
from .models import DataSource
logger = logging.getLogger(__name__)
@@ -23,19 +30,23 @@ class SyncDataSourceJob(JobRunner):
def run(self, *args, **kwargs):
datasource = DataSource.objects.get(pk=self.job.object_id)
self.logger.debug(f"Found DataSource ID {datasource.pk}")
try:
self.logger.info(f"Syncing data source {datasource}")
datasource.sync()
# Update the search cache for DataFiles belonging to this source
self.logger.debug("Updating search cache for data files")
search_backend.cache(datasource.datafiles.iterator())
except Exception as e:
self.logger.error(f"Error syncing data source: {e}")
DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
if type(e) is SyncError:
logging.error(e)
raise e
self.logger.info("Syncing completed successfully")
@system_job(interval=JobIntervalChoices.INTERVAL_DAILY)
class SystemHousekeepingJob(JobRunner):
@@ -50,16 +61,23 @@ class SystemHousekeepingJob(JobRunner):
if settings.DEBUG or 'test' in sys.argv:
return
# TODO: Migrate other housekeeping functions from the `housekeeping` management command.
self.send_census_report()
self.clear_expired_sessions()
self.prune_changelog()
self.delete_expired_jobs()
self.check_for_new_releases()
@staticmethod
def send_census_report():
"""
Send a census report (if enabled).
"""
# Skip if census reporting is disabled
if settings.ISOLATED_DEPLOYMENT or not settings.CENSUS_REPORTING_ENABLED:
logging.info("Reporting census data...")
if settings.ISOLATED_DEPLOYMENT:
logging.info("ISOLATED_DEPLOYMENT is enabled; skipping")
return
if not settings.CENSUS_REPORTING_ENABLED:
logging.info("CENSUS_REPORTING_ENABLED is disabled; skipping")
return
census_data = {
@@ -76,3 +94,94 @@ class SystemHousekeepingJob(JobRunner):
)
except requests.exceptions.RequestException:
pass
@staticmethod
def clear_expired_sessions():
"""
Clear any expired sessions from the database.
"""
logging.info("Clearing expired sessions...")
engine = import_module(settings.SESSION_ENGINE)
try:
engine.SessionStore.clear_expired()
logging.info("Sessions cleared.")
except NotImplementedError:
logging.warning(
f"The configured session engine ({settings.SESSION_ENGINE}) does not support "
f"clearing sessions; skipping."
)
@staticmethod
def prune_changelog():
"""
Delete any ObjectChange records older than the configured changelog retention time (if any).
"""
logging.info("Pruning old changelog entries...")
config = Config()
if not config.CHANGELOG_RETENTION:
logging.info("No retention period specified; skipping.")
return
cutoff = timezone.now() - timedelta(days=config.CHANGELOG_RETENTION)
logging.debug(f"Retention period: {config.CHANGELOG_RETENTION} days")
logging.debug(f"Cut-off time: {cutoff}")
count = ObjectChange.objects.filter(time__lt=cutoff).delete()[0]
logging.info(f"Deleted {count} expired records")
@staticmethod
def delete_expired_jobs():
"""
Delete any jobs older than the configured retention period (if any).
"""
logging.info("Deleting expired jobs...")
config = Config()
if not config.JOB_RETENTION:
logging.info("No retention period specified; skipping.")
return
cutoff = timezone.now() - timedelta(days=config.JOB_RETENTION)
logging.debug(f"Retention period: {config.CHANGELOG_RETENTION} days")
logging.debug(f"Cut-off time: {cutoff}")
count = Job.objects.filter(created__lt=cutoff).delete()[0]
logging.info(f"Deleted {count} expired records")
@staticmethod
def check_for_new_releases():
"""
Check for new releases and cache the latest release.
"""
logging.info("Checking for new releases...")
if settings.ISOLATED_DEPLOYMENT:
logging.info("ISOLATED_DEPLOYMENT is enabled; skipping")
return
if not settings.RELEASE_CHECK_URL:
logging.info("RELEASE_CHECK_URL is not set; skipping")
return
# Fetch the latest releases
logging.debug(f"Release check URL: {settings.RELEASE_CHECK_URL}")
try:
response = requests.get(
url=settings.RELEASE_CHECK_URL,
headers={'Accept': 'application/vnd.github.v3+json'},
proxies=resolve_proxies(url=settings.RELEASE_CHECK_URL)
)
response.raise_for_status()
except requests.exceptions.RequestException as exc:
logging.error(f"Error fetching release: {exc}")
return
# Determine the most recent stable release
releases = []
for release in response.json():
if 'tag_name' not in release or release.get('devrelease') or release.get('prerelease'):
continue
releases.append((version.parse(release['tag_name']), release.get('html_url')))
latest_release = max(releases)
logging.debug(f"Found {len(response.json())} releases; {len(releases)} usable")
logging.info(f"Latest release: {latest_release[0]}")
# Cache the most recent release
cache.set('latest_release', latest_release, None)

View File

@@ -0,0 +1,28 @@
import django.contrib.postgres.fields
import django.core.serializers.json
from django.db import migrations, models
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('core', '0015_remove_redundant_indexes'),
]
operations = [
migrations.AddField(
model_name='job',
name='log_entries',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.JSONField(
decoder=utilities.json.JobLogDecoder,
encoder=django.core.serializers.json.DjangoJSONEncoder
),
blank=True,
default=list,
size=None
),
),
]

View File

@@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0016_job_log_entries'),
]
operations = [
migrations.AddField(
model_name='objectchange',
name='message',
field=models.CharField(blank=True, editable=False, max_length=200),
),
]

View File

@@ -82,6 +82,12 @@ class ObjectChange(models.Model):
max_length=200,
editable=False
)
message = models.CharField(
verbose_name=_('message'),
max_length=200,
editable=False,
blank=True
)
prechange_data = models.JSONField(
verbose_name=_('pre-change data'),
editable=False,

View File

@@ -1,7 +1,9 @@
from django.contrib.contenttypes.models import ContentType, ContentTypeManager
from django.db.models import Q
from netbox.plugins import PluginConfig
from netbox.registry import registry
from utilities.string import title
__all__ = (
'ObjectType',
@@ -48,3 +50,29 @@ class ObjectType(ContentType):
class Meta:
proxy = True
@property
def app_labeled_name(self):
# Override ContentType's "app | model" representation style.
return f"{self.app_verbose_name} > {title(self.model_verbose_name)}"
@property
def app_verbose_name(self):
if model := self.model_class():
return model._meta.app_config.verbose_name
@property
def model_verbose_name(self):
if model := self.model_class():
return model._meta.verbose_name
@property
def model_verbose_name_plural(self):
if model := self.model_class():
return model._meta.verbose_name_plural
@property
def is_plugin_model(self):
if not (model := self.model_class()):
return # Return null if model class is invalid
return isinstance(model._meta.app_config, PluginConfig)

View File

@@ -88,19 +88,11 @@ class ManagedFile(SyncedDataMixin, models.Model):
def sync_data(self):
if self.data_file:
self.file_path = os.path.basename(self.data_path)
self._write_to_disk(self.full_path, overwrite=True)
def _write_to_disk(self, path, overwrite=False):
"""
Write the object's data to disk at the specified path
"""
# Check whether file already exists
storage = self.storage
if storage.exists(path) and not overwrite:
raise FileExistsError()
storage = self.storage
with storage.open(path, 'wb+') as new_file:
new_file.write(self.data)
with storage.open(self.full_path, 'wb+') as new_file:
new_file.write(self.data_file.data)
@cached_property
def storage(self):

View File

@@ -1,9 +1,12 @@
import logging
import uuid
from dataclasses import asdict
from functools import partial
import django_rq
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.serializers.json import DjangoJSONEncoder
from django.core.validators import MinValueValidator
@@ -14,8 +17,10 @@ from django.utils.translation import gettext as _
from rq.exceptions import InvalidJobOperation
from core.choices import JobStatusChoices
from core.dataclasses import JobLogEntry
from core.models import ObjectType
from core.signals import job_end, job_start
from utilities.json import JobLogDecoder
from utilities.querysets import RestrictedQuerySet
from utilities.rqworker import get_queue_for_model
@@ -104,6 +109,15 @@ class Job(models.Model):
verbose_name=_('job ID'),
unique=True
)
log_entries = ArrayField(
verbose_name=_('log entries'),
base_field=models.JSONField(
encoder=DjangoJSONEncoder,
decoder=JobLogDecoder,
),
blank=True,
default=list,
)
objects = RestrictedQuerySet.as_manager()
@@ -116,7 +130,7 @@ class Job(models.Model):
verbose_name_plural = _('jobs')
def __str__(self):
return str(self.job_id)
return self.name
def get_absolute_url(self):
# TODO: Employ dynamic registration
@@ -187,15 +201,14 @@ class Job(models.Model):
"""
Mark the job as completed, optionally specifying a particular termination status.
"""
valid_statuses = JobStatusChoices.TERMINAL_STATE_CHOICES
if status not in valid_statuses:
if status not in JobStatusChoices.TERMINAL_STATE_CHOICES:
raise ValueError(
_("Invalid status for job termination. Choices are: {choices}").format(
choices=', '.join(valid_statuses)
choices=', '.join(JobStatusChoices.TERMINAL_STATE_CHOICES)
)
)
# Mark the job as completed
# Set the job's status and completion time
self.status = status
if error:
self.error = error
@@ -205,6 +218,13 @@ class Job(models.Model):
# Send signal
job_end.send(self)
def log(self, record: logging.LogRecord):
"""
Record a LogRecord from Python's native logging in the job's log.
"""
entry = JobLogEntry.from_logrecord(record)
self.log_entries.append(asdict(entry))
@classmethod
def enqueue(
cls,
@@ -215,6 +235,7 @@ class Job(models.Model):
schedule_at=None,
interval=None,
immediate=False,
queue_name=None,
**kwargs
):
"""
@@ -238,7 +259,7 @@ class Job(models.Model):
object_id = instance.pk
else:
object_type = object_id = None
rq_queue_name = get_queue_for_model(object_type.model if object_type else None)
rq_queue_name = queue_name if queue_name else get_queue_for_model(object_type.model if object_type else None)
queue = django_rq.get_queue(rq_queue_name)
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
job = Job(

View File

@@ -0,0 +1,18 @@
from django.utils.translation import gettext as _
from netbox.object_actions import ObjectAction
__all__ = (
'BulkSync',
)
class BulkSync(ObjectAction):
"""
Synchronize multiple objects at once.
"""
name = 'bulk_sync'
label = _('Sync Data')
multi = True
permissions_required = {'sync'}
template_name = 'core/buttons/bulk_sync.html'

View File

@@ -162,6 +162,12 @@ def handle_deleted_object(sender, instance, **kwargs):
getattr(obj, related_field_name).remove(instance)
elif type(relation) is ManyToOneRel and relation.field.null is True:
setattr(obj, related_field_name, None)
# make sure the object hasn't been deleted - in case of
# deletion chaining of related objects
try:
obj.refresh_from_db()
except DoesNotExist:
continue
obj.save()
# Enqueue the object for event processing

View File

@@ -41,6 +41,9 @@ class ObjectChangeTable(NetBoxTable):
template_code=OBJECTCHANGE_REQUEST_ID,
verbose_name=_('Request ID')
)
message = tables.Column(
verbose_name=_('Message'),
)
actions = columns.ActionsColumn(
actions=()
)
@@ -49,5 +52,8 @@ class ObjectChangeTable(NetBoxTable):
model = ObjectChange
fields = (
'pk', 'time', 'user_name', 'full_name', 'action', 'changed_object_type', 'object_repr', 'request_id',
'actions',
'message', 'actions',
)
default_columns = (
'pk', 'time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'message', 'actions',
)

View File

@@ -1,12 +1,11 @@
import django_tables2 as tables
from django.utils.safestring import mark_safe
from core.constants import RQ_TASK_STATUSES
from netbox.registry import registry
__all__ = (
'BackendTypeColumn',
'RQJobStatusColumn',
'BadgeColumn',
)
@@ -23,14 +22,21 @@ class BackendTypeColumn(tables.Column):
return value
class RQJobStatusColumn(tables.Column):
class BadgeColumn(tables.Column):
"""
Render a colored label for the status of an RQ job.
Render a colored badge for a value.
Args:
badges: A dictionary mapping of values to core.constants.Badge instances.
"""
def __init__(self, badges, *args, **kwargs):
super().__init__(*args, **kwargs)
self.badges = badges
def render(self, value):
status = RQ_TASK_STATUSES.get(value)
return mark_safe(f'<span class="badge text-bg-{status.color}">{status.label}</span>')
badge = self.badges.get(value)
return mark_safe(f'<span class="badge text-bg-{badge.color}">{badge.label}</span>')
def value(self, value):
status = RQ_TASK_STATUSES.get(value)
return status.label
badge = self.badges.get(value)
return badge.label

View File

@@ -1,8 +1,10 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from netbox.tables import NetBoxTable, columns
from ..models import Job
from netbox.tables import BaseTable, NetBoxTable, columns
from core.constants import JOB_LOG_ENTRY_LEVELS
from core.models import Job
from core.tables.columns import BadgeColumn
class JobTable(NetBoxTable):
@@ -40,6 +42,9 @@ class JobTable(NetBoxTable):
completed = columns.DateTimeColumn(
verbose_name=_('Completed'),
)
log_entries = tables.Column(
verbose_name=_('Log Entries'),
)
actions = columns.ActionsColumn(
actions=('delete',)
)
@@ -53,3 +58,24 @@ class JobTable(NetBoxTable):
default_columns = (
'pk', 'id', 'object_type', 'object', 'name', 'status', 'created', 'started', 'completed', 'user',
)
def render_log_entries(self, value):
return len(value)
class JobLogEntryTable(BaseTable):
timestamp = columns.DateTimeColumn(
timespec='milliseconds',
verbose_name=_('Time'),
)
level = BadgeColumn(
badges=JOB_LOG_ENTRY_LEVELS,
verbose_name=_('Level'),
)
message = tables.Column(
verbose_name=_('Message'),
)
class Meta(BaseTable.Meta):
empty_text = _('No log entries')
fields = ('timestamp', 'level', 'message')

View File

@@ -2,7 +2,8 @@ import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from django_tables2.utils import A
from core.tables.columns import RQJobStatusColumn
from core.constants import RQ_TASK_STATUSES
from core.tables.columns import BadgeColumn
from netbox.tables import BaseTable, columns
@@ -84,7 +85,8 @@ class BackgroundTaskTable(BaseTable):
ended_at = columns.DateTimeColumn(
verbose_name=_("Ended")
)
status = RQJobStatusColumn(
status = BadgeColumn(
badges=RQ_TASK_STATUSES,
verbose_name=_("Status"),
accessor='get_status'
)

View File

@@ -7,8 +7,10 @@ from django.utils import timezone
from rq.job import Job as RQ_Job, JobStatus
from rq.registry import FailedJobRegistry, StartedJobRegistry
from rest_framework import status
from users.models import Token, User
from utilities.testing import APITestCase, APIViewTestCases, TestCase
from utilities.testing.utils import disable_logging
from ..models import *
@@ -100,6 +102,22 @@ class DataFileTest(
DataFile.objects.bulk_create(data_files)
class ObjectTypeTest(APITestCase):
def test_list_objects(self):
object_type_count = ObjectType.objects.count()
response = self.client.get(reverse('extras-api:objecttype-list'), **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['count'], object_type_count)
def test_get_object(self):
object_type = ObjectType.objects.first()
url = reverse('extras-api:objecttype-detail', kwargs={'pk': object_type.pk})
self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK)
class BackgroundTaskTestCase(TestCase):
user_permissions = ()
@@ -189,7 +207,8 @@ class BackgroundTaskTestCase(TestCase):
# Enqueue & run a job that will fail
job = queue.enqueue(self.dummy_job_failing)
worker = get_worker('default')
worker.work(burst=True)
with disable_logging():
worker.work(burst=True)
self.assertTrue(job.is_failed)
# Re-enqueue the failed job and check that its status has been reset
@@ -231,7 +250,8 @@ class BackgroundTaskTestCase(TestCase):
self.assertEqual(job.get_status(), JobStatus.STARTED)
response = self.client.post(reverse('core-api:rqtask-stop', args=[job.id]), **self.header)
self.assertEqual(response.status_code, 200)
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
with disable_logging():
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
started_job_registry = StartedJobRegistry(queue.name, connection=queue.connection)
self.assertEqual(len(started_job_registry), 0)

View File

@@ -6,12 +6,13 @@ from rest_framework import status
from core.choices import ObjectChangeActionChoices
from core.models import ObjectChange, ObjectType
from dcim.choices import SiteStatusChoices
from dcim.models import Site
from dcim.models import Site, CableTermination, Device, DeviceType, DeviceRole, Interface, Cable
from extras.choices import *
from extras.models import CustomField, CustomFieldChoiceSet, Tag
from utilities.testing import APITestCase
from utilities.testing.utils import create_tags, post_data
from utilities.testing.views import ModelViewTestCase
from dcim.models import Manufacturer
class ChangeLogViewTest(ModelViewTestCase):
@@ -270,6 +271,81 @@ class ChangeLogViewTest(ModelViewTestCase):
# Check that no ObjectChange records have been created
self.assertEqual(ObjectChange.objects.count(), 0)
def test_ordering_genericrelation(self):
# Create required objects first
manufacturer = Manufacturer.objects.create(name='Manufacturer 1')
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='Model 1',
slug='model-1'
)
device_role = DeviceRole.objects.create(
name='Role 1',
slug='role-1'
)
site = Site.objects.create(
name='Site 1',
slug='site-1'
)
# Create two devices
device1 = Device.objects.create(
name='Device 1',
device_type=device_type,
role=device_role,
site=site
)
device2 = Device.objects.create(
name='Device 2',
device_type=device_type,
role=device_role,
site=site
)
# Create interfaces on both devices
interface1 = Interface.objects.create(
device=device1,
name='eth0',
type='1000base-t'
)
interface2 = Interface.objects.create(
device=device2,
name='eth0',
type='1000base-t'
)
# Create a cable between the interfaces
_ = Cable.objects.create(
a_terminations=[interface1],
b_terminations=[interface2],
status='connected'
)
# Delete device1
request = {
'path': reverse('dcim:device_delete', kwargs={'pk': device1.pk}),
'data': post_data({'confirm': True}),
}
self.add_permissions(
'dcim.delete_device',
'dcim.delete_interface',
'dcim.delete_cable',
'dcim.delete_cabletermination'
)
response = self.client.post(**request)
self.assertHttpStatus(response, 302)
# Get the ObjectChange records for delete actions ordered by time
changes = ObjectChange.objects.filter(
action=ObjectChangeActionChoices.ACTION_DELETE
).order_by('time')[:3]
# Verify the order of deletion
self.assertEqual(len(changes), 3)
self.assertEqual(changes[0].changed_object_type, ContentType.objects.get_for_model(CableTermination))
self.assertEqual(changes[1].changed_object_type, ContentType.objects.get_for_model(Interface))
self.assertEqual(changes[2].changed_object_type, ContentType.objects.get_for_model(Device))
class ChangeLogAPITest(APITestCase):

View File

@@ -150,7 +150,7 @@ class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
queryset = ObjectChange.objects.all()
filterset = ObjectChangeFilterSet
ignore_fields = ('prechange_data', 'postchange_data')
ignore_fields = ('message', 'prechange_data', 'postchange_data')
@classmethod
def setUpTestData(cls):

View File

@@ -14,7 +14,7 @@ from core.choices import ObjectChangeActionChoices
from core.models import *
from dcim.models import Site
from users.models import User
from utilities.testing import TestCase, ViewTestCases, create_tags
from utilities.testing import TestCase, ViewTestCases, create_tags, disable_logging
class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -271,7 +271,8 @@ class BackgroundTaskTestCase(TestCase):
# Enqueue & run a job that will fail
job = queue.enqueue(self.dummy_job_failing)
worker = get_worker('default')
worker.work(burst=True)
with disable_logging():
worker.work(burst=True)
self.assertTrue(job.is_failed)
# Re-enqueue the failed job and check that its status has been reset
@@ -317,7 +318,8 @@ class BackgroundTaskTestCase(TestCase):
self.assertEqual(len(started_job_registry), 1)
response = self.client.get(reverse('core:background_task_stop', args=[job.id]))
self.assertEqual(response.status_code, 302)
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
with disable_logging():
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
self.assertEqual(len(started_job_registry), 0)
canceled_job_registry = FailedJobRegistry(queue.name, connection=queue.connection)

View File

@@ -22,6 +22,7 @@ from rq.worker_registration import clean_worker_registry
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
from netbox.config import get_config, PARAMS
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
from netbox.registry import registry
from netbox.views import generic
from netbox.views.generic.base import BaseObjectView
@@ -31,13 +32,13 @@ from utilities.forms import ConfirmationForm
from utilities.htmx import htmx_partial
from utilities.json import ConfigJSONEncoder
from utilities.query import count_related
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, ViewTab, register_model_view
from . import filtersets, forms, tables
from .choices import DataSourceStatusChoices
from .jobs import SyncDataSourceJob
from .models import *
from .plugins import get_catalog_plugins, get_local_plugins
from .tables import CatalogPluginTable, PluginVersionTable
from .tables import CatalogPluginTable, JobLogEntryTable, PluginVersionTable
#
@@ -119,6 +120,11 @@ class DataSourceBulkEditView(generic.BulkEditView):
form = forms.DataSourceBulkEditForm
@register_model_view(DataSource, 'bulk_rename', path='rename', detail=False)
class DataSourceBulkRenameView(generic.BulkRenameView):
queryset = DataSource.objects.all()
@register_model_view(DataSource, 'bulk_delete', path='delete', detail=False)
class DataSourceBulkDeleteView(generic.BulkDeleteView):
queryset = DataSource.objects.annotate(
@@ -138,14 +144,13 @@ class DataFileListView(generic.ObjectListView):
filterset = filtersets.DataFileFilterSet
filterset_form = forms.DataFileFilterForm
table = tables.DataFileTable
actions = {
'bulk_delete': {'delete'},
}
actions = (BulkDelete,)
@register_model_view(DataFile)
class DataFileView(generic.ObjectView):
queryset = DataFile.objects.all()
actions = (DeleteObject,)
@register_model_view(DataFile, 'delete')
@@ -170,15 +175,32 @@ class JobListView(generic.ObjectListView):
filterset = filtersets.JobFilterSet
filterset_form = forms.JobFilterForm
table = tables.JobTable
actions = {
'export': {'view'},
'bulk_delete': {'delete'},
}
actions = (BulkExport, BulkDelete)
@register_model_view(Job)
class JobView(generic.ObjectView):
queryset = Job.objects.all()
actions = (DeleteObject,)
@register_model_view(Job, 'log')
class JobLogView(generic.ObjectView):
queryset = Job.objects.all()
actions = (DeleteObject,)
template_name = 'core/job/log.html'
tab = ViewTab(
label=_('Log'),
badge=lambda obj: len(obj.log_entries),
weight=500,
)
def get_extra_context(self, request, instance):
table = JobLogEntryTable(instance.log_entries)
table.configure(request)
return {
'table': table,
}
@register_model_view(Job, 'delete')
@@ -204,9 +226,7 @@ class ObjectChangeListView(generic.ObjectListView):
filterset_form = forms.ObjectChangeFilterForm
table = tables.ObjectChangeTable
template_name = 'core/objectchange_list.html'
actions = {
'export': {'view'},
}
actions = (BulkExport,)
@register_model_view(ObjectChange)
@@ -223,6 +243,7 @@ class ObjectChangeView(generic.ObjectView):
data=related_changes[:50],
orderable=False
)
related_changes_table.configure(request)
objectchanges = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
changed_object_type=instance.changed_object_type,
@@ -273,6 +294,7 @@ class ConfigRevisionListView(generic.ObjectListView):
filterset = filtersets.ConfigRevisionFilterSet
filterset_form = forms.ConfigRevisionFilterForm
table = tables.ConfigRevisionTable
actions = (AddObject, BulkExport)
@register_model_view(ConfigRevision)

View File

@@ -9,7 +9,7 @@ from dcim.models import (
InventoryItemTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
)
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
from utilities.api import get_serializer_for_model
from wireless.choices import *
from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer
@@ -31,7 +31,11 @@ __all__ = (
)
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
class ComponentTemplateSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer):
pass
class ConsolePortTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
nested=True,
required=False,
@@ -59,7 +63,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'description')
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class ConsoleServerPortTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
nested=True,
required=False,
@@ -87,7 +91,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'description')
class PowerPortTemplateSerializer(ValidatedModelSerializer):
class PowerPortTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
nested=True,
required=False,
@@ -116,7 +120,7 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'description')
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
class PowerOutletTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
nested=True,
required=False,
@@ -156,7 +160,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'description')
class InterfaceTemplateSerializer(ValidatedModelSerializer):
class InterfaceTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
nested=True,
required=False,
@@ -202,7 +206,7 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'description')
class RearPortTemplateSerializer(ValidatedModelSerializer):
class RearPortTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
required=False,
nested=True,
@@ -226,7 +230,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'description')
class FrontPortTemplateSerializer(ValidatedModelSerializer):
class FrontPortTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
nested=True,
required=False,
@@ -251,7 +255,7 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'description')
class ModuleBayTemplateSerializer(ValidatedModelSerializer):
class ModuleBayTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
nested=True,
required=False,
@@ -274,7 +278,7 @@ class ModuleBayTemplateSerializer(ValidatedModelSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'description')
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
class DeviceBayTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
nested=True
)
@@ -288,7 +292,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'description')
class InventoryItemTemplateSerializer(ValidatedModelSerializer):
class InventoryItemTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
nested=True
)

View File

@@ -461,6 +461,7 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
Interface.objects.select_related("device", "cable"),
],
),
'virtual_circuit_termination',
'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination
'ip_addresses', # Referenced by Interface.count_ipaddresses()
'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups()

View File

@@ -874,6 +874,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_100ME_T1 = '100base-t1'
TYPE_100ME_SFP = '100base-x-sfp'
TYPE_1GE_FIXED = '1000base-t'
TYPE_1GE_SX_FIXED = '1000base-sx'
TYPE_1GE_LX_FIXED = '1000base-lx'
TYPE_1GE_TX_FIXED = '1000base-tx'
TYPE_1GE_GBIC = '1000base-x-gbic'
@@ -1038,6 +1039,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'),
(TYPE_100ME_T1, '100BASE-T1 (10/100ME Single Pair)'),
(TYPE_1GE_FIXED, '1000BASE-T (1GE)'),
(TYPE_1GE_SX_FIXED, '1000BASE-SX (1GE)'),
(TYPE_1GE_LX_FIXED, '1000BASE-LX (1GE)'),
(TYPE_1GE_TX_FIXED, '1000BASE-TX (1GE)'),
(TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'),
@@ -1238,6 +1240,8 @@ class InterfaceSpeedChoices(ChoiceSet):
(10000, '10 Mbps'),
(100000, '100 Mbps'),
(1000000, '1 Gbps'),
(2500000, '2.5 Gbps'),
(5000000, '5 Gbps'),
(10000000, '10 Gbps'),
(25000000, '25 Gbps'),
(40000000, '40 Gbps'),

View File

@@ -53,6 +53,11 @@ WIRELESS_IFACE_TYPES = [
InterfaceTypeChoices.TYPE_802151,
InterfaceTypeChoices.TYPE_802154,
InterfaceTypeChoices.TYPE_OTHER_WIRELESS,
InterfaceTypeChoices.TYPE_GSM,
InterfaceTypeChoices.TYPE_CDMA,
InterfaceTypeChoices.TYPE_LTE,
InterfaceTypeChoices.TYPE_4G,
InterfaceTypeChoices.TYPE_5G,
]
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES

View File

@@ -2012,6 +2012,21 @@ class InterfaceFilterSet(
'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
}.get(value, queryset.none())
# Override the method on CabledObjectFilterSet to also check for wireless links
def filter_occupied(self, queryset, name, value):
if value:
return queryset.filter(
Q(cable__isnull=False) |
Q(wireless_link__isnull=False) |
Q(mark_connected=True)
)
else:
return queryset.filter(
cable__isnull=True,
wireless_link__isnull=True,
mark_connected=False
)
class FrontPortFilterSet(
ModularDeviceComponentFilterSet,

View File

@@ -11,6 +11,7 @@ from ipam.choices import VLANQinQRoleChoices
from ipam.models import ASN, VLAN, VLANGroup, VRF
from netbox.choices import *
from netbox.forms import NetBoxModelBulkEditForm
from netbox.forms.mixins import ChangeLoggingMixin
from tenancy.models import Tenant
from users.models import User
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
@@ -1037,7 +1038,11 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
# Device component templates
#
class ConsolePortTemplateBulkEditForm(BulkEditForm):
class ComponentTemplateBulkEditForm(ChangeLoggingMixin, BulkEditForm):
pass
class ConsolePortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ConsolePortTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -1056,7 +1061,7 @@ class ConsolePortTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('label', 'type', 'description')
class ConsoleServerPortTemplateBulkEditForm(BulkEditForm):
class ConsoleServerPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ConsoleServerPortTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -1079,7 +1084,7 @@ class ConsoleServerPortTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('label', 'type', 'description')
class PowerPortTemplateBulkEditForm(BulkEditForm):
class PowerPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=PowerPortTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -1114,7 +1119,7 @@ class PowerPortTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('label', 'type', 'maximum_draw', 'allocated_draw', 'description')
class PowerOutletTemplateBulkEditForm(BulkEditForm):
class PowerOutletTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=PowerOutletTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -1165,7 +1170,7 @@ class PowerOutletTemplateBulkEditForm(BulkEditForm):
self.fields['power_port'].widget.attrs['disabled'] = True
class InterfaceTemplateBulkEditForm(BulkEditForm):
class InterfaceTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=InterfaceTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -1216,7 +1221,7 @@ class InterfaceTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('label', 'description', 'poe_mode', 'poe_type', 'rf_role')
class FrontPortTemplateBulkEditForm(BulkEditForm):
class FrontPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=FrontPortTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -1243,7 +1248,7 @@ class FrontPortTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('description',)
class RearPortTemplateBulkEditForm(BulkEditForm):
class RearPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=RearPortTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -1270,7 +1275,7 @@ class RearPortTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('description',)
class ModuleBayTemplateBulkEditForm(BulkEditForm):
class ModuleBayTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ModuleBayTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -1288,7 +1293,7 @@ class ModuleBayTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('label', 'position', 'description')
class DeviceBayTemplateBulkEditForm(BulkEditForm):
class DeviceBayTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=DeviceBayTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -1306,7 +1311,7 @@ class DeviceBayTemplateBulkEditForm(BulkEditForm):
nullable_fields = ('label', 'description')
class InventoryItemTemplateBulkEditForm(BulkEditForm):
class InventoryItemTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=InventoryItemTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -1779,6 +1784,13 @@ class InventoryItemBulkEditForm(
)
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Remove parent device passed as context to avoid conflicts with the actual device field
# on this form (see bug #19464)
self.initial.pop('device', None)
#
# Device component roles

View File

@@ -470,8 +470,8 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
class Meta:
model = ModuleType
fields = [
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'comments',
'tags',
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile',
'comments', 'tags'
]

View File

@@ -1507,7 +1507,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
tx_power = forms.IntegerField(
required=False,
label=_('Transmit power (dBm)'),
min_value=0,
min_value=-40,
max_value=127
)
vrf_id = DynamicModelMultipleChoiceField(

View File

@@ -66,6 +66,10 @@ class ScopedForm(forms.Form):
if self.instance and scope_type_id != self.instance.scope_type_id:
self.initial['scope'] = None
else:
# Clear the initial scope value if scope_type is not set
self.initial['scope'] = None
class ScopedBulkEditForm(forms.Form):
scope_type = ContentTypeChoiceField(

View File

@@ -11,6 +11,7 @@ from extras.models import ConfigTemplate
from ipam.choices import VLANQinQRoleChoices
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF
from netbox.forms import NetBoxModelForm
from netbox.forms.mixins import ChangeLoggingMixin
from tenancy.forms import TenancyForm
from users.models import User
from utilities.forms import add_blank_choice, get_field_value
@@ -973,7 +974,7 @@ class VCMemberSelectForm(forms.Form):
# Device component templates
#
class ComponentTemplateForm(forms.ModelForm):
class ComponentTemplateForm(ChangeLoggingMixin, forms.ModelForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(),

View File

@@ -426,6 +426,11 @@ class VirtualChassisCreateForm(NetBoxModelForm):
help_text=_('Position of the first member device. Increases by one for each additional member.')
)
fieldsets = (
FieldSet('name', 'domain', 'description', 'tags', name=_('Virtual Chassis')),
FieldSet('region', 'site_group', 'site', 'rack', 'members', 'initial_position', name=_('Member Devices')),
)
class Meta:
model = VirtualChassis
fields = [

View File

@@ -90,7 +90,7 @@ __all__ = (
)
@strawberry_django.filter(models.Cable, lookups=True)
@strawberry_django.filter_type(models.Cable, lookups=True)
class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
type: Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
status: Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
@@ -107,7 +107,7 @@ class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
)
@strawberry_django.filter(models.CableTermination, lookups=True)
@strawberry_django.filter_type(models.CableTermination, lookups=True)
class CableTerminationFilter(ChangeLogFilterMixin):
cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
cable_id: ID | None = strawberry_django.filter_field()
@@ -120,7 +120,7 @@ class CableTerminationFilter(ChangeLogFilterMixin):
termination_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ConsolePort, lookups=True)
@strawberry_django.filter_type(models.ConsolePort, lookups=True)
class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@@ -130,14 +130,14 @@ class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
)
@strawberry_django.filter(models.ConsolePortTemplate, lookups=True)
@strawberry_django.filter_type(models.ConsolePortTemplate, lookups=True)
class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.ConsoleServerPort, lookups=True)
@strawberry_django.filter_type(models.ConsoleServerPort, lookups=True)
class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@@ -147,14 +147,14 @@ class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectMode
)
@strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True)
@strawberry_django.filter_type(models.ConsoleServerPortTemplate, lookups=True)
class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
)
@strawberry_django.filter(models.Device, lookups=True)
@strawberry_django.filter_type(models.Device, lookups=True)
class DeviceFilter(
ContactFilterMixin,
TenancyFilterMixin,
@@ -271,7 +271,7 @@ class DeviceFilter(
inventory_item_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DeviceBay, lookups=True)
@strawberry_django.filter_type(models.DeviceBay, lookups=True)
class DeviceBayFilter(ComponentModelFilterMixin):
installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -279,12 +279,12 @@ class DeviceBayFilter(ComponentModelFilterMixin):
installed_device_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DeviceBayTemplate, lookups=True)
@strawberry_django.filter_type(models.DeviceBayTemplate, lookups=True)
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin):
pass
@strawberry_django.filter(models.InventoryItemTemplate, lookups=True)
@strawberry_django.filter_type(models.InventoryItemTemplate, lookups=True)
class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -304,13 +304,13 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
part_id: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DeviceRole, lookups=True)
@strawberry_django.filter_type(models.DeviceRole, lookups=True)
class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
vm_role: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DeviceType, lookups=True)
@strawberry_django.filter_type(models.DeviceType, lookups=True)
class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -382,7 +382,7 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
inventory_item_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.FrontPort, lookups=True)
@strawberry_django.filter_type(models.FrontPort, lookups=True)
class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@@ -395,7 +395,7 @@ class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
)
@strawberry_django.filter(models.FrontPortTemplate, lookups=True)
@strawberry_django.filter_type(models.FrontPortTemplate, lookups=True)
class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@@ -408,7 +408,7 @@ class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
)
@strawberry_django.filter(models.MACAddress, lookups=True)
@strawberry_django.filter_type(models.MACAddress, lookups=True)
class MACAddressFilter(PrimaryModelFilterMixin):
mac_address: FilterLookup[str] | None = strawberry_django.filter_field()
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
@@ -417,7 +417,7 @@ class MACAddressFilter(PrimaryModelFilterMixin):
assigned_object_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.Interface, lookups=True)
@strawberry_django.filter_type(models.Interface, lookups=True)
class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin):
vcdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -486,7 +486,7 @@ class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin
)
@strawberry_django.filter(models.InterfaceTemplate, lookups=True)
@strawberry_django.filter_type(models.InterfaceTemplate, lookups=True)
class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@@ -508,7 +508,7 @@ class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
)
@strawberry_django.filter(models.InventoryItem, lookups=True)
@strawberry_django.filter_type(models.InventoryItem, lookups=True)
class InventoryItemFilter(ComponentModelFilterMixin):
parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -535,12 +535,12 @@ class InventoryItemFilter(ComponentModelFilterMixin):
discovered: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.InventoryItemRole, lookups=True)
@strawberry_django.filter_type(models.InventoryItemRole, lookups=True)
class InventoryItemRoleFilter(OrganizationalModelFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.Location, lookups=True)
@strawberry_django.filter_type(models.Location, lookups=True)
class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilterMixin):
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field()
@@ -556,12 +556,12 @@ class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilt
)
@strawberry_django.filter(models.Manufacturer, lookups=True)
@strawberry_django.filter_type(models.Manufacturer, lookups=True)
class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilterMixin):
pass
@strawberry_django.filter(models.Module, lookups=True)
@strawberry_django.filter_type(models.Module, lookups=True)
class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field()
@@ -610,7 +610,7 @@ class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
)
@strawberry_django.filter(models.ModuleBay, lookups=True)
@strawberry_django.filter_type(models.ModuleBay, lookups=True)
class ModuleBayFilter(ModularComponentModelFilterMixin):
parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -619,17 +619,17 @@ class ModuleBayFilter(ModularComponentModelFilterMixin):
position: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ModuleBayTemplate, lookups=True)
@strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True)
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin):
position: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ModuleTypeProfile, lookups=True)
@strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True)
class ModuleTypeProfileFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ModuleType, lookups=True)
@strawberry_django.filter_type(models.ModuleType, lookups=True)
class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -676,7 +676,7 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
) = strawberry_django.filter_field()
@strawberry_django.filter(models.Platform, lookups=True)
@strawberry_django.filter_type(models.Platform, lookups=True)
class PlatformFilter(OrganizationalModelFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -688,7 +688,7 @@ class PlatformFilter(OrganizationalModelFilterMixin):
config_template_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.PowerFeed, lookups=True)
@strawberry_django.filter_type(models.PowerFeed, lookups=True)
class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -723,7 +723,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM
)
@strawberry_django.filter(models.PowerOutlet, lookups=True)
@strawberry_django.filter_type(models.PowerOutlet, lookups=True)
class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@@ -738,7 +738,7 @@ class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.PowerOutletTemplate, lookups=True)
@strawberry_django.filter_type(models.PowerOutletTemplate, lookups=True)
class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@@ -752,7 +752,7 @@ class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
)
@strawberry_django.filter(models.PowerPanel, lookups=True)
@strawberry_django.filter_type(models.PowerPanel, lookups=True)
class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilterMixin):
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field()
@@ -765,7 +765,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo
name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.PowerPort, lookups=True)
@strawberry_django.filter_type(models.PowerPort, lookups=True)
class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@@ -778,7 +778,7 @@ class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
)
@strawberry_django.filter(models.PowerPortTemplate, lookups=True)
@strawberry_django.filter_type(models.PowerPortTemplate, lookups=True)
class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@@ -791,7 +791,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
)
@strawberry_django.filter(models.RackType, lookups=True)
@strawberry_django.filter_type(models.RackType, lookups=True)
class RackTypeFilter(RackBaseFilterMixin):
form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@@ -804,7 +804,7 @@ class RackTypeFilter(RackBaseFilterMixin):
slug: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.Rack, lookups=True)
@strawberry_django.filter_type(models.Rack, lookups=True)
class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, RackBaseFilterMixin):
form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field()
@@ -836,7 +836,7 @@ class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
)
@strawberry_django.filter(models.RackReservation, lookups=True)
@strawberry_django.filter_type(models.RackReservation, lookups=True)
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
rack_id: ID | None = strawberry_django.filter_field()
@@ -848,12 +848,12 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
description: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.RackRole, lookups=True)
@strawberry_django.filter_type(models.RackRole, lookups=True)
class RackRoleFilter(OrganizationalModelFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.RearPort, lookups=True)
@strawberry_django.filter_type(models.RearPort, lookups=True)
class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@@ -862,7 +862,7 @@ class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMi
)
@strawberry_django.filter(models.RearPortTemplate, lookups=True)
@strawberry_django.filter_type(models.RearPortTemplate, lookups=True)
class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@@ -871,7 +871,7 @@ class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
)
@strawberry_django.filter(models.Region, lookups=True)
@strawberry_django.filter_type(models.Region, lookups=True)
class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -881,7 +881,7 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
)
@strawberry_django.filter(models.Site, lookups=True)
@strawberry_django.filter_type(models.Site, lookups=True)
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field()
@@ -915,7 +915,7 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
)
@strawberry_django.filter(models.SiteGroup, lookups=True)
@strawberry_django.filter_type(models.SiteGroup, lookups=True)
class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
@@ -925,7 +925,7 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
)
@strawberry_django.filter(models.VirtualChassis, lookups=True)
@strawberry_django.filter_type(models.VirtualChassis, lookups=True)
class VirtualChassisFilter(PrimaryModelFilterMixin):
master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
master_id: ID | None = strawberry_django.filter_field()
@@ -937,7 +937,7 @@ class VirtualChassisFilter(PrimaryModelFilterMixin):
member_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.VirtualDeviceContext, lookups=True)
@strawberry_django.filter_type(models.VirtualDeviceContext, lookups=True)
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field()

View File

@@ -33,6 +33,7 @@ if TYPE_CHECKING:
from tenancy.graphql.types import TenantType
from users.graphql.types import UserType
from virtualization.graphql.types import ClusterType, VMInterfaceType, VirtualMachineType
from vpn.graphql.types import L2VPNTerminationType
from wireless.graphql.types import WirelessLANType, WirelessLinkType
__all__ = (
@@ -440,6 +441,7 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P
primary_mac_address: Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')] | None
qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
vlan_translation_policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] | None
l2vpn_termination: Annotated["L2VPNTerminationType", strawberry.lazy('vpn.graphql.types')] | None
vdcs: List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]
tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
@@ -541,10 +543,10 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi
class ManufacturerType(OrganizationalObjectType, ContactsMixin):
platforms: List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]
device_types: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
device_types: List[Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]]
inventory_item_templates: List[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]
inventory_items: List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]
module_types: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
module_types: List[Annotated["ModuleTypeType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(
@@ -617,11 +619,11 @@ class ModuleTypeType(NetBoxObjectType):
frontporttemplates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
consoleserverporttemplates: List[Annotated["ConsoleServerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
interfacetemplates: List[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]]
powerporttemplates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
powerporttemplates: List[Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
poweroutlettemplates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
rearporttemplates: List[Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
instances: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
consoleporttemplates: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
instances: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
consoleporttemplates: List[Annotated["ConsolePortTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type(

View File

@@ -100,3 +100,16 @@ class Migration(migrations.Migration):
),
),
]
def oc_rename_type(objectchange, reverting):
for data in (objectchange.prechange_data, objectchange.postchange_data):
if data is None:
continue
if 'type' in data:
data['form_factor'] = data.pop('type')
objectchange_migrators = {
'dcim.rack': oc_rename_type,
}

View File

@@ -26,49 +26,50 @@ def set_null_values(apps, schema_editor):
RackType = apps.get_model('dcim', 'RackType')
RearPort = apps.get_model('dcim', 'RearPort')
Site = apps.get_model('dcim', 'Site')
db_alias = schema_editor.connection.alias
Cable.objects.filter(length_unit='').update(length_unit=None)
Cable.objects.filter(type='').update(type=None)
ConsolePort.objects.filter(cable_end='').update(cable_end=None)
ConsolePort.objects.filter(type='').update(type=None)
ConsolePortTemplate.objects.filter(type='').update(type=None)
ConsoleServerPort.objects.filter(cable_end='').update(cable_end=None)
ConsoleServerPort.objects.filter(type='').update(type=None)
ConsoleServerPortTemplate.objects.filter(type='').update(type=None)
Device.objects.filter(airflow='').update(airflow=None)
Device.objects.filter(face='').update(face=None)
DeviceType.objects.filter(airflow='').update(airflow=None)
DeviceType.objects.filter(subdevice_role='').update(subdevice_role=None)
DeviceType.objects.filter(weight_unit='').update(weight_unit=None)
FrontPort.objects.filter(cable_end='').update(cable_end=None)
Interface.objects.filter(cable_end='').update(cable_end=None)
Interface.objects.filter(mode='').update(mode=None)
Interface.objects.filter(poe_mode='').update(poe_mode=None)
Interface.objects.filter(poe_type='').update(poe_type=None)
Interface.objects.filter(rf_channel='').update(rf_channel=None)
Interface.objects.filter(rf_role='').update(rf_role=None)
InterfaceTemplate.objects.filter(poe_mode='').update(poe_mode=None)
InterfaceTemplate.objects.filter(poe_type='').update(poe_type=None)
InterfaceTemplate.objects.filter(rf_role='').update(rf_role=None)
ModuleType.objects.filter(airflow='').update(airflow=None)
ModuleType.objects.filter(weight_unit='').update(weight_unit=None)
PowerFeed.objects.filter(cable_end='').update(cable_end=None)
PowerOutlet.objects.filter(cable_end='').update(cable_end=None)
PowerOutlet.objects.filter(feed_leg='').update(feed_leg=None)
PowerOutlet.objects.filter(type='').update(type=None)
PowerOutletTemplate.objects.filter(feed_leg='').update(feed_leg=None)
PowerOutletTemplate.objects.filter(type='').update(type=None)
PowerPort.objects.filter(cable_end='').update(cable_end=None)
PowerPort.objects.filter(type='').update(type=None)
PowerPortTemplate.objects.filter(type='').update(type=None)
Rack.objects.filter(airflow='').update(airflow=None)
Rack.objects.filter(form_factor='').update(form_factor=None)
Rack.objects.filter(outer_unit='').update(outer_unit=None)
Rack.objects.filter(weight_unit='').update(weight_unit=None)
RackType.objects.filter(outer_unit='').update(outer_unit=None)
RackType.objects.filter(weight_unit='').update(weight_unit=None)
RearPort.objects.filter(cable_end='').update(cable_end=None)
Site.objects.filter(time_zone='').update(time_zone=None)
Cable.objects.using(db_alias).filter(length_unit='').update(length_unit=None)
Cable.objects.using(db_alias).filter(type='').update(type=None)
ConsolePort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
ConsolePort.objects.using(db_alias).filter(type='').update(type=None)
ConsolePortTemplate.objects.using(db_alias).filter(type='').update(type=None)
ConsoleServerPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
ConsoleServerPort.objects.using(db_alias).filter(type='').update(type=None)
ConsoleServerPortTemplate.objects.using(db_alias).filter(type='').update(type=None)
Device.objects.using(db_alias).filter(airflow='').update(airflow=None)
Device.objects.using(db_alias).filter(face='').update(face=None)
DeviceType.objects.using(db_alias).filter(airflow='').update(airflow=None)
DeviceType.objects.using(db_alias).filter(subdevice_role='').update(subdevice_role=None)
DeviceType.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
FrontPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
Interface.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
Interface.objects.using(db_alias).filter(mode='').update(mode=None)
Interface.objects.using(db_alias).filter(poe_mode='').update(poe_mode=None)
Interface.objects.using(db_alias).filter(poe_type='').update(poe_type=None)
Interface.objects.using(db_alias).filter(rf_channel='').update(rf_channel=None)
Interface.objects.using(db_alias).filter(rf_role='').update(rf_role=None)
InterfaceTemplate.objects.using(db_alias).filter(poe_mode='').update(poe_mode=None)
InterfaceTemplate.objects.using(db_alias).filter(poe_type='').update(poe_type=None)
InterfaceTemplate.objects.using(db_alias).filter(rf_role='').update(rf_role=None)
ModuleType.objects.using(db_alias).filter(airflow='').update(airflow=None)
ModuleType.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
PowerFeed.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
PowerOutlet.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
PowerOutlet.objects.using(db_alias).filter(feed_leg='').update(feed_leg=None)
PowerOutlet.objects.using(db_alias).filter(type='').update(type=None)
PowerOutletTemplate.objects.using(db_alias).filter(feed_leg='').update(feed_leg=None)
PowerOutletTemplate.objects.using(db_alias).filter(type='').update(type=None)
PowerPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
PowerPort.objects.using(db_alias).filter(type='').update(type=None)
PowerPortTemplate.objects.using(db_alias).filter(type='').update(type=None)
Rack.objects.using(db_alias).filter(airflow='').update(airflow=None)
Rack.objects.using(db_alias).filter(form_factor='').update(form_factor=None)
Rack.objects.using(db_alias).filter(outer_unit='').update(outer_unit=None)
Rack.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
RackType.objects.using(db_alias).filter(outer_unit='').update(outer_unit=None)
RackType.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
RearPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
Site.objects.using(db_alias).filter(time_zone='').update(time_zone=None)
class Migration(migrations.Migration):

View File

@@ -1,4 +1,6 @@
import django.db.models.deletion
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db import migrations, models
@@ -6,19 +8,26 @@ def populate_mac_addresses(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')
Interface = apps.get_model('dcim', 'Interface')
MACAddress = apps.get_model('dcim', 'MACAddress')
db_alias = schema_editor.connection.alias
interface_ct = ContentType.objects.get_for_model(Interface)
mac_addresses = [
MACAddress(
mac_address=interface.mac_address, assigned_object_type=interface_ct, assigned_object_id=interface.pk
mac_address=interface.mac_address,
assigned_object_type=interface_ct,
assigned_object_id=interface.pk
)
for interface in Interface.objects.filter(mac_address__isnull=False)
for interface in Interface.objects.using(db_alias).filter(mac_address__isnull=False)
]
MACAddress.objects.bulk_create(mac_addresses, batch_size=100)
MACAddress.objects.using(db_alias).bulk_create(mac_addresses, batch_size=100)
# TODO: Optimize interface updates
for mac_address in mac_addresses:
Interface.objects.filter(pk=mac_address.assigned_object_id).update(primary_mac_address=mac_address)
Interface.objects.using(db_alias).filter(
pk=mac_address.assigned_object_id
).update(
primary_mac_address=mac_address
)
class Migration(migrations.Migration):
@@ -44,3 +53,43 @@ class Migration(migrations.Migration):
name='mac_address',
),
]
# See peer migrator in virtualization.0048_populate_mac_addresses before making changes
def oc_interface_primary_mac_address(objectchange, reverting):
MACAddress = apps.get_model('dcim', 'MACAddress')
interface_ct = ContentType.objects.get_by_natural_key('dcim', 'interface')
# Swap data order if the change is being reverted
if not reverting:
before, after = objectchange.prechange_data, objectchange.postchange_data
else:
before, after = objectchange.postchange_data, objectchange.prechange_data
if after.get('mac_address') != before.get('mac_address'):
# Create & assign the new MACAddress (if any)
if after.get('mac_address'):
mac = MACAddress.objects.create(
mac_address=after['mac_address'],
assigned_object_type=interface_ct,
assigned_object_id=objectchange.changed_object_id,
)
after['primary_mac_address'] = mac.pk
else:
after['primary_mac_address'] = None
# Delete the old MACAddress (if any)
if before.get('mac_address'):
MACAddress.objects.filter(
mac_address=before['mac_address'],
assigned_object_type=interface_ct,
assigned_object_id=objectchange.changed_object_id,
).delete()
before['primary_mac_address'] = None
before.pop('mac_address', None)
after.pop('mac_address', None)
objectchange_migrators = {
'dcim.interface': oc_interface_primary_mac_address,
}

View File

@@ -11,13 +11,16 @@ def load_initial_data(apps, schema_editor):
Load initial ModuleTypeProfile objects from file.
"""
ModuleTypeProfile = apps.get_model('dcim', 'ModuleTypeProfile')
db_alias = schema_editor.connection.alias
initial_profiles = (
'cpu',
'fan',
'gpu',
'hard_disk',
'memory',
'power_supply'
'power_supply',
'expansion_card'
)
for name in initial_profiles:
@@ -25,7 +28,7 @@ def load_initial_data(apps, schema_editor):
with file_path.open('r') as f:
data = json.load(f)
try:
ModuleTypeProfile.objects.create(**data)
ModuleTypeProfile.objects.using(db_alias).create(**data)
except Exception as e:
print(f"Error loading data from {file_path}")
raise e

View File

@@ -0,0 +1,54 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0207_remove_redundant_indexes'),
('extras', '0129_fix_script_paths'),
]
operations = [
migrations.AlterField(
model_name='platform',
name='name',
field=models.CharField(max_length=100),
),
migrations.AlterField(
model_name='platform',
name='slug',
field=models.SlugField(max_length=100),
),
migrations.AddConstraint(
model_name='platform',
constraint=models.UniqueConstraint(
fields=('manufacturer', 'name'),
name='dcim_platform_manufacturer_name'
),
),
migrations.AddConstraint(
model_name='platform',
constraint=models.UniqueConstraint(
condition=models.Q(('manufacturer__isnull', True)),
fields=('name',),
name='dcim_platform_name',
violation_error_message='Platform name must be unique.'
),
),
migrations.AddConstraint(
model_name='platform',
constraint=models.UniqueConstraint(
fields=('manufacturer', 'slug'),
name='dcim_platform_manufacturer_slug'
),
),
migrations.AddConstraint(
model_name='platform',
constraint=models.UniqueConstraint(
condition=models.Q(('manufacturer__isnull', True)),
fields=('slug',),
name='dcim_platform_slug',
violation_error_message='Platform slug must be unique.'
),
),
]

View File

@@ -0,0 +1,24 @@
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0208_platform_manufacturer_uniqueness'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='tx_power',
field=models.SmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(-40),
django.core.validators.MaxValueValidator(127)
]
),
),
]

View File

@@ -0,0 +1,44 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0209_interface_tx_power_negative'),
('extras', '0129_fix_script_paths'),
]
operations = [
migrations.AddConstraint(
model_name='devicerole',
constraint=models.UniqueConstraint(
fields=('parent', 'name'),
name='dcim_devicerole_parent_name'
),
),
migrations.AddConstraint(
model_name='devicerole',
constraint=models.UniqueConstraint(
condition=models.Q(('parent__isnull', True)),
fields=('name',),
name='dcim_devicerole_name',
violation_error_message='A top-level device role with this name already exists.'
),
),
migrations.AddConstraint(
model_name='devicerole',
constraint=models.UniqueConstraint(
fields=('parent', 'slug'),
name='dcim_devicerole_parent_slug'
),
),
migrations.AddConstraint(
model_name='devicerole',
constraint=models.UniqueConstraint(
condition=models.Q(('parent__isnull', True)),
fields=('slug',),
name='dcim_devicerole_slug',
violation_error_message='A top-level device role with this slug already exists.'
),
),
]

View File

@@ -0,0 +1,15 @@
{
"name": "Expansion card",
"schema": {
"properties": {
"connector_type": {
"type": "string",
"description": "Connector type e.g. PCIe x4"
},
"bandwidth": {
"type": "integer",
"description": "Total Bandwidth for this module"
}
}
}
}

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