Compare commits

...

143 Commits

Author SHA1 Message Date
Jeremy Stretch
b72793a85a Merge pull request #9706 from netbox-community/develop
Release v3.2.6
2022-07-11 12:09:21 -04:00
jeremystretch
68f24755aa Release v3.2.6 2022-07-11 11:41:28 -04:00
jeremystretch
5a4467a4a8 Fixes #9687: Don't restrict custom text field lengths when entering via UI form 2022-07-11 11:12:06 -04:00
jeremystretch
7c109ffd8c Fixes #9697: Fix device role link under device view 2022-07-11 11:02:18 -04:00
jeremystretch
6415661b61 Remove extraneous argument to GenericRelation 2022-07-11 10:33:14 -04:00
jeremystretch
ed7f42a803 Fixes #9704: Include last_updated field on JournalEntry REST API serializer 2022-07-11 10:28:37 -04:00
jeremystretch
e2af716a81 #9686: Add default accessor to TenantGroupColumn 2022-07-11 10:03:45 -04:00
jeremystretch
d3f91ce0a6 Changelog for #9632, #9686 2022-07-11 09:57:12 -04:00
Jeremy Stretch
dde005366a Merge pull request #9700 from PieterL75/issue9656_journalviewlayout
Fixes: #9656 Re-order journal list and form
2022-07-11 09:54:48 -04:00
Jeremy Stretch
85cab8d9b0 Merge pull request #9696 from kkthxbye-code/fix-9632
Fixes #9632 - Focus on select field after populating data
2022-07-11 09:51:45 -04:00
Jeremy Stretch
a49d3d2ddc Merge pull request #9695 from hagbarddenstore/issue_9686
Add Tenant Group column to tables #9686
2022-07-11 09:50:42 -04:00
kkthxbye-code
93c30c94b3 Focus on select field after populating data 2022-07-10 19:16:16 +02:00
Kim Johansson
1539769c08 Prefetch Tenant Group on user configurable tables
Prefetch the Tenant Group in views which allows its table to be configured
by the user. This decreases the amount of database queries that are required
to fetch the data.
2022-07-10 17:17:56 +02:00
Kim Johansson
c7ece43a18 Remove Tenant Group from child tables
Removes the Tenant Group column from tables which aren't configurable
by the user.
2022-07-10 17:16:12 +02:00
Kim Johansson
69a22ffe5e Prefetch Tenant Group in global search
Configure the prefetch to also include the Tenant Group, avoids additional
database queries when the Tenant Group column is to be rendered.

NOTE: If no personalisation of the global search tables should be done,
this commit can be reverted.
2022-07-10 15:38:21 +02:00
Kim Johansson
e6bfde1397 Replace TenantColumn with new TenancyColumnsMixin
Replaces all usages of the TenantColumn with the new TenancyColumnsMixin.

This enables the user to add a column for Tenant Group on all tables which
also has a column for Tenant.
2022-07-10 15:13:48 +02:00
Kim Johansson
bd60d46b82 Table mixin for Tenancy columns
A mixin to add the Tenant and Tenant Group columns to a table.
2022-07-10 15:08:55 +02:00
Kim Johansson
3c2a55a521 Add TenantGroupColumn to display Tenant Group on tables
Works the same as the existing TenantColumn, but displats the Tenant Group of
the Tenant.

Views should prefetch the Tenants Group for this to be efficient in large
tables.
2022-07-10 11:58:45 +02:00
jeremystretch
a40ab9ffb1 Fixes #9657: Fix filtering for custom fields and webhooks in the UI 2022-07-08 14:59:16 -04:00
jeremystretch
55b3e4eeb3 Fixes #9682: Fix bulk assignment of ASNs to sites 2022-07-08 14:16:42 -04:00
Pieter Lambrecht
13f854c91f Re-order journal list and form 2022-07-06 14:10:10 +02:00
jeremystretch
29f629156a Update NOTICE file 2022-07-01 11:36:34 -04:00
jeremystretch
8e200a9cb4 #9403: Add labels to device VC fields 2022-06-28 16:24:56 -04:00
jeremystretch
ccb7e96d8a Changelog for #8854, #9403, #9540 2022-06-28 16:22:38 -04:00
Jeremy Stretch
f75ddeb721 Merge pull request #9622 from cpund/9403-branch
Closes #9403: Add support for setting a virtual chassis on a device in the GUI
2022-06-28 16:21:22 -04:00
Jeremy Stretch
221ddc6d0f Merge pull request #9600 from huntabyte/issue-8854
Fixes #8854: Remote auth default groups added to new remote auth users
2022-06-28 15:29:23 -04:00
Jeremy Stretch
31c752bf3a Merge pull request #9607 from huntabyte/feature-9540
Closes #9540: Filter IP addresses by assigned Device/VM
2022-06-28 15:08:37 -04:00
Hunter Johnston
2077378ae1 Closes #9540: Filter IP addresses by assigned Device/VM 2022-06-25 15:41:31 -04:00
Hunter Johnston
9b91c2a886 syntax: Removed dev imports 2022-06-23 23:29:14 -04:00
Hunter Johnston
d8b40056b5 Fixes #8854: Remote auth default groups added to new remote auth users 2022-06-23 20:54:26 -04:00
jeremystretch
4315c4697c Ignore default field values which reference ConfigItems when calculating migrations 2022-06-23 17:44:19 -04:00
jeremystretch
b77013c859 Docs & cleanup for #7702 2022-06-23 17:26:20 -04:00
Jeremy Stretch
f7de2611c1 Merge pull request #9576 from huntabyte/feature-7702
Closes #7702: Add Power feed defaults to user configurations
2022-06-23 17:00:06 -04:00
Hunter Johnston
c330282919 Fix syntax error 2022-06-23 14:56:24 -04:00
Hunter Johnston
db807ab4a6 Closes #7702: Add power feed defaults to user configurations 2022-06-23 14:16:09 -04:00
jeremystretch
d55e3c352a Changelog for #9396, #9575, #9597 2022-06-23 14:14:02 -04:00
Hunter Johnston
afec53cea3 Fixes #9575: Add exception handling to services (#9586) 2022-06-23 14:11:59 -04:00
Hunter Johnston
6cb8b9110e Closes #9396: Query modules by module bay & display installed_modules for module_bay REST API endpoint (#9574)
* Closes #9396 - Added ability to query modules by module bay & installed_modules for module bay REST API endpoint

* Closes #9396 - Added ability to query modules by module bay & installed_modules for module bay REST API endpoint

* Closes #9396 - Added ability to query modules by module bay & installed_modules for module bay REST API endpoint
2022-06-23 13:28:36 -04:00
Hunter Johnston
52178f78d1 Closes #7702: Add Powerfeed Defaults to User Configurations 2022-06-21 12:58:41 -04:00
jeremystretch
575e2c443b PRVB 2022-06-20 11:38:49 -04:00
Jeremy Stretch
7c09259b7d Merge pull request #9569 from netbox-community/develop
Release v3.2.5
2022-06-20 11:35:48 -04:00
jeremystretch
7ba268946a Release v3.2.5 2022-06-20 11:22:36 -04:00
jeremystretch
8074ca95bd Closes #9453: Disable default loggers when running tests 2022-06-20 11:17:15 -04:00
jeremystretch
d691ea92d0 #9525: Add button colors 2022-06-20 09:44:59 -04:00
jeremystretch
903a3e1a9c Changelog for #8944, #9108, #9556 2022-06-20 08:34:05 -04:00
Jeremy Stretch
4109113319 Merge pull request #9558 from kkthxbye-code/fix-9556
Fixes #9556 - Don't close select field when multiple select (v2)
2022-06-20 08:25:49 -04:00
Jeremy Stretch
872c11502f Merge pull request #9522 from kkthxbye-code/fix-9108
Fixes #9108 & #8944 - Sanitize HTML after rendering markdown
2022-06-20 08:16:43 -04:00
jeremystretch
10cb4f359a Changelog for #9417, #9517, #9525 2022-06-20 08:06:49 -04:00
Jeremy Stretch
45babf162e Merge pull request #9562 from huntabyte/feature-9525
Closes: #9525 - Add split button functionality to table rows
2022-06-20 08:03:20 -04:00
Jeremy Stretch
3434428357 Merge pull request #9563 from huntabyte/feature-9417
Closes #9417: Pre-populate manufacturer when adding modules to devices
2022-06-20 07:49:10 -04:00
Jeremy Stretch
25128bd06f Merge pull request #9564 from huntabyte/feature-9517
Closes #9517: Linkify Power Port on Power Outlet Object View
2022-06-20 07:45:30 -04:00
Hunter Johnston
e7620b0dd0 Closes #9517: Linkify Power Port on Power Outlet Object View 2022-06-19 22:10:01 -04:00
Hunter Johnston
65683d0df1 Closes #9417: Pre-populate manufacturer when adding modules to devices 2022-06-19 20:00:15 -04:00
Hunter Johnston
ff2ccfd670 Closes #9525: Added split button functionality to ActionsColumn 2022-06-19 19:12:52 -04:00
Hunter Johnston
b1ec703ba9 Implemented feature #9525 2022-06-18 23:08:06 -04:00
Hunter Johnston
3d785d836d Implemented feature #9525 2022-06-18 23:05:18 -04:00
kkthxbye-code
7c79c90cd2 Sanitize HTML after rendering markdown 2022-06-17 23:16:57 +02:00
kkthxbye-code
a6e285316a Don't close select field when multiple select 2022-06-17 22:53:51 +02:00
jeremystretch
e6018cd38f Closes #9534: Add VLAN group selector to interface bulk edit forms 2022-06-17 14:51:45 -04:00
jeremystretch
92a6523bf3 Fixes #9549: Fix device counts for rack list under rack role view 2022-06-17 14:40:37 -04:00
jeremystretch
2815eca260 Fixes #9503: Hyperlinks in ack elevation SVGs must always use absolute URLs 2022-06-17 14:36:55 -04:00
jeremystretch
896ebf01b1 Changelog for #8704, #9533, #9374, #9466, #9537 2022-06-17 14:04:57 -04:00
Jeremy Stretch
a71b2e231b Merge pull request #9309 from CroogQT/multiselect
Multiselect
2022-06-17 14:00:54 -04:00
Jeremy Stretch
56f3aaf7c8 Merge pull request #9473 from kkthxbye-code/fix-9466
Fixes #9466 - Clear webhook queue on script failure
2022-06-17 13:58:26 -04:00
Jeremy Stretch
87a9cc0b9e Merge pull request #9527 from kkthxbye-code/fix-9374
Partially fixes #9374 - Implement a custom paginator for DeviceViewSet to improve config_context load times
2022-06-17 13:44:37 -04:00
Jeremy Stretch
972a1fdd14 Merge pull request #9543 from hagbarddenstore/fix-9537
Replace None in templates with placeholder filter
2022-06-17 13:16:46 -04:00
Jeremy Stretch
723954f0d9 Merge pull request #9547 from kkthxbye-code/fix-9533
Fixes #9533 - Move markdown documentation to docs
2022-06-17 13:14:44 -04:00
kkthxbye-code
cf76d5c46a Move markdown documentation to docs 2022-06-16 22:26:37 +02:00
Kim Johansson
e8b970608e Replace None in templates with placeholder filter
To be consistent, all uses of — or None is replaced with the
placeholder filter.

Fixes #9537
2022-06-15 22:33:21 +02:00
Jeremy Stretch
83fdfaa0eb Fixes #9524: Correct order of VLAN fields under VM interface creation form 2022-06-13 19:14:29 -04:00
Jeremy Stretch
86c35a403a Changelog for #9501, #9512 2022-06-13 19:05:16 -04:00
Jeremy Stretch
e96c382138 Merge pull request #9520 from kkthxbye-code/fix-9501
Fixes #9501 - Add configuration option JINJA2_FILTERS
2022-06-13 18:55:25 -04:00
Craig Pund
6876c9878e added field definitions for device form 2022-06-13 17:06:08 -04:00
Craig Pund
29a5fb041f add fields for virtual chassis to device_edit form 2022-06-13 17:04:25 -04:00
kkthxbye-code
8ef74192ec Implement a custom paginator for DeviceViewSet
Running count on the annotated query for loading config_context is slow. The custom paginator removes the annotation before getting the count.
2022-06-13 20:45:08 +02:00
Jeremy Stretch
135ce93d03 Merge pull request #9523 from kkthxbye-code/fix-9512
Fixes #9512 -  Add distinct to Site search to prevent duplicates when search matches ASN
2022-06-13 12:12:34 -04:00
kkthxbye
f13b090b5c Add distinct to Site search to prevent duplicates when search matches ASN 2022-06-13 07:56:31 +02:00
kkthxbye-code
d1aa820856 Add configuration option JINJA2_FILTERS 2022-06-10 23:13:49 +02:00
jeremystretch
c81c3d11ed Fixes #9495: Correct link to contacts in contact groups table column 2022-06-09 10:20:44 -04:00
jeremystretch
36c65b7b22 Closes #8893: Include count of IP ranges under tenant view 2022-06-07 11:12:40 -04:00
jeremystretch
8a4c808be5 Closes #8882: Support filtering IP addresses by multiple parent prefixes 2022-06-07 11:00:14 -04:00
jeremystretch
6ed2dbf172 Fixes #9486: Fix redirect URL when adding device components from the module view 2022-06-07 10:06:19 -04:00
jeremystretch
1b8350fe48 Add warning against bumping stale issues 2022-06-07 09:59:59 -04:00
jeremystretch
15080aad66 Changelog for #9480, #9484 2022-06-07 08:51:53 -04:00
Jeremy Stretch
7a7f7c5dec Merge pull request #9483 from kkthxbye-code/fix-9480
Fixes #9480 - Make the Service and ServiceTemplate tables sortable by ports
2022-06-07 08:46:13 -04:00
Jeremy Stretch
c958208c47 Merge pull request #9485 from kkthxbye-code/fix-9484
Fixes #9484 - List services listening on all IPs in IPAddressView
2022-06-07 08:43:14 -04:00
kkthxbye-code
9f4e565b8e List services listening on all IPs in IPAddressView 2022-06-06 16:28:33 +02:00
kkthxbye-code
bb2d21abdd Make the Service and ServiceTemplate tables sortable by ports 2022-06-05 10:31:21 +02:00
kkthxbye
e2eb7fdfb5 Clear webhook queue on script failure 2022-06-03 13:03:58 +02:00
jeremystretch
3fbf1f7e71 PRVB 2022-05-31 15:31:22 -04:00
Jeremy Stretch
9d308e6246 Merge pull request #9454 from netbox-community/develop
Release v3.2.4
2022-05-31 15:28:13 -04:00
jeremystretch
6c035eb13d Release v3.2.4 2022-05-31 15:08:33 -04:00
jeremystretch
b0a56a71bb Fixes #9291: Improve data validation for MultiObjectVar script fields 2022-05-31 13:37:14 -04:00
jeremystretch
201b9f635e Fixes #9402: Fix custom field population when creating a virtual chassis 2022-05-31 13:26:25 -04:00
jeremystretch
f1d0d8e57a Fixes #9407: Clean up display of prefixes values when exporting prefixes list 2022-05-31 12:23:22 -04:00
jeremystretch
5838a9f3a0 Closes #9451: Add export_raw argument for TemplateColumn 2022-05-31 12:20:39 -04:00
jeremystretch
998a392bd3 Fixes #9425: Fix bulk import for object and multi-object custom fields 2022-05-31 11:37:30 -04:00
jeremystretch
a0a87fc4c0 Changelog for #9420, #9430 2022-05-31 09:14:23 -04:00
Jeremy Stretch
6c0b4c66c0 Merge pull request #9438 from kkthxbye-code/fix-9420
Fixes #9420: Allow script inheritance
2022-05-31 09:12:49 -04:00
Jeremy Stretch
2c8a1ed69c Merge pull request #9435 from kkthxbye-code/fix-9430
Fixes #9435 - Make sure initial data is passed as array for DynamicModelChoiceFields
2022-05-31 09:09:42 -04:00
kkthxbye-code
fe899d9d7c Iterate base classes when searching for ScriptVariables 2022-05-28 11:29:18 +02:00
kkthxbye-code
6d3cded579 Make sure initial data is passed as array for DynamicModelChoiceFields 2022-05-27 20:41:50 +02:00
jeremystretch
2e5a5f71ba Changelog for #9277 2022-05-24 16:00:18 -04:00
Jeremy Stretch
72516c00fb Merge pull request #9415 from tyler-8/csrf_cookie_setting
Add optional CSRF_COOKIE_NAME setting, update example config, and docs.
2022-05-24 15:58:00 -04:00
tyler-8
d34d5869be Add optional CSRF_COOKIE_NAME setting, update example config, and docs. 2022-05-24 10:57:38 -04:00
jeremystretch
72726c784a Clean up imports 2022-05-24 09:56:14 -04:00
jeremystretch
662b02e2d8 Closes #9347: Include services in global search 2022-05-24 09:49:36 -04:00
jeremystretch
a9ec1a7b4e Closes #9379: Redirect to virtual chassis view after adding a member device 2022-05-24 09:20:05 -04:00
jeremystretch
f03c5037c4 Fixes #9387: Ensure ActionsColumn extra_buttons are always displayed 2022-05-24 09:14:25 -04:00
Jeremy Stretch
a52c68f4c2 Merge pull request #9406 from lastorel/9365-rolefilter
add `role_id` attribute to filter form of InventoryItem
2022-05-24 08:52:06 -04:00
jeremystretch
a73dda35e8 Bump stale to v5 2022-05-24 08:39:43 -04:00
lastorel
0570203891 add role attribute to filter inventoryitems 2022-05-22 17:22:28 +03:00
jeremystretch
3b3247592e Changelog for #9098 2022-05-18 08:42:20 -04:00
Jeremy Stretch
17292324a3 Merge pull request #9383 from bluikko/patch-1
Add other power, front/rear port types
2022-05-18 08:41:01 -04:00
bluikko
e5aa9d47f7 Add other power, front/rear port types
Fixes #9098
2022-05-18 15:08:08 +07:00
jeremystretch
9e1d8beaf0 Changelog for #9239, #9358 2022-05-16 09:56:02 -04:00
jeremystretch
17fb562740 #9239: Organize contact form fields 2022-05-16 09:55:17 -04:00
Jeremy Stretch
2910aaeec0 Merge pull request #9362 from kkthxbye-code/fix-9358
Fixes #9358 - Annotate provider table in ASN view with count_circuits
2022-05-16 09:31:45 -04:00
Jeremy Stretch
aeef12cdc0 Merge pull request #9364 from kkthxbye-code/fix-9239
Fixes #9239 - Add contact_group to ContactModelFilterSet
2022-05-16 09:28:46 -04:00
kkthxbye-code
8ad203f97a Added contact_group to region, site, manufacturer, tenant filters 2022-05-14 17:53:40 +02:00
kkthxbye-code
aba4e03d3b Add contact_group to ContactModelFilterSet 2022-05-14 17:48:37 +02:00
kkthxbye-code
6a99b36cce Fix provider table in ASN view when ordering by circuit_count 2022-05-14 12:01:49 +02:00
Daniel Sheppard
f415d81049 Fixes #8374 - Display device type and asset tag if name is blank but asset tag is populated 2022-05-13 09:49:07 -05:00
Daniel Sheppard
24ff360ee0 Fixes #8922 - Add service list to IP address view 2022-05-13 09:40:24 -05:00
Daniel Sheppard
2a4c728375 Merge remote-tracking branch 'origin/develop' into develop 2022-05-13 09:29:12 -05:00
Daniel Sheppard
752a497218 Fixes #9094 - Fix partial address search within Prefix and Aggregate filters 2022-05-13 09:28:24 -05:00
Daniel Sheppard
1d4409c703 Fixes #9094 - Fix partial address search within Prefix and Aggregate filters 2022-05-13 09:08:00 -05:00
jeremystretch
3c7c8c8776 PRVB 2022-05-12 14:14:40 -04:00
CroogQT
124e93f737 yarn bundle. 2022-05-06 12:16:45 -07:00
CroogQT
fbd933b56a prettier fixes 2022-05-06 11:44:34 -07:00
CroogQT
9c5355a300 added JSDoc comments 2022-05-06 11:43:18 -07:00
CroogQT
491a4e7d78 various punctuation and spacing fixes 2022-05-06 11:33:00 -07:00
CroogQT
90d8395a2c Fixed variable type issue...i think. 2022-05-05 15:24:16 -07:00
CroogQT
11f7e3099d fixed text deselection and refactor 2022-05-05 15:01:40 -07:00
CroogQT
ef29bffb72 is this supposed to be ignored? 2022-05-05 13:27:09 -07:00
CroogQT
3effa37fa7 click event calls multiselect function 2022-05-05 13:24:50 -07:00
CroogQT
1493c920fd silly text highlight workaround... 2022-05-05 13:24:12 -07:00
CroogQT
ea9258d36c added main multi-select function 2022-05-05 13:23:43 -07:00
CroogQT
db142061ff clicking a PkCheckbox updates state 2022-05-05 12:37:28 -07:00
CroogQT
c536944a10 now exports multiselect function 2022-05-05 12:36:17 -07:00
CroogQT
ae7ddecaa6 now exports previousPkCheck.ts 2022-05-05 12:14:15 -07:00
CroogQT
2e38e62101 create store to store previously checked element 2022-05-05 12:13:02 -07:00
CroogQT
2979a64ce3 add file, skeleton from "select all" 2022-05-05 12:11:02 -07:00
120 changed files with 1542 additions and 491 deletions

View File

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

View File

@@ -8,7 +8,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
- uses: actions/stale@v5
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an
@@ -27,7 +27,10 @@ jobs:
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. NetBox
is governed by a small group of core maintainers which means not all opened
issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
issues may receive direct feedback. **Do not** attempt to circumvent this
process by "bumping" the issue; doing so will result in its immediate closure
and you may be barred from participating in any future discussions. Please see
our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
stale-pr-label: 'pending closure'
stale-pr-message: >
This PR has been automatically marked as stale because it has not had

View File

@@ -160,9 +160,9 @@ to aid in issue management.
It is natural that some new issues get more attention than others. The stale
bot helps bring renewed attention to potentially valuable issues that may have
been overlooked. **Do not** comment on an issue that has been marked stale in
an effort to circumvent the bot: Doing so will not remove the stale label.
(Stale labels can be removed only by maintainers.)
been overlooked. **Do not** comment on a stale issue merely to "bump" it in an
effort to circumvent the bot: This will result in the immediate closure of the
issue, and you may be barred from participating in future discussions.
## Maintainer Guidance

6
NOTICE
View File

@@ -1 +1,7 @@
Copyrighted and licensed under Apache License 2.0 by DigitalOcean, LLC.
This project contains code developed expressly for NetBox, and its reuse in
other projects may introduce issues affecting performance, data integrity,
and security.
For more information, please see https://github.com/netbox-community/netbox.

View File

@@ -1,3 +1,7 @@
# HTML sanitizer
# https://github.com/mozilla/bleach
bleach
# The Python web framework on which NetBox is built
# https://github.com/django/django
Django
@@ -44,7 +48,8 @@ django-tables2
# User-defined tags for objects
# https://github.com/alex/django-taggit
django-taggit
# Will evaluate v3.0 during NetBox v3.3 beta
django-taggit>=2.1.0,<3.0
# A Django field for representing time zones
# https://github.com/mfogel/django-timezone-field/

View File

@@ -43,18 +43,6 @@ changes in the database indefinitely.
---
## JOBRESULT_RETENTION
Default: 90
The number of days to retain job results (scripts and reports). Set this to `0` to retain
job results in the database indefinitely.
!!! warning
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
---
## CUSTOM_VALIDATORS
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
@@ -110,6 +98,18 @@ Setting this to False will disable the GraphQL API.
---
## JOBRESULT_RETENTION
Default: 90
The number of days to retain job results (scripts and reports). Set this to `0` to retain
job results in the database indefinitely.
!!! warning
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
---
## MAINTENANCE_MODE
Default: False
@@ -185,6 +185,30 @@ The default maximum number of objects to display per page within each list of ob
---
## POWERFEED_DEFAULT_AMPERAGE
Default: 15
The default value for the `amperage` field when creating new power feeds.
---
## POWERFEED_DEFAULT_MAX_UTILIZATION
Default: 80
The default value (percentage) for the `max_utilization` field when creating new power feeds.
---
## POWERFEED_DEFAULT_VOLTAGE
Default: 120
The default value for the `voltage` field when creating new power feeds.
---
## PREFER_IPV4
Default: False

View File

@@ -66,6 +66,14 @@ CORS_ORIGIN_WHITELIST = [
---
## CSRF_COOKIE_NAME
Default: `csrftoken`
The name of the cookie to use for the cross-site request forgery (CSRF) authentication token. See the [Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#csrf-cookie-name) for more detail.
---
## CSRF_TRUSTED_ORIGINS
Default: `[]`
@@ -247,6 +255,23 @@ HTTP_PROXIES = {
---
## JINJA2_FILTERS
Default: `{}`
A dictionary of custom jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example:
```python
def uppercase(x):
return str(x).upper()
JINJA2_FILTERS = {
'uppercase': uppercase,
}
```
---
## INTERNAL_IPS
Default: `('127.0.0.1', '::1')`

View File

@@ -10,7 +10,7 @@ Within the database, custom fields are stored as JSON data directly alongside ea
Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field:
* Text: Free-form text (up to 255 characters)
* Text: Free-form text (intended for single-line use)
* Long text: Free-form of any length; supports Markdown rendering
* Integer: A whole number (positive or negative)
* Boolean: True or false

View File

@@ -85,4 +85,5 @@ The table column classes listed below are supported for use in plugins. These cl
::: netbox.tables.TemplateColumn
selection:
members: false
members:
- __init__

353
docs/reference/markdown.md Normal file
View File

@@ -0,0 +1,353 @@
---
hide:
- toc
---
# Markdown
NetBox supports markdown rendering for certain text fields.
## Syntax
##### Table of Contents
[Headers](#headers)
[Emphasis](#emphasis)
[Lists](#lists)
[Links](#links)
[Images](#images)
[Code Blocks](#code)
[Tables](#tables)
[Blockquotes](#blockquotes)
[Inline HTML](#html)
[Horizontal Rule](#hr)
[Line Breaks](#lines)
<a name="headers"></a>
## Headers
```no-highlight
# H1
## H2
### H3
#### H4
##### H5
###### H6
Alternatively, for H1 and H2, an underline-ish style:
Alt-H1
======
Alt-H2
------
```
# H1
## H2
### H3
#### H4
##### H5
###### H6
<a name="emphasis"></a>
## Emphasis
```no-highlight
Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
```
Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
<a name="lists"></a>
## Lists
(In this example, leading and trailing spaces are shown with with dots: ⋅)
```no-highlight
1. First ordered list item
2. Another item
⋅⋅* Unordered sub-list.
1. Actual numbers don't matter, just that it's a number
⋅⋅1. Ordered sub-list
4. And another item.
⋅⋅⋅You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
⋅⋅⋅To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅
⋅⋅⋅Note that this line is separate, but within the same paragraph.⋅⋅
⋅⋅⋅(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
* Unordered list can use asterisks
- Or minuses
+ Or pluses
```
1. First ordered list item
2. Another item
* Unordered sub-list.
1. Actual numbers don't matter, just that it's a number
1. Ordered sub-list
4. And another item.
You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
To have a line break without a paragraph, you will need to use two trailing spaces.
Note that this line is separate, but within the same paragraph.
(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
* Unordered list can use asterisks
- Or minuses
+ Or pluses
<a name="links"></a>
## Links
There are two ways to create links.
```no-highlight
[I'm an inline-style link](https://www.google.com)
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
[I'm a reference-style link][Arbitrary case-insensitive reference text]
[You can use numbers for reference-style link definitions][1]
Or leave it empty and use the [link text itself].
URLs and URLs in angle brackets will automatically get turned into links.
http://www.example.com or <http://www.example.com> and sometimes
example.com (but not on Github, for example).
Some text to show that the reference links can follow later.
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
[link text itself]: http://www.reddit.com
```
[I'm an inline-style link](https://www.google.com)
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
[I'm a reference-style link][Arbitrary case-insensitive reference text]
[You can use numbers for reference-style link definitions][1]
Or leave it empty and use the [link text itself].
URLs and URLs in angle brackets will automatically get turned into links.
http://www.example.com or <http://www.example.com> and sometimes
example.com (but not on Github, for example).
Some text to show that the reference links can follow later.
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
[link text itself]: http://www.reddit.com
<a name="images"></a>
## Images
```
Here's the Netbox logo (hover to see the title text):
Inline-style:
![alt text](/static/netbox_logo.png "Logo Title Text 1")
Reference-style:
![alt text][logo]
[logo]: /static/netbox_logo.png "Logo Title Text 2"
```
Here's the Netbox logo (hover to see the title text):
Inline-style:
![alt text](/static/netbox_logo.png "Logo Title Text 1")
Reference-style:
![alt text][logo]
[logo]: /static/netbox_logo.png "Logo Title Text 2"
<a name="code"></a>
## Code blocks
```
Inline `code` has `back-ticks around` it.
```
Inline `code` has `back-ticks around` it.
Blocks of code are fenced by lines with three back-ticks <code>```</code>
````
```
var s = "Code block";
alert(s);
```
````
```
var s = "Code block";
alert(s);
```
<a name="tables"></a>
## Tables
```no-highlight
Colons can be used to align columns.
| Tables | Are | Cool |
| ------------- |:-------------:| -----:|
| col 3 is | right-aligned | $1600 |
| col 2 is | centered | $12 |
| zebra stripes | are neat | $1 |
There must be at least 3 dashes separating each header cell.
The outer pipes (|) are optional, and you don't need to make the
raw Markdown line up prettily. You can also use inline Markdown.
Markdown | Less | Pretty
--- | --- | ---
*Still* | `renders` | **nicely**
1 | 2 | 3
```
Colons can be used to align columns.
| Tables | Are | Cool |
| ------------- |:-------------:| -----:|
| col 3 is | right-aligned | $1600 |
| col 2 is | centered | $12 |
| zebra stripes | are neat | $1 |
There must be at least 3 dashes separating each header cell. The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.
Markdown | Less | Pretty
--- | --- | ---
*Still* | `renders` | **nicely**
1 | 2 | 3
<a name="blockquotes"></a>
## Blockquotes
```no-highlight
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
```
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
<a name="html"></a>
## Inline HTML
You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
```no-highlight
<dl>
<dt>Definition list</dt>
<dd>Is something people use sometimes.</dd>
<dt>Markdown in HTML</dt>
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
</dl>
```
<dl>
<dt>Definition list</dt>
<dd>Is something people use sometimes.</dd>
<dt>Markdown in HTML</dt>
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
</dl>
<a name="hr"></a>
## Horizontal Rule
```
Three or more...
---
Hyphens
***
Asterisks
___
Underscores
```
Three or more...
---
Hyphens
***
Asterisks
___
Underscores
<a name="lines"></a>
## Line Breaks
```
Here's a line for us to start with.
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
This line is also a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
```
Here's a line for us to start with.
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
This line is also begins a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
Based on [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) by [adam-p](https://github.com/adam-p) licensed under [CC-BY](https://creativecommons.org/licenses/by/3.0/)

View File

@@ -1,5 +1,88 @@
# NetBox v3.2
## v3.2.6 (2022-07-11)
### Enhancements
* [#7702](https://github.com/netbox-community/netbox/issues/7702) - Enable dynamic configuration for default powerfeed attributes
* [#9396](https://github.com/netbox-community/netbox/issues/9396) - Allow filtering modules by bay ID
* [#9403](https://github.com/netbox-community/netbox/issues/9403) - Enable modifying virtual chassis properties when creating/editing a device
* [#9540](https://github.com/netbox-community/netbox/issues/9540) - Add filters for assigned device & VM to IP addresses list
* [#9686](https://github.com/netbox-community/netbox/issues/9686) - Add tenant group column for all object tables with tenant assignments
### Bug Fixes
* [#8854](https://github.com/netbox-community/netbox/issues/8854) - Fix `REMOTE_AUTH_DEFAULT_GROUPS` for social-auth backends
* [#9575](https://github.com/netbox-community/netbox/issues/9575) - Fix AttributeError exception for FHRP group with an IP address assigned
* [#9597](https://github.com/netbox-community/netbox/issues/9597) - Include `installed_module` in module bay REST API serializer
* [#9632](https://github.com/netbox-community/netbox/issues/9632) - Automatically focus on search box when expanding dropdowns
* [#9657](https://github.com/netbox-community/netbox/issues/9657) - Fix filtering for custom fields and webhooks in the UI
* [#9682](https://github.com/netbox-community/netbox/issues/9682) - Fix bulk assignment of ASNs to sites
* [#9687](https://github.com/netbox-community/netbox/issues/9687) - Don't restrict custom text field lengths when entering via UI form
* [#9704](https://github.com/netbox-community/netbox/issues/9704) - Include `last_updated` field on JournalEntry REST API serializer
---
## v3.2.5 (2022-06-20)
### Enhancements
* [#8704](https://github.com/netbox-community/netbox/issues/8704) - Shift-click to select multiple objects in a list
* [#8882](https://github.com/netbox-community/netbox/issues/8882) - Support filtering IP addresses by multiple parent prefixes
* [#8893](https://github.com/netbox-community/netbox/issues/8893) - Include count of IP ranges under tenant view
* [#9417](https://github.com/netbox-community/netbox/issues/9417) - Initialize manufacturer selection when inserting a new module
* [#9501](https://github.com/netbox-community/netbox/issues/9501) - Add support for custom Jinja2 filters
* [#9517](https://github.com/netbox-community/netbox/issues/9517) - Linkify related power port on power outlet view
* [#9525](https://github.com/netbox-community/netbox/issues/9525) - Provide one-click edit link for objects in tables
* [#9533](https://github.com/netbox-community/netbox/issues/9533) - Move Markdown reference to local documentation
* [#9534](https://github.com/netbox-community/netbox/issues/9534) - Add VLAN group selector to interface bulk edit forms
* [#9556](https://github.com/netbox-community/netbox/issues/9556) - Leave dropdown open upon selection for multi-select fields
### Bug Fixes
* [#8944](https://github.com/netbox-community/netbox/issues/8944) - Fix rendering of Markdown links with colons
* [#9108](https://github.com/netbox-community/netbox/issues/9108) - Fix rendering of bracketed Markdown links
* [#9374](https://github.com/netbox-community/netbox/issues/9374) - Improve performance when retrieving devices/VMs with config context data
* [#9466](https://github.com/netbox-community/netbox/issues/9466) - Avoid sending webhooks after script/report failure
* [#9480](https://github.com/netbox-community/netbox/issues/9480) - Fix sorting services & service templates by port numbers
* [#9484](https://github.com/netbox-community/netbox/issues/9484) - Include services listening on "all IPs" under IP address view
* [#9486](https://github.com/netbox-community/netbox/issues/9486) - Fix redirect URL when adding device components from the module view
* [#9495](https://github.com/netbox-community/netbox/issues/9495) - Correct link to contacts in contact groups table column
* [#9503](https://github.com/netbox-community/netbox/issues/9503) - Hyperlinks in rack elevation SVGs must always use absolute URLs
* [#9512](https://github.com/netbox-community/netbox/issues/9512) - Fix duplicate site results when searching by ASN
* [#9524](https://github.com/netbox-community/netbox/issues/9524) - Correct order of VLAN fields under VM interface creation form
* [#9537](https://github.com/netbox-community/netbox/issues/9537) - Ensure consistent use of placeholder tag throughout UI
* [#9549](https://github.com/netbox-community/netbox/issues/9549) - Fix device counts for rack list under rack role view
---
## v3.2.4 (2022-05-31)
### Enhancements
* [#8374](https://github.com/netbox-community/netbox/issues/8374) - Display device type and asset tag if name is blank but asset tag is populated
* [#8922](https://github.com/netbox-community/netbox/issues/8922) - Add service list to IP address view
* [#9098](https://github.com/netbox-community/netbox/issues/9098) - Add "other" types for power ports/outlets, pass-through ports
* [#9239](https://github.com/netbox-community/netbox/issues/9239) - Enable filtering by contact group for all models which support contact assignment
* [#9277](https://github.com/netbox-community/netbox/issues/9277) - Introduce `CSRF_COOKIE_NAME` configuration parameter
* [#9347](https://github.com/netbox-community/netbox/issues/9347) - Include services in global search
* [#9379](https://github.com/netbox-community/netbox/issues/9379) - Redirect to virtual chassis view after adding a member device
* [#9451](https://github.com/netbox-community/netbox/issues/9451) - Add `export_raw` argument for TemplateColumn
### Bug Fixes
* [#9094](https://github.com/netbox-community/netbox/issues/9094) - Fix partial address search within Prefix and Aggregate filters
* [#9291](https://github.com/netbox-community/netbox/issues/9291) - Improve data validation for MultiObjectVar script fields
* [#9358](https://github.com/netbox-community/netbox/issues/9358) - Annotate circuit count for providers list under ASN view
* [#9387](https://github.com/netbox-community/netbox/issues/9387) - Ensure ActionsColumn `extra_buttons` are always displayed
* [#9402](https://github.com/netbox-community/netbox/issues/9402) - Fix custom field population when creating a virtual chassis
* [#9407](https://github.com/netbox-community/netbox/issues/9407) - Clean up display of prefixes values when exporting prefixes list
* [#9420](https://github.com/netbox-community/netbox/issues/9420) - Fix custom script class inheritance
* [#9425](https://github.com/netbox-community/netbox/issues/9425) - Fix bulk import for object and multi-object custom fields
* [#9430](https://github.com/netbox-community/netbox/issues/9430) - Fix passing of initial form data for DynamicModelChoiceFields
---
## v3.2.3 (2022-05-12)
### Enhancements

View File

@@ -136,6 +136,7 @@ nav:
- Overview: 'graphql-api/overview.md'
- Reference:
- Conditions: 'reference/conditions.md'
- Markdown: 'reference/markdown.md'
- Development:
- Introduction: 'development/index.md'
- Getting Started: 'development/getting-started.md'

View File

@@ -23,7 +23,7 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
(None, ('q', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('ASN', ('asn',)),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -87,7 +87,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
('Attributes', ('type_id', 'status', 'commit_rate')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(),

View File

@@ -2,7 +2,7 @@ import django_tables2 as tables
from circuits.models import *
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn
from tenancy.tables import TenancyColumnsMixin
from .columns import CommitRateColumn
__all__ = (
@@ -39,7 +39,7 @@ class CircuitTypeTable(NetBoxTable):
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
class CircuitTable(NetBoxTable):
class CircuitTable(TenancyColumnsMixin, NetBoxTable):
cid = tables.Column(
linkify=True,
verbose_name='Circuit ID'
@@ -48,7 +48,6 @@ class CircuitTable(NetBoxTable):
linkify=True
)
status = columns.ChoiceFieldColumn()
tenant = TenantColumn()
termination_a = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
verbose_name='Side A'
@@ -69,7 +68,7 @@ class CircuitTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Circuit
fields = (
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'tenant_group', 'termination_a', 'termination_z', 'install_date',
'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (

View File

@@ -30,7 +30,7 @@ class ProviderView(generic.ObjectView):
circuits = Circuit.objects.restrict(request.user, 'view').filter(
provider=instance
).prefetch_related(
'type', 'tenant', 'terminations__site'
'type', 'tenant', 'tenant__group', 'terminations__site'
)
circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
circuits_table.configure(request)
@@ -91,7 +91,7 @@ class ProviderNetworkView(generic.ObjectView):
Q(termination_a__provider_network=instance.pk) |
Q(termination_z__provider_network=instance.pk)
).prefetch_related(
'type', 'tenant', 'terminations__site'
'type', 'tenant', 'tenant__group', 'terminations__site'
)
circuits_table = tables.CircuitTable(circuits, user=request.user)
circuits_table.configure(request)
@@ -192,7 +192,7 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
class CircuitListView(generic.ObjectListView):
queryset = Circuit.objects.prefetch_related(
'provider', 'type', 'tenant', 'termination_a', 'termination_z'
'provider', 'type', 'tenant', 'tenant__group', 'termination_a', 'termination_z'
)
filterset = filtersets.CircuitFilterSet
filterset_form = forms.CircuitFilterForm

View File

@@ -5,6 +5,7 @@ from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
__all__ = [
'ComponentNestedModuleSerializer',
'ModuleBayNestedModuleSerializer',
'NestedCableSerializer',
'NestedConsolePortSerializer',
'NestedConsolePortTemplateSerializer',
@@ -281,6 +282,14 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name']
class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
class Meta:
model = models.Module
fields = ['id', 'url', 'display', 'serial']
class ComponentNestedModuleSerializer(WritableNestedSerializer):
"""
Used by device component serializers.

View File

@@ -886,12 +886,12 @@ class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
class ModuleBaySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
device = NestedDeviceSerializer()
# installed_module = NestedModuleSerializer(required=False, allow_null=True)
installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
class Meta:
model = ModuleBay
fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'position', 'description', 'tags', 'custom_fields',
'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags', 'custom_fields',
'created', 'last_updated',
]

View File

@@ -19,6 +19,7 @@ from ipam.models import Prefix, VLAN
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.exceptions import ServiceUnavailable
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import StripCountAnnotationsPaginator
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.config import get_config
from utilities.api import get_serializer_for_model
@@ -392,6 +393,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
)
filterset_class = filtersets.DeviceFilterSet
pagination_class = StripCountAnnotationsPaginator
def get_serializer_class(self):
"""
@@ -609,7 +611,7 @@ class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
class ModuleBayViewSet(NetBoxModelViewSet):
queryset = ModuleBay.objects.prefetch_related('tags')
queryset = ModuleBay.objects.prefetch_related('tags', 'installed_module')
serializer_class = serializers.ModuleBaySerializer
filterset_class = filtersets.ModuleBayFilterSet
brief_prefetch_fields = ['device']

View File

@@ -354,6 +354,7 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_UBIQUITI_SMARTPOWER = 'ubiquiti-smartpower'
# Other
TYPE_HARDWIRED = 'hardwired'
TYPE_OTHER = 'other'
CHOICES = (
('IEC 60320', (
@@ -471,6 +472,7 @@ class PowerPortTypeChoices(ChoiceSet):
)),
('Other', (
(TYPE_HARDWIRED, 'Hardwired'),
(TYPE_OTHER, 'Other'),
)),
)
@@ -580,6 +582,7 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_UBIQUITI_SMARTPOWER = 'ubiquiti-smartpower'
# Other
TYPE_HARDWIRED = 'hardwired'
TYPE_OTHER = 'other'
CHOICES = (
('IEC 60320', (
@@ -690,6 +693,7 @@ class PowerOutletTypeChoices(ChoiceSet):
)),
('Other', (
(TYPE_HARDWIRED, 'Hardwired'),
(TYPE_OTHER, 'Other'),
)),
)
@@ -1047,6 +1051,7 @@ class PortTypeChoices(ChoiceSet):
TYPE_URM_P2 = 'urm-p2'
TYPE_URM_P4 = 'urm-p4'
TYPE_URM_P8 = 'urm-p8'
TYPE_OTHER = 'other'
CHOICES = (
(
@@ -1099,6 +1104,12 @@ class PortTypeChoices(ChoiceSet):
(TYPE_URM_P4, 'URM-P4'),
(TYPE_URM_P8, 'URM-P8'),
(TYPE_SPLICE, 'Splice'),
),
),
(
'Other',
(
(TYPE_OTHER, 'Other'),
)
)
)

View File

@@ -49,15 +49,6 @@ WIRELESS_IFACE_TYPES = [
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
#
# Power feeds
#
POWERFEED_VOLTAGE_DEFAULT = 120
POWERFEED_AMPERAGE_DEFAULT = 20
POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
#
# Device components
#

View File

@@ -163,7 +163,7 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
qs_filter |= Q(asns__asn=int(value.strip()))
except ValueError:
pass
return queryset.filter(qs_filter)
return queryset.filter(qs_filter).distinct()
class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalModelFilterSet):
@@ -992,6 +992,12 @@ class ModuleFilterSet(NetBoxModelFilterSet):
to_field_name='model',
label='Module type (model)',
)
module_bay_id = django_filters.ModelMultipleChoiceFilter(
field_name='module_bay',
queryset=ModuleBay.objects.all(),
to_field_name='id',
label='Module Bay (ID)'
)
device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',

View File

@@ -6,7 +6,7 @@ from timezone_field import TimeZoneFormField
from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from ipam.models import ASN, VLAN, VRF
from ipam.models import ASN, VLAN, VLANGroup, VRF
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import (
@@ -1067,13 +1067,32 @@ class InterfaceBulkEditForm(
required=False,
widget=BulkEditNullBooleanSelect
)
mode = forms.ChoiceField(
choices=add_blank_choice(InterfaceModeChoices),
required=False,
initial='',
widget=StaticSelect()
)
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
label='VLAN group'
)
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False
required=False,
query_params={
'group_id': '$vlan_group',
},
label='Untagged VLAN'
)
tagged_vlans = DynamicModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False
required=False,
query_params={
'group_id': '$vlan_group',
},
label='Tagged VLANs'
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
@@ -1087,13 +1106,13 @@ class InterfaceBulkEditForm(
('Addressing', ('vrf', 'mac_address', 'wwn')),
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
('Related Interfaces', ('parent', 'bridge', 'lag')),
('802.1Q Switching', ('mode', 'untagged_vlan', 'tagged_vlans')),
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width')),
)
nullable_fields = (
'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description',
'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans',
'vrf',
'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'vlan_group', 'untagged_vlan',
'tagged_vlans', 'vrf',
)
def __init__(self, *args, **kwargs):

View File

@@ -108,7 +108,7 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Region
fieldsets = (
(None, ('q', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role'))
('Contacts', ('contact', 'contact_role', 'contact_group'))
)
parent_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -122,7 +122,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = SiteGroup
fieldsets = (
(None, ('q', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role'))
('Contacts', ('contact', 'contact_role', 'contact_group'))
)
parent_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
@@ -138,7 +138,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
(None, ('q', 'tag')),
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
status = MultipleChoiceField(
choices=SiteStatusChoices,
@@ -168,7 +168,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
(None, ('q', 'tag')),
('Parent', ('region_id', 'site_group_id', 'site_id', 'parent_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -214,7 +214,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
('Function', ('status', 'role_id')),
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -329,7 +329,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Manufacturer
fieldsets = (
(None, ('q', 'tag')),
('Contacts', ('contact', 'contact_role'))
('Contacts', ('contact', 'contact_role', 'contact_group'))
)
tag = TagFilterField(model)
@@ -518,7 +518,7 @@ class DeviceFilterForm(
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
)),
@@ -788,7 +788,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
fieldsets = (
(None, ('q', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Contacts', ('contact', 'contact_role')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@@ -1102,7 +1102,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
model = InventoryItem
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
)
role_id = DynamicModelMultipleChoiceField(

View File

@@ -521,13 +521,28 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
required=False,
label=''
)
virtual_chassis = DynamicModelChoiceField(
queryset=VirtualChassis.objects.all(),
required=False
)
vc_position = forms.IntegerField(
required=False,
label='Position',
help_text="The position in the virtual chassis this device is identified by"
)
vc_priority = forms.IntegerField(
required=False,
label='Priority',
help_text="The priority of the device in the virtual chassis"
)
class Meta:
model = Device
fields = [
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
'cluster_group', 'cluster', 'tenant_group', 'tenant', 'comments', 'tags', 'local_context_data'
'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority',
'comments', 'tags', 'local_context_data'
]
help_texts = {
'device_role': "The function this device serves",

View File

@@ -256,6 +256,8 @@ class VirtualChassisCreateForm(NetBoxModelForm):
]
def clean(self):
super().clean()
if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
raise forms.ValidationError({
'initial_position': "A position must be specified for the first VC member."

View File

@@ -386,9 +386,9 @@ class Migration(migrations.Migration):
('type', models.CharField(default='primary', max_length=50)),
('supply', models.CharField(default='ac', max_length=50)),
('phase', models.CharField(default='single-phase', max_length=50)),
('voltage', models.SmallIntegerField(default=120, validators=[utilities.validators.ExclusionValidator([0])])),
('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])),
('max_utilization', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
('voltage', models.SmallIntegerField(validators=[utilities.validators.ExclusionValidator([0])])),
('amperage', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1)])),
('max_utilization', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
('available_power', models.PositiveIntegerField(default=0, editable=False)),
('comments', models.TextField(blank=True)),
],

View File

@@ -95,8 +95,7 @@ class ModularComponentModel(ComponentModel):
inventory_items = GenericRelation(
to='dcim.InventoryItem',
content_type_field='component_type',
object_id_field='component_id',
related_name='%(class)ss',
object_id_field='component_id'
)
class Meta:

View File

@@ -748,8 +748,12 @@ class Device(NetBoxModel, ConfigContextModel):
return f'{self.name} ({self.asset_tag})'
elif self.name:
return self.name
elif self.virtual_chassis and self.asset_tag:
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.asset_tag})'
elif self.virtual_chassis:
return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})'
elif self.device_type and self.asset_tag:
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.asset_tag})'
elif self.device_type:
return f'{self.device_type.manufacturer} {self.device_type.model} ({self.pk})'
return super().__str__()

View File

@@ -6,6 +6,7 @@ from django.urls import reverse
from dcim.choices import *
from dcim.constants import *
from netbox.config import ConfigItem
from netbox.models import NetBoxModel
from utilities.validators import ExclusionValidator
from .device_components import LinkTermination, PathEndpoint
@@ -105,16 +106,16 @@ class PowerFeed(NetBoxModel, PathEndpoint, LinkTermination):
default=PowerFeedPhaseChoices.PHASE_SINGLE
)
voltage = models.SmallIntegerField(
default=POWERFEED_VOLTAGE_DEFAULT,
default=ConfigItem('POWERFEED_DEFAULT_VOLTAGE'),
validators=[ExclusionValidator([0])]
)
amperage = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1)],
default=POWERFEED_AMPERAGE_DEFAULT
default=ConfigItem('POWERFEED_DEFAULT_AMPERAGE')
)
max_utilization = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(100)],
default=POWERFEED_MAX_UTILIZATION_DEFAULT,
default=ConfigItem('POWERFEED_DEFAULT_MAX_UTILIZATION'),
help_text="Maximum permissible draw (percentage)"
)
available_power = models.PositiveIntegerField(

View File

@@ -114,7 +114,7 @@ class RackElevationSVG:
# Embed front device type image if one exists
if self.include_images and device.device_type.front_image:
image = drawing.image(
href=device.device_type.front_image.url,
href='{}{}'.format(self.base_url, device.device_type.front_image.url),
insert=start,
size=end,
class_='device-image'
@@ -140,7 +140,7 @@ class RackElevationSVG:
# Embed rear device type image if one exists
if self.include_images and device.device_type.rear_image:
image = drawing.image(
href=device.device_type.rear_image.url,
href='{}{}'.format(self.base_url, device.device_type.rear_image.url),
insert=start,
size=end,
class_='device-image'
@@ -151,9 +151,9 @@ class RackElevationSVG:
stroke_width='0.2em', stroke_linejoin='round', class_='device-image-label'))
link.add(drawing.text(get_device_name(device), insert=text, fill='white', class_='device-image-label'))
@staticmethod
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
link_url = '{}?{}'.format(
def _draw_empty(self, drawing, rack, start, end, text, id_, face_id, class_, reservation):
link_url = '{}{}?{}'.format(
self.base_url,
reverse('dcim:device_add'),
urlencode({
'site': rack.site.pk,

View File

@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
from dcim.models import Cable
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn
from tenancy.tables import TenancyColumnsMixin
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
__all__ = (
@@ -15,7 +15,7 @@ __all__ = (
# Cables
#
class CableTable(NetBoxTable):
class CableTable(TenancyColumnsMixin, NetBoxTable):
termination_a_parent = tables.TemplateColumn(
template_code=CABLE_TERMINATION_PARENT,
accessor=Accessor('termination_a'),
@@ -53,7 +53,6 @@ class CableTable(NetBoxTable):
verbose_name='Termination B'
)
status = columns.ChoiceFieldColumn()
tenant = TenantColumn()
length = columns.TemplateColumn(
template_code=CABLE_LENGTH,
order_by=('_abs_length', 'length_unit')
@@ -67,7 +66,7 @@ class CableTable(NetBoxTable):
model = Cable
fields = (
'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b',
'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated',
'status', 'type', 'tenant', 'tenant_group', 'color', 'length', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',

View File

@@ -6,7 +6,7 @@ from dcim.models import (
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
)
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn
from tenancy.tables import TenancyColumnsMixin
from .template_code import *
__all__ = (
@@ -137,13 +137,12 @@ class PlatformTable(NetBoxTable):
# Devices
#
class DeviceTable(NetBoxTable):
class DeviceTable(TenancyColumnsMixin, NetBoxTable):
name = tables.TemplateColumn(
order_by=('_name',),
template_code=DEVICE_LINK
)
status = columns.ChoiceFieldColumn()
tenant = TenantColumn()
site = tables.Column(
linkify=True
)
@@ -200,7 +199,7 @@ class DeviceTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Device
fields = (
'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
'created', 'last_updated',
@@ -211,12 +210,11 @@ class DeviceTable(NetBoxTable):
)
class DeviceImportTable(NetBoxTable):
class DeviceImportTable(TenancyColumnsMixin, NetBoxTable):
name = tables.TemplateColumn(
template_code=DEVICE_LINK
)
status = columns.ChoiceFieldColumn()
tenant = TenantColumn()
site = tables.Column(
linkify=True
)
@@ -232,7 +230,7 @@ class DeviceImportTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Device
fields = ('id', 'name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
fields = ('id', 'name', 'status', 'tenant', 'tenant_group', 'site', 'rack', 'position', 'device_role', 'device_type')
empty_text = False

View File

@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
from dcim.models import Rack, RackReservation, RackRole
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn
from tenancy.tables import TenancyColumnsMixin
__all__ = (
'RackTable',
@@ -37,7 +37,7 @@ class RackRoleTable(NetBoxTable):
# Racks
#
class RackTable(NetBoxTable):
class RackTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column(
order_by=('_name',),
linkify=True
@@ -48,7 +48,6 @@ class RackTable(NetBoxTable):
site = tables.Column(
linkify=True
)
tenant = TenantColumn()
status = columns.ChoiceFieldColumn()
role = columns.ColoredLabelColumn()
u_height = tables.TemplateColumn(
@@ -87,7 +86,7 @@ class RackTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Rack
fields = (
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag',
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial', 'asset_tag',
'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
)
@@ -101,7 +100,7 @@ class RackTable(NetBoxTable):
# Rack reservations
#
class RackReservationTable(NetBoxTable):
class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
reservation = tables.Column(
accessor='pk',
linkify=True
@@ -110,7 +109,6 @@ class RackReservationTable(NetBoxTable):
accessor=Accessor('rack__site'),
linkify=True
)
tenant = TenantColumn()
rack = tables.Column(
linkify=True
)
@@ -125,7 +123,7 @@ class RackReservationTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = RackReservation
fields = (
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')

View File

@@ -2,7 +2,7 @@ import django_tables2 as tables
from dcim.models import Location, Region, Site, SiteGroup
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn
from tenancy.tables import TenancyColumnsMixin
from .template_code import LOCATION_BUTTONS
__all__ = (
@@ -75,7 +75,7 @@ class SiteGroupTable(NetBoxTable):
# Sites
#
class SiteTable(NetBoxTable):
class SiteTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
@@ -96,7 +96,6 @@ class SiteTable(NetBoxTable):
url_params={'site_id': 'pk'},
verbose_name='ASN Count'
)
tenant = TenantColumn()
comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
@@ -108,7 +107,7 @@ class SiteTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Site
fields = (
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'tenant_group', 'asns', 'asn_count',
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
'contacts', 'tags', 'created', 'last_updated', 'actions',
)
@@ -119,14 +118,13 @@ class SiteTable(NetBoxTable):
# Locations
#
class LocationTable(NetBoxTable):
class LocationTable(TenancyColumnsMixin, NetBoxTable):
name = columns.MPTTColumn(
linkify=True
)
site = tables.Column(
linkify=True
)
tenant = TenantColumn()
rack_count = columns.LinkedCountColumn(
viewname='dcim:rack_list',
url_params={'location_id': 'pk'},
@@ -150,7 +148,7 @@ class LocationTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Location
fields = (
'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
'pk', 'id', 'name', 'site', 'tenant', 'tenant_group', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
'tags', 'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')

View File

@@ -385,7 +385,7 @@ MODULEBAY_BUTTONS = """
<i class="mdi mdi-server-minus" aria-hidden="true" title="Remove module"></i>
</a>
{% else %}
<a href="{% url 'dcim:module_add' %}?device={{ record.device.pk }}&module_bay={{ record.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-success btn-sm">
<a href="{% url 'dcim:module_add' %}?device={{ record.device.pk }}&module_bay={{ record.pk }}&manufacturer={{ object.device_type.manufacturer_id }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-success btn-sm">
<i class="mdi mdi-server-plus" aria-hidden="true" title="Install module"></i>
</a>
{% endif %}

View File

@@ -1849,6 +1849,11 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'module_type': [module_types[0].model, module_types[1].model]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_module_bay(self):
module_bays = ModuleBay.objects.all()[:2]
params = {'module_bay_id': [module_bays[0].pk, module_bays[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device(self):
device_types = Device.objects.all()[:2]
params = {'device_id': [device_types[0].pk, device_types[1].pk]}

View File

@@ -510,8 +510,8 @@ class RackRoleView(generic.ObjectView):
queryset = RackRole.objects.all()
def get_extra_context(self, request, instance):
racks = Rack.objects.restrict(request.user, 'view').filter(
role=instance
racks = Rack.objects.restrict(request.user, 'view').filter(role=instance).annotate(
device_count=count_related(Device, 'rack')
)
racks_table = tables.RackTable(racks, user=request.user, exclude=(
@@ -561,7 +561,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
class RackListView(generic.ObjectListView):
queryset = Rack.objects.prefetch_related(
'site', 'location', 'tenant', 'role', 'devices__device_type'
'site', 'location', 'tenant', 'tenant_group', 'role', 'devices__device_type'
).annotate(
device_count=count_related(Device, 'rack')
)

View File

@@ -15,6 +15,9 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
('Rack Elevations', {
'fields': ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH'),
}),
('Power', {
'fields': ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')
}),
('IPAM', {
'fields': ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4'),
}),

View File

@@ -221,7 +221,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
model = JournalEntry
fields = [
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
'created_by', 'kind', 'comments', 'tags', 'custom_fields',
'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
]
def validate(self, data):

View File

@@ -32,6 +32,9 @@ class WebhookFilterSet(BaseFilterSet):
method='search',
label='Search',
)
content_type_id = MultiValueNumberFilter(
field_name='content_types__id'
)
content_types = ContentTypeFilter()
http_method = django_filters.MultipleChoiceFilter(
choices=WebhookHttpMethodChoices
@@ -40,8 +43,8 @@ class WebhookFilterSet(BaseFilterSet):
class Meta:
model = Webhook
fields = [
'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled',
'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
'id', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', 'http_method',
'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
]
def search(self, queryset, name, value):
@@ -58,11 +61,17 @@ class CustomFieldFilterSet(BaseFilterSet):
method='search',
label='Search',
)
type = django_filters.MultipleChoiceFilter(
choices=CustomFieldTypeChoices
)
content_type_id = MultiValueNumberFilter(
field_name='content_types__id'
)
content_types = ContentTypeFilter()
class Meta:
model = CustomField
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight', 'description']
fields = ['id', 'name', 'required', 'filter_logic', 'weight', 'description']
def search(self, queryset, name, value):
if not value.strip():

View File

@@ -27,6 +27,12 @@ class CustomFieldCSVForm(CSVModelForm):
choices=CustomFieldTypeChoices,
help_text='Field data type (e.g. text, integer, etc.)'
)
object_type = CSVContentTypeField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
required=False,
help_text="Object type (for object or multi-object fields)"
)
choices = SimpleArrayField(
base_field=forms.CharField(),
required=False,
@@ -36,8 +42,9 @@ class CustomFieldCSVForm(CSVModelForm):
class Meta:
model = CustomField
fields = (
'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default',
'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
'name', 'label', 'type', 'content_types', 'object_type', 'required', 'description', 'weight',
'filter_logic', 'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum',
'validation_regex',
)

View File

@@ -32,12 +32,13 @@ __all__ = (
class CustomFieldFilterForm(FilterForm):
fieldsets = (
(None, ('q',)),
('Attributes', ('type', 'content_types', 'weight', 'required')),
('Attributes', ('type', 'content_type_id', 'weight', 'required')),
)
content_types = ContentTypeMultipleChoiceField(
content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
required=False
required=False,
label='Object type'
)
type = MultipleChoiceField(
choices=CustomFieldTypeChoices,
@@ -110,13 +111,14 @@ class ExportTemplateFilterForm(FilterForm):
class WebhookFilterForm(FilterForm):
fieldsets = (
(None, ('q',)),
('Attributes', ('content_types', 'http_method', 'enabled')),
('Attributes', ('content_type_id', 'http_method', 'enabled')),
('Events', ('type_create', 'type_update', 'type_delete')),
)
content_types = ContentTypeMultipleChoiceField(
content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('webhooks'),
required=False
required=False,
label='Object type'
)
http_method = MultipleChoiceField(
choices=WebhookHttpMethodChoices,

View File

@@ -14,6 +14,7 @@ from extras.choices import JobResultStatusChoices
from extras.context_managers import change_logging
from extras.models import JobResult
from extras.scripts import get_script
from extras.signals import clear_webhooks
from utilities.exceptions import AbortTransaction
from utilities.utils import NetBoxFakeRequest
@@ -49,7 +50,7 @@ class Command(BaseCommand):
except AbortTransaction:
script.log_info("Database changes have been reverted automatically.")
clear_webhooks.send(request)
except Exception as e:
stacktrace = traceback.format_exc()
script.log_failure(
@@ -58,7 +59,7 @@ class Command(BaseCommand):
script.log_info("Database changes have been reverted due to error.")
logger.error(f"Exception raised during script execution: {e}")
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
clear_webhooks.send(request)
finally:
job_result.data = ScriptOutputSerializer(script).data
job_result.save()

View File

@@ -365,13 +365,8 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
# Text
else:
if self.type == CustomFieldTypeChoices.TYPE_LONGTEXT:
max_length = None
widget = forms.Textarea
else:
max_length = 255
widget = None
field = forms.CharField(max_length=max_length, required=required, initial=initial, widget=widget)
widget = forms.Textarea if self.type == CustomFieldTypeChoices.TYPE_LONGTEXT else None
field = forms.CharField(required=required, initial=initial, widget=widget)
if self.validation_regex:
field.validators = [
RegexValidator(

View File

@@ -17,6 +17,7 @@ from django.utils.functional import classproperty
from extras.api.serializers import ScriptOutputSerializer
from extras.choices import JobResultStatusChoices, LogLevelChoices
from extras.signals import clear_webhooks
from ipam.formfields import IPAddressFormField, IPNetworkFormField
from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
from utilities.exceptions import AbortTransaction
@@ -306,9 +307,16 @@ class BaseScript:
@classmethod
def _get_vars(cls):
vars = {}
for name, attr in cls.__dict__.items():
if name not in vars and issubclass(attr.__class__, ScriptVariable):
vars[name] = attr
# Iterate all base classes looking for ScriptVariables
for base_class in inspect.getmro(cls):
# When object is reached there's no reason to continue
if base_class is object:
break
for name, attr in base_class.__dict__.items():
if name not in vars and issubclass(attr.__class__, ScriptVariable):
vars[name] = attr
# Order variables according to field_order
field_order = getattr(cls.Meta, 'field_order', None)
@@ -458,7 +466,7 @@ def run_script(data, request, commit=True, *args, **kwargs):
except AbortTransaction:
script.log_info("Database changes have been reverted automatically.")
clear_webhooks.send(request)
except Exception as e:
stacktrace = traceback.format_exc()
script.log_failure(
@@ -467,7 +475,7 @@ def run_script(data, request, commit=True, *args, **kwargs):
script.log_info("Database changes have been reverted due to error.")
logger.error(f"Exception raised during script execution: {e}")
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
clear_webhooks.send(request)
finally:
job_result.data = ScriptOutputSerializer(script).data
job_result.save()

View File

@@ -7,7 +7,9 @@ from django.test import TestCase
from circuits.models import Provider
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
from extras.choices import (
CustomFieldTypeChoices, CustomFieldFilterLogicChoices, JournalEntryKindChoices, ObjectChangeActionChoices,
)
from extras.filtersets import *
from extras.models import *
from ipam.models import IPAddress
@@ -16,6 +18,65 @@ from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests, cr
from virtualization.models import Cluster, ClusterGroup, ClusterType
class CustomFieldTestCase(TestCase, BaseFilterSetTests):
queryset = CustomField.objects.all()
filterset = CustomFieldFilterSet
@classmethod
def setUpTestData(cls):
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
custom_fields = (
CustomField(
name='Custom Field 1',
type=CustomFieldTypeChoices.TYPE_TEXT,
required=True,
weight=100,
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
),
CustomField(
name='Custom Field 2',
type=CustomFieldTypeChoices.TYPE_INTEGER,
required=False,
weight=200,
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
),
CustomField(
name='Custom Field 3',
type=CustomFieldTypeChoices.TYPE_BOOLEAN,
required=False,
weight=300,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
),
)
CustomField.objects.bulk_create(custom_fields)
custom_fields[0].content_types.add(content_types[0])
custom_fields[1].content_types.add(content_types[1])
custom_fields[2].content_types.add(content_types[2])
def test_name(self):
params = {'name': ['Custom Field 1', 'Custom Field 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_types(self):
params = {'content_types': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_required(self):
params = {'required': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_weight(self):
params = {'weight': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_filter_logic(self):
params = {'filter_logic': CustomFieldFilterLogicChoices.FILTER_LOOSE}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class WebhookTestCase(TestCase, BaseFilterSetTests):
queryset = Webhook.objects.all()
filterset = WebhookFilterSet
@@ -62,6 +123,8 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
def test_content_types(self):
params = {'content_types': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_type_create(self):
params = {'type_create': True}

View File

@@ -39,10 +39,11 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
'name,label,type,content_types,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex',
'field4,Field 4,text,dcim.site,100,exact,,,,[a-z]{3}',
'field5,Field 5,integer,dcim.site,100,exact,,1,100,',
'field6,Field 6,select,dcim.site,100,exact,"A,B,C",,,',
'name,label,type,content_types,object_type,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex',
'field4,Field 4,text,dcim.site,,100,exact,,,,[a-z]{3}',
'field5,Field 5,integer,dcim.site,,100,exact,,1,100,',
'field6,Field 6,select,dcim.site,,100,exact,"A,B,C",,,',
'field7,Field 7,object,dcim.site,dcim.region,100,exact,,,,',
)
cls.bulk_edit_data = {

View File

@@ -145,9 +145,11 @@ class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
if not value.strip():
return queryset
qs_filter = Q(description__icontains=value)
qs_filter |= Q(prefix__contains=value.strip())
try:
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
qs_filter |= Q(prefix__contains=value.strip())
except (AddrFormatError, ValueError):
pass
return queryset.filter(qs_filter)
@@ -334,9 +336,11 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
if not value.strip():
return queryset
qs_filter = Q(description__icontains=value)
qs_filter |= Q(prefix__contains=value.strip())
try:
prefix = str(netaddr.IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
qs_filter |= Q(prefix__contains=value.strip())
except (AddrFormatError, ValueError):
pass
return queryset.filter(qs_filter)
@@ -460,7 +464,7 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
field_name='address',
lookup_expr='family'
)
parent = django_filters.CharFilter(
parent = MultiValueCharFilter(
method='search_by_parent',
label='Parent prefix',
)
@@ -567,14 +571,16 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
return queryset.filter(qs_filter)
def search_by_parent(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
try:
query = str(netaddr.IPNetwork(value.strip()).cidr)
return queryset.filter(address__net_host_contained=query)
except (AddrFormatError, ValueError):
return queryset.none()
q = Q()
for prefix in value:
try:
query = str(netaddr.IPNetwork(prefix.strip()).cidr)
q |= Q(address__net_host_contained=query)
except (AddrFormatError, ValueError):
return queryset.none()
return queryset.filter(q)
def filter_address(self, queryset, name, value):
try:

View File

@@ -1,7 +1,8 @@
from django import forms
from django.utils.translation import gettext as _
from dcim.models import Location, Rack, Region, Site, SiteGroup
from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
from virtualization.models import VirtualMachine
from ipam.choices import *
from ipam.constants import *
from ipam.models import *
@@ -265,6 +266,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
('VRF', ('vrf_id', 'present_in_vrf_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Device/VM', ('device_id', 'virtual_machine_id')),
)
parent = forms.CharField(
required=False,
@@ -298,6 +300,16 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
required=False,
label=_('Present in VRF')
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
label=_('Assigned Device'),
)
virtual_machine_id = DynamicModelMultipleChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
label=_('Assigned VM'),
)
status = MultipleChoiceField(
choices=IPAddressStatusChoices,
required=False

View File

@@ -4,7 +4,7 @@ from django_tables2.utils import Accessor
from ipam.models import *
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn
from tenancy.tables import TenancyColumnsMixin, TenantColumn
__all__ = (
'AggregateTable',
@@ -99,7 +99,7 @@ class RIRTable(NetBoxTable):
# ASNs
#
class ASNTable(NetBoxTable):
class ASNTable(TenancyColumnsMixin, NetBoxTable):
asn = tables.Column(
linkify=True
)
@@ -122,7 +122,6 @@ class ASNTable(NetBoxTable):
linkify_item=True,
verbose_name='Sites'
)
tenant = TenantColumn()
tags = columns.TagColumn(
url_name='ipam:asn_list'
)
@@ -130,7 +129,7 @@ class ASNTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = ASN
fields = (
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'description', 'sites', 'tags',
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description', 'sites', 'tags',
'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant')
@@ -140,12 +139,11 @@ class ASNTable(NetBoxTable):
# Aggregates
#
class AggregateTable(NetBoxTable):
class AggregateTable(TenancyColumnsMixin, NetBoxTable):
prefix = tables.Column(
linkify=True,
verbose_name='Aggregate'
)
tenant = TenantColumn()
date_added = tables.DateColumn(
format="Y-m-d",
verbose_name='Added'
@@ -164,7 +162,7 @@ class AggregateTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Aggregate
fields = (
'pk', 'id', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags',
'pk', 'id', 'prefix', 'rir', 'tenant', 'tenant_group', 'child_count', 'utilization', 'date_added', 'description', 'tags',
'created', 'last_updated',
)
default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description')
@@ -225,9 +223,10 @@ class PrefixUtilizationColumn(columns.UtilizationColumn):
"""
class PrefixTable(NetBoxTable):
prefix = tables.TemplateColumn(
class PrefixTable(TenancyColumnsMixin, NetBoxTable):
prefix = columns.TemplateColumn(
template_code=PREFIX_LINK,
export_raw=True,
attrs={'td': {'class': 'text-nowrap'}}
)
prefix_flat = tables.TemplateColumn(
@@ -255,7 +254,6 @@ class PrefixTable(NetBoxTable):
template_code=VRF_LINK,
verbose_name='VRF'
)
tenant = TenantColumn()
site = tables.Column(
linkify=True
)
@@ -288,7 +286,7 @@ class PrefixTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Prefix
fields = (
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site',
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group', 'site',
'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', 'created', 'last_updated',
)
default_columns = (
@@ -302,7 +300,7 @@ class PrefixTable(NetBoxTable):
#
# IP ranges
#
class IPRangeTable(NetBoxTable):
class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
start_address = tables.Column(
linkify=True
)
@@ -316,7 +314,6 @@ class IPRangeTable(NetBoxTable):
role = tables.Column(
linkify=True
)
tenant = TenantColumn()
utilization = columns.UtilizationColumn(
accessor='utilization',
orderable=False
@@ -328,7 +325,7 @@ class IPRangeTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = IPRange
fields = (
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'description',
'utilization', 'tags', 'created', 'last_updated',
)
default_columns = (
@@ -343,7 +340,7 @@ class IPRangeTable(NetBoxTable):
# IPAddresses
#
class IPAddressTable(NetBoxTable):
class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
address = tables.TemplateColumn(
template_code=IPADDRESS_LINK,
verbose_name='IP Address'
@@ -356,7 +353,6 @@ class IPAddressTable(NetBoxTable):
default=AVAILABLE_LABEL
)
role = columns.ChoiceFieldColumn()
tenant = TenantColumn()
assigned_object = tables.Column(
linkify=True,
orderable=False,
@@ -385,7 +381,7 @@ class IPAddressTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = IPAddress
fields = (
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name', 'description',
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'assigned', 'dns_name', 'description',
'tags', 'created', 'last_updated',
)
default_columns = (

View File

@@ -14,7 +14,8 @@ class ServiceTemplateTable(NetBoxTable):
linkify=True
)
ports = tables.Column(
accessor=tables.A('port_list')
accessor=tables.A('port_list'),
order_by=tables.A('ports'),
)
tags = columns.TagColumn(
url_name='ipam:servicetemplate_list'
@@ -35,7 +36,8 @@ class ServiceTable(NetBoxTable):
order_by=('device', 'virtual_machine')
)
ports = tables.Column(
accessor=tables.A('port_list')
accessor=tables.A('port_list'),
order_by=tables.A('ports'),
)
tags = columns.TagColumn(
url_name='ipam:service_list'

View File

@@ -5,7 +5,7 @@ from django_tables2.utils import Accessor
from dcim.models import Interface
from ipam.models import *
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn
from tenancy.tables import TenancyColumnsMixin, TenantColumn
from virtualization.models import VMInterface
__all__ = (
@@ -90,7 +90,7 @@ class VLANGroupTable(NetBoxTable):
# VLANs
#
class VLANTable(NetBoxTable):
class VLANTable(TenancyColumnsMixin, NetBoxTable):
vid = tables.TemplateColumn(
template_code=VLAN_LINK,
verbose_name='VID'
@@ -104,7 +104,6 @@ class VLANTable(NetBoxTable):
group = tables.Column(
linkify=True
)
tenant = TenantColumn()
status = columns.ChoiceFieldColumn(
default=AVAILABLE_LABEL
)
@@ -123,7 +122,7 @@ class VLANTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = VLAN
fields = (
'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags',
'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role', 'description', 'tags',
'created', 'last_updated',
)
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')

View File

@@ -2,7 +2,7 @@ import django_tables2 as tables
from ipam.models import *
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenantColumn
from tenancy.tables import TenancyColumnsMixin
__all__ = (
'RouteTargetTable',
@@ -20,14 +20,13 @@ VRF_TARGETS = """
# VRFs
#
class VRFTable(NetBoxTable):
class VRFTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
rd = tables.Column(
verbose_name='RD'
)
tenant = TenantColumn()
enforce_unique = columns.BooleanColumn(
verbose_name='Unique'
)
@@ -46,7 +45,7 @@ class VRFTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = VRF
fields = (
'pk', 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets',
'pk', 'id', 'name', 'rd', 'tenant', 'tenant_group', 'enforce_unique', 'description', 'import_targets', 'export_targets',
'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
@@ -56,16 +55,15 @@ class VRFTable(NetBoxTable):
# Route targets
#
class RouteTargetTable(NetBoxTable):
class RouteTargetTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
tenant = TenantColumn()
tags = columns.TagColumn(
url_name='ipam:vrf_list'
)
class Meta(NetBoxTable.Meta):
model = RouteTarget
fields = ('pk', 'id', 'name', 'tenant', 'description', 'tags', 'created', 'last_updated',)
fields = ('pk', 'id', 'name', 'tenant', 'tenant_group', 'description', 'tags', 'created', 'last_updated',)
default_columns = ('pk', 'name', 'tenant', 'description')

View File

@@ -823,10 +823,8 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self):
params = {'parent': '10.0.0.0/24'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'parent': '2001:db8::/64'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'parent': ['10.0.0.0/30', '2001:db8::/126']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
def test_filter_address(self):
# Check IPv4 and IPv6, with and without a mask

View File

@@ -4,15 +4,15 @@ from django.db.models.expressions import RawSQL
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from circuits.models import Provider
from circuits.models import Provider, Circuit
from circuits.tables import ProviderTable
from dcim.filtersets import InterfaceFilterSet
from dcim.models import Interface, Site
from dcim.models import Interface, Site, Device
from dcim.tables import SiteTable
from netbox.views import generic
from utilities.utils import count_related
from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.models import VMInterface
from virtualization.models import VMInterface, VirtualMachine
from . import filtersets, forms, tables
from .constants import *
from .models import *
@@ -225,7 +225,9 @@ class ASNView(generic.ObjectView):
sites_table.configure(request)
# Gather assigned Providers
providers = instance.providers.restrict(request.user, 'view')
providers = instance.providers.restrict(request.user, 'view').annotate(
count_circuits=count_related(Circuit, 'provider')
)
providers_table = ProviderTable(providers, user=request.user)
providers_table.configure(request)
@@ -296,7 +298,7 @@ class AggregatePrefixesView(generic.ObjectChildrenView):
def get_children(self, request, parent):
return Prefix.objects.restrict(request.user, 'view').filter(
prefix__net_contained_or_equal=str(parent.prefix)
).prefetch_related('site', 'role', 'tenant', 'vlan')
).prefetch_related('site', 'role', 'tenant', 'tenant__group', 'vlan')
def prep_table_data(self, request, queryset, parent):
# Determine whether to show assigned prefixes, available prefixes, or both
@@ -468,7 +470,7 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
def get_children(self, request, parent):
return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
'site', 'vrf', 'vlan', 'role', 'tenant',
'site', 'vrf', 'vlan', 'role', 'tenant', 'tenant__group'
)
def prep_table_data(self, request, queryset, parent):
@@ -497,7 +499,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
def get_children(self, request, parent):
return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related(
'vrf', 'role', 'tenant',
'vrf', 'role', 'tenant', 'tenant__group',
)
def get_extra_context(self, request, instance):
@@ -585,7 +587,7 @@ class IPRangeIPAddressesView(generic.ObjectChildrenView):
def get_children(self, request, parent):
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related(
'vrf', 'role', 'tenant',
'vrf', 'role', 'tenant', 'tenant__group',
)
def get_extra_context(self, request, instance):
@@ -674,11 +676,29 @@ class IPAddressView(generic.ObjectView):
related_ips_table = tables.IPAddressTable(related_ips, orderable=False)
related_ips_table.configure(request)
# Find services belonging to the IP
service_filter = Q(ipaddresses=instance)
# Find services listening on all IPs on the assigned device/vm
try:
if instance.assigned_object and instance.assigned_object.parent_object:
parent_object = instance.assigned_object.parent_object
if isinstance(parent_object, VirtualMachine):
service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None))
elif isinstance(parent_object, Device):
service_filter |= (Q(device=parent_object) & Q(ipaddresses=None))
except AttributeError:
pass
services = Service.objects.restrict(request.user, 'view').filter(service_filter)
return {
'parent_prefixes_table': parent_prefixes_table,
'duplicate_ips_table': duplicate_ips_table,
'more_duplicate_ips': duplicate_ips.count() > 10,
'related_ips_table': related_ips_table,
'services': services,
}

View File

@@ -16,7 +16,7 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
def paginate_queryset(self, queryset, request, view=None):
if isinstance(queryset, QuerySet):
self.count = queryset.count()
self.count = self.get_queryset_count(queryset)
else:
# We're dealing with an iterable, not a QuerySet
self.count = len(queryset)
@@ -52,6 +52,9 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
return self.default_limit
def get_queryset_count(self, queryset):
return queryset.count()
def get_next_link(self):
# Pagination has been disabled
@@ -67,3 +70,16 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
return None
return super().get_previous_link()
class StripCountAnnotationsPaginator(OptionalLimitOffsetPagination):
"""
Strips the annotations on the queryset before getting the count
to optimize pagination of complex queries.
"""
def get_queryset_count(self, queryset):
# Clone the queryset to avoid messing up the actual query
cloned_queryset = queryset.all()
cloned_queryset.query.annotations.clear()
return cloned_queryset.count()

View File

@@ -348,3 +348,26 @@ class LDAPBackend:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
return obj
# Custom Social Auth Pipeline Handlers
def user_default_groups_handler(backend, user, response, *args, **kwargs):
"""
Custom pipeline handler which adds remote auth users to the default group specified in the
configuration file.
"""
logger = logging.getLogger('netbox.auth.user_default_groups_handler')
if settings.REMOTE_AUTH_DEFAULT_GROUPS:
# Assign default groups to the user
group_list = []
for name in settings.REMOTE_AUTH_DEFAULT_GROUPS:
try:
group_list.append(Group.objects.get(name=name))
except Group.DoesNotExist:
logging.error(
f"Could not assign group {name} to remotely-authenticated user {user}: Group not found")
if group_list:
user.groups.add(*group_list)
else:
user.groups.clear()
logger.debug(f"Stripping user {user} from Groups")

View File

@@ -82,6 +82,31 @@ PARAMS = (
field=forms.IntegerField
),
# Power
ConfigParam(
name='POWERFEED_DEFAULT_VOLTAGE',
label='Powerfeed voltage',
default=120,
description="Default voltage for powerfeeds",
field=forms.IntegerField
),
ConfigParam(
name='POWERFEED_DEFAULT_AMPERAGE',
label='Powerfeed amperage',
default=15,
description="Default amperage for powerfeeds",
field=forms.IntegerField
),
ConfigParam(
name='POWERFEED_DEFAULT_MAX_UTILIZATION',
label='Powerfeed max utilization',
default=80,
description="Default max utilization for powerfeeds",
field=forms.IntegerField
),
# Security
ConfigParam(
name='ALLOWED_URL_SCHEMES',

View File

@@ -202,6 +202,9 @@ RQ_DEFAULT_TIMEOUT = 300
# this setting is derived from the installed location.
# SCRIPTS_ROOT = '/opt/netbox/netbox/scripts'
# The name to use for the csrf token cookie.
CSRF_COOKIE_NAME = 'csrftoken'
# The name to use for the session cookie.
SESSION_COOKIE_NAME = 'sessionid'

View File

@@ -36,3 +36,8 @@ REDIS = {
}
SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
LOGGING = {
'version': 1,
'disable_existing_loggers': True
}

View File

@@ -1,32 +1,24 @@
from collections import OrderedDict
from typing import Dict
from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
import circuits.filtersets
import circuits.tables
import dcim.filtersets
import dcim.tables
import ipam.filtersets
import ipam.tables
import tenancy.filtersets
import tenancy.tables
import virtualization.filtersets
import virtualization.tables
from circuits.models import Circuit, ProviderNetwork, Provider
from circuits.tables import CircuitTable, ProviderNetworkTable, ProviderTable
from dcim.filtersets import (
CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, LocationFilterSet, ModuleFilterSet, ModuleTypeFilterSet,
PowerFeedFilterSet, RackFilterSet, RackReservationFilterSet, SiteFilterSet, VirtualChassisFilterSet,
)
from dcim.models import (
Cable, Device, DeviceType, Location, Module, ModuleType, PowerFeed, Rack, RackReservation, Site, VirtualChassis,
)
from dcim.tables import (
CableTable, DeviceTable, DeviceTypeTable, LocationTable, ModuleTable, ModuleTypeTable, PowerFeedTable, RackTable,
RackReservationTable, SiteTable, VirtualChassisTable,
)
from ipam.filtersets import (
AggregateFilterSet, ASNFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet,
)
from ipam.models import Aggregate, ASN, IPAddress, Prefix, VLAN, VRF
from ipam.tables import AggregateTable, ASNTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
from tenancy.filtersets import ContactFilterSet, TenantFilterSet
from ipam.models import Aggregate, ASN, IPAddress, Prefix, Service, VLAN, VRF
from tenancy.models import Contact, Tenant, ContactAssignment
from tenancy.tables import ContactTable, TenantTable
from utilities.utils import count_related
from virtualization.filtersets import ClusterFilterSet, VirtualMachineFilterSet
from virtualization.models import Cluster, VirtualMachine
from virtualization.tables import ClusterTable, VirtualMachineTable
SEARCH_MAX_RESULTS = 15
@@ -36,22 +28,22 @@ CIRCUIT_TYPES = OrderedDict(
'queryset': Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider')
),
'filterset': ProviderFilterSet,
'table': ProviderTable,
'filterset': circuits.filtersets.ProviderFilterSet,
'table': circuits.tables.ProviderTable,
'url': 'circuits:provider_list',
}),
('circuit', {
'queryset': Circuit.objects.prefetch_related(
'type', 'provider', 'tenant', 'terminations__site'
'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
),
'filterset': CircuitFilterSet,
'table': CircuitTable,
'filterset': circuits.filtersets.CircuitFilterSet,
'table': circuits.tables.CircuitTable,
'url': 'circuits:circuit_list',
}),
('providernetwork', {
'queryset': ProviderNetwork.objects.prefetch_related('provider'),
'filterset': ProviderNetworkFilterSet,
'table': ProviderNetworkTable,
'filterset': circuits.filtersets.ProviderNetworkFilterSet,
'table': circuits.tables.ProviderNetworkTable,
'url': 'circuits:providernetwork_list',
}),
)
@@ -61,23 +53,23 @@ CIRCUIT_TYPES = OrderedDict(
DCIM_TYPES = OrderedDict(
(
('site', {
'queryset': Site.objects.prefetch_related('region', 'tenant'),
'filterset': SiteFilterSet,
'table': SiteTable,
'queryset': Site.objects.prefetch_related('region', 'tenant', 'tenant__group'),
'filterset': dcim.filtersets.SiteFilterSet,
'table': dcim.tables.SiteTable,
'url': 'dcim:site_list',
}),
('rack', {
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role').annotate(
'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
device_count=count_related(Device, 'rack')
),
'filterset': RackFilterSet,
'table': RackTable,
'filterset': dcim.filtersets.RackFilterSet,
'table': dcim.tables.RackTable,
'url': 'dcim:rack_list',
}),
('rackreservation', {
'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
'filterset': RackReservationFilterSet,
'table': RackReservationTable,
'filterset': dcim.filtersets.RackReservationFilterSet,
'table': dcim.tables.RackReservationTable,
'url': 'dcim:rackreservation_list',
}),
('location', {
@@ -94,60 +86,60 @@ DCIM_TYPES = OrderedDict(
'rack_count',
cumulative=True
).prefetch_related('site'),
'filterset': LocationFilterSet,
'table': LocationTable,
'filterset': dcim.filtersets.LocationFilterSet,
'table': dcim.tables.LocationTable,
'url': 'dcim:location_list',
}),
('devicetype', {
'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
instance_count=count_related(Device, 'device_type')
),
'filterset': DeviceTypeFilterSet,
'table': DeviceTypeTable,
'filterset': dcim.filtersets.DeviceTypeFilterSet,
'table': dcim.tables.DeviceTypeTable,
'url': 'dcim:devicetype_list',
}),
('device', {
'queryset': Device.objects.prefetch_related(
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
'device_type__manufacturer', 'device_role', 'tenant', 'tenant__group', 'site', 'rack', 'primary_ip4', 'primary_ip6',
),
'filterset': DeviceFilterSet,
'table': DeviceTable,
'filterset': dcim.filtersets.DeviceFilterSet,
'table': dcim.tables.DeviceTable,
'url': 'dcim:device_list',
}),
('moduletype', {
'queryset': ModuleType.objects.prefetch_related('manufacturer').annotate(
instance_count=count_related(Module, 'module_type')
),
'filterset': ModuleTypeFilterSet,
'table': ModuleTypeTable,
'filterset': dcim.filtersets.ModuleTypeFilterSet,
'table': dcim.tables.ModuleTypeTable,
'url': 'dcim:moduletype_list',
}),
('module', {
'queryset': Module.objects.prefetch_related(
'module_type__manufacturer', 'device', 'module_bay',
),
'filterset': ModuleFilterSet,
'table': ModuleTable,
'filterset': dcim.filtersets.ModuleFilterSet,
'table': dcim.tables.ModuleTable,
'url': 'dcim:module_list',
}),
('virtualchassis', {
'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
member_count=count_related(Device, 'virtual_chassis')
),
'filterset': VirtualChassisFilterSet,
'table': VirtualChassisTable,
'filterset': dcim.filtersets.VirtualChassisFilterSet,
'table': dcim.tables.VirtualChassisTable,
'url': 'dcim:virtualchassis_list',
}),
('cable', {
'queryset': Cable.objects.all(),
'filterset': CableFilterSet,
'table': CableTable,
'filterset': dcim.filtersets.CableFilterSet,
'table': dcim.tables.CableTable,
'url': 'dcim:cable_list',
}),
('powerfeed', {
'queryset': PowerFeed.objects.all(),
'filterset': PowerFeedFilterSet,
'table': PowerFeedTable,
'filterset': dcim.filtersets.PowerFeedFilterSet,
'table': dcim.tables.PowerFeedTable,
'url': 'dcim:powerfeed_list',
}),
)
@@ -156,41 +148,47 @@ DCIM_TYPES = OrderedDict(
IPAM_TYPES = OrderedDict(
(
('vrf', {
'queryset': VRF.objects.prefetch_related('tenant'),
'filterset': VRFFilterSet,
'table': VRFTable,
'queryset': VRF.objects.prefetch_related('tenant', 'tenant__group'),
'filterset': ipam.filtersets.VRFFilterSet,
'table': ipam.tables.VRFTable,
'url': 'ipam:vrf_list',
}),
('aggregate', {
'queryset': Aggregate.objects.prefetch_related('rir'),
'filterset': AggregateFilterSet,
'table': AggregateTable,
'filterset': ipam.filtersets.AggregateFilterSet,
'table': ipam.tables.AggregateTable,
'url': 'ipam:aggregate_list',
}),
('prefix', {
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
'filterset': PrefixFilterSet,
'table': PrefixTable,
'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'),
'filterset': ipam.filtersets.PrefixFilterSet,
'table': ipam.tables.PrefixTable,
'url': 'ipam:prefix_list',
}),
('ipaddress', {
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'),
'filterset': IPAddressFilterSet,
'table': IPAddressTable,
'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group'),
'filterset': ipam.filtersets.IPAddressFilterSet,
'table': ipam.tables.IPAddressTable,
'url': 'ipam:ipaddress_list',
}),
('vlan', {
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'),
'filterset': VLANFilterSet,
'table': VLANTable,
'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role'),
'filterset': ipam.filtersets.VLANFilterSet,
'table': ipam.tables.VLANTable,
'url': 'ipam:vlan_list',
}),
('asn', {
'queryset': ASN.objects.prefetch_related('rir', 'tenant'),
'filterset': ASNFilterSet,
'table': ASNTable,
'queryset': ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group'),
'filterset': ipam.filtersets.ASNFilterSet,
'table': ipam.tables.ASNTable,
'url': 'ipam:asn_list',
}),
('service', {
'queryset': Service.objects.prefetch_related('device', 'virtual_machine'),
'filterset': ipam.filtersets.ServiceFilterSet,
'table': ipam.tables.ServiceTable,
'url': 'ipam:service_list',
}),
)
)
@@ -198,15 +196,15 @@ TENANCY_TYPES = OrderedDict(
(
('tenant', {
'queryset': Tenant.objects.prefetch_related('group'),
'filterset': TenantFilterSet,
'table': TenantTable,
'filterset': tenancy.filtersets.TenantFilterSet,
'table': tenancy.tables.TenantTable,
'url': 'tenancy:tenant_list',
}),
('contact', {
'queryset': Contact.objects.prefetch_related('group', 'assignments').annotate(
assignment_count=count_related(ContactAssignment, 'contact')),
'filterset': ContactFilterSet,
'table': ContactTable,
'filterset': tenancy.filtersets.ContactFilterSet,
'table': tenancy.tables.ContactTable,
'url': 'tenancy:contact_list',
}),
)
@@ -219,16 +217,16 @@ VIRTUALIZATION_TYPES = OrderedDict(
device_count=count_related(Device, 'cluster'),
vm_count=count_related(VirtualMachine, 'cluster')
),
'filterset': ClusterFilterSet,
'table': ClusterTable,
'filterset': virtualization.filtersets.ClusterFilterSet,
'table': virtualization.tables.ClusterTable,
'url': 'virtualization:cluster_list',
}),
('virtualmachine', {
'queryset': VirtualMachine.objects.prefetch_related(
'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
'cluster', 'tenant', 'tenant__group', 'platform', 'primary_ip4', 'primary_ip6',
),
'filterset': VirtualMachineFilterSet,
'table': VirtualMachineTable,
'filterset': virtualization.filtersets.VirtualMachineFilterSet,
'table': virtualization.tables.VirtualMachineTable,
'url': 'virtualization:virtualmachine_list',
}),
)

View File

@@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
# Environment setup
#
VERSION = '3.2.3'
VERSION = '3.2.6'
# Hostname
HOSTNAME = platform.node()
@@ -84,6 +84,7 @@ if BASE_PATH:
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
@@ -95,6 +96,7 @@ EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {})
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None)
INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
JINJA2_FILTERS = getattr(configuration, 'JINJA2_FILTERS', {})
LOGGING = getattr(configuration, 'LOGGING', {})
LOGIN_PERSISTENCE = getattr(configuration, 'LOGIN_PERSISTENCE', False)
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
@@ -481,6 +483,19 @@ for param in dir(configuration):
SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.social_auth.associate_by_email',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'netbox.authentication.user_default_groups_handler',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
)
#
# Django Prometheus

View File

@@ -90,6 +90,15 @@ class TemplateColumn(tables.TemplateColumn):
"""
PLACEHOLDER = mark_safe('&mdash;')
def __init__(self, export_raw=False, **kwargs):
"""
Args:
export_raw: If true, data export returns the raw field value rather than the rendered template. (Default:
False)
"""
super().__init__(**kwargs)
self.export_raw = export_raw
def render(self, *args, **kwargs):
ret = super().render(*args, **kwargs)
if not ret.strip():
@@ -97,6 +106,10 @@ class TemplateColumn(tables.TemplateColumn):
return ret
def value(self, **kwargs):
if self.export_raw:
# Skip template rendering and export raw value
return kwargs.get('value')
ret = super().value(**kwargs)
if ret == self.PLACEHOLDER:
return ''
@@ -153,6 +166,7 @@ class ActionsItem:
title: str
icon: str
permission: Optional[str] = None
css_class: Optional[str] = 'secondary'
class ActionsColumn(tables.Column):
@@ -162,19 +176,22 @@ class ActionsColumn(tables.Column):
:param actions: The ordered list of dropdown menu items to include
:param extra_buttons: A Django template string which renders additional buttons preceding the actions dropdown
:param split_actions: When True, converts the actions dropdown menu into a split button with first action as the
direct button link and icon (default: True)
"""
attrs = {'td': {'class': 'text-end text-nowrap noprint'}}
empty_values = ()
actions = {
'edit': ActionsItem('Edit', 'pencil', 'change'),
'delete': ActionsItem('Delete', 'trash-can-outline', 'delete'),
'edit': ActionsItem('Edit', 'pencil', 'change', 'warning'),
'delete': ActionsItem('Delete', 'trash-can-outline', 'delete', 'danger'),
'changelog': ActionsItem('Changelog', 'history'),
}
def __init__(self, *args, actions=('edit', 'delete', 'changelog'), extra_buttons='', **kwargs):
def __init__(self, *args, actions=('edit', 'delete', 'changelog'), extra_buttons='', split_actions=True, **kwargs):
super().__init__(*args, **kwargs)
self.extra_buttons = extra_buttons
self.split_actions = split_actions
# Determine which actions to enable
self.actions = {
@@ -192,32 +209,62 @@ class ActionsColumn(tables.Column):
model = table.Meta.model
request = getattr(table, 'context', {}).get('request')
url_appendix = f'?return_url={request.path}' if request else ''
html = ''
links = []
# Compile actions menu
button = None
dropdown_class = 'secondary'
dropdown_links = []
user = getattr(request, 'user', AnonymousUser())
for action, attrs in self.actions.items():
for idx, (action, attrs) in enumerate(self.actions.items()):
permission = f'{model._meta.app_label}.{attrs.permission}_{model._meta.model_name}'
if attrs.permission is None or user.has_perm(permission):
url = reverse(get_viewname(model, action), kwargs={'pk': record.pk})
links.append(f'<li><a class="dropdown-item" href="{url}{url_appendix}">'
f'<i class="mdi mdi-{attrs.icon}"></i> {attrs.title}</a></li>')
if not links:
return ''
# Render a separate button if a) only one action exists, or b) if split_actions is True
if len(self.actions) == 1 or (self.split_actions and idx == 0):
dropdown_class = attrs.css_class
button = (
f'<a class="btn btn-sm btn-{attrs.css_class}" href="{url}{url_appendix}" type="button">'
f'<i class="mdi mdi-{attrs.icon}"></i></a>'
)
menu = f'<span class="dropdown">' \
f'<a class="btn btn-sm btn-secondary dropdown-toggle" href="#" type="button" data-bs-toggle="dropdown">' \
f'<i class="mdi mdi-wrench"></i></a>' \
f'<ul class="dropdown-menu">{"".join(links)}</ul></span>'
# Add dropdown menu items
else:
dropdown_links.append(
f'<li><a class="dropdown-item" href="{url}{url_appendix}">'
f'<i class="mdi mdi-{attrs.icon}"></i> {attrs.title}</a></li>'
)
# Create the actions dropdown menu
if button and dropdown_links:
html += (
f'<span class="btn-group dropdown">'
f' {button}'
f' <a class="btn btn-sm btn-{dropdown_class} dropdown-toggle" type="button" data-bs-toggle="dropdown" style="padding-left: 2px">'
f' <span class="visually-hidden">Toggle Dropdown</span></a>'
f' <ul class="dropdown-menu">{"".join(dropdown_links)}</ul>'
f'</span>'
)
elif button:
html += button
elif dropdown_links:
html += (
f'<span class="btn-group dropdown">'
f' <a class="btn btn-sm btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">'
f' <span class="visually-hidden">Toggle Dropdown</span></a>'
f' <ul class="dropdown-menu">{"".join(dropdown_links)}</ul>'
f'</span>'
)
# Render any extra buttons from template code
if self.extra_buttons:
template = Template(self.extra_buttons)
context = getattr(table, "context", Context())
context.update({'record': record})
menu = template.render(context) + menu
html = template.render(context) + html
return mark_safe(menu)
return mark_safe(html)
class ChoiceFieldColumn(tables.Column):

View File

@@ -8,6 +8,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.db import transaction, IntegrityError
from django.db.models import ManyToManyField, ProtectedError
from django.db.models.fields.reverse_related import ManyToManyRel
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
@@ -484,7 +485,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
setattr(obj, name, None if model_field.null else '')
# ManyToManyFields
elif isinstance(model_field, ManyToManyField):
elif isinstance(model_field, (ManyToManyField, ManyToManyRel)):
if form.cleaned_data[name]:
getattr(obj, name).set(form.cleaned_data[name])
# Normal fields

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,7 @@ import { initDepthToggle } from './depthToggle';
import { initMoveButtons } from './moveOptions';
import { initReslug } from './reslug';
import { initSelectAll } from './selectAll';
import { initSelectMultiple } from './selectMultiple';
export function initButtons(): void {
for (const func of [
@@ -10,6 +11,7 @@ export function initButtons(): void {
initConnectionToggle,
initReslug,
initSelectAll,
initSelectMultiple,
initMoveButtons,
]) {
func();

View File

@@ -0,0 +1,105 @@
import { getElements } from '../util';
import { StateManager } from 'src/state';
import { previousPkCheckState } from '../stores';
type PreviousPkCheckState = { element: Nullable<HTMLInputElement> };
/**
* If there is a text selection, removes it.
*/
function removeTextSelection(): void {
window.getSelection()?.removeAllRanges();
}
/**
* Sets the state object passed in to the eventTargetElement object passed in.
*
* @param eventTargetElement HTML Input Element, retrieved from getting the target of the
* event passed in from handlePkCheck()
* @param state PreviousPkCheckState object.
*/
function updatePreviousPkCheckState(
eventTargetElement: HTMLInputElement,
state: StateManager<PreviousPkCheckState>,
): void {
state.set('element', eventTargetElement);
}
/**
* For all checkboxes between eventTargetElement and previousStateElement in elementList, toggle
* "checked" value to eventTargetElement.checked
*
* @param eventTargetElement HTML Input Element, retrieved from getting the target of the
* event passed in from handlePkCheck()
* @param state PreviousPkCheckState object.
*/
function toggleCheckboxRange(
eventTargetElement: HTMLInputElement,
previousStateElement: HTMLInputElement,
elementList: Generator,
): void {
let changePkCheckboxState = false;
for (const element of elementList) {
const typedElement = element as HTMLInputElement;
//Change loop's current checkbox state to eventTargetElement checkbox state
if (changePkCheckboxState === true) {
typedElement.checked = eventTargetElement.checked;
}
//The previously clicked checkbox was above the shift clicked checkbox
if (element === previousStateElement) {
if (changePkCheckboxState === true) {
changePkCheckboxState = false;
return;
}
changePkCheckboxState = true;
typedElement.checked = eventTargetElement.checked;
}
//The previously clicked checkbox was below the shift clicked checkbox
if (element === eventTargetElement) {
if (changePkCheckboxState === true) {
changePkCheckboxState = false;
return;
}
changePkCheckboxState = true;
}
}
}
/**
* IF the shift key is pressed and there is state is not null, toggleCheckboxRange between the
* event target element and the state element.
*
* @param event Mouse event.
* @param state PreviousPkCheckState object.
*/
function handlePkCheck(event: MouseEvent, state: StateManager<PreviousPkCheckState>): void {
const eventTargetElement = event.target as HTMLInputElement;
const previousStateElement = state.get('element');
updatePreviousPkCheckState(eventTargetElement, state);
//Stop if user is not holding shift key
if (!event.shiftKey) {
return;
}
removeTextSelection();
//If no previous state, store event target element as previous state and return
if (previousStateElement === null) {
return updatePreviousPkCheckState(eventTargetElement, state);
}
const checkboxList = getElements<HTMLInputElement>('input[type="checkbox"][name="pk"]');
toggleCheckboxRange(eventTargetElement, previousStateElement, checkboxList);
}
/**
* Initialize table select all elements.
*/
export function initSelectMultiple(): void {
const checkboxElements = getElements<HTMLInputElement>('input[type="checkbox"][name="pk"]');
for (const element of checkboxElements) {
element.addEventListener('click', event => {
removeTextSelection();
//Stop propogation to avoid event firing multiple times
event.stopPropagation();
handlePkCheck(event, previousPkCheckState);
});
}
}

View File

@@ -205,6 +205,11 @@ export class APISelect {
onChange: () => this.handleSlimChange(),
});
// Don't close on select if multiple select
if (this.base.multiple) {
this.slim.config.closeOnSelect = false;
}
// Initialize API query properties.
this.getStaticParams();
this.getDynamicParams();
@@ -406,6 +411,7 @@ export class APISelect {
} finally {
this.setOptionStyles();
this.enable();
this.slim.slim.search.input.focus();
this.base.dispatchEvent(this.loadEvent);
}
}

View File

@@ -1,2 +1,3 @@
export * from './objectDepth';
export * from './rackImages';
export * from './previousPkCheck';

View File

@@ -0,0 +1,6 @@
import { createState } from '../state';
export const previousPkCheckState = createState<{ element: Nullable<HTMLInputElement> }>(
{ element: null },
{ persist: false },
);

View File

@@ -10,7 +10,7 @@
{% if termination_a %}
{{ termination_a.site }} {% if termination_a.interface %}- {{ termination_a.interface.device }} {{ termination_a.interface }}{% endif %}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</li>
<li>
@@ -18,7 +18,7 @@
{% if termination_z %}
{{ termination_z.site }} {% if termination_z.interface %}- {{ termination_z.interface.device }} {{ termination_z.interface }}{% endif %}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</li>
</ul>

View File

@@ -94,7 +94,7 @@
{% elif termination.port_speed %}
{{ termination.port_speed|humanize_speed }}
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -50,7 +50,7 @@
{% if object.portal_url %}
<a href="{{ object.portal_url }}">{{ object.portal_url }}</a>
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -40,7 +40,7 @@
{% if object.color %}
<span class="color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -50,7 +50,7 @@
{% if object.length %}
{{ object.length|floatformat }} {{ object.get_length_unit_display }}
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -23,7 +23,7 @@
{% endfor %}
{{ object.site.region|linkify }}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -40,19 +40,13 @@
{% endfor %}
{{ object.location|linkify }}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Rack</th>
<td>
{% if object.rack %}
<a href="{% url 'dcim:rack' pk=object.rack.pk %}">{{ object.rack }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
<td>{{ object.rack|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">Position</th>
@@ -69,7 +63,7 @@
{% elif object.rack and object.device_type.u_height %}
<span class="badge bg-warning">Not racked</span>
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -161,9 +155,7 @@
</tr>
<tr>
<th scope="row">Role</th>
<td>
<a href="{% url 'dcim:device_list' %}?role={{ object.device_role.slug }}">{{ object.device_role }}</a>
</td>
<td>{{ object.device_role|linkify }}</td>
</tr>
<tr>
<th scope="row">Platform</th>
@@ -173,14 +165,14 @@
<th scope="row">Primary IPv4</th>
<td>
{% if object.primary_ip4 %}
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}">{{ object.primary_ip4.address.ip }}</a>
<a href="{{ object.primary_ip4.get_absolute_url }}">{{ object.primary_ip4.address.ip }}</a>
{% if object.primary_ip4.nat_inside %}
(NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }})
{% elif object.primary_ip4.nat_outside %}
(NAT: {{ object.primary_ip4.nat_outside.address.ip|linkify }})
{% endif %}
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -188,14 +180,14 @@
<th scope="row">Primary IPv6</th>
<td>
{% if object.primary_ip6 %}
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}">{{ object.primary_ip6.address.ip }}</a>
<a href="{{ object.primary_ip6.get_absolute_url }}">{{ object.primary_ip6.address.ip }}</a>
{% if object.primary_ip6.nat_inside %}
(NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }})
{% elif object.primary_ip6.nat_outside %}
(NAT: {{ object.primary_ip6.nat_outside.address.ip|linkify }})
{% endif %}
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -86,6 +86,15 @@
{% render_field form.tenant %}
</div>
<div class="field-group my-5">
<div class="row mb-2">
<h5 class="offset-sm-3">Virtual Chassis</h5>
</div>
{% render_field form.virtual_chassis %}
{% render_field form.vc_position %}
{% render_field form.vc_priority %}
</div>
{% if form.custom_fields %}
<div class="field-group my-5">
<div class="row mb-2">

View File

@@ -54,7 +54,7 @@
{% if object.vm_role %}
<a href="{% url 'virtualization:virtualmachine_list' %}?role_id={{ object.pk }}">{{ virtualmachine_count }}</a>
{% else %}
&mdash;
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -55,7 +55,7 @@
<img src="{{ object.front_image.url }}" alt="{{ object.front_image.name }}" class="img-fluid" />
</a>
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -67,7 +67,7 @@
<img src="{{ object.rear_image.url }}" alt="{{ object.rear_image.name }}" class="img-fluid" />
</a>
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -321,7 +321,7 @@
{% if object.rf_channel_frequency %}
{{ object.rf_channel_frequency|simplify_decimal }} MHz
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
{% if peer %}
@@ -329,7 +329,7 @@
{% if peer.rf_channel_frequency %}
{{ peer.rf_channel_frequency|simplify_decimal }} MHz
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
{% endif %}
@@ -340,7 +340,7 @@
{% if object.rf_channel_width %}
{{ object.rf_channel_width|simplify_decimal }} MHz
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
{% if peer %}
@@ -348,7 +348,7 @@
{% if peer.rf_channel_width %}
{{ peer.rf_channel_width|simplify_decimal }} MHz
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
{% endif %}

View File

@@ -18,25 +18,25 @@
</button>
<ul class="dropdown-menu" aria-labeled-by="add-components">
{% if perms.dcim.add_consoleport %}
<li><a class="dropdown-item" href="{% url 'dcim:consoleport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Console Ports</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:consoleport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.device.pk %}">Console Ports</a></li>
{% endif %}
{% if perms.dcim.add_consoleserverport %}
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Server Ports</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.device.pk %}">Console Server Ports</a></li>
{% endif %}
{% if perms.dcim.add_powerport %}
<li><a class="dropdown-item" href="{% url 'dcim:powerport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Ports</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:powerport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.device.pk %}">Power Ports</a></li>
{% endif %}
{% if perms.dcim.add_poweroutlet %}
<li><a class="dropdown-item" href="{% url 'dcim:poweroutlet_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}">Power Outlets</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:poweroutlet_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.device.pk %}">Power Outlets</a></li>
{% endif %}
{% if perms.dcim.add_interface %}
<li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.device.pk %}">Interfaces</a></li>
{% endif %}
{% if perms.dcim.add_frontport %}
<li><a class="dropdown-item" href="{% url 'dcim:frontport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Front Ports</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:frontport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.device.pk %}">Front Ports</a></li>
{% endif %}
{% if perms.dcim.add_rearport %}
<li><a class="dropdown-item" href="{% url 'dcim:rearport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Ports</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:rearport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.device.pk %}">Rear Ports</a></li>
{% endif %}
</ul>
</div>

View File

@@ -44,7 +44,7 @@
{% if object.connected_endpoint %}
{{ object.connected_endpoint.device|linkify }} ({{ object.connected_endpoint }})
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -44,7 +44,7 @@
</tr>
<tr>
<th scope="row">Power Port</th>
<td>{{ object.power_port }}</td>
<td>{{ object.power_port|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">Feed Leg</th>

View File

@@ -53,7 +53,7 @@
{% endfor %}
{{ object.location|linkify }}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -115,7 +115,7 @@
{% if object.type %}
{{ object.get_type_display }}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -133,7 +133,7 @@
{% if object.outer_width %}
<span>{{ object.outer_width }} {{ object.get_outer_unit_display }}</span>
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -143,7 +143,7 @@
{% if object.outer_depth %}
<span>{{ object.outer_depth }} {{ object.get_outer_unit_display }}</span>
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -34,7 +34,7 @@
{% endfor %}
{{ object.region|linkify }}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -47,7 +47,7 @@
{% endfor %}
{{ object.group|linkify }}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -79,7 +79,7 @@
{{ object.time_zone }} (UTC {{ object.time_zone|tzoffset }})<br />
<small class="text-muted">Site time: {% timezone object.time_zone %}{% annotated_now %}{% endtimezone %}</small>
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -94,7 +94,7 @@
</div>
<span>{{ object.physical_address|linebreaksbr }}</span>
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -113,7 +113,7 @@
</div>
<span>{{ object.latitude }}, {{ object.longitude }}</span>
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -15,74 +15,70 @@
{% block content %}
<div class="row">
<div class="col col-md-4">
<div class="card">
<h5 class="card-header">
Virtual Chassis
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Domain</th>
<td>{{ object.domain|placeholder }}</td>
</tr>
<tr>
<th scope="row">Master</th>
<td>{{ object.master|linkify }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
<div class="card">
<h5 class="card-header">Virtual Chassis</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Domain</th>
<td>{{ object.domain|placeholder }}</td>
</tr>
<tr>
<th scope="row">Master</th>
<td>{{ object.master|linkify }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-8">
<div class="card">
<h5 class="card-header">
Members
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th>Device</th>
<th>Position</th>
<th>Master</th>
<th>Priority</th>
</tr>
{% for vc_member in members %}
<tr{% if vc_member == device %} class="info"{% endif %}>
<td>
{{ vc_member|linkify }}
</td>
<td>
{% badge vc_member.vc_position show_empty=True %}
</td>
<td>
{% if object.master == vc_member %}
{% checkmark True %}
{% endif %}
</td>
<td>
{{ vc_member.vc_priority|placeholder }}
</td>
</tr>
{% endfor %}
</table>
</div>
{% if perms.dcim.change_virtualchassis %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:virtualchassis_add_member' pk=object.pk %}?site={{ object.master.site.pk }}&rack={{ object.master.rack.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Member
</a>
</div>
{% endif %}
<div class="card">
<h5 class="card-header">Members</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th>Device</th>
<th>Position</th>
<th>Master</th>
<th>Priority</th>
</tr>
{% for vc_member in members %}
<tr{% if vc_member == device %} class="info"{% endif %}>
<td>
{{ vc_member|linkify }}
</td>
<td>
{% badge vc_member.vc_position show_empty=True %}
</td>
<td>
{% if object.master == vc_member %}
{% checkmark True %}
{% endif %}
</td>
<td>
{{ vc_member.vc_priority|placeholder }}
</td>
</tr>
{% endfor %}
</table>
</div>
{% plugin_right_page object %}
{% if perms.dcim.change_virtualchassis %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:virtualchassis_add_member' pk=object.pk %}?site={{ object.master.site.pk }}&rack={{ object.master.rack.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Member
</a>
</div>
{% endif %}
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -57,7 +57,7 @@
{% if device.rack %}
{{ device.rack }} / {{ device.position }}
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
<td>{{ device.serial|placeholder }}</td>

View File

@@ -57,7 +57,7 @@
{% if object.choices %}
{{ object.choices|join:", " }}
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -105,7 +105,7 @@
{% if object.validation_regex %}
<code>{{ object.validation_regex }}</code>
{% else %}
&mdash;
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -57,7 +57,7 @@
{% elif obj %}
{{ obj }}
{% else %}
<span class="muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
<td class="rendered-markdown">{{ message|markdown }}</td>

View File

@@ -5,25 +5,29 @@
{% render_errors form %}
{% block content %}
{% if perms.extras.add_journalentry %}
<form action="{% url 'extras:journalentry_add' %}" method="post" enctype="multipart/form-data">
<div class="container">
<div class="field-group">
<h4>New Journal Entry</h4>
{% csrf_token %}
{% render_form form %}
</div>
<div class="col col-md-12 text-end my-3">
<a href="{{ object.get_absolute_url }}" class="btn btn-outline-danger">Cancel</a>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
{% endif %}
<div class="card">
<div class="card-body table-responsive">
{% render_table table 'inc/table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
</div>
{% if perms.extras.add_journalentry %}
<div class="card">
<div class="card-body table-responsive">
<h4 class="card-header">New Journal Entry</h4>
<form action="{% url 'extras:journalentry_add' %}" method="post" enctype="multipart/form-data">
<div class="container">
<div class="field-group">
{% csrf_token %}
{% render_form form %}
</div>
<div class="col col-md-12 text-end my-3">
<a href="{{ object.get_absolute_url }}" class="btn btn-outline-danger">Cancel</a>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -76,14 +76,14 @@ Context:
{% if field.required %}
{% checkmark True true="Required" %}
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
<td>
{% if field.to_field_name %}
<code>{{ field.to_field_name }}</code>
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
<td>

View File

@@ -37,7 +37,7 @@
{% elif field.required %}
<span class="text-warning">Not defined</span>
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -52,7 +52,7 @@
{% if object.role %}
<a href="{% url 'ipam:ipaddress_list' %}?role={{ object.role }}">{{ object.get_role_display }}</a>
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -73,7 +73,7 @@
{% endif %}
{{ object.assigned_object|linkify }}
{% else %}
<span class="text-muted">&mdash;</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -86,7 +86,7 @@
({{ object.nat_inside.assigned_object.parent_object|linkify }})
{% endif %}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -128,6 +128,24 @@
<div class="my-3">
{% include 'inc/panel_table.html' with table=related_ips_table heading='Related IP Addresses' %}
</div>
<div class="card">
<h5 class="card-header">
Services
</h5>
<div class="card-body">
{% if services %}
<table class="table table-hover">
{% for service in services %}
{% include 'ipam/inc/service.html' %}
{% endfor %}
</table>
{% else %}
<div class="text-muted">
None
</div>
{% endif %}
</div>
</div>
{% plugin_right_page object %}
</div>
</div>

View File

@@ -39,7 +39,7 @@
{% if aggregate %}
<a href="{% url 'ipam:aggregate' pk=aggregate.pk %}">{{ aggregate.prefix }}</a> ({{ aggregate.rir }})
{% else %}
<span class="text-warning">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -52,7 +52,7 @@
{% endif %}
{{ object.site|linkify }}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -65,7 +65,7 @@
{% endif %}
{{ object.vlan|linkify }}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -138,7 +138,7 @@
{{ first_available_ip }}
{% endif %}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
{% endwith %}
</td>

View File

@@ -45,7 +45,7 @@
{% if ipranges_count %}
<a href="{% url 'ipam:iprange_list' %}?role_id={{ object.pk }}">{{ ipranges_count }}</a>
{% else %}
&mdash;
{{ ''|placeholder }}
{% endif %}
{% endwith %}
</td>
@@ -57,7 +57,7 @@
{% if vlans_count %}
<a href="{% url 'ipam:vlan_list' %}?role_id={{ object.pk }}">{{ vlans_count }}</a>
{% else %}
&mdash;
{{ ''|placeholder }}
{% endif %}
{% endwith %}
</td>

View File

@@ -44,7 +44,7 @@
{% for ipaddress in object.ipaddresses.all %}
{{ ipaddress|linkify }}<br />
{% empty %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endfor %}
</td>
</tr>

View File

@@ -21,7 +21,7 @@
{% endif %}
{{ object.site|linkify }}
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -56,7 +56,7 @@
{% if object.role %}
<a href="{% url 'ipam:vlan_list' %}?role={{ object.role.slug }}">{{ object.role }}</a>
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -35,7 +35,7 @@
{% if object.phone %}
<a href="tel:{{ object.phone }}">{{ object.phone }}</a>
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
@@ -45,7 +45,7 @@
{% if object.email %}
<a href="mailto:{{ object.email }}">{{ object.email }}</a>
{% else %}
<span class="text-muted">None</span>
{{ ''|placeholder }}
{% endif %}
</td>
</tr>

View File

@@ -77,6 +77,10 @@
<h2><a href="{% url 'ipam:prefix_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.prefix_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
<p>Prefixes</p>
</div>
<div class="col col-md-4 text-center">
<h2><a href="{% url 'ipam:iprange_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.iprange_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.iprange_count }}</a></h2>
<p>IP Ranges</p>
</div>
<div class="col col-md-4 text-center">
<h2><a href="{% url 'ipam:ipaddress_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.ipaddress_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.ipaddress_count }}</a></h2>
<p>IP addresses</p>

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