Compare commits

..

71 Commits

Author SHA1 Message Date
Jeremy Stretch
0dbfbf6941 Merge pull request #13591 from netbox-community/develop
Correct version number
2023-08-28 17:07:15 -04:00
Jeremy Stretch
d515530277 Merge branch 'master' into develop 2023-08-28 17:05:59 -04:00
Jeremy Stretch
4343e0566b Correct version number 2023-08-28 17:04:37 -04:00
Jeremy Stretch
8555269f7e Merge pull request #13589 from netbox-community/develop
Release v3.5.9
2023-08-28 16:58:09 -04:00
Jeremy Stretch
f42a2ac10c Merge branch 'master' into develop 2023-08-28 16:19:44 -04:00
Jeremy Stretch
4ea3a29c0e Release v3.5.9 2023-08-28 16:13:13 -04:00
Arthur Hanson
29877c9abe 12489 Use HTMX for Location and Non-Racked Devices in Site detail view (#12491)
* 12489 use htmx for site view locations and non-racked-devices

* 12489 remove now unused queries in context

* adds device type and role to device component filter #12015

* Revert "Fixes #12463: Fix the association of completed jobs with reports & scripts in the REST API"

This reverts commit a29a07ed26.

* 12489 update nonracked_devices on rack and location templates

* 12489 fix whitespace issue

* Undo errant commits

* 12489 update site id in templates

* 12489 remove nonracked_devices include

* 12489 add has_position filter

* Use empty lookup for position field

* Remove non-racked devices list from rack view (was moved to a tab)

* Clean up location and device tables

* Restore plugins block on rack template

---------

Co-authored-by: Abhimanyu Saharan <desk.abhimanyu@gmail.com>
Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
2023-08-28 16:03:35 -04:00
Jeremy Stretch
480f83c42d Closes #13585: Introduce 'empty' lookup for numeric value filters 2023-08-28 15:25:37 -04:00
Jeremy Stretch
faf89350ac Fixes #13569: Fix selection widgets for related interfaces when bulk editing interfaces under device view 2023-08-28 13:04:42 -04:00
Jeremy Stretch
d9c3ce935f Changelog for #12825, #13313, #13415, #13507, #13542, #13543, #13544, #13556 2023-08-28 09:10:44 -04:00
Abhimanyu Saharan
8d8f57e8b8 Adds parent filter on iprange (#13568)
* adds parent filter on iprange #13313

* lint fix

* adds filterset test

* Filter should match both start & end of IP range

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-08-28 09:05:43 -04:00
Abhimanyu Saharan
0a3be0b7ea adds related models count on custom field #12825 2023-08-28 08:34:33 -04:00
Abhimanyu Saharan
00ebdfe0df adds related models count on custom field #12825 2023-08-28 08:34:33 -04:00
Jeremy Stretch
d79fa131bb Closes #13415: Pass request context when rendering custom links in a table column 2023-08-25 13:14:47 -04:00
Abhimanyu Saharan
be2b24a155 fixes the swagger schema for token provisioning #13557 2023-08-25 09:45:03 -04:00
Abhimanyu Saharan
03b341dbfd adds missing status choicefield for vdc #13556 2023-08-25 09:40:04 -04:00
Arthur
ca5e69897d 13396 upgrade graphiql 2023-08-24 14:17:09 -04:00
Abhimanyu Saharan
3090dd4934 Fixed permission for config context UI view (#13547)
* fixed permission for config context UI view #13543

* removed extras.view_configcontext permission #13543
2023-08-24 14:13:31 -04:00
Abhimanyu Saharan
1f1d1ee502 adds additional safe HTTP headers to request #13542 2023-08-24 14:12:08 -04:00
Abhimanyu Saharan
1c2cf11f47 fixes global search when the content type is not found #13507 2023-08-24 14:09:48 -04:00
Jeremy Stretch
08961e751d Revert changes from #13373 pending further discussion around implementation
This reverts commit 66e4e31209.
2023-08-24 14:02:15 -04:00
Abhimanyu Saharan
88bf82be05 clear all cache when lazy is not used #13544 2023-08-24 10:12:48 -04:00
Jeremy Stretch
506884bc4d Changelog for #11272, #13516, #13530, #13536 2023-08-23 14:44:14 -04:00
Jeremy Stretch
646fa341ab Closes #13470: Remove misleading statement about access to report results 2023-08-23 14:41:38 -04:00
Jeremy Stretch
d73f7b1943 Fixes #13530: Ensure script log messages are cast as strings for proper serialization 2023-08-23 14:41:21 -04:00
Abhimanyu Saharan
a75e8416a4 adds vlan child table to vlan group #13536 2023-08-23 13:39:10 -04:00
Arthur
f743f2cfb8 11272 make position field work correctly when internationalizion enabled 2023-08-23 13:30:01 -04:00
Jeremy Stretch
3c0a3ca703 Fixes #13516: Plugin utility functions should be importable from extras.plugins 2023-08-22 10:27:21 -04:00
Jeremy Stretch
45062697c5 Changelog for #11508, #13358, #13477, #13478, #13500, #13503 2023-08-21 15:10:12 -04:00
Arthur Hanson
66e4e31209 11508 Add group assignments for Azure SSO (#13373)
* 11508 temp azure changes

* 11508 map AzureAD groups to NetBox groups

* 11508 add is_active, reset superuser and staff based on Azure

* 11508 remove is_active, add documentation use azuread

* 11508 remove addition to settings

* 11508 review changes, add additional logging and error checking

* 11508 review changes, remove extra flag

* 11508 review changes, change SOCIAL_AUTH_ to REMOTE_AUTH_BACKEND

* 11508 clear user groups

* 11508 clear user groups

* 11508 review feedback change config key

* 11508 review changes

* 11508 review changes - add error checking

* 11508 review changes - flexible config params
2023-08-21 14:42:16 -04:00
kkthxbye-code
c86cfe3cbf Correct filter name in redirect after bulk edit
* Added modified_by_request filter to ChangeLoggedFilterSet
2023-08-21 14:35:08 -04:00
Arthur
28e112743f 13503 fix rack space utilization graph for internationalization 2023-08-21 14:21:50 -04:00
Abhimanyu Saharan
4004966b16 fix content type filter on export template #13478 2023-08-17 15:29:21 -04:00
Arthur
fe95cb434a 13500 fix l2vpntermination bulk update 2023-08-17 15:25:23 -04:00
Alexander Haase
16e2283d19 Fix git DataSource clone authentication
Anonymous git clones (in GitLab) require the username and password not
to be set in order to successfully clone. This patch will define clone
args only, if the username passed is not empty.
2023-08-15 13:29:03 -04:00
Jeremy Stretch
c46536f469 Merge pull request #13474 from jose-d/develop-1
upgrading.md: there shouldbe OLDVER instead of NEWVER
2023-08-15 11:26:43 -04:00
jose_d
9450ce4c3a upgrading.md: there shouldbe OLDVER instead of NEWVER 2023-08-15 16:19:31 +02:00
Jeremy Stretch
1c9a8ec6bd PRVB 2023-08-15 10:00:24 -04:00
Jeremy Stretch
8f5005efd5 Merge pull request #13472 from netbox-community/develop
Release v3.5.8
2023-08-15 09:56:23 -04:00
Jeremy Stretch
e61795d5c6 Release v3.5.8 2023-08-15 09:18:15 -04:00
Joel D. Tague
892c10b1f0 feat: add 200Gbps & 400Gbps interface speed options 2023-08-15 09:11:40 -04:00
Abhimanyu Saharan
ea107b6b86 adds object view to allow changelog page to be opened #13463 2023-08-14 09:47:58 -04:00
Jeremy Stretch
b9b9c065cc Changelog for #10030, #11578, #12639 2023-08-14 08:55:47 -04:00
Jeremy Stretch
b583770765 Fixes #13451: Disable table ordering for custom link columns 2023-08-14 08:51:16 -04:00
Abhimanyu Saharan
37d6f6abca Merge pull request #13461 from netbox-community/fix/13460-spelling
Fixed spelling for Attributes
2023-08-14 01:18:37 -07:00
Abhimanyu Saharan
be3f48c677 Fixed spelling for Attributes #13460 2023-08-14 13:29:11 +05:30
kkthxbye
5de9d3f15f Fixes #12639 - Make sure name expansions throws a validation error on decrementing ranges (#13326)
* Fixes #12639 - Make sure name expansions throws a validation error on decrementing ranges

* Fix pep8

* Also fail on equal start & end values

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-08-11 11:53:16 -04:00
Daniel W. Anner
40afe6cf36 Feature - Schema Generation (#13353)
* Schema generation is working

* Added option to either dump to a file or the console

* Moving schema file and utilizing settings definition for file paths

* Cleaning up the imports and fixing a few pythonic issues

* Tweak command flags

* Clean up choices mapping

* Misc cleanup

* Rename & move template file

* Move management command from extras to dcim

* Update release checklist

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-08-11 11:00:26 -04:00
Arthur Hanson
9fd07b594c 11578 mark swagger available- apis to accept lists in post (#13445)
* 11578 change swagger for available-ips to accept lists

* 11578 change swagger for available-xxx to accept lists
2023-08-11 09:49:03 -04:00
Jeremy Stretch
dc7411e4c5 Fixes #13446: Don't disable bulk edit/delete buttons after deselecting "select all" checkbox 2023-08-11 08:56:58 -04:00
Jeremy Stretch
72e1e8fab1 Changelog for #11675, #11922, #12665, #13368, #13414 2023-08-09 15:02:49 -04:00
Arthur
dcdb4d27ec 12665 add semicolon to link sanitation safe string 2023-08-09 14:49:34 -04:00
kkthxbye-code
9b1406a1a7 Don't hide HIDDEN_IFUNSET custom fields from bulk import fields 2023-08-09 14:47:20 -04:00
Abhimanyu Saharan
545769ad88 Adds generic object children template (#13388)
* adds generic tab view template #12110

* Rename view_tab.html and move to generic/

* Fix console ports template

* Move bulk operations view resolution to template

* Avoid setting default template_name on ObjectChildrenView

* Move base_template and table_config context vars to base context

* removed bulk_delete_control from templates

* refactored bulk_controls view

* fixed table_config

* renamed object_tab.html to objectchildren_list.html

* removed unused import

* Refactor template blocks for bulk operation buttons

* Rename object children generic template

* Move disconnect bulk action into a separate template for device components

* Fix cluster devices & VM interfaces views

* minor button label change

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-08-09 14:16:03 -04:00
Jeremy Stretch
f5a1f83f9f Closes #13368: Report installed plugins during server error (#13387)
* Introduce get_installed_plugins() utility

* Extend 500 error template to list installed plugins

* Move get_plugin_config() to extras.plugins.utils
2023-08-07 15:29:20 -04:00
Jeremy Stretch
f9648d8544 Closes #13400: Add 'name' property to BaseTable class 2023-08-07 10:48:41 -04:00
Jeremy Stretch
2236b86c35 Closes #11922: Populate assigned VDCs when adding a child interface 2023-08-04 15:25:59 -04:00
Jeremy Stretch
0dd319d0c8 Closes #11675: Add support for specifying import/export route targets during VRF bulk import 2023-08-04 15:25:06 -04:00
Jeremy Stretch
88562d7dcf Changelog for #12750, #12889, #13033, #13151, #13343, #13369 2023-08-04 13:36:33 -04:00
Abhimanyu Saharan
01bb09db67 adds delete for SyncedDataMixin when related AutoSyncRecord is available #12750 2023-08-04 13:25:56 -04:00
Henrik Strand
43ce453938 Adding interface TYPE_400GE_CFP2/400gbase-x-cfp2 (#13338)
* Added 400G CFP2 to InterfaceTypeChoices

* Added new type to choises
2023-08-04 11:32:52 -04:00
Jeremy Stretch
7f22c6bf12 Include notes re: demo data and netbox-docker 2023-08-04 10:12:15 -04:00
Jeremy Stretch
93a862cded Add stadium analogy and behavior anti-patterns 2023-08-04 08:55:43 -04:00
Jeremy Stretch
9cc295827b Fixes #13369: Fix job termination status for failed reports 2023-08-04 08:12:52 -04:00
Matej Vadnjal
a807cca29e Fixes #13033: add formatted speed column to Interfaces (#13275)
* Fixes #13033: add formatted speed column to Interfaces

* use TemplateColumn instead of own class
2023-08-02 16:08:14 -04:00
Abhimanyu Saharan
57860f26b7 Adds assigned bool for IP address API (#13301)
* adds assigned bool for ip address API #13151

* Add filterset test

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2023-08-02 15:45:09 -04:00
Abhimanyu Saharan
ab916a1819 fixes dummy payload URL for webhook test 2023-08-02 15:23:05 -04:00
Abhimanyu Saharan
a68831d3a1 fixes provider_network_id for related circuits #13343 2023-08-02 15:17:14 -04:00
Jeremy Stretch
a4c9cbc6dd Remove hard-coded test runner 2023-08-02 08:55:38 -04:00
Jeremy Stretch
0b10131564 Satisfy PEP8 E721 linter complaints 2023-07-30 13:34:08 -04:00
Jeremy Stretch
006c353d46 PRVB 2023-07-28 10:31:54 -04:00
104 changed files with 1929 additions and 2292 deletions

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.5.7
placeholder: v3.5.9
validations:
required: true
- type: dropdown

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.5.7
placeholder: v3.5.9
validations:
required: true
- type: dropdown

View File

@@ -14,12 +14,25 @@
</div>
<h3></h3>
Some general tips for engaging here on GitHub:
## :information_source: Welcome to the Stadium!
In her book [Working in Public](https://www.amazon.com/Working-Public-Making-Maintenance-Software/dp/0578675862), Nadia Eghbal defines four production models for open source projects, categorized by contributor and user growth: federations, clubs, toys, and stadiums. The NetBox project fits her definition of a stadium very well:
> Stadiums are projects with low contributor growth and high user growth. While they may receive casual contributions, their regular contributor base does not grow proportionately to their users. As a result, they tend to be powered by one or a few developers.
The bulk of NetBox's development is carried out by a handful of core maintainers, with occasional contributions from collaborators in the community. We find the stadium analogy very useful in conveying the roles and obligations of both contributors and users.
If you're a contributor, actively working on the center stage, you have an obligation to produce quality content that will benefit the project as a whole. Conversely, if you're in the audience consuming the work being produced, you have the option of making requests and suggestions, but must also recognize that contributors are under no obligation to act on them.
NetBox users are welcome to participate in either role, on stage or in the crowd. We ask only that you acknowledge the role you've chosen and respect the roles of others.
### General Tips for Working on GitHub
* Register for a free [GitHub account](https://github.com/signup) if you haven't already.
* You can use [GitHub Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) for formatting text and adding images.
* To help mitigate notification spam, please avoid "bumping" issues with no activity. (To vote an issue up or down, use a :thumbsup: or :thumbsdown: reaction.)
* Please avoid pinging members with `@` unless they've previously expressed interest or involvement with that particular issue.
* Familiarize yourself with [this list of discussion anti-patterns](https://github.com/bradfitz/issue-tracker-behaviors) and make every effort to avoid them.
## :bug: Reporting Bugs

View File

@@ -0,0 +1,562 @@
{
"type": "object",
"additionalProperties": false,
"definitions": {
"airflow": {
"type": "string",
"enum": [
"front-to-rear",
"rear-to-front",
"left-to-right",
"right-to-left",
"side-to-rear",
"passive",
"mixed"
]
},
"weight-unit": {
"type": "string",
"enum": [
"kg",
"g",
"lb",
"oz"
]
},
"subdevice-role": {
"type": "string",
"enum": [
"parent",
"child"
]
},
"console-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"de-9",
"db-25",
"rj-11",
"rj-12",
"rj-45",
"mini-din-8",
"usb-a",
"usb-b",
"usb-c",
"usb-mini-a",
"usb-mini-b",
"usb-micro-a",
"usb-micro-b",
"usb-micro-ab",
"other"
]
}
}
},
"console-server-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"de-9",
"db-25",
"rj-11",
"rj-12",
"rj-45",
"mini-din-8",
"usb-a",
"usb-b",
"usb-c",
"usb-mini-a",
"usb-mini-b",
"usb-micro-a",
"usb-micro-b",
"usb-micro-ab",
"other"
]
}
}
},
"power-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"iec-60320-c6",
"iec-60320-c8",
"iec-60320-c14",
"iec-60320-c16",
"iec-60320-c20",
"iec-60320-c22",
"iec-60309-p-n-e-4h",
"iec-60309-p-n-e-6h",
"iec-60309-p-n-e-9h",
"iec-60309-2p-e-4h",
"iec-60309-2p-e-6h",
"iec-60309-2p-e-9h",
"iec-60309-3p-e-4h",
"iec-60309-3p-e-6h",
"iec-60309-3p-e-9h",
"iec-60309-3p-n-e-4h",
"iec-60309-3p-n-e-6h",
"iec-60309-3p-n-e-9h",
"iec-60906-1",
"nbr-14136-10a",
"nbr-14136-20a",
"nema-1-15p",
"nema-5-15p",
"nema-5-20p",
"nema-5-30p",
"nema-5-50p",
"nema-6-15p",
"nema-6-20p",
"nema-6-30p",
"nema-6-50p",
"nema-10-30p",
"nema-10-50p",
"nema-14-20p",
"nema-14-30p",
"nema-14-50p",
"nema-14-60p",
"nema-15-15p",
"nema-15-20p",
"nema-15-30p",
"nema-15-50p",
"nema-15-60p",
"nema-l1-15p",
"nema-l5-15p",
"nema-l5-20p",
"nema-l5-30p",
"nema-l5-50p",
"nema-l6-15p",
"nema-l6-20p",
"nema-l6-30p",
"nema-l6-50p",
"nema-l10-30p",
"nema-l14-20p",
"nema-l14-30p",
"nema-l14-50p",
"nema-l14-60p",
"nema-l15-20p",
"nema-l15-30p",
"nema-l15-50p",
"nema-l15-60p",
"nema-l21-20p",
"nema-l21-30p",
"nema-l22-30p",
"cs6361c",
"cs6365c",
"cs8165c",
"cs8265c",
"cs8365c",
"cs8465c",
"ita-c",
"ita-e",
"ita-f",
"ita-ef",
"ita-g",
"ita-h",
"ita-i",
"ita-j",
"ita-k",
"ita-l",
"ita-m",
"ita-n",
"ita-o",
"usb-a",
"usb-b",
"usb-c",
"usb-mini-a",
"usb-mini-b",
"usb-micro-a",
"usb-micro-b",
"usb-micro-ab",
"usb-3-b",
"usb-3-micro-b",
"dc-terminal",
"saf-d-grid",
"neutrik-powercon-20",
"neutrik-powercon-32",
"neutrik-powercon-true1",
"neutrik-powercon-true1-top",
"ubiquiti-smartpower",
"hardwired",
"other"
]
}
}
},
"power-outlet": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"iec-60320-c5",
"iec-60320-c7",
"iec-60320-c13",
"iec-60320-c15",
"iec-60320-c19",
"iec-60320-c21",
"iec-60309-p-n-e-4h",
"iec-60309-p-n-e-6h",
"iec-60309-p-n-e-9h",
"iec-60309-2p-e-4h",
"iec-60309-2p-e-6h",
"iec-60309-2p-e-9h",
"iec-60309-3p-e-4h",
"iec-60309-3p-e-6h",
"iec-60309-3p-e-9h",
"iec-60309-3p-n-e-4h",
"iec-60309-3p-n-e-6h",
"iec-60309-3p-n-e-9h",
"iec-60906-1",
"nbr-14136-10a",
"nbr-14136-20a",
"nema-1-15r",
"nema-5-15r",
"nema-5-20r",
"nema-5-30r",
"nema-5-50r",
"nema-6-15r",
"nema-6-20r",
"nema-6-30r",
"nema-6-50r",
"nema-10-30r",
"nema-10-50r",
"nema-14-20r",
"nema-14-30r",
"nema-14-50r",
"nema-14-60r",
"nema-15-15r",
"nema-15-20r",
"nema-15-30r",
"nema-15-50r",
"nema-15-60r",
"nema-l1-15r",
"nema-l5-15r",
"nema-l5-20r",
"nema-l5-30r",
"nema-l5-50r",
"nema-l6-15r",
"nema-l6-20r",
"nema-l6-30r",
"nema-l6-50r",
"nema-l10-30r",
"nema-l14-20r",
"nema-l14-30r",
"nema-l14-50r",
"nema-l14-60r",
"nema-l15-20r",
"nema-l15-30r",
"nema-l15-50r",
"nema-l15-60r",
"nema-l21-20r",
"nema-l21-30r",
"nema-l22-30r",
"CS6360C",
"CS6364C",
"CS8164C",
"CS8264C",
"CS8364C",
"CS8464C",
"ita-e",
"ita-f",
"ita-g",
"ita-h",
"ita-i",
"ita-j",
"ita-k",
"ita-l",
"ita-m",
"ita-n",
"ita-o",
"ita-multistandard",
"usb-a",
"usb-micro-b",
"usb-c",
"dc-terminal",
"hdot-cx",
"saf-d-grid",
"neutrik-powercon-20a",
"neutrik-powercon-32a",
"neutrik-powercon-true1",
"neutrik-powercon-true1-top",
"ubiquiti-smartpower",
"hardwired",
"other"
]
},
"feed-leg": {
"type": "string",
"enum": [
"A",
"B",
"C"
]
}
}
},
"interface": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"virtual",
"bridge",
"lag",
"100base-fx",
"100base-lfx",
"100base-tx",
"100base-t1",
"1000base-t",
"2.5gbase-t",
"5gbase-t",
"10gbase-t",
"10gbase-cx4",
"1000base-x-gbic",
"1000base-x-sfp",
"10gbase-x-sfpp",
"10gbase-x-xfp",
"10gbase-x-xenpak",
"10gbase-x-x2",
"25gbase-x-sfp28",
"50gbase-x-sfp56",
"40gbase-x-qsfpp",
"50gbase-x-sfp28",
"100gbase-x-cfp",
"100gbase-x-cfp2",
"200gbase-x-cfp2",
"400gbase-x-cfp2",
"100gbase-x-cfp4",
"100gbase-x-cxp",
"100gbase-x-cpak",
"100gbase-x-dsfp",
"100gbase-x-sfpdd",
"100gbase-x-qsfp28",
"100gbase-x-qsfpdd",
"200gbase-x-qsfp56",
"200gbase-x-qsfpdd",
"400gbase-x-qsfpdd",
"400gbase-x-osfp",
"400gbase-x-cdfp",
"400gbase-x-cfp8",
"800gbase-x-qsfpdd",
"800gbase-x-osfp",
"1000base-kx",
"10gbase-kr",
"10gbase-kx4",
"25gbase-kr",
"40gbase-kr4",
"50gbase-kr",
"100gbase-kp4",
"100gbase-kr2",
"100gbase-kr4",
"ieee802.11a",
"ieee802.11g",
"ieee802.11n",
"ieee802.11ac",
"ieee802.11ad",
"ieee802.11ax",
"ieee802.11ay",
"ieee802.15.1",
"other-wireless",
"gsm",
"cdma",
"lte",
"sonet-oc3",
"sonet-oc12",
"sonet-oc48",
"sonet-oc192",
"sonet-oc768",
"sonet-oc1920",
"sonet-oc3840",
"1gfc-sfp",
"2gfc-sfp",
"4gfc-sfp",
"8gfc-sfpp",
"16gfc-sfpp",
"32gfc-sfp28",
"64gfc-qsfpp",
"128gfc-qsfp28",
"infiniband-sdr",
"infiniband-ddr",
"infiniband-qdr",
"infiniband-fdr10",
"infiniband-fdr",
"infiniband-edr",
"infiniband-hdr",
"infiniband-ndr",
"infiniband-xdr",
"t1",
"e1",
"t3",
"e3",
"xdsl",
"docsis",
"gpon",
"xg-pon",
"xgs-pon",
"ng-pon2",
"epon",
"10g-epon",
"cisco-stackwise",
"cisco-stackwise-plus",
"cisco-flexstack",
"cisco-flexstack-plus",
"cisco-stackwise-80",
"cisco-stackwise-160",
"cisco-stackwise-320",
"cisco-stackwise-480",
"cisco-stackwise-1t",
"juniper-vcp",
"extreme-summitstack",
"extreme-summitstack-128",
"extreme-summitstack-256",
"extreme-summitstack-512",
"other"
]
},
"poe_mode": {
"type": "string",
"enum": [
"pd",
"pse"
]
},
"poe_type": {
"type": "string",
"enum": [
"type1-ieee802.3af",
"type2-ieee802.3at",
"type3-ieee802.3bt",
"type4-ieee802.3bt",
"passive-24v-2pair",
"passive-24v-4pair",
"passive-48v-2pair",
"passive-48v-4pair"
]
}
}
},
"front-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"8p8c",
"8p6c",
"8p4c",
"8p2c",
"6p6c",
"6p4c",
"6p2c",
"4p4c",
"4p2c",
"gg45",
"tera-4p",
"tera-2p",
"tera-1p",
"110-punch",
"bnc",
"f",
"n",
"mrj21",
"fc",
"lc",
"lc-pc",
"lc-upc",
"lc-apc",
"lsh",
"lsh-pc",
"lsh-upc",
"lsh-apc",
"lx5",
"lx5-pc",
"lx5-upc",
"lx5-apc",
"mpo",
"mtrj",
"sc",
"sc-pc",
"sc-upc",
"sc-apc",
"st",
"cs",
"sn",
"sma-905",
"sma-906",
"urm-p2",
"urm-p4",
"urm-p8",
"splice",
"other"
]
}
}
},
"rear-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"8p8c",
"8p6c",
"8p4c",
"8p2c",
"6p6c",
"6p4c",
"6p2c",
"4p4c",
"4p2c",
"gg45",
"tera-4p",
"tera-2p",
"tera-1p",
"110-punch",
"bnc",
"f",
"n",
"mrj21",
"fc",
"lc",
"lc-pc",
"lc-upc",
"lc-apc",
"lsh",
"lsh-pc",
"lsh-upc",
"lsh-apc",
"lx5",
"lx5-pc",
"lx5-upc",
"lx5-apc",
"mpo",
"mtrj",
"sc",
"sc-pc",
"sc-upc",
"sc-apc",
"st",
"cs",
"sn",
"sma-905",
"sma-906",
"urm-p2",
"urm-p4",
"urm-p8",
"splice",
"other"
]
}
}
}
}
}

View File

@@ -111,7 +111,7 @@ The following methods are available to log results within a report:
The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status. Log messages also support using markdown syntax and will be rendered on the report result page.
To perform additional tasks, such as sending an email or calling a webhook, before or after a report is run, extend the `pre_run()` and/or `post_run()` methods, respectively. The status of a completed report is available as `self.failed` and the results object is `self.result`.
To perform additional tasks, such as sending an email or calling a webhook, before or after a report is run, extend the `pre_run()` and/or `post_run()` methods, respectively.
By default, reports within a module are ordered alphabetically in the reports list page. To return reports in a specific order, you can define the `report_order` variable at the end of your module. The `report_order` variable is a tuple which contains each Report class in the desired order. Any reports that are omitted from this list will be listed last.

View File

@@ -43,10 +43,22 @@ Follow these instructions to perform a new installation of NetBox in a temporary
Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below.
### Rebuild Demo Data (After Release)
After the release of a new minor version, generate a new demo data snapshot compatible with the new release. See the [`netbox-demo-data`](https://github.com/netbox-community/netbox-demo-data) repository for instructions.
---
## Patch Releases
### Notify netbox-docker Project of Any Relevant Changes
Notify the [`netbox-docker`](https://github.com/netbox-community/netbox-docker) maintainers (in **#netbox-docker**) of any changes that may be relevant to their build process, including:
* Significant changes to `upgrade.sh`
* Increases in minimum versions for service dependencies (PostgreSQL, Redis, etc.)
* Any changes to the reference installation
### Update Requirements
Before each release, update each of NetBox's Python dependencies to its most recent stable version. These are defined in `requirements.txt`, which is updated from `base_requirements.txt` using `pip`. To do this:
@@ -58,6 +70,16 @@ Before each release, update each of NetBox's Python dependencies to its most rec
In cases where upgrading a dependency to its most recent release is breaking, it should be constrained to its current minor version in `base_requirements.txt` with an explanatory comment and revisited for the next major NetBox release (see the [Address Constrained Dependencies](#address-constrained-dependencies) section above).
### Rebuild the Device Type Definition Schema
Run the following command to update the device type definition validation schema:
```nohighlight
./manage.py buildschema --write
```
This will automatically update the schema file at `contrib/generated_schema.json`.
### Update Version and Changelog
* Update the `VERSION` constant in `settings.py` to the new release version.

View File

@@ -59,7 +59,7 @@ Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if pres
```no-highlight
# Set $OLDVER to the NetBox version currently installed
NEWVER=3.4.9
OLDVER=3.4.9
sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/
sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/
sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/

View File

@@ -61,13 +61,14 @@ These lookup expressions can be applied by adding a suffix to the desired field'
Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
| Filter | Description |
|--------|-------------|
| `n` | Not equal to |
| `lt` | Less than |
| `lte` | Less than or equal to |
| `gt` | Greater than |
| `gte` | Greater than or equal to |
| Filter | Description |
|---------|--------------------------|
| `n` | Not equal to |
| `lt` | Less than |
| `lte` | Less than or equal to |
| `gt` | Greater than |
| `gte` | Greater than or equal to |
| `empty` | Is empty/null (boolean) |
Here is an example of a numeric field lookup expression that will return all VLANs with a VLAN ID greater than 900:
@@ -79,18 +80,18 @@ GET /api/ipam/vlans/?vid__gt=900
String based (char) fields (Name, Address, etc) support these lookup expressions:
| Filter | Description |
|--------|-------------|
| `n` | Not equal to |
| `ic` | Contains (case-insensitive) |
| `nic` | Does not contain (case-insensitive) |
| `isw` | Starts with (case-insensitive) |
| `nisw` | Does not start with (case-insensitive) |
| `iew` | Ends with (case-insensitive) |
| `niew` | Does not end with (case-insensitive) |
| `ie` | Exact match (case-insensitive) |
| `nie` | Inverse exact match (case-insensitive) |
| `empty` | Is empty (boolean) |
| Filter | Description |
|---------|----------------------------------------|
| `n` | Not equal to |
| `ic` | Contains (case-insensitive) |
| `nic` | Does not contain (case-insensitive) |
| `isw` | Starts with (case-insensitive) |
| `nisw` | Does not start with (case-insensitive) |
| `iew` | Ends with (case-insensitive) |
| `niew` | Does not end with (case-insensitive) |
| `ie` | Exact match (case-insensitive) |
| `nie` | Inverse exact match (case-insensitive) |
| `empty` | Is empty/null (boolean) |
Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:

View File

@@ -1,5 +1,62 @@
# NetBox v3.5
## v3.5.9 (2023-08-28)
### Enhancements
* [#12489](https://github.com/netbox-community/netbox/issues/12489) - Dynamically render location and device lists under site and location views
* [#12825](https://github.com/netbox-community/netbox/issues/12825) - Display assigned values count per obejct type under custom field view
* [#13313](https://github.com/netbox-community/netbox/issues/13313) - Enable filtering IP ranges by containing prefix
* [#13415](https://github.com/netbox-community/netbox/issues/13415) - Include request object in custom link renderer on tables
* [#13536](https://github.com/netbox-community/netbox/issues/13536) - Move child VLANs list to a separate tab under VLAN group view
* [#13542](https://github.com/netbox-community/netbox/issues/13542) - Pass additional HTTP headers through to custom script context
* [#13585](https://github.com/netbox-community/netbox/issues/13585) - Introduce `empty` lookup for numeric value filters
### Bug Fixes
* [#11272](https://github.com/netbox-community/netbox/issues/11272) - Fix localization support for device position field
* [#13358](https://github.com/netbox-community/netbox/issues/13358) - Git backend should send HTTP auth headers only if credentials have been defined
* [#13477](https://github.com/netbox-community/netbox/issues/13477) - Fix filtering of modified objects after bulk import/update
* [#13478](https://github.com/netbox-community/netbox/issues/13478) - Fix filtering of export templates by content type under web UI
* [#13500](https://github.com/netbox-community/netbox/issues/13500) - Fix form validation for bulk update of L2VPN terminations via bulk import form
* [#13503](https://github.com/netbox-community/netbox/issues/13503) - Fix utilization graph proportions when localization is enabled
* [#13507](https://github.com/netbox-community/netbox/issues/13507) - Avoid raising exception for invalid content type during global search
* [#13516](https://github.com/netbox-community/netbox/issues/13516) - Plugin utility functions should be importable from `extras.plugins`
* [#13530](https://github.com/netbox-community/netbox/issues/13530) - Ensure script log messages can be serialized as JSON data
* [#13543](https://github.com/netbox-community/netbox/issues/13543) - Config context tab under device/VM view should not require `extras.view_configcontext` permission
* [#13544](https://github.com/netbox-community/netbox/issues/13544) - Ensure `reindex` command clears all cached values when not in lazy mode
* [#13556](https://github.com/netbox-community/netbox/issues/13556) - Correct REST API representation of VDC status choice
* [#13569](https://github.com/netbox-community/netbox/issues/13569) - Fix selection widgets for related interfaces when bulk editing interfaces under device view
---
## v3.5.8 (2023-08-15)
### Enhancements
* [#10030](https://github.com/netbox-community/netbox/issues/10030) - Ship a validation schema for the device type library with each release
* [#11675](https://github.com/netbox-community/netbox/issues/11675) - Add support for specifying import/export route targets during VRF bulk import
* [#11922](https://github.com/netbox-community/netbox/issues/11922) - Automatically populate any VDC assignments from the parent when adding a child interface via the UI
* [#12889](https://github.com/netbox-community/netbox/issues/12889) - Add 400GE CFP2 interface type
* [#13033](https://github.com/netbox-community/netbox/issues/13033) - Add human-friendly speed column to interfaces table
* [#13151](https://github.com/netbox-community/netbox/issues/13151) - Add "assigned" filter for IP addresses
* [#13368](https://github.com/netbox-community/netbox/issues/13368) - List installed plugins on the server error report page
* [#13442](https://github.com/netbox-community/netbox/issues/13442) - Add 200 and 400 Gbps speeds to dropdown choices on interface form
### Bug Fixes
* [#11578](https://github.com/netbox-community/netbox/issues/11578) - Fix schema definition for available IP & VLAN REST API endpoints
* [#12639](https://github.com/netbox-community/netbox/issues/12639) - Raise validation error for invalid alphanumeric ranges when creating objects
* [#12665](https://github.com/netbox-community/netbox/issues/12665) - Avoid escaping semicolons when rendering custom links
* [#12750](https://github.com/netbox-community/netbox/issues/12750) - Automatically delete an AutoSyncRecord when its object is deleted
* [#13343](https://github.com/netbox-community/netbox/issues/13343) - Fix filtering of circuits under provider network view
* [#13369](https://github.com/netbox-community/netbox/issues/13369) - Fix job termination status for failed reports
* [#13414](https://github.com/netbox-community/netbox/issues/13414) - Fix support for "hide-if-unset" custom fields on bulk import forms
* [#13446](https://github.com/netbox-community/netbox/issues/13446) - Don't disable bulk edit/delete buttons after deselecting "select all" checkbox
* [#13451](https://github.com/netbox-community/netbox/issues/13451) - Disable table ordering for custom link columns
---
## v3.5.7 (2023-07-28)
### Enhancements

View File

@@ -163,7 +163,7 @@ class ProviderNetworkView(generic.ObjectView):
related_models = (
(
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
'providernetwork_id',
'provider_network_id',
),
)

View File

@@ -103,12 +103,13 @@ class GitBackend(DataBackend):
}
if self.url_scheme in ('http', 'https'):
clone_args.update(
{
"username": self.params.get('username'),
"password": self.params.get('password'),
}
)
if self.params.get('username'):
clone_args.update(
{
"username": self.params.get('username'),
"password": self.params.get('password'),
}
)
if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'):
if proxy := settings.HTTP_PROXIES.get(self.url_scheme):

View File

@@ -714,6 +714,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True)
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
# Related object counts
interface_count = serializers.IntegerField(read_only=True)

View File

@@ -834,6 +834,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
TYPE_200GE_QSFP_DD = '200gbase-x-qsfpdd'
TYPE_400GE_CFP2 = '400gbase-x-cfp2'
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
TYPE_400GE_OSFP = '400gbase-x-osfp'
TYPE_400GE_CDFP = '400gbase-x-cdfp'
@@ -976,6 +977,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_100GE_CFP, 'CFP (100GE)'),
(TYPE_100GE_CFP2, 'CFP2 (100GE)'),
(TYPE_200GE_CFP2, 'CFP2 (200GE)'),
(TYPE_400GE_CFP2, 'CFP2 (400GE)'),
(TYPE_100GE_CFP4, 'CFP4 (100GE)'),
(TYPE_100GE_CXP, 'CXP (100GE)'),
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
@@ -1139,6 +1141,8 @@ class InterfaceSpeedChoices(ChoiceSet):
(25000000, '25 Gbps'),
(40000000, '40 Gbps'),
(100000000, '100 Gbps'),
(200000000, '200 Gbps'),
(400000000, '400 Gbps'),
]

View File

@@ -401,12 +401,13 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
position = forms.DecimalField(
required=False,
help_text=_("The lowest-numbered unit occupied by the device"),
localize=True,
widget=APISelect(
api_url='/api/dcim/racks/{{rack}}/elevation/',
attrs={
'disabled-indicator': 'device',
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
}
},
)
)
device_type = DynamicModelChoiceField(
@@ -1042,6 +1043,9 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
queryset=VirtualDeviceContext.objects.all(),
required=False,
label='Virtual Device Contexts',
initial_params={
'interfaces': '$parent',
},
query_params={
'device_id': '$device',
}

View File

@@ -52,7 +52,10 @@ class ComponentCreateForm(forms.Form):
super().clean()
# Validate that all replication fields generate an equal number of values
pattern_count = len(self.cleaned_data[self.replication_fields[0]])
if not (patterns := self.cleaned_data.get(self.replication_fields[0])):
return
pattern_count = len(patterns)
for field_name in self.replication_fields:
value_count = len(self.cleaned_data[field_name])
if self.cleaned_data[field_name] and value_count != pattern_count:

View File

@@ -53,23 +53,23 @@ class LinkPeerType(graphene.Union):
@classmethod
def resolve_type(cls, instance, info):
if type(instance) == CircuitTermination:
if type(instance) is CircuitTermination:
return CircuitTerminationType
if type(instance) == ConsolePortType:
if type(instance) is ConsolePortType:
return ConsolePortType
if type(instance) == ConsoleServerPort:
if type(instance) is ConsoleServerPort:
return ConsoleServerPortType
if type(instance) == FrontPort:
if type(instance) is FrontPort:
return FrontPortType
if type(instance) == Interface:
if type(instance) is Interface:
return InterfaceType
if type(instance) == PowerFeed:
if type(instance) is PowerFeed:
return PowerFeedType
if type(instance) == PowerOutlet:
if type(instance) is PowerOutlet:
return PowerOutletType
if type(instance) == PowerPort:
if type(instance) is PowerPort:
return PowerPortType
if type(instance) == RearPort:
if type(instance) is RearPort:
return RearPortType
@@ -89,23 +89,23 @@ class CableTerminationTerminationType(graphene.Union):
@classmethod
def resolve_type(cls, instance, info):
if type(instance) == CircuitTermination:
if type(instance) is CircuitTermination:
return CircuitTerminationType
if type(instance) == ConsolePortType:
if type(instance) is ConsolePortType:
return ConsolePortType
if type(instance) == ConsoleServerPort:
if type(instance) is ConsoleServerPort:
return ConsoleServerPortType
if type(instance) == FrontPort:
if type(instance) is FrontPort:
return FrontPortType
if type(instance) == Interface:
if type(instance) is Interface:
return InterfaceType
if type(instance) == PowerFeed:
if type(instance) is PowerFeed:
return PowerFeedType
if type(instance) == PowerOutlet:
if type(instance) is PowerOutlet:
return PowerOutletType
if type(instance) == PowerPort:
if type(instance) is PowerPort:
return PowerPortType
if type(instance) == RearPort:
if type(instance) is RearPort:
return RearPortType
@@ -123,19 +123,19 @@ class InventoryItemTemplateComponentType(graphene.Union):
@classmethod
def resolve_type(cls, instance, info):
if type(instance) == ConsolePortTemplate:
if type(instance) is ConsolePortTemplate:
return ConsolePortTemplateType
if type(instance) == ConsoleServerPortTemplate:
if type(instance) is ConsoleServerPortTemplate:
return ConsoleServerPortTemplateType
if type(instance) == FrontPortTemplate:
if type(instance) is FrontPortTemplate:
return FrontPortTemplateType
if type(instance) == InterfaceTemplate:
if type(instance) is InterfaceTemplate:
return InterfaceTemplateType
if type(instance) == PowerOutletTemplate:
if type(instance) is PowerOutletTemplate:
return PowerOutletTemplateType
if type(instance) == PowerPortTemplate:
if type(instance) is PowerPortTemplate:
return PowerPortTemplateType
if type(instance) == RearPortTemplate:
if type(instance) is RearPortTemplate:
return RearPortTemplateType
@@ -153,17 +153,17 @@ class InventoryItemComponentType(graphene.Union):
@classmethod
def resolve_type(cls, instance, info):
if type(instance) == ConsolePort:
if type(instance) is ConsolePort:
return ConsolePortType
if type(instance) == ConsoleServerPort:
if type(instance) is ConsoleServerPort:
return ConsoleServerPortType
if type(instance) == FrontPort:
if type(instance) is FrontPort:
return FrontPortType
if type(instance) == Interface:
if type(instance) is Interface:
return InterfaceType
if type(instance) == PowerOutlet:
if type(instance) is PowerOutlet:
return PowerOutletType
if type(instance) == PowerPort:
if type(instance) is PowerPort:
return PowerPortType
if type(instance) == RearPort:
if type(instance) is RearPort:
return RearPortType

View File

@@ -0,0 +1,62 @@
import json
import os
from django.conf import settings
from django.core.management.base import BaseCommand
from jinja2 import FileSystemLoader, Environment
from dcim.choices import *
TEMPLATE_FILENAME = 'devicetype_schema.jinja2'
OUTPUT_FILENAME = 'contrib/generated_schema.json'
CHOICES_MAP = {
'airflow_choices': DeviceAirflowChoices,
'weight_unit_choices': WeightUnitChoices,
'subdevice_role_choices': SubdeviceRoleChoices,
'console_port_type_choices': ConsolePortTypeChoices,
'console_server_port_type_choices': ConsolePortTypeChoices,
'power_port_type_choices': PowerPortTypeChoices,
'power_outlet_type_choices': PowerOutletTypeChoices,
'power_outlet_feedleg_choices': PowerOutletFeedLegChoices,
'interface_type_choices': InterfaceTypeChoices,
'interface_poe_mode_choices': InterfacePoEModeChoices,
'interface_poe_type_choices': InterfacePoETypeChoices,
'front_port_type_choices': PortTypeChoices,
'rear_port_type_choices': PortTypeChoices,
}
class Command(BaseCommand):
help = "Generate JSON schema for validating NetBox device type definitions"
def add_arguments(self, parser):
parser.add_argument(
'--write',
action='store_true',
help="Write the generated schema to file"
)
def handle(self, *args, **kwargs):
# Initialize template
template_loader = FileSystemLoader(searchpath=f'{settings.TEMPLATES_DIR}/extras/schema/')
template_env = Environment(loader=template_loader)
template = template_env.get_template(TEMPLATE_FILENAME)
# Render template
context = {
key: json.dumps(choices.values())
for key, choices in CHOICES_MAP.items()
}
rendered = template.render(**context)
if kwargs['write']:
# $root/contrib/generated_schema.json
filename = os.path.join(os.path.split(settings.BASE_DIR)[0], OUTPUT_FILENAME)
with open(filename, mode='w', encoding='UTF-8') as f:
f.write(json.dumps(json.loads(rendered), indent=4))
f.write('\n')
f.close()
self.stdout.write(self.style.SUCCESS(f"Schema written to {filename}."))
else:
self.stdout.write(rendered)

View File

@@ -545,6 +545,11 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
}
)
mgmt_only = columns.BooleanColumn()
speed_formatted = columns.TemplateColumn(
template_code='{% load helpers %}{{ value|humanize_speed }}',
accessor=Accessor('speed'),
verbose_name='Speed'
)
wireless_link = tables.Column(
linkify=True
)
@@ -568,7 +573,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
model = models.Interface
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn',
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',

View File

@@ -1,4 +1,5 @@
import traceback
from collections import defaultdict
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
@@ -45,6 +46,15 @@ CABLE_TERMINATION_TYPES = {
class DeviceComponentsView(generic.ObjectChildrenView):
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename', 'bulk_disconnect')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
'bulk_rename': {'change'},
'bulk_disconnect': {'change'},
})
queryset = Device.objects.all()
def get_children(self, request, parent):
@@ -388,32 +398,8 @@ class SiteView(generic.ObjectView):
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'),
)
locations = Location.objects.add_related_count(
Location.objects.all(),
Rack,
'location',
'rack_count',
cumulative=True
)
locations = Location.objects.add_related_count(
locations,
Device,
'location',
'device_count',
cumulative=True
).restrict(request.user, 'view').filter(site=instance)
nonracked_devices = Device.objects.filter(
site=instance,
rack__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
return {
'related_models': related_models,
'locations': locations,
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
'total_nonracked_devices_count': nonracked_devices.count(),
}
@@ -485,16 +471,8 @@ class LocationView(generic.ObjectView):
(Device.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'),
)
nonracked_devices = Device.objects.filter(
location=instance,
rack__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
return {
'related_models': related_models,
'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
'total_nonracked_devices_count': nonracked_devices.count(),
}
@@ -1997,6 +1975,7 @@ class DeviceModuleBaysView(DeviceComponentsView):
table = tables.DeviceModuleBayTable
filterset = filtersets.ModuleBayFilterSet
template_name = 'dcim/device/modulebays.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
tab = ViewTab(
label=_('Module Bays'),
badge=lambda obj: obj.modulebays.count(),
@@ -2012,6 +1991,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
table = tables.DeviceDeviceBayTable
filterset = filtersets.DeviceBayFilterSet
template_name = 'dcim/device/devicebays.html'
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
tab = ViewTab(
label=_('Device Bays'),
badge=lambda obj: obj.devicebays.count(),
@@ -2023,6 +2003,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
@register_model_view(Device, 'inventory')
class DeviceInventoryView(DeviceComponentsView):
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
child_model = InventoryItem
table = tables.DeviceInventoryItemTable
filterset = filtersets.InventoryItemFilterSet
@@ -2042,7 +2023,6 @@ class DeviceConfigContextView(ObjectConfigContextView):
base_template = 'dcim/device/base.html'
tab = ViewTab(
label=_('Config Context'),
permission='extras.view_configcontext',
weight=2000
)

View File

@@ -104,7 +104,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
('Data', ('data_source_id', 'data_file_id')),
('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
('Attributes', ('content_type_id', 'mime_type', 'file_extension', 'as_attachment')),
)
data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
@@ -119,9 +119,10 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
'source_id': '$data_source_id'
}
)
content_types = ContentTypeMultipleChoiceField(
content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
required=False
required=False,
label=_('Content type')
)
mime_type = forms.CharField(
required=False,

View File

@@ -69,10 +69,7 @@ class Command(BaseCommand):
if not kwargs['lazy']:
self.stdout.write('Clearing cached values... ', ending='')
self.stdout.flush()
content_types = [
ContentType.objects.get_for_model(model) for model in indexers.keys()
]
deleted_count = search_backend.clear(content_types)
deleted_count = search_backend.clear()
self.stdout.write(f'{deleted_count} entries deleted.')
# Index models

View File

@@ -285,7 +285,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
text = clean_html(text, allowed_schemes)
# Sanitize link
link = urllib.parse.quote(link, safe='/:?&=%+[]@#,')
link = urllib.parse.quote(link, safe='/:?&=%+[]@#,;')
# Verify link scheme is allowed
result = urllib.parse.urlparse(link)

View File

@@ -2,7 +2,6 @@ import collections
from importlib import import_module
from django.apps import AppConfig
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import import_string
from packaging import version
@@ -12,6 +11,7 @@ from netbox.search import register_search
from .navigation import *
from .registration import *
from .templates import *
from .utils import *
# Initialize plugin registry
registry['plugins'].update({
@@ -146,23 +146,3 @@ class PluginConfig(AppConfig):
for setting, value in cls.default_settings.items():
if setting not in user_config:
user_config[setting] = value
#
# Utilities
#
def get_plugin_config(plugin_name, parameter, default=None):
"""
Return the value of the specified plugin configuration parameter.
Args:
plugin_name: The name of the plugin
parameter: The name of the configuration parameter
default: The value to return if the parameter is not defined (default: None)
"""
try:
plugin_config = settings.PLUGINS_CONFIG[plugin_name]
return plugin_config.get(parameter, default)
except KeyError:
raise ImproperlyConfigured(f"Plugin {plugin_name} is not registered.")

View File

@@ -0,0 +1,37 @@
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
__all__ = (
'get_installed_plugins',
'get_plugin_config',
)
def get_installed_plugins():
"""
Return a dictionary mapping the names of installed plugins to their versions.
"""
plugins = {}
for plugin_name in settings.PLUGINS:
plugin_name = plugin_name.rsplit('.', 1)[-1]
plugin_config = apps.get_app_config(plugin_name)
plugins[plugin_name] = getattr(plugin_config, 'version', None)
return dict(sorted(plugins.items()))
def get_plugin_config(plugin_name, parameter, default=None):
"""
Return the value of the specified plugin configuration parameter.
Args:
plugin_name: The name of the plugin
parameter: The name of the configuration parameter
default: The value to return if the parameter is not defined (default: None)
"""
try:
plugin_config = settings.PLUGINS_CONFIG[plugin_name]
return plugin_config.get(parameter, default)
except KeyError:
raise ImproperlyConfigured(f"Plugin {plugin_name} is not registered.")

View File

@@ -214,20 +214,18 @@ class Report(object):
self.active_test = method_name
test_method = getattr(self, method_name)
test_method()
job.data = self._results
if self.failed:
self.logger.warning("Report failed")
job.status = JobStatusChoices.STATUS_FAILED
job.terminate(status=JobStatusChoices.STATUS_FAILED)
else:
self.logger.info("Report completed successfully")
job.status = JobStatusChoices.STATUS_COMPLETED
job.terminate()
except Exception as e:
stacktrace = traceback.format_exc()
self.log_failure(None, f"An exception occurred: {type(e).__name__}: {e} <pre>{stacktrace}</pre>")
logger.error(f"Exception raised during report execution: {e}")
job.terminate(status=JobStatusChoices.STATUS_ERRORED)
finally:
job.data = self._results
job.terminate()
# Perform any post-run tasks
self.post_run()

View File

@@ -401,23 +401,23 @@ class BaseScript:
def log_debug(self, message):
self.logger.log(logging.DEBUG, message)
self.log.append((LogLevelChoices.LOG_DEFAULT, message))
self.log.append((LogLevelChoices.LOG_DEFAULT, str(message)))
def log_success(self, message):
self.logger.log(logging.INFO, message) # No syslog equivalent for SUCCESS
self.log.append((LogLevelChoices.LOG_SUCCESS, message))
self.log.append((LogLevelChoices.LOG_SUCCESS, str(message)))
def log_info(self, message):
self.logger.log(logging.INFO, message)
self.log.append((LogLevelChoices.LOG_INFO, message))
self.log.append((LogLevelChoices.LOG_INFO, str(message)))
def log_warning(self, message):
self.logger.log(logging.WARNING, message)
self.log.append((LogLevelChoices.LOG_WARNING, message))
self.log.append((LogLevelChoices.LOG_WARNING, str(message)))
def log_failure(self, message):
self.logger.log(logging.ERROR, message)
self.log.append((LogLevelChoices.LOG_FAILURE, message))
self.log.append((LogLevelChoices.LOG_FAILURE, str(message)))
# Convenience functions

View File

@@ -965,11 +965,13 @@ class ChangeLoggedFilterSetTestCase(TestCase):
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
Site(name='Site 3', slug='site-3'),
Site(name='Site 4', slug='site-4'),
)
Site.objects.bulk_create(sites)
# Simulate *creation* changelog records for two of the sites
request_id = uuid.uuid4()
cls.create_request_id = request_id
objectchanges = (
ObjectChange(
changed_object_type=content_type,
@@ -988,6 +990,7 @@ class ChangeLoggedFilterSetTestCase(TestCase):
# Simulate *update* changelog records for two of the sites
request_id = uuid.uuid4()
cls.update_request_id = request_id
objectchanges = (
ObjectChange(
changed_object_type=content_type,
@@ -1004,14 +1007,36 @@ class ChangeLoggedFilterSetTestCase(TestCase):
)
ObjectChange.objects.bulk_create(objectchanges)
# Simulate *create* and *update* changelog records for two of the sites
request_id = uuid.uuid4()
cls.create_update_request_id = request_id
objectchanges = (
ObjectChange(
changed_object_type=content_type,
changed_object_id=sites[2].pk,
action=ObjectChangeActionChoices.ACTION_CREATE,
request_id=request_id
),
ObjectChange(
changed_object_type=content_type,
changed_object_id=sites[3].pk,
action=ObjectChangeActionChoices.ACTION_UPDATE,
request_id=request_id
),
)
ObjectChange.objects.bulk_create(objectchanges)
def test_created_by_request(self):
request_id = ObjectChange.objects.filter(action=ObjectChangeActionChoices.ACTION_CREATE).first().request_id
params = {'created_by_request': request_id}
params = {'created_by_request': self.create_request_id}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(self.queryset.count(), 3)
self.assertEqual(self.queryset.count(), 4)
def test_updated_by_request(self):
request_id = ObjectChange.objects.filter(action=ObjectChangeActionChoices.ACTION_UPDATE).first().request_id
params = {'updated_by_request': request_id}
params = {'updated_by_request': self.update_request_id}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(self.queryset.count(), 3)
self.assertEqual(self.queryset.count(), 4)
def test_modified_by_request(self):
params = {'modified_by_request': self.create_update_request_id}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
self.assertEqual(self.queryset.count(), 4)

View File

@@ -5,8 +5,9 @@ from django.core.exceptions import ImproperlyConfigured
from django.test import Client, TestCase, override_settings
from django.urls import reverse
from extras.plugins import PluginMenu, get_plugin_config
from extras.plugins import PluginMenu
from extras.tests.dummy_plugin import config as dummy_config
from extras.plugins.utils import get_plugin_config
from netbox.graphql.schema import Query
from netbox.registry import registry

View File

@@ -31,8 +31,8 @@ class WebhookTest(APITestCase):
def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site)
DUMMY_URL = "http://localhost/"
DUMMY_SECRET = "LOOKATMEIMASECRETSTRING"
DUMMY_URL = 'http://localhost:9000/'
DUMMY_SECRET = 'LOOKATMEIMASECRETSTRING'
webhooks = Webhook.objects.bulk_create((
Webhook(name='Webhook 1', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers='X-Foo: Bar'),
@@ -259,7 +259,7 @@ class WebhookTest(APITestCase):
name='Conditional Webhook',
type_create=True,
type_update=True,
payload_url='http://localhost/',
payload_url='http://localhost:9000/',
conditions={
'and': [
{

View File

@@ -43,6 +43,21 @@ class CustomFieldListView(generic.ObjectListView):
class CustomFieldView(generic.ObjectView):
queryset = CustomField.objects.all()
def get_extra_context(self, request, instance):
related_models = ()
for content_type in instance.content_types.all():
related_models += (
content_type.model_class().objects.restrict(request.user, 'view').exclude(
Q(**{f'custom_field_data__{instance.name}': ''}) |
Q(**{f'custom_field_data__{instance.name}': None})
),
)
return {
'related_models': related_models
}
@register_model_view(CustomField, 'edit')
class CustomFieldEditView(generic.ObjectEditView):

View File

@@ -224,7 +224,10 @@ class AvailableASNsView(ObjectValidationMixin, APIView):
return Response(serializer.data)
@extend_schema(methods=["post"], responses={201: serializers.ASNSerializer(many=True)})
@extend_schema(methods=["post"],
responses={201: serializers.ASNSerializer(many=True)},
request=serializers.ASNSerializer(many=True),
)
@advisory_lock(ADVISORY_LOCK_KEYS['available-asns'])
def post(self, request, pk):
self.queryset = self.queryset.restrict(request.user, 'add')
@@ -293,7 +296,10 @@ class AvailablePrefixesView(ObjectValidationMixin, APIView):
return Response(serializer.data)
@extend_schema(methods=["post"], responses={201: serializers.PrefixSerializer(many=True)})
@extend_schema(methods=["post"],
responses={201: serializers.PrefixSerializer(many=True)},
request=serializers.PrefixSerializer(many=True),
)
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
def post(self, request, pk):
self.queryset = self.queryset.restrict(request.user, 'add')
@@ -388,7 +394,10 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView):
return Response(serializer.data)
@extend_schema(methods=["post"], responses={201: serializers.IPAddressSerializer(many=True)})
@extend_schema(methods=["post"],
responses={201: serializers.IPAddressSerializer(many=True)},
request=serializers.IPAddressSerializer(many=True),
)
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
def post(self, request, pk):
self.queryset = self.queryset.restrict(request.user, 'add')
@@ -468,7 +477,10 @@ class AvailableVLANsView(ObjectValidationMixin, APIView):
return Response(serializer.data)
@extend_schema(methods=["post"], responses={201: serializers.VLANSerializer(many=True)})
@extend_schema(methods=["post"],
responses={201: serializers.VLANSerializer(many=True)},
request=serializers.VLANSerializer(many=True),
)
@advisory_lock(ADVISORY_LOCK_KEYS['available-vlans'])
def post(self, request, pk):
self.queryset = self.queryset.restrict(request.user, 'add')

View File

@@ -467,6 +467,10 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
choices=IPRangeStatusChoices,
null_value=None
)
parent = MultiValueCharFilter(
method='search_by_parent',
label=_('Parent prefix'),
)
class Meta:
model = IPRange
@@ -501,6 +505,18 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
except ValidationError:
return queryset.none()
def search_by_parent(self, queryset, name, value):
if not value:
return queryset
q = Q()
for prefix in value:
try:
query = str(netaddr.IPNetwork(prefix.strip()).cidr)
q |= Q(start_address__net_host_contained=query, end_address__net_host_contained=query)
except (AddrFormatError, ValueError):
return queryset.none()
return queryset.filter(q)
class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
family = django_filters.NumberFilter(
@@ -591,6 +607,10 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
method='_assigned_to_interface',
label=_('Is assigned to an interface'),
)
assigned = django_filters.BooleanFilter(
method='_assigned',
label=_('Is assigned'),
)
status = django_filters.MultipleChoiceFilter(
choices=IPAddressStatusChoices,
null_value=None
@@ -706,6 +726,18 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
assigned_object_id__isnull=False
)
def _assigned(self, queryset, name, value):
if value:
return queryset.exclude(
assigned_object_type__isnull=True,
assigned_object_id__isnull=True
)
else:
return queryset.filter(
assigned_object_type__isnull=True,
assigned_object_id__isnull=True
)
class FHRPGroupFilterSet(NetBoxModelFilterSet):
protocol = django_filters.MultipleChoiceFilter(

View File

@@ -1,7 +1,6 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.translation import gettext as _
from dcim.models import Device, Interface, Site
@@ -10,7 +9,9 @@ from ipam.constants import *
from ipam.models import *
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
from utilities.forms.fields import (
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField
)
from virtualization.models import VirtualMachine, VMInterface
__all__ = (
@@ -41,10 +42,25 @@ class VRFImportForm(NetBoxModelImportForm):
to_field_name='name',
help_text=_('Assigned tenant')
)
import_targets = CSVModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False,
to_field_name='name',
help_text=_('Import route targets')
)
export_targets = CSVModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False,
to_field_name='name',
help_text=_('Export route targets')
)
class Meta:
model = VRF
fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments', 'tags')
fields = (
'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'comments',
'tags',
)
class RouteTargetImportForm(NetBoxModelImportForm):
@@ -532,9 +548,11 @@ class L2VPNTerminationImportForm(NetBoxModelImportForm):
if self.cleaned_data.get('device') and self.cleaned_data.get('virtual_machine'):
raise ValidationError('Cannot import device and VM interface terminations simultaneously.')
if not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')):
if not self.instance and not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')):
raise ValidationError('Each termination must specify either an interface or a VLAN.')
if self.cleaned_data.get('interface') and self.cleaned_data.get('vlan'):
raise ValidationError('Cannot assign both an interface and a VLAN.')
self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')
# if this is an update we might not have interface or vlan in the form data
if self.cleaned_data.get('interface') or self.cleaned_data.get('vlan'):
self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')

View File

@@ -253,7 +253,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPRange
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attriubtes', ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')),
('Attributes', ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
family = forms.ChoiceField(

View File

@@ -24,11 +24,11 @@ class IPAddressAssignmentType(graphene.Union):
@classmethod
def resolve_type(cls, instance, info):
if type(instance) == Interface:
if type(instance) is Interface:
return InterfaceType
if type(instance) == FHRPGroup:
if type(instance) is FHRPGroup:
return FHRPGroupType
if type(instance) == VMInterface:
if type(instance) is VMInterface:
return VMInterfaceType
@@ -42,11 +42,11 @@ class L2VPNAssignmentType(graphene.Union):
@classmethod
def resolve_type(cls, instance, info):
if type(instance) == Interface:
if type(instance) is Interface:
return InterfaceType
if type(instance) == VLAN:
if type(instance) is VLAN:
return VLANType
if type(instance) == VMInterface:
if type(instance) is VMInterface:
return VMInterfaceType
@@ -59,9 +59,9 @@ class FHRPGroupInterfaceType(graphene.Union):
@classmethod
def resolve_type(cls, instance, info):
if type(instance) == Interface:
if type(instance) is Interface:
return InterfaceType
if type(instance) == VMInterface:
if type(instance) is VMInterface:
return VMInterfaceType
@@ -79,17 +79,17 @@ class VLANGroupScopeType(graphene.Union):
@classmethod
def resolve_type(cls, instance, info):
if type(instance) == Cluster:
if type(instance) is Cluster:
return ClusterType
if type(instance) == ClusterGroup:
if type(instance) is ClusterGroup:
return ClusterGroupType
if type(instance) == Location:
if type(instance) is Location:
return LocationType
if type(instance) == Rack:
if type(instance) is Rack:
return RackType
if type(instance) == Region:
if type(instance) is Region:
return RegionType
if type(instance) == Site:
if type(instance) is Site:
return SiteType
if type(instance) == SiteGroup:
if type(instance) is SiteGroup:
return SiteGroupType

View File

@@ -116,6 +116,12 @@ class VLANGroup(OrganizationalModel):
return available_vids[0]
return None
def get_child_vlans(self):
"""
Return all VLANs within this group.
"""
return VLAN.objects.filter(group=self).order_by('vid')
class VLAN(PrimaryModel):
"""

View File

@@ -10,7 +10,6 @@ from ipam.models import *
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
from tenancy.models import Tenant, TenantGroup
from rest_framework import serializers
class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
@@ -807,6 +806,12 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self):
params = {'parent': ['10.0.1.0/24', '10.0.2.0/24']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'parent': ['10.0.1.0/25']} # Range 10.0.1.100-199 is not fully contained by 10.0.1.0/25
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IPAddress.objects.all()
@@ -992,6 +997,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'fhrpgroup_id': [fhrp_groups[0].pk, fhrp_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_assigned(self):
params = {'assigned': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
params = {'assigned': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_assigned_to_interface(self):
params = {'assigned_to_interface': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)

View File

@@ -121,7 +121,7 @@ def add_available_vlans(vlans, vlan_group=None):
})
vlans = list(vlans) + new_vlans
vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid'])
vlans.sort(key=lambda v: v.vid if type(v) is VLAN else v['vid'])
return vlans

View File

@@ -216,7 +216,7 @@ class ASNRangeASNsView(generic.ObjectChildrenView):
child_model = ASN
table = tables.ASNTable
filterset = filtersets.ASNFilterSet
template_name = 'ipam/asnrange/asns.html'
template_name = 'generic/object_children.html'
tab = ViewTab(
label=_('ASNs'),
badge=lambda x: x.get_child_asns().count(),
@@ -816,7 +816,6 @@ class IPAddressAssignView(generic.ObjectView):
table = None
if form.is_valid():
addresses = self.queryset.prefetch_related('vrf', 'tenant')
# Limit to 100 results
addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100]
@@ -866,7 +865,7 @@ class IPAddressRelatedIPsView(generic.ObjectChildrenView):
child_model = IPAddress
table = tables.IPAddressTable
filterset = filtersets.IPAddressFilterSet
template_name = 'ipam/ipaddress/ip_addresses.html'
template_name = 'generic/object_children.html'
tab = ViewTab(
label=_('Related IPs'),
badge=lambda x: x.get_related_ips().count(),
@@ -898,21 +897,8 @@ class VLANGroupView(generic.ObjectView):
(VLAN.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
)
# TODO: Replace with embedded table
vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related(
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)),
'tenant', 'site', 'role',
).order_by('vid')
vlans = add_available_vlans(vlans, vlan_group=instance)
vlans_table = tables.VLANTable(vlans, user=request.user, exclude=('group',))
if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
vlans_table.columns.show('pk')
vlans_table.configure(request)
return {
'related_models': related_models,
'vlans_table': vlans_table,
}
@@ -945,6 +931,30 @@ class VLANGroupBulkDeleteView(generic.BulkDeleteView):
table = tables.VLANGroupTable
@register_model_view(VLANGroup, 'vlans')
class VLANGroupVLANsView(generic.ObjectChildrenView):
queryset = VLANGroup.objects.all()
child_model = VLAN
table = tables.VLANTable
filterset = filtersets.VLANFilterSet
template_name = 'generic/object_children.html'
tab = ViewTab(
label=_('VLANs'),
badge=lambda x: x.get_child_vlans().count(),
permission='ipam.view_vlan',
weight=500
)
def get_children(self, request, parent):
return parent.get_child_vlans().restrict(request.user, 'view').prefetch_related(
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user)),
'tenant', 'site', 'role',
)
def prep_table_data(self, request, queryset, parent):
return add_available_vlans(parent.get_child_vlans(), parent)
#
# FHRP groups
#
@@ -963,7 +973,6 @@ class FHRPGroupView(generic.ObjectView):
queryset = FHRPGroup.objects.all()
def get_extra_context(self, request, instance):
# Get assigned interfaces
members_table = tables.FHRPGroupAssignmentTable(
data=FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(group=instance),
@@ -1077,7 +1086,7 @@ class VLANInterfacesView(generic.ObjectChildrenView):
child_model = Interface
table = tables.VLANDevicesTable
filterset = InterfaceFilterSet
template_name = 'ipam/vlan/interfaces.html'
template_name = 'generic/object_children.html'
tab = ViewTab(
label=_('Device Interfaces'),
badge=lambda x: x.get_interfaces().count(),
@@ -1095,7 +1104,7 @@ class VLANVMInterfacesView(generic.ObjectChildrenView):
child_model = VMInterface
table = tables.VLANVirtualMachinesTable
filterset = VMInterfaceFilterSet
template_name = 'ipam/vlan/vminterfaces.html'
template_name = 'generic/object_children.html'
tab = ViewTab(
label=_('VM Interfaces'),
badge=lambda x: x.get_vminterfaces().count(),

View File

@@ -11,6 +11,7 @@ from rest_framework.reverse import reverse
from rest_framework.views import APIView
from rq.worker import Worker
from extras.plugins.utils import get_installed_plugins
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
@@ -61,19 +62,11 @@ class StatusView(APIView):
installed_apps[app_config.name] = version
installed_apps = {k: v for k, v in sorted(installed_apps.items())}
# Gather installed plugins
plugins = {}
for plugin_name in settings.PLUGINS:
plugin_name = plugin_name.rsplit('.', 1)[-1]
plugin_config = apps.get_app_config(plugin_name)
plugins[plugin_name] = getattr(plugin_config, 'version', None)
plugins = {k: v for k, v in sorted(plugins.items())}
return Response({
'django-version': DJANGO_VERSION,
'installed-apps': installed_apps,
'netbox-version': settings.VERSION,
'plugins': plugins,
'plugins': get_installed_plugins(),
'python-version': platform.python_version(),
'rq-workers-running': Worker.count(get_connection('default')),
})

View File

@@ -246,18 +246,22 @@ class ChangeLoggedModelFilterSet(BaseFilterSet):
updated_by_request = django_filters.UUIDFilter(
method='filter_by_request'
)
modified_by_request = django_filters.UUIDFilter(
method='filter_by_request'
)
def filter_by_request(self, queryset, name, value):
content_type = ContentType.objects.get_for_model(self.Meta.model)
action = {
'created_by_request': ObjectChangeActionChoices.ACTION_CREATE,
'updated_by_request': ObjectChangeActionChoices.ACTION_UPDATE,
'created_by_request': Q(action=ObjectChangeActionChoices.ACTION_CREATE),
'updated_by_request': Q(action=ObjectChangeActionChoices.ACTION_UPDATE),
'modified_by_request': Q(action__in=[ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE]),
}.get(name)
request_id = value
pks = ObjectChange.objects.filter(
action,
changed_object_type=content_type,
action=action,
request_id=request_id
request_id=request_id,
).values_list('changed_object_id', flat=True)
return queryset.filter(pk__in=pks)

View File

@@ -78,7 +78,10 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type).filter(
ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE
ui_visibility__in=[
CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET,
]
)
def _get_form_field(self, customfield):

View File

@@ -442,6 +442,19 @@ class SyncedDataMixin(models.Model):
return ret
def delete(self, *args, **kwargs):
from core.models import AutoSyncRecord
# Delete AutoSyncRecord
content_type = ContentType.objects.get_for_model(self)
AutoSyncRecord.objects.filter(
datafile=self.data_file,
object_type=content_type,
object_id=self.pk
).delete()
return super().delete(*args, **kwargs)
def resolve_data_file(self):
"""
Determine the designated DataFile object identified by its parent DataSource and its path. Returns None if

View File

@@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
# Environment setup
#
VERSION = '3.5.7'
VERSION = '3.5.9'
# Hostname
HOSTNAME = platform.node()
@@ -461,8 +461,6 @@ LOGIN_REDIRECT_URL = f'/{BASE_PATH}'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
TEST_RUNNER = "django_rich.test.RichRunner"
# Exclude potentially sensitive models from wildcard view exemption. These may still be exempted
# by specifying the model individually in the EXEMPT_VIEW_PERMISSIONS configuration parameter.
EXEMPT_EXCLUDE_MODELS = (

View File

@@ -4,6 +4,7 @@ from urllib.parse import quote
import django_tables2 as tables
from django.conf import settings
from django.contrib.auth.context_processors import auth
from django.contrib.auth.models import AnonymousUser
from django.db.models import DateField, DateTimeField
from django.template import Context, Template
@@ -504,31 +505,38 @@ class CustomLinkColumn(tables.Column):
"""
def __init__(self, customlink, *args, **kwargs):
self.customlink = customlink
kwargs['accessor'] = Accessor('pk')
if 'verbose_name' not in kwargs:
kwargs['verbose_name'] = customlink.name
kwargs.setdefault('accessor', Accessor('pk'))
kwargs.setdefault('orderable', False)
kwargs.setdefault('verbose_name', customlink.name)
super().__init__(*args, **kwargs)
def render(self, record):
try:
rendered = self.customlink.render({
'object': record,
'obj': record, # TODO: Remove in NetBox v3.5
def _render_customlink(self, record, table):
context = {
'object': record,
'obj': record, # TODO: Remove in NetBox v3.5
'debug': settings.DEBUG,
}
if request := getattr(table, 'context', {}).get('request'):
# If the request is available, include it as context
context.update({
'request': request,
**auth(request),
})
if rendered:
return self.customlink.render(context)
def render(self, record, table, **kwargs):
try:
if rendered := self._render_customlink(record, table):
return mark_safe(f'<a href="{rendered["link"]}"{rendered["link_target"]}>{rendered["text"]}</a>')
except Exception as e:
return mark_safe(f'<span class="text-danger" title="{e}"><i class="mdi mdi-alert"></i> Error</span>')
return ''
def value(self, record):
def value(self, record, table, **kwargs):
try:
rendered = self.customlink.render({
'object': record,
'obj': record, # TODO: Remove in NetBox v3.5
})
if rendered:
if rendered := self._render_customlink(record, table):
return rendered['link']
except Exception:
pass

View File

@@ -54,7 +54,7 @@ class BaseTable(tables.Table):
# 3. Meta.fields
selected_columns = None
if user is not None and not isinstance(user, AnonymousUser):
selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
selected_columns = user.config.get(f"tables.{self.name}.columns")
if not selected_columns:
selected_columns = getattr(self.Meta, 'default_columns', self.Meta.fields)
@@ -113,6 +113,10 @@ class BaseTable(tables.Table):
columns.append((name, column.verbose_name))
return columns
@property
def name(self):
return self.__class__.__name__
@property
def available_columns(self):
return self._get_columns(visible=False)
@@ -138,17 +142,16 @@ class BaseTable(tables.Table):
"""
# Save ordering preference
if request.user.is_authenticated:
table_name = self.__class__.__name__
if self.prefixed_order_by_field in request.GET:
if request.GET[self.prefixed_order_by_field]:
# If an ordering has been specified as a query parameter, save it as the
# user's preferred ordering for this table.
ordering = request.GET.getlist(self.prefixed_order_by_field)
request.user.config.set(f'tables.{table_name}.ordering', ordering, commit=True)
request.user.config.set(f'tables.{self.name}.ordering', ordering, commit=True)
else:
# If the ordering has been set to none (empty), clear any existing preference.
request.user.config.clear(f'tables.{table_name}.ordering', commit=True)
elif ordering := request.user.config.get(f'tables.{table_name}.ordering'):
request.user.config.clear(f'tables.{self.name}.ordering', commit=True)
elif ordering := request.user.config.get(f'tables.{self.name}.ordering'):
# If no ordering has been specified, set the preferred ordering (if any).
self.order_by = ordering

View File

@@ -11,6 +11,8 @@ from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
from django.views.generic import View
from sentry_sdk import capture_message
from extras.plugins.utils import get_installed_plugins
__all__ = (
'handler_404',
'handler_500',
@@ -53,4 +55,5 @@ def handler_500(request, template_name=ERROR_500_TEMPLATE_NAME):
'exception': str(type_),
'netbox_version': settings.VERSION,
'python_version': platform.python_version(),
'plugins': get_installed_plugins(),
}))

View File

@@ -458,7 +458,7 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
messages.success(request, msg)
view_name = get_viewname(model, action='list')
results_url = f"{reverse(view_name)}?created_by_request={request.id}"
results_url = f"{reverse(view_name)}?modified_by_request={request.id}"
return redirect(results_url)
except (AbortTransaction, ValidationError):

View File

@@ -143,9 +143,12 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
return render(request, self.get_template_name(), {
'object': instance,
'child_model': self.child_model,
'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
'table': table,
'table_config': f'{table.name}_config',
'actions': actions,
'tab': self.tab,
'return_url': request.get_full_path(),
**self.get_extra_context(request, instance),
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
"license": "Apache-2.0",
"private": true,
"dependencies": {
"graphiql": "1.4.1",
"graphiql": "1.8.9",
"graphql": ">= v14.5.0 <= 15.5.0",
"react": "17.0.2",
"react-dom": "17.0.2",

View File

@@ -1,4 +1,4 @@
import { getElement, getElements, findFirstAdjacent } from '../util';
import { getElements, findFirstAdjacent } from '../util';
/**
* If any PK checkbox is checked, uncheck the select all table checkbox and the select all
@@ -63,29 +63,6 @@ function handleSelectAllToggle(event: Event): void {
}
}
/**
* Synchronize the select all confirmation checkbox state with the select all confirmation button
* disabled state. If the select all confirmation checkbox is checked, the buttons should be
* enabled. If not, the buttons should be disabled.
*
* @param event Change Event
*/
function handleSelectAll(event: Event): void {
const target = event.currentTarget as HTMLInputElement;
const selectAllBox = getElement<HTMLDivElement>('select-all-box');
if (selectAllBox !== null) {
for (const button of selectAllBox.querySelectorAll<HTMLButtonElement>(
'button[type="submit"]',
)) {
if (target.checked) {
button.disabled = false;
} else {
button.disabled = true;
}
}
}
}
/**
* Initialize table select all elements.
*/
@@ -98,9 +75,4 @@ export function initSelectAll(): void {
for (const element of getElements<HTMLInputElement>('input[type="checkbox"][name="pk"]')) {
element.addEventListener('change', handlePkCheck);
}
const selectAll = getElement<HTMLInputElement>('select-all');
if (selectAll !== null) {
selectAll.addEventListener('change', handleSelectAll);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,10 @@
{{ error }}
Python version: {{ python_version }}
NetBox version: {{ netbox_version }}</pre>
NetBox version: {{ netbox_version }}
Plugins: {% for plugin, version in plugins.items %}
{{ plugin }}: {{ version }}{% empty %}None installed{% endfor %}
</pre>
<p>
If further assistance is required, please post to the <a href="https://github.com/netbox-community/netbox/discussions">NetBox discussion forum</a> on GitHub.
</p>

View File

@@ -0,0 +1,23 @@
{% extends 'generic/object_children.html' %}
{% load helpers %}
{% block bulk_edit_controls %}
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
{% if 'bulk_edit' in actions and bulk_edit_view %}
<button type="submit" name="_edit"
formaction="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
</button>
{% endif %}
{% endwith %}
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
{% if 'bulk_rename' in actions and bulk_rename_view %}
<button type="submit" name="_rename"
formaction="{% url bulk_rename_view %}?return_url={{ return_url }}"
class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
{% endif %}
{% endwith %}
{% endblock bulk_edit_controls %}

View File

@@ -1,57 +1,27 @@
{% extends 'dcim/device/base.html' %}
{% load render_table from django_tables2 %}
{% extends 'dcim/device/components_base.html' %}
{% load helpers %}
{% load static %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
{% block bulk_delete_controls %}
{{ block.super }}
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect"
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
{% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
</div>
</div>
{% if perms.dcim.add_consoleport %}
<div class="bulk-button-group">
<a href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-sm btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Port
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_consoleport %}
<div class="bulk-button-group">
<a href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}"
class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Ports
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@@ -1,57 +1,27 @@
{% extends 'dcim/device/base.html' %}
{% load render_table from django_tables2 %}
{% extends 'dcim/device/components_base.html' %}
{% load helpers %}
{% load static %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
{% block bulk_delete_controls %}
{{ block.super }}
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect"
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if 'bulk_delete' in actions %}
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
{% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
</div>
</div>
{% if perms.dcim.add_consoleserverport %}
<div class="bulk-button-group">
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Server Ports
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_consoleserverport %}
<div class="bulk-button-group">
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}"
class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Server Ports
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@@ -1,50 +1,13 @@
{% extends 'dcim/device/base.html' %}
{% load render_table from django_tables2 %}
{% load helpers %}
{% load static %}
{% extends 'dcim/device/components_base.html' %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:devicebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
<button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
</div>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
{% if perms.dcim.add_devicebay %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_devicebay %}
<div class="bulk-button-group">
<a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Device Bays
</a>
<a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}"
class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Device Bays
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% endif %}
{% endblock bulk_extra_controls %}

View File

@@ -1,57 +1,27 @@
{% extends 'dcim/device/base.html' %}
{% load render_table from django_tables2 %}
{% extends 'dcim/device/components_base.html' %}
{% load helpers %}
{% load static %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
{% block bulk_delete_controls %}
{{ block.super }}
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect"
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if 'bulk_delete' in actions %}
<button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
{% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
</div>
</div>
{% if perms.dcim.add_frontport %}
<div class="bulk-button-group">
<a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add front ports
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_frontport %}
<div class="bulk-button-group">
<a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}"
class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add front ports
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@@ -1,66 +1,27 @@
{% extends 'dcim/device/base.html' %}
{% load render_table from django_tables2 %}
{% extends 'dcim/device/components_base.html' %}
{% load helpers %}
{% load static %}
{% block content %}
{% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<div class="btn-group" role="group">
<button type="submit" name="_edit"
formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
<button type="submit" name="_rename"
formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete"
formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% block bulk_delete_controls %}
{{ block.super }}
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect"
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
{% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect"
formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
</div>
</div>
{% endwith %}
{% endblock bulk_delete_controls %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_interface %}
<div class="bulk-button-group">
<a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Interfaces
</a>
</div>
<div class="bulk-button-group">
<a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Interfaces
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% endblock bulk_extra_controls %}

View File

@@ -1,50 +1,13 @@
{% extends 'dcim/device/base.html' %}
{% load render_table from django_tables2 %}
{% load helpers %}
{% load static %}
{% extends 'dcim/device/components_base.html' %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:inventoryitem_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
<button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
</div>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'dcim:inventoryitem_bulk_delete' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
{% if perms.dcim.add_inventoryitem %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_inventoryitem %}
<div class="bulk-button-group">
<a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Inventory Item
</a>
<a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}"
class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Inventory Item
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% endif %}
{% endblock bulk_extra_controls %}

View File

@@ -1,46 +1,13 @@
{% extends 'dcim/device/base.html' %}
{% load render_table from django_tables2 %}
{% load helpers %}
{% load static %}
{% extends 'dcim/device/components_base.html' %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:modulebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
<button type="submit" name="_rename" formaction="{% url 'dcim:modulebay_bulk_rename' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
</div>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit" formaction="{% url 'dcim:modulebay_bulk_delete' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
{% if perms.dcim.add_modulebay %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_modulebay %}
<div class="bulk-button-group">
<a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Module Bays
</a>
<a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}"
class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Module Bays
</a>
</div>
{% endif %}
</div>
</form>
{% table_config_form table %}
{% endblock %}
{% endif %}
{% endblock bulk_extra_controls %}

View File

@@ -1,57 +1,27 @@
{% extends 'dcim/device/base.html' %}
{% load render_table from django_tables2 %}
{% extends 'dcim/device/components_base.html' %}
{% load helpers %}
{% load static %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
{% block bulk_delete_controls %}
{{ block.super }}
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect"
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if 'bulk_delete' in actions %}
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
{% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
</div>
</div>
{% if perms.dcim.add_poweroutlet %}
<div class="bulk-button-group">
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Outlets
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_poweroutlet %}
<div class="bulk-button-group">
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}"
class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Outlets
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@@ -1,57 +1,27 @@
{% extends 'dcim/device/base.html' %}
{% load render_table from django_tables2 %}
{% extends 'dcim/device/components_base.html' %}
{% load helpers %}
{% load static %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
{% block bulk_delete_controls %}
{{ block.super }}
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect"
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
<button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
{% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
</div>
</div>
{% if perms.dcim.add_powerport %}
<div class="bulk-button-group">
<a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-sm btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Port
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_powerport %}
<div class="bulk-button-group">
<a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}"
class="btn btn-sm btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Port
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@@ -1,57 +1,27 @@
{% extends 'dcim/device/base.html' %}
{% load render_table from django_tables2 %}
{% load static %}
{% extends 'dcim/device/components_base.html' %}
{% load helpers %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
{% block bulk_delete_controls %}
{{ block.super }}
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect"
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
</div>
{% endif %}
<div class="btn-group" role="group">
{% if 'bulk_delete' in actions %}
<button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
{% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
</div>
</div>
{% if perms.dcim.add_rearport %}
<div class="bulk-button-group">
<a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add rear ports
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}
{% endwith %}
{% endblock bulk_delete_controls %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if perms.dcim.add_rearport %}
<div class="bulk-button-group">
<a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}"
class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add rear ports
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@@ -1,76 +0,0 @@
{% load helpers %}
<div class="card">
<h5 class="card-header">
Non-Racked Devices
</h5>
<div class="card-body">
{% if nonracked_devices %}
<table class="table table-hover">
<tr>
<th>Name</th>
<th>Role</th>
<th>Type</th>
<th colspan="2">Parent Device</th>
</tr>
{% for device in nonracked_devices %}
<tr{% if device.device_type.u_height %} class="warning"{% endif %}>
<td>
<a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
</td>
<td>{{ device.device_role }}</td>
<td>{{ device.device_type }}</td>
{% if device.parent_bay %}
<td>{{ device.parent_bay.device|linkify }}</td>
<td>{{ device.parent_bay }}</td>
{% else %}
<td colspan="2" class="text-muted">&mdash;</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% if total_nonracked_devices_count > nonracked_devices.count %}
{% if object|meta:'verbose_name' == 'site' %}
<div class="text-muted">
Displaying {{ nonracked_devices.count }} of {{ total_nonracked_devices_count }} devices (<a href="{% url 'dcim:device_list' %}?site_id={{ object.pk }}&rack_id=null">View full list</a>)
</div>
{% elif object|meta:'verbose_name' == 'location' %}
<div class="text-muted">
Displaying {{ nonracked_devices.count }} of {{ total_nonracked_devices_count }} devices (<a href="{% url 'dcim:device_list' %}?location_id={{ object.pk }}&rack_id=null">View full list</a>)
</div>
{% endif %}
{% endif %}
{% else %}
<div class="text-muted">
None
</div>
{% endif %}
</div>
{% if perms.dcim.add_device %}
{% if object|meta:'verbose_name' == 'rack' %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&rack={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add a Non-Racked Device
</a>
</div>
{% elif object|meta:'verbose_name' == 'site' %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add a Non-Racked Device
</a>
</div>
{% elif object|meta:'verbose_name' == 'location' %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&location={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add a Non-Racked Device
</a>
</div>
{% endif %}
{% endif %}
</div>

View File

@@ -65,7 +65,6 @@
</div>
<div class="col col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'dcim/inc/nonracked_devices.html' %}
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
</div>
@@ -78,6 +77,27 @@
hx-get="{% url 'dcim:location_list' %}?parent_id={{ object.pk }}"
hx-trigger="load"
></div>
{% if perms.dcim.add_location %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:location_add' %}?site={{ object.site.pk }}&parent={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a Location
</a>
</div>
{% endif %}
</div>
<div class="card">
<h5 class="card-header">Non-Racked Devices</h5>
<div class="card-body htmx-container table-responsive"
hx-get="{% url 'dcim:device_list' %}?location_id={{ object.pk }}&rack_id=null&parent_bay_id=null"
hx-trigger="load"
></div>
{% if perms.dcim.add_device %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.site.pk }}&location={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a Device
</a>
</div>
{% endif %}
</div>
{% plugin_full_width_page object %}
</div>

View File

@@ -1,5 +1,4 @@
{% extends 'dcim/rack/base.html' %}
{% load helpers %}
{% extends 'generic/object_children.html' %}
{% block extra_controls %}
{% if perms.dcim.add_device %}
@@ -10,42 +9,4 @@
</a>
</div>
{% endif %}
{% endblock %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<button type="submit" name="_edit"
formaction="{% url 'dcim:device_bulk_edit' %}?return_url={% url 'dcim:rack_nonracked_devices' pk=object.pk %}"
class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit"
formaction="{% url 'dcim:device_bulk_delete' %}?return_url={% url 'dcim:rack_nonracked_devices' pk=object.pk %}"
class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
</div>
</form>
{% endblock content %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% endblock extra_controls %}

View File

@@ -1,43 +1,12 @@
{% extends 'dcim/rack/base.html' %}
{% load helpers %}
{% extends 'generic/object_children.html' %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="RackReservationTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<button type="submit" name="_edit" formaction="{% url 'dcim:rackreservation_bulk_edit' %}?return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit" formaction="{% url 'dcim:rackreservation_bulk_delete' %}?return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
{% if perms.dcim.add_rackreservation %}
{% block extra_controls %}
{% if perms.dcim.add_rackreservation %}
<div class="bulk-button-group">
<a href="{% url 'dcim:rackreservation_add' %}?rack={{ object.pk }}&return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add reservation
</a>
<a href="{% url 'dcim:rackreservation_add' %}?rack={{ object.pk }}&return_url={% url 'dcim:rack_reservations' pk=object.pk %}"
class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add reservation
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% endif %}
{% endblock extra_controls %}

View File

@@ -131,56 +131,40 @@
</div>
<div class="col col-md-6">
{% include 'inc/panels/related_objects.html' with filter_name='site_id' %}
<div class="card">
<h5 class="card-header">Locations</h5>
<div class='card-body'>
{% if locations %}
<table class="table table-hover">
<tr>
<th>Location</th>
<th>Racks</th>
<th>Devices</th>
<th></th>
</tr>
{% for location in locations %}
<tr>
<td>
{% for i in location.level|as_range %}<i class="mdi mdi-circle-small"></i>{% endfor %}
{{ location|linkify }}
</td>
<td>
<a href="{% url 'dcim:rack_list' %}?location_id={{ location.pk }}">{{ location.rack_count }}</a>
</td>
<td>
<a href="{% url 'dcim:device_list' %}?location_id={{ location.pk }}">{{ location.device_count }}</a>
</td>
<td class="text-end noprint">
<a href="{% url 'dcim:rack_elevation_list' %}?location_id={{ location.pk }}" class="btn btn-sm btn-primary" title="View Elevations">
<i class="mdi mdi-server"></i>
</a>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
{% if perms.dcim.add_location %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:location_add' %}?site={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a location
</a>
</div>
{% endif %}
</div>
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% include 'dcim/inc/nonracked_devices.html' %}
<div class="card">
<h5 class="card-header">Locations</h5>
<div class="card-body htmx-container table-responsive"
hx-get="{% url 'dcim:location_list' %}?site_id={{ object.pk }}"
hx-trigger="load"
></div>
{% if perms.dcim.add_location %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:location_add' %}?site={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a Location
</a>
</div>
{% endif %}
</div>
<div class="card">
<h5 class="card-header">Non-Racked Devices</h5>
<div class="card-body htmx-container table-responsive"
hx-get="{% url 'dcim:device_list' %}?site_id={{ object.pk }}&rack_id=null&parent_bay_id=null"
hx-trigger="load"
></div>
{% if perms.dcim.add_device %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?site={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a Device
</a>
</div>
{% endif %}
</div>
{% plugin_full_width_page object %}
</div>
</div>

View File

@@ -134,6 +134,24 @@
</table>
</div>
</div>
<div class="card">
<h5 class="card-header">Related Objects</h5>
<ul class="list-group list-group-flush">
{% for qs in related_models %}
<a class="list-group-item list-group-item-action d-flex justify-content-between">
{{ qs.model|meta:"verbose_name_plural"|bettertitle }}
{% with count=qs.count %}
{% if count %}
<span class="badge bg-primary rounded-pill">{{ count }}</span>
{% else %}
<span class="badge bg-light rounded-pill">&mdash;</span>
{% endif %}
{% endwith %}
</a>
{% endfor %}
</ul>
</div>
{% plugin_right_page object %}
</div>
</div>

View File

@@ -0,0 +1,4 @@
{% extends 'generic/object.html' %}
{% block tabs %}
{% endblock %}

View File

@@ -0,0 +1,93 @@
{
"type": "object",
"additionalProperties": false,
"definitions": {
"airflow": {
"type": "string",
"enum": {{ airflow_choices }}
},
"weight-unit": {
"type": "string",
"enum": {{ weight_unit_choices }}
},
"subdevice-role": {
"type": "string",
"enum": {{ subdevice_role_choices }}
},
"console-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ console_port_type_choices }}
}
}
},
"console-server-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ console_server_port_type_choices }}
}
}
},
"power-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ power_port_type_choices }}
}
}
},
"power-outlet": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ power_outlet_type_choices }}
},
"feed-leg": {
"type": "string",
"enum": {{ power_outlet_feedleg_choices }}
}
}
},
"interface": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ interface_type_choices }}
},
"poe_mode": {
"type": "string",
"enum": {{ interface_poe_mode_choices }}
},
"poe_type": {
"type": "string",
"enum": {{ interface_poe_type_choices }}
}
}
},
"front-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ front_port_type_choices }}
}
}
},
"rear-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ rear_port_type_choices}}
}
}
}
}
}

View File

@@ -0,0 +1,57 @@
{% extends base_template %}
{% load helpers %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
{% block bulk_controls %}
<div class="bulk-button-group">
<div class="btn-group" role="group">
{# Bulk edit buttons #}
{% block bulk_edit_controls %}
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
{% if 'bulk_edit' in actions and bulk_edit_view %}
<button type="submit" name="_edit"
formaction="{% url bulk_edit_view %}?return_url={{ return_url }}"
class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
</button>
{% endif %}
{% endwith %}
{% endblock bulk_edit_controls %}
</div>
<div class="btn-group" role="group">
{# Bulk delete buttons #}
{% block bulk_delete_controls %}
{% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %}
{% if 'bulk_delete' in actions and bulk_delete_view %}
<button type="submit"
formaction="{% url bulk_delete_view %}?return_url={{ return_url }}"
class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete Selected
</button>
{% endif %}
{% endwith %}
{% endblock bulk_delete_controls %}
</div>
</div>
<div class="bulk-button-group">
{# Other bulk action buttons #}
{% block bulk_extra_controls %}{% endblock %}
</div>
{% endblock bulk_controls %}
</div>
</form>
{% endblock content %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}

View File

@@ -1,5 +1,4 @@
{% extends 'ipam/aggregate/base.html' %}
{% load helpers %}
{% extends 'generic/object_children.html' %}
{% block extra_controls %}
{% include 'ipam/inc/toggle_available.html' %}
@@ -9,38 +8,4 @@
</a>
{% endif %}
{{ block.super }}
{% endblock %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<button type="submit" name="_edit" formaction="{% url 'ipam:prefix_bulk_edit' %}?return_url={% url 'ipam:aggregate_prefixes' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'ipam:prefix_bulk_delete' %}?return_url={% url 'ipam:aggregate_prefixes' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
</div>
</form>
{% endblock %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% endblock extra_controls %}

View File

@@ -1,36 +0,0 @@
{% extends 'ipam/asnrange/base.html' %}
{% load helpers %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="ASNTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<button type="submit" name="_edit" formaction="{% url 'ipam:asn_bulk_edit' %}?return_url={% url 'ipam:asnrange_asns' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'ipam:asn_bulk_delete' %}?return_url={% url 'ipam:asnrange_asns' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
</div>
</form>
{% endblock %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}

View File

@@ -1,19 +0,0 @@
{% extends 'ipam/ipaddress/base.html' %}
{% load helpers %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
</form>
{% endblock %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}

View File

@@ -1,44 +1,9 @@
{% extends 'ipam/iprange/base.html' %}
{% load helpers %}
{% extends 'generic/object_children.html' %}
{% block extra_controls %}
{% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and object.first_available_ip %}
{% if perms.ipam.add_ipaddress and object.first_available_ip %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ object.first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-sm btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add IP Address
</a>
{% endif %}
{% endblock %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<button type="submit" name="_edit" formaction="{% url 'ipam:ipaddress_bulk_edit' %}?return_url={% url 'ipam:iprange_ipaddresses' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'ipam:ipaddress_bulk_delete' %}?return_url={% url 'ipam:iprange_ipaddresses' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
</div>
</form>
{% endblock %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% endblock extra_controls %}

View File

@@ -1,5 +1,4 @@
{% extends 'ipam/prefix/base.html' %}
{% load helpers %}
{% extends 'generic/object_children.html' %}
{% block extra_controls %}
{% if perms.ipam.add_ipaddress and first_available_ip %}
@@ -7,38 +6,4 @@
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add IP Address
</a>
{% endif %}
{% endblock %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<button type="submit" name="_edit" formaction="{% url 'ipam:ipaddress_bulk_edit' %}?return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'ipam:ipaddress_bulk_delete' %}?return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
</div>
</form>
{% endblock %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% endblock extra_controls %}

View File

@@ -1,5 +1,4 @@
{% extends 'ipam/prefix/base.html' %}
{% load helpers %}
{% extends 'generic/object_children.html' %}
{% block extra_controls %}
{% if perms.ipam.add_iprange and first_available_ip %}
@@ -7,38 +6,4 @@
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add IP Range
</a>
{% endif %}
{% endblock %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="IPRangeTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<button type="submit" name="_edit" formaction="{% url 'ipam:iprange_bulk_edit' %}?return_url={% url 'ipam:prefix_ipranges' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'ipam:iprange_bulk_delete' %}?return_url={% url 'ipam:prefix_ipranges' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
</div>
</form>
{% endblock %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% endblock extra_controls %}

View File

@@ -1,5 +1,4 @@
{% extends 'ipam/prefix/base.html' %}
{% load helpers %}
{% extends 'generic/object_children.html' %}
{% block extra_controls %}
{% include 'ipam/inc/toggle_available.html' %}
@@ -8,39 +7,4 @@
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Prefix
</a>
{% endif %}
{{ block.super }}
{% endblock %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<button type="submit" name="_edit" formaction="{% url 'ipam:prefix_bulk_edit' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'ipam:prefix_bulk_delete' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
</div>
</form>
{% endblock %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% endblock extra_controls %}

View File

@@ -1,20 +0,0 @@
{% extends 'ipam/vlan/base.html' %}
{% load helpers %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="VLANDevicesTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
</form>
{% endblock content %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}

View File

@@ -1,20 +0,0 @@
{% extends 'ipam/vlan/base.html' %}
{% load helpers %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="VLANVirtualMachinesTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
</form>
{% endblock content %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}

View File

@@ -58,15 +58,4 @@
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">VLANs</h5>
<div class="card-body table-responsive">
{% render_table vlans_table 'inc/table.html' %}
{% include 'inc/paginator.html' with paginator=vlans_table.paginator page=vlans_table.page %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends base_template %}
{% extends 'generic/object_children.html' %}
{% load helpers %}
{% block extra_controls %}
@@ -10,20 +10,3 @@
{% endwith %}
{% endif %}
{% endblock %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="ContactAssignmentTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
</form>
{% endblock content %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}

View File

@@ -1,30 +1,13 @@
{% extends 'virtualization/cluster/base.html' %}
{% extends 'generic/object_children.html' %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
<form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if perms.virtualization.change_cluster %}
<button type="submit" name="_remove" class="btn btn-danger btn-sm">
{% block bulk_delete_controls %}
{{ block.super }}
{% if 'bulk_remove_devices' in actions %}
<button type="submit" name="_remove"
formaction="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}?return_url={{ return_url }}"
class="btn btn-danger btn-sm">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Remove Devices
</button>
{% endif %}
</div>
</div>
</form>
{% endblock content %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
</button>
{% endif %}
{% endblock bulk_delete_controls %}

View File

@@ -1,35 +0,0 @@
{% extends 'virtualization/cluster/base.html' %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint bulk-buttons">
<div class="bulk-button-group">
{% if 'bulk_edit' in actions %}
<button type="submit" name="_edit" formaction="{% url 'virtualization:virtualmachine_bulk_edit' %}?return_url={% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button>
{% endif %}
{% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'virtualization:virtualmachine_bulk_delete' %}?return_url={% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button>
{% endif %}
</div>
</div>
</form>
{% endblock content %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}

View File

@@ -1,47 +1,13 @@
{% extends 'virtualization/virtualmachine/base.html' %}
{% load render_table from django_tables2 %}
{% extends 'generic/object_children.html' %}
{% load helpers %}
{% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineVMInterfaceTable_config" %}
<form method="post">
{% csrf_token %}
<div class="card">
<div class="card-body htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
<div class="noprint">
{% if perms.virtualization.change_vminterface %}
<div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'virtualization:vminterface_bulk_edit' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-warning btn-sm">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
</button>
<button type="submit" name="_rename" formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Rename
</button>
</div>
{% endif %}
{% if perms.virtualization.delete_vminterface %}
<button type="submit" name="_delete" formaction="{% url 'virtualization:vminterface_bulk_delete' %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-danger btn-sm">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete
{% block bulk_edit_controls %}
{{ block.super }}
{% if 'bulk_rename' in actions %}
<button type="submit" name="_rename"
formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={{ return_url }}"
class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button>
{% endif %}
{% if perms.virtualization.add_vminterface %}
<div class="float-end">
<a href="{% url 'virtualization:vminterface_add' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-primary btn-sm">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add interfaces
</a>
</div>
{% endif %}
</div>
</form>
{% endblock content %}
{% block modals %}
{{ block.super }}
{% table_config_form table %}
{% endblock modals %}
{% endif %}
{% endblock bulk_edit_controls %}

View File

@@ -41,11 +41,6 @@ class ObjectContactsView(generic.ObjectChildrenView):
return table
def get_extra_context(self, request, instance):
return {
'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
}
#
# Tenant groups
#

View File

@@ -70,7 +70,13 @@ class TokenProvisionView(APIView):
"""
permission_classes = []
# @extend_schema(methods=["post"], responses={201: serializers.TokenSerializer})
@extend_schema(
request=serializers.TokenProvisionSerializer,
responses={
201: serializers.TokenSerializer,
401: OpenApiTypes.OBJECT,
}
)
def post(self, request):
serializer = serializers.TokenProvisionSerializer(data=request.data)
serializer.is_valid()

View File

@@ -20,7 +20,8 @@ FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(
lte='lte',
lt='lt',
gte='gte',
gt='gt'
gt='gt',
empty='isnull',
)
FILTER_NEGATION_LOOKUP_MAP = dict(
@@ -45,6 +46,10 @@ HTTP_REQUEST_META_SAFE_COPY = [
'HTTP_REFERER',
'HTTP_USER_AGENT',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED_HOST',
'HTTP_X_FORWARDED_PORT',
'HTTP_X_FORWARDED_PROTO',
'HTTP_X_REAL_IP',
'QUERY_STRING',
'REMOTE_ADDR',
'REMOTE_HOST',

View File

@@ -103,6 +103,10 @@ class RestrictedGenericForeignKey(GenericForeignKey):
# We avoid looking for values if either ct_id or fkey value is None
ct_id = getattr(instance, ct_attname)
if ct_id is not None:
# Check if the content type actually exists
if not self.get_content_type(id=ct_id, using=instance._state.db).model_class():
continue
fk_val = getattr(instance, self.fk_field)
if fk_val is not None:
fk_dict[ct_id].add(fk_val)
@@ -127,13 +131,14 @@ class RestrictedGenericForeignKey(GenericForeignKey):
if ct_id is None:
return None
else:
model = self.get_content_type(
if model := self.get_content_type(
id=ct_id, using=obj._state.db
).model_class()
return (
model._meta.pk.get_prep_value(getattr(obj, self.fk_field)),
model,
)
).model_class():
return (
model._meta.pk.get_prep_value(getattr(obj, self.fk_field)),
model,
)
return None
return (
ret_val,

View File

@@ -123,9 +123,9 @@ class BulkImportForm(BootstrapMixin, SyncedDataMixin, forms.Form):
records = []
try:
for data in yaml.load_all(data, Loader=yaml.SafeLoader):
if type(data) == list:
if type(data) is list:
records.extend(data)
elif type(data) == dict:
elif type(data) is dict:
records.append(data)
else:
raise forms.ValidationError({

View File

@@ -60,6 +60,9 @@ def parse_alphanumeric_range(string):
except ValueError:
begin, end = dash_range, dash_range
if begin.isdigit() and end.isdigit():
if int(begin) >= int(end):
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
for n in list(range(int(begin), int(end) + 1)):
values.append(n)
else:
@@ -71,6 +74,10 @@ def parse_alphanumeric_range(string):
# Not a valid range (more than a single character)
if not len(begin) == len(end) == 1:
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
if ord(begin) >= ord(end):
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
for n in list(range(ord(begin), ord(end) + 1)):
values.append(chr(n))
return values

View File

@@ -1,3 +1,4 @@
{% load l10n %}
<div class="progress">
<div
role="progressbar"
@@ -5,7 +6,7 @@
aria-valuemax="100"
aria-valuenow="{{ utilization }}"
class="progress-bar {{ bar_class }}"
style="width: {{ utilization }}%;"
style="width: {{ utilization|unlocalize }}%;"
>
{% if utilization >= 35 %}{{ utilization|floatformat:1 }}%{% endif %}
</div>

View File

@@ -114,7 +114,7 @@ def annotated_date(date_value):
if not date_value:
return ''
if type(date_value) == datetime.date:
if type(date_value) is datetime.date:
long_ts = date(date_value, 'DATE_FORMAT')
short_ts = date(date_value, 'SHORT_DATE_FORMAT')
else:

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